@geminixiang/mikan 0.5.2 → 1.0.0-beta.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/CHANGELOG.md +38 -0
- package/README.md +3 -3
- package/dist/adapter.d.ts +1 -1
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js.map +1 -1
- package/dist/adapters/shared.js +2 -2
- package/dist/adapters/shared.js.map +1 -1
- package/dist/agent.d.ts +11 -2
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +211 -119
- package/dist/agent.js.map +1 -1
- package/dist/commands/admin.d.ts.map +1 -1
- package/dist/commands/admin.js +1 -5
- package/dist/commands/admin.js.map +1 -1
- package/dist/commands/auto-reply.d.ts +3 -0
- package/dist/commands/auto-reply.d.ts.map +1 -1
- package/dist/commands/auto-reply.js +3 -0
- package/dist/commands/auto-reply.js.map +1 -1
- package/dist/commands/model.d.ts +2 -3
- package/dist/commands/model.d.ts.map +1 -1
- package/dist/commands/model.js +1 -4
- package/dist/commands/model.js.map +1 -1
- package/dist/commands/new.d.ts.map +1 -1
- package/dist/commands/new.js +1 -5
- package/dist/commands/new.js.map +1 -1
- package/dist/commands/registry.d.ts.map +1 -1
- package/dist/commands/registry.js +2 -5
- package/dist/commands/registry.js.map +1 -1
- package/dist/commands/types.d.ts +6 -2
- package/dist/commands/types.d.ts.map +1 -1
- package/dist/commands/types.js.map +1 -1
- package/dist/config.d.ts +42 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +93 -7
- package/dist/config.js.map +1 -1
- package/dist/events.d.ts +0 -1
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +1 -6
- package/dist/events.js.map +1 -1
- package/dist/execution-resolver.d.ts.map +1 -1
- package/dist/execution-resolver.js +9 -6
- package/dist/execution-resolver.js.map +1 -1
- package/dist/harness/auth.d.ts +16 -0
- package/dist/harness/auth.d.ts.map +1 -0
- package/dist/harness/auth.js +86 -0
- package/dist/harness/auth.js.map +1 -0
- package/dist/harness/extensions/loader.d.ts +43 -0
- package/dist/harness/extensions/loader.d.ts.map +1 -0
- package/dist/harness/extensions/loader.js +325 -0
- package/dist/harness/extensions/loader.js.map +1 -0
- package/dist/harness/extensions/registry.d.ts +23 -0
- package/dist/harness/extensions/registry.d.ts.map +1 -0
- package/dist/harness/extensions/registry.js +45 -0
- package/dist/harness/extensions/registry.js.map +1 -0
- package/dist/harness/extensions/types.d.ts +225 -0
- package/dist/harness/extensions/types.d.ts.map +1 -0
- package/dist/harness/extensions/types.js +2 -0
- package/dist/harness/extensions/types.js.map +1 -0
- package/dist/harness/http.d.ts +16 -0
- package/dist/harness/http.d.ts.map +1 -0
- package/dist/harness/http.js +71 -0
- package/dist/harness/http.js.map +1 -0
- package/dist/harness/index.d.ts +19 -0
- package/dist/harness/index.d.ts.map +1 -0
- package/dist/harness/index.js +18 -0
- package/dist/harness/index.js.map +1 -0
- package/dist/harness/models.d.ts +34 -0
- package/dist/harness/models.d.ts.map +1 -0
- package/dist/harness/models.js +248 -0
- package/dist/harness/models.js.map +1 -0
- package/dist/harness/runner.d.ts +124 -0
- package/dist/harness/runner.d.ts.map +1 -0
- package/dist/harness/runner.js +436 -0
- package/dist/harness/runner.js.map +1 -0
- package/dist/harness/session-store.d.ts +77 -0
- package/dist/harness/session-store.d.ts.map +1 -0
- package/dist/harness/session-store.js +248 -0
- package/dist/harness/session-store.js.map +1 -0
- package/dist/harness/settings.d.ts +47 -0
- package/dist/harness/settings.d.ts.map +1 -0
- package/dist/harness/settings.js +29 -0
- package/dist/harness/settings.js.map +1 -0
- package/dist/harness/skills.d.ts +54 -0
- package/dist/harness/skills.d.ts.map +1 -0
- package/dist/harness/skills.js +203 -0
- package/dist/harness/skills.js.map +1 -0
- package/dist/harness/types.d.ts +32 -0
- package/dist/harness/types.d.ts.map +1 -0
- package/dist/harness/types.js +2 -0
- package/dist/harness/types.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +11 -14
- package/dist/log.js.map +1 -1
- package/dist/main.js +67 -8
- package/dist/main.js.map +1 -1
- package/dist/model-registry.d.ts +2 -2
- package/dist/model-registry.d.ts.map +1 -1
- package/dist/model-registry.js.map +1 -1
- package/dist/provisioner.d.ts +1 -4
- package/dist/provisioner.d.ts.map +1 -1
- package/dist/provisioner.js +3 -8
- package/dist/provisioner.js.map +1 -1
- package/dist/runtime/agent-run-controller.d.ts.map +1 -1
- package/dist/runtime/agent-run-controller.js +6 -3
- package/dist/runtime/agent-run-controller.js.map +1 -1
- package/dist/runtime/conversation-runtime.js +1 -1
- package/dist/runtime/conversation-runtime.js.map +1 -1
- package/dist/runtime/types.d.ts +4 -3
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/types.js.map +1 -1
- package/dist/sandbox/container.d.ts +1 -0
- package/dist/sandbox/container.d.ts.map +1 -1
- package/dist/sandbox/container.js +2 -2
- package/dist/sandbox/container.js.map +1 -1
- package/dist/sandbox/host.d.ts.map +1 -1
- package/dist/sandbox/host.js +0 -4
- package/dist/sandbox/host.js.map +1 -1
- package/dist/sandbox/image.d.ts.map +1 -1
- package/dist/sandbox/image.js +0 -3
- package/dist/sandbox/image.js.map +1 -1
- package/dist/sandbox/index.d.ts.map +1 -1
- package/dist/sandbox/index.js +4 -1
- package/dist/sandbox/index.js.map +1 -1
- package/dist/sandbox/types.d.ts +2 -2
- package/dist/sandbox/types.d.ts.map +1 -1
- package/dist/sandbox/types.js.map +1 -1
- package/dist/sessions/agent-memory-file-manager.d.ts.map +1 -1
- package/dist/sessions/agent-memory-file-manager.js +8 -8
- package/dist/sessions/agent-memory-file-manager.js.map +1 -1
- package/dist/sessions/metadata.js +2 -2
- package/dist/sessions/metadata.js.map +1 -1
- package/dist/sessions/store.d.ts +3 -3
- package/dist/sessions/store.d.ts.map +1 -1
- package/dist/sessions/store.js +17 -43
- package/dist/sessions/store.js.map +1 -1
- package/dist/sessions/types.d.ts +1 -1
- package/dist/sessions/types.d.ts.map +1 -1
- package/dist/sessions/types.js.map +1 -1
- package/dist/store.d.ts +1 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +18 -11
- package/dist/store.js.map +1 -1
- package/dist/tools/edit.d.ts.map +1 -1
- package/dist/tools/edit.js +1 -3
- package/dist/tools/edit.js.map +1 -1
- package/dist/tools/event.d.ts.map +1 -1
- package/dist/tools/event.js +7 -74
- package/dist/tools/event.js.map +1 -1
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +1 -3
- package/dist/tools/read.js.map +1 -1
- package/dist/tools/write.d.ts.map +1 -1
- package/dist/tools/write.js +3 -7
- package/dist/tools/write.js.map +1 -1
- package/dist/trigger.d.ts +2 -0
- package/dist/trigger.d.ts.map +1 -1
- package/dist/trigger.js +7 -13
- package/dist/trigger.js.map +1 -1
- package/dist/types.d.ts +21 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/vault/index.d.ts.map +1 -1
- package/dist/vault/index.js +7 -1
- package/dist/vault/index.js.map +1 -1
- package/dist/web/admin/portal.d.ts +1 -1
- package/dist/web/admin/portal.d.ts.map +1 -1
- package/dist/web/admin/portal.js +394 -73
- package/dist/web/admin/portal.js.map +1 -1
- package/dist/web/admin/provider-models.d.ts +2 -2
- package/dist/web/admin/provider-models.d.ts.map +1 -1
- package/dist/web/admin/provider-models.js +4 -2
- package/dist/web/admin/provider-models.js.map +1 -1
- package/dist/web/admin/store.d.ts.map +1 -1
- package/dist/web/admin/store.js +3 -12
- package/dist/web/admin/store.js.map +1 -1
- package/dist/web/admin/types.d.ts +7 -5
- package/dist/web/admin/types.d.ts.map +1 -1
- package/dist/web/admin/types.js.map +1 -1
- package/dist/web/login/oauth.d.ts.map +1 -1
- package/dist/web/login/oauth.js +41 -23
- package/dist/web/login/oauth.js.map +1 -1
- package/dist/web/login/store.d.ts +0 -4
- package/dist/web/login/store.d.ts.map +1 -1
- package/dist/web/login/store.js +3 -16
- package/dist/web/login/store.js.map +1 -1
- package/dist/web/server.js +2 -2
- package/dist/web/server.js.map +1 -1
- package/dist/web/session-view/portal.d.ts.map +1 -1
- package/dist/web/session-view/portal.js +5 -16
- package/dist/web/session-view/portal.js.map +1 -1
- package/dist/web/session-view/service.js +7 -13
- package/dist/web/session-view/service.js.map +1 -1
- package/dist/web/session-view/store.d.ts.map +1 -1
- package/dist/web/session-view/store.js +2 -7
- package/dist/web/session-view/store.js.map +1 -1
- package/dist/web/token-store.d.ts +2 -8
- package/dist/web/token-store.d.ts.map +1 -1
- package/dist/web/token-store.js +11 -12
- package/dist/web/token-store.js.map +1 -1
- package/package.json +6 -3
- package/dist/context.d.ts +0 -2
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -2
- package/dist/context.js.map +0 -1
package/dist/web/admin/portal.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, rmSync, statSync } from "fs";
|
|
2
|
-
import { homedir } from "os";
|
|
3
2
|
import { basename, join, resolve as pathResolve, sep as pathSep } from "path";
|
|
4
|
-
import {
|
|
3
|
+
import { MikanModels, SessionStore } from "../../harness/index.js";
|
|
5
4
|
import { loadConversationAutoReplyConfig, loadGlobalSettings, resolveConversationSettings, saveConversationAutoReplyConfig, updateConversationSettings, updateGlobalSettings, } from "../../config.js";
|
|
6
5
|
import { escapeHtml } from "../../utils/html.js";
|
|
7
6
|
import { readRawBody } from "../../utils/http-body.js";
|
|
@@ -12,7 +11,7 @@ import { resolveActorVaultKey } from "../../vault/routing.js";
|
|
|
12
11
|
import { sharedVaultKey } from "../../vault/index.js";
|
|
13
12
|
import { modelKey, resolveAdminModelAccessStatuses } from "./provider-models.js";
|
|
14
13
|
// ── Handler ────────────────────────────────────────────────────────────────────
|
|
15
|
-
export function handleAdminRequest(req, res, url, services) {
|
|
14
|
+
export async function handleAdminRequest(req, res, url, services) {
|
|
16
15
|
if (!url.pathname.startsWith("/admin"))
|
|
17
16
|
return false;
|
|
18
17
|
if (req.method === "GET" && url.pathname === "/admin") {
|
|
@@ -31,13 +30,13 @@ export function handleAdminRequest(req, res, url, services) {
|
|
|
31
30
|
return true;
|
|
32
31
|
}
|
|
33
32
|
if (url.pathname.startsWith("/admin/api/")) {
|
|
34
|
-
routeApiRequest(req, res, url, services);
|
|
33
|
+
await routeApiRequest(req, res, url, services);
|
|
35
34
|
return true;
|
|
36
35
|
}
|
|
37
36
|
return false;
|
|
38
37
|
}
|
|
39
38
|
// ── API routing ────────────────────────────────────────────────────────────────
|
|
40
|
-
function routeApiRequest(req, res, url, services) {
|
|
39
|
+
async function routeApiRequest(req, res, url, services) {
|
|
41
40
|
if (req.method === "GET") {
|
|
42
41
|
const token = services.adminTokenStore.peek(url.searchParams.get("token") ?? "");
|
|
43
42
|
if (!token) {
|
|
@@ -56,6 +55,10 @@ function routeApiRequest(req, res, url, services) {
|
|
|
56
55
|
serveSessionUsage(res, services);
|
|
57
56
|
return;
|
|
58
57
|
}
|
|
58
|
+
if (url.pathname === "/admin/api/conversation-usage") {
|
|
59
|
+
serveConversationUsage(res, url, services);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
59
62
|
if (url.pathname === "/admin/api/conversation-state") {
|
|
60
63
|
serveConversationState(res, url, services, token);
|
|
61
64
|
return;
|
|
@@ -99,59 +102,60 @@ function routeApiRequest(req, res, url, services) {
|
|
|
99
102
|
jsonRes(res, 404, { error: "Not found" });
|
|
100
103
|
return;
|
|
101
104
|
}
|
|
102
|
-
if (req.method
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
126
|
-
if (url.pathname === "/admin/api/conversations/session-link") {
|
|
127
|
-
serveConversationSessionLink(res, body, services, token);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
if (url.pathname === "/admin/api/conversations/login-link") {
|
|
131
|
-
serveConversationLoginLink(res, body, services, token);
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
if (url.pathname === "/admin/api/conversations/events/delete") {
|
|
135
|
-
serveConversationEventDelete(res, body, services, token);
|
|
136
|
-
return;
|
|
137
|
-
}
|
|
138
|
-
if (url.pathname === "/admin/api/settings/model") {
|
|
139
|
-
serveGlobalModelUpdate(res, body);
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
if (url.pathname === "/admin/api/settings/sandbox") {
|
|
143
|
-
serveGlobalSandboxUpdate(res, body);
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
if (url.pathname === "/admin/api/settings/slack") {
|
|
147
|
-
serveGlobalSlackUpdate(res, body);
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
jsonRes(res, 404, { error: "Not found" });
|
|
151
|
-
});
|
|
105
|
+
if (req.method !== "POST") {
|
|
106
|
+
jsonRes(res, 405, { error: "Method not allowed" });
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const body = await readJsonBody(req, res);
|
|
110
|
+
if (!body)
|
|
111
|
+
return;
|
|
112
|
+
const rawToken = typeof body.token === "string" ? body.token : "";
|
|
113
|
+
const token = services.adminTokenStore.peek(rawToken);
|
|
114
|
+
if (!token) {
|
|
115
|
+
jsonRes(res, 403, { error: "Unauthorized" });
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (url.pathname === "/admin/api/conversations/model") {
|
|
119
|
+
serveConversationModelUpdate(res, body, services, token);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (url.pathname === "/admin/api/conversations/sandbox") {
|
|
123
|
+
serveConversationSandboxUpdate(res, body, services, token);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (url.pathname === "/admin/api/conversations/auto-reply") {
|
|
127
|
+
serveConversationAutoReplyUpdate(res, body, services, token);
|
|
152
128
|
return;
|
|
153
129
|
}
|
|
154
|
-
|
|
130
|
+
if (url.pathname === "/admin/api/conversations/slack") {
|
|
131
|
+
serveConversationSlackUpdate(res, body, services, token);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (url.pathname === "/admin/api/conversations/session-link") {
|
|
135
|
+
serveConversationSessionLink(res, body, services, token);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (url.pathname === "/admin/api/conversations/login-link") {
|
|
139
|
+
serveConversationLoginLink(res, body, services, token);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (url.pathname === "/admin/api/conversations/events/delete") {
|
|
143
|
+
serveConversationEventDelete(res, body, services, token);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (url.pathname === "/admin/api/settings/model") {
|
|
147
|
+
serveGlobalModelUpdate(res, body);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (url.pathname === "/admin/api/settings/sandbox") {
|
|
151
|
+
serveGlobalSandboxUpdate(res, body);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (url.pathname === "/admin/api/settings/slack") {
|
|
155
|
+
serveGlobalSlackUpdate(res, body);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
jsonRes(res, 404, { error: "Not found" });
|
|
155
159
|
}
|
|
156
160
|
// ── Scope helpers ──────────────────────────────────────────────────────────────
|
|
157
161
|
function resolveConversationId(requested, token) {
|
|
@@ -197,7 +201,9 @@ function listConversationDirs(workingDir) {
|
|
|
197
201
|
const dir = join(workingDir, name);
|
|
198
202
|
try {
|
|
199
203
|
const items = readdirSync(dir);
|
|
200
|
-
|
|
204
|
+
// Conversation settings migrated to the state dir, so a sessions/
|
|
205
|
+
// subdir is the primary marker for existing conversation dirs.
|
|
206
|
+
return items.some((item) => item === "sessions" || SETTINGS_FILES.has(item) || item.endsWith(".jsonl"));
|
|
201
207
|
}
|
|
202
208
|
catch {
|
|
203
209
|
return false;
|
|
@@ -288,7 +294,7 @@ function listConversationSessionUsage(workingDir, conversationId, label) {
|
|
|
288
294
|
}
|
|
289
295
|
function readSessionUsage(sessionFile, conversationId, label) {
|
|
290
296
|
try {
|
|
291
|
-
const manager =
|
|
297
|
+
const manager = SessionStore.open(sessionFile);
|
|
292
298
|
const header = manager.getHeader();
|
|
293
299
|
if (!header)
|
|
294
300
|
return [];
|
|
@@ -329,6 +335,114 @@ function readSessionUsage(sessionFile, conversationId, label) {
|
|
|
329
335
|
function numberOrZero(value) {
|
|
330
336
|
return typeof value === "number" && Number.isFinite(value) ? value : 0;
|
|
331
337
|
}
|
|
338
|
+
function localDayKey(d) {
|
|
339
|
+
const year = d.getFullYear();
|
|
340
|
+
const month = String(d.getMonth() + 1).padStart(2, "0");
|
|
341
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
342
|
+
return `${year}-${month}-${day}`;
|
|
343
|
+
}
|
|
344
|
+
function emptyBucket(date) {
|
|
345
|
+
return { date, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0, cost: 0 };
|
|
346
|
+
}
|
|
347
|
+
/** Per-conversation daily token usage over the last N days (N clamped to 1..7). */
|
|
348
|
+
function serveConversationUsage(res, url, services) {
|
|
349
|
+
const workingDir = requireAdminWorkingDir(res, services);
|
|
350
|
+
if (!workingDir)
|
|
351
|
+
return;
|
|
352
|
+
const conversationId = url.searchParams.get("conversationId") ?? "";
|
|
353
|
+
if (!conversationId || !listConversationDirs(workingDir).includes(conversationId)) {
|
|
354
|
+
jsonRes(res, 400, { error: "Unknown conversationId" });
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const days = 14;
|
|
358
|
+
const today = new Date();
|
|
359
|
+
today.setHours(0, 0, 0, 0);
|
|
360
|
+
const buckets = new Map();
|
|
361
|
+
const order = [];
|
|
362
|
+
for (let i = days - 1; i >= 0; i--) {
|
|
363
|
+
const d = new Date(today);
|
|
364
|
+
d.setDate(today.getDate() - i);
|
|
365
|
+
const key = localDayKey(d);
|
|
366
|
+
order.push(key);
|
|
367
|
+
buckets.set(key, emptyBucket(key));
|
|
368
|
+
}
|
|
369
|
+
const cutoff = new Date(today);
|
|
370
|
+
cutoff.setDate(today.getDate() - (days - 1));
|
|
371
|
+
const flags = { hasOlder: false };
|
|
372
|
+
const sessionDir = join(workingDir, conversationId, "sessions");
|
|
373
|
+
try {
|
|
374
|
+
for (const entry of readdirSync(sessionDir, { withFileTypes: true })) {
|
|
375
|
+
if (!entry.isFile() || !entry.name.endsWith(".jsonl"))
|
|
376
|
+
continue;
|
|
377
|
+
accumulateSessionUsageByDay(join(sessionDir, entry.name), cutoff, buckets, flags);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
// No sessions directory yet — return empty buckets.
|
|
382
|
+
}
|
|
383
|
+
const series = order.map((key) => buckets.get(key) ?? emptyBucket(key));
|
|
384
|
+
const totals = series.reduce((sum, b) => {
|
|
385
|
+
sum.input += b.input;
|
|
386
|
+
sum.output += b.output;
|
|
387
|
+
sum.cacheRead += b.cacheRead;
|
|
388
|
+
sum.cacheWrite += b.cacheWrite;
|
|
389
|
+
sum.total += b.total;
|
|
390
|
+
sum.cost += b.cost;
|
|
391
|
+
return sum;
|
|
392
|
+
}, emptyBucket(""));
|
|
393
|
+
jsonRes(res, 200, {
|
|
394
|
+
conversationId,
|
|
395
|
+
label: conversationDisplayLabel(services, conversationId),
|
|
396
|
+
days,
|
|
397
|
+
hasOlder: flags.hasOlder,
|
|
398
|
+
buckets: series,
|
|
399
|
+
totals: {
|
|
400
|
+
input: totals.input,
|
|
401
|
+
output: totals.output,
|
|
402
|
+
cacheRead: totals.cacheRead,
|
|
403
|
+
cacheWrite: totals.cacheWrite,
|
|
404
|
+
total: totals.total,
|
|
405
|
+
cost: totals.cost,
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
function accumulateSessionUsageByDay(sessionFile, cutoff, buckets, flags) {
|
|
410
|
+
try {
|
|
411
|
+
const manager = SessionStore.open(sessionFile);
|
|
412
|
+
if (!manager.getHeader())
|
|
413
|
+
return;
|
|
414
|
+
for (const entry of manager.getEntries()) {
|
|
415
|
+
if (entry.type !== "message" || entry.message.role !== "assistant")
|
|
416
|
+
continue;
|
|
417
|
+
const usage = entry.message.usage;
|
|
418
|
+
if (!usage || !entry.timestamp)
|
|
419
|
+
continue;
|
|
420
|
+
const when = new Date(entry.timestamp);
|
|
421
|
+
if (Number.isNaN(when.getTime()))
|
|
422
|
+
continue;
|
|
423
|
+
if (when < cutoff) {
|
|
424
|
+
flags.hasOlder = true;
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
const bucket = buckets.get(localDayKey(when));
|
|
428
|
+
if (!bucket)
|
|
429
|
+
continue;
|
|
430
|
+
const input = numberOrZero(usage.input);
|
|
431
|
+
const output = numberOrZero(usage.output);
|
|
432
|
+
const cacheRead = numberOrZero(usage.cacheRead);
|
|
433
|
+
const cacheWrite = numberOrZero(usage.cacheWrite);
|
|
434
|
+
bucket.input += input;
|
|
435
|
+
bucket.output += output;
|
|
436
|
+
bucket.cacheRead += cacheRead;
|
|
437
|
+
bucket.cacheWrite += cacheWrite;
|
|
438
|
+
bucket.total += input + output + cacheRead + cacheWrite;
|
|
439
|
+
bucket.cost += numberOrZero(usage.cost?.total);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
// Skip unreadable session files.
|
|
444
|
+
}
|
|
445
|
+
}
|
|
332
446
|
function serveConversationState(res, url, services, token) {
|
|
333
447
|
const workingDir = requireAdminWorkingDir(res, services);
|
|
334
448
|
if (!workingDir)
|
|
@@ -385,8 +499,7 @@ function serveGlobalSettings(res) {
|
|
|
385
499
|
}
|
|
386
500
|
async function serveModelsList(res) {
|
|
387
501
|
try {
|
|
388
|
-
const
|
|
389
|
-
const registry = ModelRegistry.create(authStorage);
|
|
502
|
+
const registry = MikanModels.create();
|
|
390
503
|
const availableModels = await registry.getAvailable();
|
|
391
504
|
const statuses = await resolveAdminModelAccessStatuses(registry, availableModels);
|
|
392
505
|
const models = availableModels.map((model) => ({
|
|
@@ -489,9 +602,12 @@ function serveConversationSlackUpdate(res, body, services, token) {
|
|
|
489
602
|
}
|
|
490
603
|
function serveConversationAutoReplyUpdate(res, body, services, token) {
|
|
491
604
|
const enabled = body.enabled === true;
|
|
492
|
-
const rules =
|
|
493
|
-
|
|
494
|
-
|
|
605
|
+
const rules = body.rules;
|
|
606
|
+
if (rules !== undefined &&
|
|
607
|
+
(!Array.isArray(rules) || rules.some((rule) => typeof rule !== "string"))) {
|
|
608
|
+
jsonRes(res, 400, { error: "rules must be an array of strings" });
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
495
611
|
const scope = resolveTargetConversation(body, token);
|
|
496
612
|
if (scope.error) {
|
|
497
613
|
jsonRes(res, 403, { error: scope.error });
|
|
@@ -707,7 +823,7 @@ function buildTree(startDir, relPrefix) {
|
|
|
707
823
|
catch {
|
|
708
824
|
return null;
|
|
709
825
|
}
|
|
710
|
-
const name = rel === "" ? "." : (rel
|
|
826
|
+
const name = rel === "" ? "." : basename(rel);
|
|
711
827
|
if (!stats.isDirectory()) {
|
|
712
828
|
counter.value += 1;
|
|
713
829
|
return {
|
|
@@ -1153,19 +1269,17 @@ function jsonRes(res, status, body) {
|
|
|
1153
1269
|
});
|
|
1154
1270
|
res.end(JSON.stringify(body));
|
|
1155
1271
|
}
|
|
1156
|
-
async function readJsonBody(req, res
|
|
1272
|
+
async function readJsonBody(req, res) {
|
|
1157
1273
|
const data = await readRawBody(req, res, 32 * 1024);
|
|
1158
1274
|
if (data === null)
|
|
1159
|
-
return;
|
|
1160
|
-
let parsed;
|
|
1275
|
+
return null;
|
|
1161
1276
|
try {
|
|
1162
|
-
|
|
1277
|
+
return JSON.parse(data);
|
|
1163
1278
|
}
|
|
1164
1279
|
catch {
|
|
1165
1280
|
jsonRes(res, 400, { error: "Invalid JSON" });
|
|
1166
|
-
return;
|
|
1281
|
+
return null;
|
|
1167
1282
|
}
|
|
1168
|
-
callback(parsed);
|
|
1169
1283
|
}
|
|
1170
1284
|
const esc = escapeHtml;
|
|
1171
1285
|
// ── HTML ───────────────────────────────────────────────────────────────────────
|
|
@@ -1268,11 +1382,18 @@ function renderAdminPage(token) {
|
|
|
1268
1382
|
<header class="sect-head">
|
|
1269
1383
|
<div>
|
|
1270
1384
|
<p class="eyebrow">Token Usage</p>
|
|
1271
|
-
<h2 class="card-title">Top 20 sessions</h2>
|
|
1272
1385
|
</div>
|
|
1273
|
-
<button class="refresh-btn" onclick="
|
|
1386
|
+
<button class="refresh-btn" onclick="loadTokenUsage()">↻</button>
|
|
1274
1387
|
</header>
|
|
1388
|
+
<h2 class="card-subtitle" style="margin-bottom:10px">Top 20 sessions</h3>
|
|
1275
1389
|
<div id="session-usage-content"><div class="loading-msg">Loading…</div></div>
|
|
1390
|
+
<h2 class="card-subtitle" style="margin:24px 0 10px">Usage timeline</h3>
|
|
1391
|
+
<div class="timeline-controls">
|
|
1392
|
+
<label>Conversation
|
|
1393
|
+
<select id="timeline-conv" onchange="loadUsageTimeline()"></select>
|
|
1394
|
+
</label>
|
|
1395
|
+
</div>
|
|
1396
|
+
<div id="usage-timeline-content"><div class="loading-msg">Loading…</div></div>
|
|
1276
1397
|
</section>
|
|
1277
1398
|
|
|
1278
1399
|
<section class="card sect">
|
|
@@ -1803,7 +1924,7 @@ function renderAdminPage(token) {
|
|
|
1803
1924
|
if (globalLoaded) return;
|
|
1804
1925
|
globalLoaded = true;
|
|
1805
1926
|
loadAllConversations();
|
|
1806
|
-
|
|
1927
|
+
loadTokenUsage();
|
|
1807
1928
|
loadGlobalSettings();
|
|
1808
1929
|
loadGlobalSkills();
|
|
1809
1930
|
loadEvents();
|
|
@@ -1862,6 +1983,140 @@ function renderAdminPage(token) {
|
|
|
1862
1983
|
return Number(value || 0).toLocaleString('en-US');
|
|
1863
1984
|
}
|
|
1864
1985
|
|
|
1986
|
+
let timelineConvLoaded = false;
|
|
1987
|
+
let timelineData = null;
|
|
1988
|
+
let timelineFilter = null;
|
|
1989
|
+
async function ensureTimelineConvOptions() {
|
|
1990
|
+
if (timelineConvLoaded) return;
|
|
1991
|
+
const sel = document.getElementById('timeline-conv');
|
|
1992
|
+
const prev = sel.value;
|
|
1993
|
+
const data = await apiGet('/admin/api/conversations');
|
|
1994
|
+
if (!data.conversations.length) {
|
|
1995
|
+
sel.innerHTML = '<option value="">No conversations</option>';
|
|
1996
|
+
timelineConvLoaded = true;
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1999
|
+
const want = prev || defaultConversationId;
|
|
2000
|
+
sel.innerHTML = data.conversations.map((c) =>
|
|
2001
|
+
'<option value="' + escAttr(c.conversationId) + '"' +
|
|
2002
|
+
(c.conversationId === want ? ' selected' : '') + '>' +
|
|
2003
|
+
escHtml(c.label || c.conversationId) + '</option>'
|
|
2004
|
+
).join('');
|
|
2005
|
+
timelineConvLoaded = true;
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
// Shared refresh for the merged Token Usage section: reloads the session
|
|
2009
|
+
// ranking and re-fetches the conversation list (so new sessions appear),
|
|
2010
|
+
// preserving the currently selected conversation.
|
|
2011
|
+
async function loadTokenUsage() {
|
|
2012
|
+
loadSessionUsage();
|
|
2013
|
+
timelineConvLoaded = false;
|
|
2014
|
+
await loadUsageTimeline();
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
async function loadUsageTimeline() {
|
|
2018
|
+
const container = document.getElementById('usage-timeline-content');
|
|
2019
|
+
try {
|
|
2020
|
+
await ensureTimelineConvOptions();
|
|
2021
|
+
const conv = document.getElementById('timeline-conv').value;
|
|
2022
|
+
if (!conv) {
|
|
2023
|
+
container.innerHTML = '<div class="empty-state">No conversations found</div>';
|
|
2024
|
+
return;
|
|
2025
|
+
}
|
|
2026
|
+
container.innerHTML = '<div class="loading-msg">Loading…</div>';
|
|
2027
|
+
const data = await apiGet('/admin/api/conversation-usage?conversationId=' +
|
|
2028
|
+
encodeURIComponent(conv));
|
|
2029
|
+
timelineData = data;
|
|
2030
|
+
container.innerHTML = renderUsageTimeline(data);
|
|
2031
|
+
} catch (err) {
|
|
2032
|
+
container.innerHTML = '<div class="err-msg">' + escHtml(err.message) + '</div>';
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
function tlCard(label, value) {
|
|
2037
|
+
return '<div class="tl-card"><div class="tl-card-label">' + label +
|
|
2038
|
+
'</div><div class="tl-card-value">' + value + '</div></div>';
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
const TL_SERIES = [
|
|
2042
|
+
{ key: 'cacheRead', seg: 'tl-cache-read', sw: 'sw-cache-read', label: 'Cache read' },
|
|
2043
|
+
{ key: 'cacheWrite', seg: 'tl-cache-write', sw: 'sw-cache-write', label: 'Cache write' },
|
|
2044
|
+
{ key: 'input', seg: 'tl-input', sw: 'sw-input', label: 'Input' },
|
|
2045
|
+
{ key: 'output', seg: 'tl-output', sw: 'sw-output', label: 'Output' },
|
|
2046
|
+
];
|
|
2047
|
+
|
|
2048
|
+
// Click a legend item to show only that series; click it again for all.
|
|
2049
|
+
function toggleTimelineFilter(key) {
|
|
2050
|
+
timelineFilter = timelineFilter === key ? null : key;
|
|
2051
|
+
if (timelineData) {
|
|
2052
|
+
document.getElementById('usage-timeline-content').innerHTML = renderUsageTimeline(timelineData);
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
function renderUsageTimeline(data) {
|
|
2057
|
+
const buckets = data.buckets || [];
|
|
2058
|
+
const totals = data.totals || { total: 0, cost: 0, cacheRead: 0 };
|
|
2059
|
+
const cacheHit = totals.total > 0 ? Math.round((totals.cacheRead / totals.total) * 100) : 0;
|
|
2060
|
+
const cards = '<div class="tl-cards">' +
|
|
2061
|
+
tlCard('Total cost', totals.cost > 0 ? '$' + Number(totals.cost).toFixed(4) : '—') +
|
|
2062
|
+
tlCard('Total tokens', fmtNum(totals.total)) +
|
|
2063
|
+
tlCard('Cache hit', cacheHit + '%') +
|
|
2064
|
+
'</div>';
|
|
2065
|
+
|
|
2066
|
+
if (totals.total === 0) {
|
|
2067
|
+
const emptyNote = data.hasOlder
|
|
2068
|
+
? '<div class="tl-note">No usage in the last 14 days · earlier activity exists</div>'
|
|
2069
|
+
: '<div class="empty-state">No token usage in the last 14 days</div>';
|
|
2070
|
+
return cards + emptyNote;
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
const active = TL_SERIES.find((s) => s.key === timelineFilter) || null;
|
|
2074
|
+
const valueOf = (b) => active ? (b[active.key] || 0) : b.total;
|
|
2075
|
+
const max = Math.max(1, ...buckets.map(valueOf));
|
|
2076
|
+
const px = (v) => Math.round((v / max) * 180);
|
|
2077
|
+
|
|
2078
|
+
const legend = '<div class="tl-legend">' + TL_SERIES.map((s) => {
|
|
2079
|
+
const cls = 'tl-legend-item' +
|
|
2080
|
+
(active && active.key === s.key ? ' active' : (active ? ' dim' : ''));
|
|
2081
|
+
return '<span class="' + cls + '" onclick="toggleTimelineFilter(\\'' + s.key + '\\')">' +
|
|
2082
|
+
'<i class="sw ' + s.sw + '"></i>' + s.label + '</span>';
|
|
2083
|
+
}).join('') + '</div>';
|
|
2084
|
+
|
|
2085
|
+
const bars = buckets.map((b) => {
|
|
2086
|
+
const val = valueOf(b);
|
|
2087
|
+
const tip = active
|
|
2088
|
+
? b.date + ' · ' + active.label + ': ' + fmtNum(b[active.key] || 0) + ' tokens'
|
|
2089
|
+
: b.date + ' · ' + fmtNum(b.total) + ' tokens' +
|
|
2090
|
+
(b.cost > 0 ? ' · $' + Number(b.cost).toFixed(4) : '');
|
|
2091
|
+
let inner;
|
|
2092
|
+
if (val <= 0) {
|
|
2093
|
+
inner = '<span class="tl-empty"></span>';
|
|
2094
|
+
} else if (active) {
|
|
2095
|
+
inner = '<span class="tl-seg ' + active.seg + '" style="height:' + px(val) + 'px"></span>';
|
|
2096
|
+
} else {
|
|
2097
|
+
inner = '<span class="tl-seg tl-output" style="height:' + px(b.output) + 'px"></span>' +
|
|
2098
|
+
'<span class="tl-seg tl-input" style="height:' + px(b.input) + 'px"></span>' +
|
|
2099
|
+
'<span class="tl-seg tl-cache-write" style="height:' + px(b.cacheWrite) + 'px"></span>' +
|
|
2100
|
+
'<span class="tl-seg tl-cache-read" style="height:' + px(b.cacheRead) + 'px"></span>';
|
|
2101
|
+
}
|
|
2102
|
+
return '<div class="tl-bar">' +
|
|
2103
|
+
'<span class="tl-tip">' + escHtml(tip) + '</span>' +
|
|
2104
|
+
'<div class="tl-fill">' + inner + '</div>' +
|
|
2105
|
+
'</div>';
|
|
2106
|
+
}).join('');
|
|
2107
|
+
|
|
2108
|
+
const axis = buckets.length
|
|
2109
|
+
? '<div class="tl-axis"><span>' + escHtml(buckets[0].date.slice(5)) +
|
|
2110
|
+
'</span><span>' + escHtml(buckets[buckets.length - 1].date.slice(5)) + '</span></div>'
|
|
2111
|
+
: '';
|
|
2112
|
+
const note = data.hasOlder
|
|
2113
|
+
? '<div class="tl-note">Showing last 14 days · earlier activity not shown</div>'
|
|
2114
|
+
: '<div class="tl-note">Showing last 14 days</div>';
|
|
2115
|
+
const peak = '<div class="tl-peak" style="bottom:180px"><span class="tl-peak-label">' +
|
|
2116
|
+
fmtNum(max) + ' tokens</span></div>';
|
|
2117
|
+
return cards + legend + '<div class="tl-chart">' + peak + bars + '</div>' + axis + note;
|
|
2118
|
+
}
|
|
2119
|
+
|
|
1865
2120
|
async function loadGlobalSettings() {
|
|
1866
2121
|
const container = document.getElementById('global-settings-content');
|
|
1867
2122
|
container.innerHTML = '<div class="loading-msg">Loading…</div>';
|
|
@@ -2233,6 +2488,72 @@ const adminViewStyles = `
|
|
|
2233
2488
|
}
|
|
2234
2489
|
.usage-table code { font-size: 0.72rem; }
|
|
2235
2490
|
|
|
2491
|
+
.timeline-controls {
|
|
2492
|
+
display: flex; flex-wrap: wrap; gap: 18px; align-items: center; margin-bottom: 16px;
|
|
2493
|
+
}
|
|
2494
|
+
.timeline-controls label {
|
|
2495
|
+
display: flex; align-items: center; gap: 8px;
|
|
2496
|
+
font-size: 0.76rem; color: var(--muted);
|
|
2497
|
+
}
|
|
2498
|
+
.timeline-controls select {
|
|
2499
|
+
padding: 6px 10px; border: 1px solid var(--border); border-radius: 8px;
|
|
2500
|
+
background: #fff; color: var(--text); font-size: 0.8rem; max-width: 240px;
|
|
2501
|
+
}
|
|
2502
|
+
.tl-note { margin-top: 8px; font-size: 0.72rem; color: var(--subtle); }
|
|
2503
|
+
.tl-cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; margin-bottom: 16px; }
|
|
2504
|
+
.tl-card { background: rgba(0,0,0,0.025); border-radius: 10px; padding: 10px 12px; }
|
|
2505
|
+
.tl-card-label { font-size: 0.73rem; color: var(--muted); margin-bottom: 4px; }
|
|
2506
|
+
.tl-card-value { font-size: 1.35rem; font-weight: 600; color: var(--text); }
|
|
2507
|
+
.tl-legend { display: flex; gap: 16px; font-size: 0.73rem; color: var(--muted); margin-bottom: 10px; }
|
|
2508
|
+
.tl-legend span { display: inline-flex; align-items: center; gap: 6px; }
|
|
2509
|
+
.tl-legend-item { cursor: pointer; transition: opacity 100ms; }
|
|
2510
|
+
.tl-legend-item:hover { text-decoration: underline; }
|
|
2511
|
+
.tl-legend-item.active { color: var(--text); font-weight: 600; text-decoration: underline; }
|
|
2512
|
+
.tl-legend-item.dim { opacity: 0.4; }
|
|
2513
|
+
.sw { width: 10px; height: 10px; border-radius: 2px; display: inline-block; }
|
|
2514
|
+
.sw-cache-read { background: rgba(0,0,0,0.18); }
|
|
2515
|
+
.sw-cache-write { background: #3b6fb0; }
|
|
2516
|
+
.sw-input { background: var(--accent); }
|
|
2517
|
+
.sw-output { background: var(--ok-text); }
|
|
2518
|
+
.tl-chart {
|
|
2519
|
+
position: relative;
|
|
2520
|
+
display: flex; align-items: flex-end; gap: 6px;
|
|
2521
|
+
height: 216px; border-bottom: 1px solid var(--border);
|
|
2522
|
+
}
|
|
2523
|
+
.tl-peak {
|
|
2524
|
+
position: absolute; left: 0; right: 0; height: 0;
|
|
2525
|
+
border-top: 1px dashed var(--accent); pointer-events: none;
|
|
2526
|
+
}
|
|
2527
|
+
.tl-peak-label {
|
|
2528
|
+
position: absolute; left: 0; top: -15px;
|
|
2529
|
+
font-size: 0.7rem; color: var(--accent); white-space: nowrap;
|
|
2530
|
+
}
|
|
2531
|
+
.tl-bar {
|
|
2532
|
+
position: relative; flex: 1; min-width: 0; height: 180px;
|
|
2533
|
+
display: flex; flex-direction: column; justify-content: flex-end;
|
|
2534
|
+
border-radius: 6px 6px 0 0; transition: background 80ms ease;
|
|
2535
|
+
}
|
|
2536
|
+
.tl-bar:hover { background: rgba(0,0,0,0.06); }
|
|
2537
|
+
.tl-bar:hover .tl-seg { opacity: 0.55; }
|
|
2538
|
+
.tl-fill {
|
|
2539
|
+
display: flex; flex-direction: column; justify-content: flex-end;
|
|
2540
|
+
border-radius: 3px 3px 0 0; overflow: hidden;
|
|
2541
|
+
}
|
|
2542
|
+
.tl-tip {
|
|
2543
|
+
position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%);
|
|
2544
|
+
margin-bottom: 6px; padding: 5px 9px; border-radius: 6px;
|
|
2545
|
+
background: var(--text); color: #fafafa; font-size: 0.7rem; line-height: 1.35;
|
|
2546
|
+
white-space: nowrap; opacity: 0; pointer-events: none; z-index: 5;
|
|
2547
|
+
}
|
|
2548
|
+
.tl-bar:hover .tl-tip { opacity: 1; }
|
|
2549
|
+
.tl-seg { display: block; width: 100%; }
|
|
2550
|
+
.tl-seg.tl-output { background: var(--ok-text); }
|
|
2551
|
+
.tl-seg.tl-input { background: var(--accent); }
|
|
2552
|
+
.tl-seg.tl-cache-write { background: #3b6fb0; }
|
|
2553
|
+
.tl-seg.tl-cache-read { background: rgba(0,0,0,0.18); }
|
|
2554
|
+
.tl-empty { display: block; width: 100%; height: 3px; background: var(--border); }
|
|
2555
|
+
.tl-axis { display: flex; justify-content: space-between; margin-top: 6px; font-size: 0.7rem; color: var(--subtle); }
|
|
2556
|
+
|
|
2236
2557
|
.status-pill {
|
|
2237
2558
|
display: inline-flex; padding: 2px 9px; border-radius: 999px;
|
|
2238
2559
|
font-size: 0.7rem; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase;
|