cc-viewer 1.7.11 → 1.7.13
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/dist/assets/App-CtPaGTFc.js +2 -0
- package/dist/assets/{MdxEditorPanel-Br1zGDDJ.js → MdxEditorPanel-CdZjsgUe.js} +1 -1
- package/dist/assets/{Mobile-C_HxuKWt.js → Mobile-BV5o9yFd.js} +1 -1
- package/dist/assets/{ProxyStatsModal-Boxtr7ht.js → ProxyStatsModal-DGMd41pC.js} +1 -1
- package/dist/assets/index-DSTQIMmZ.js +2 -0
- package/dist/assets/seqResourceLoaders-DGyoo_5i.js +2 -0
- package/dist/assets/{seqResourceLoaders-0RXZfUKp.css → seqResourceLoaders-pBUn3c_a.css} +1 -1
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/server/lib/context-rules.js +38 -1
- package/server/lib/context-watcher.js +19 -4
- package/server/lib/log-management.js +57 -0
- package/server/lib/log-watcher.js +1 -1
- package/server/lib/v2/adapter.js +9 -110
- package/server/lib/v2/convert-manager.js +7 -1
- package/server/lib/v2/migrate-prompt.js +39 -1
- package/server/lib/v2/session-list.js +276 -0
- package/server/lib/v2/session-select.js +4 -4
- package/server/routes/events.js +1 -1
- package/server/routes/logs.js +50 -8
- package/src/utils/effectiveModel.js +4 -0
- package/src/utils/helpers.js +6 -3
- package/dist/assets/App-o7gt7lIy.js +0 -2
- package/dist/assets/index-gH_W4sVT.js +0 -2
- package/dist/assets/seqResourceLoaders-ByYE7WxI.js +0 -2
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
// Wire Format v2 — session-row cache for the log list (P0-A, 2026-07-31).
|
|
2
|
+
//
|
|
3
|
+
// `GET /api/local-logs` re-scans every session on every call: full journal
|
|
4
|
+
// fold, recursive dir walk, 256KB prompts head read, meta read — all sync,
|
|
5
|
+
// all blocking the event loop. With 100+ sessions this costs seconds.
|
|
6
|
+
//
|
|
7
|
+
// The journal is append-only (the single write path is Journal.writeReq/
|
|
8
|
+
// writeDone → AsyncWriteQueue.appendTo; creation is a one-time 'wx' sentinel;
|
|
9
|
+
// no truncate/rename/unlink anywhere). Content changes land in the journal
|
|
10
|
+
// LAST within a request (blobs → conversation → journal line → prompts), so
|
|
11
|
+
// (size, mtimeMs) is an exact freshness key for everything up to the journal
|
|
12
|
+
// line. prompts.jsonl is appended strictly AFTER the journal line (and is
|
|
13
|
+
// backfilled without a journal write on crash-resume), so the freshness key
|
|
14
|
+
// spans BOTH files: `journalSize:journalMtimeMs:promptsSize:promptsMtimeMs`.
|
|
15
|
+
// A repeated key means identical row inputs; any content change bumps one of
|
|
16
|
+
// the two stats.
|
|
17
|
+
//
|
|
18
|
+
// Cache shape: Map<projectDir, Map<sid, Row>> keyed by the project dir, with
|
|
19
|
+
// per-sid rows keyed by the freshness pair above. Insertion-order eviction
|
|
20
|
+
// caps the project map at MAX_PROJECTS. Rows are re-copied on return (see
|
|
21
|
+
// copyRow) so callers can't mutate the cache.
|
|
22
|
+
|
|
23
|
+
import { statSync, existsSync, readFileSync, openSync, readSync, closeSync } from 'node:fs';
|
|
24
|
+
import { join } from 'node:path';
|
|
25
|
+
import { listSessionIds, readJsonlTolerant } from './replay.js';
|
|
26
|
+
import { readPromptsHead, collectPromptsFromEvents } from '../user-prompt-extract.js';
|
|
27
|
+
import { isDiscardableSession } from './session-select.js';
|
|
28
|
+
import { isSupportedWireFormat, dirSizeSync } from './layout.js';
|
|
29
|
+
import { reportSwallowed } from '../error-report.js';
|
|
30
|
+
|
|
31
|
+
const MAX_PROJECTS = 32;
|
|
32
|
+
|
|
33
|
+
// ─── internal helpers (verbatim extraction from adapter.js) ──────────────────
|
|
34
|
+
|
|
35
|
+
/** Bounded head read: parse the FIRST JSONL line of a file without loading the
|
|
36
|
+
* whole thing (a main conversation's opening snapshot can be multi-MB; the
|
|
37
|
+
* list only wants a preview). Returns null on any shortfall. */
|
|
38
|
+
function readFirstJsonLine(path, budget = 256 * 1024) {
|
|
39
|
+
let fd;
|
|
40
|
+
try {
|
|
41
|
+
fd = openSync(path, 'r');
|
|
42
|
+
const buf = Buffer.alloc(budget);
|
|
43
|
+
const n = readSync(fd, buf, 0, budget, 0);
|
|
44
|
+
const head = buf.toString('utf-8', 0, n);
|
|
45
|
+
const nl = head.indexOf('\n');
|
|
46
|
+
if (nl <= 0) return null; // no complete first line inside the budget
|
|
47
|
+
return JSON.parse(head.slice(0, nl));
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
} finally {
|
|
51
|
+
if (fd !== undefined) { try { closeSync(fd); } catch { /* already closed */ } }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─── per-session summarization ───────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Compute the summary row for one session dir. This is the exact logic that
|
|
59
|
+
* used to live inline in listV2Sessions (adapter.js). All gates and error
|
|
60
|
+
* directions are preserved verbatim:
|
|
61
|
+
*
|
|
62
|
+
* - wireFormat gate (meta.json) → skip + reportSwallowed
|
|
63
|
+
* - wireFormat gate (journal sentinel) → skip + reportSwallowed
|
|
64
|
+
* - error→keep: journal stat failure skips the row, never caches
|
|
65
|
+
* - discard short-circuit: !leader && !hasMainOrTeammate && isDiscardableSession()
|
|
66
|
+
* - no "journal non-empty" guard — torn creation is tolerated as turns:0
|
|
67
|
+
*
|
|
68
|
+
* @returns {object|null} row {sid, dir, startTs, leader, turns, size, preview, discard}
|
|
69
|
+
* or null if the session should be skipped
|
|
70
|
+
*/
|
|
71
|
+
function summarizeSession(projectDir, sid) {
|
|
72
|
+
const dir = join(projectDir, 'sessions', sid);
|
|
73
|
+
// Journal existence is the cheapest gate — a session dir without one is
|
|
74
|
+
// either torn at creation or not a session at all.
|
|
75
|
+
if (!existsSync(join(dir, 'journal.jsonl'))) return null;
|
|
76
|
+
let meta = null;
|
|
77
|
+
try { meta = JSON.parse(readFileSync(join(dir, 'meta.json'), 'utf-8')); } catch { /* tolerated — journal is self-describing */ }
|
|
78
|
+
if (meta && meta.wireFormat != null && !isSupportedWireFormat(meta.wireFormat)) {
|
|
79
|
+
// Reader version gate (spec §14): don't list a session this build
|
|
80
|
+
// can't read — a garbage preview/turn-count is worse than absence.
|
|
81
|
+
reportSwallowed('v2-read.unsupported-wire-format', new Error(`${sid}: wireFormat=${meta.wireFormat}`));
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// turns = main requests that completed (journal two-phase fold). The
|
|
86
|
+
// journal sentinel is checked in the same pass: per §14 the per-file
|
|
87
|
+
// sentinel WINS over meta.json, and readSession/adapter refuse such a
|
|
88
|
+
// session — listing it would show a phantom row that opens empty.
|
|
89
|
+
const reqKind = new Map();
|
|
90
|
+
let turns = 0;
|
|
91
|
+
let sentinelVersion = null;
|
|
92
|
+
let hasMainOrTeammate = false;
|
|
93
|
+
for (const line of readJsonlTolerant(join(dir, 'journal.jsonl'))) {
|
|
94
|
+
if (line.ph === 'req') {
|
|
95
|
+
reqKind.set(line.seq, line.kind);
|
|
96
|
+
if (line.kind === 'main' || line.kind === 'teammate') hasMainOrTeammate = true;
|
|
97
|
+
}
|
|
98
|
+
else if (line.ph === 'done' && reqKind.get(line.seq) === 'main') {
|
|
99
|
+
turns++;
|
|
100
|
+
reqKind.delete(line.seq); // fold duplicate done lines (§14)
|
|
101
|
+
} else if (line.ph === 'meta' && typeof line.wireFormat === 'number' && !isSupportedWireFormat(line.wireFormat)) {
|
|
102
|
+
sentinelVersion = line.wireFormat;
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (sentinelVersion != null) {
|
|
107
|
+
reportSwallowed('v2-read.unsupported-wire-format', new Error(`${sid}: wireFormat=${sentinelVersion} (journal sentinel)`));
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// preview = ALL user prompts of the session, from the prompts.jsonl
|
|
112
|
+
// display cache (written by V2Writer / the converter; bounded head read
|
|
113
|
+
// so the list stays O(budget) per session). Sessions predating the
|
|
114
|
+
// cache fall back to the first epoch's first line — routed through the
|
|
115
|
+
// shared extractor so command/caveat chrome never leaks into the row.
|
|
116
|
+
let preview = readPromptsHead(join(dir, 'prompts.jsonl'));
|
|
117
|
+
if (preview.length === 0) {
|
|
118
|
+
const first = readFirstJsonLine(join(dir, 'conversations', 'main', 'e0.jsonl'));
|
|
119
|
+
if (first && Array.isArray(first.msgs)) {
|
|
120
|
+
preview = collectPromptsFromEvents([first]);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
sid,
|
|
126
|
+
dir,
|
|
127
|
+
startTs: (meta && meta.startTs) || '',
|
|
128
|
+
leader: (meta && meta.leader) || null,
|
|
129
|
+
turns,
|
|
130
|
+
size: dirSizeSync(dir),
|
|
131
|
+
preview,
|
|
132
|
+
// Discardable-session verdict. KEEP IN SYNC: session-select.js
|
|
133
|
+
// isDiscardableSession is the canonical rule; this fold pre-computes
|
|
134
|
+
// it for free over the FULL journal (the canonical scan is 8MB-
|
|
135
|
+
// budgeted — intentional asymmetry, a first main sits at the head).
|
|
136
|
+
// When the fold says discard, the canonical predicate CONFIRMS it:
|
|
137
|
+
// readJsonlTolerant swallows an I/O error (Windows EBUSY/EPERM lock)
|
|
138
|
+
// into zero lines, which must KEEP the session, not hide it — the
|
|
139
|
+
// canonical path carries that error→keep direction (ioErrorResult).
|
|
140
|
+
// Main-bearing sessions never pay the extra read; probe journals are
|
|
141
|
+
// ~3 lines.
|
|
142
|
+
discard: !(meta && meta.leader) && !hasMainOrTeammate && isDiscardableSession(dir, meta),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ─── row cache ───────────────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
/** Map<projectDir, Map<sid, {key, row}>> — insertion-order eviction. */
|
|
149
|
+
const _cache = new Map();
|
|
150
|
+
|
|
151
|
+
function _evictProjects() {
|
|
152
|
+
while (_cache.size > MAX_PROJECTS) {
|
|
153
|
+
const oldest = _cache.keys().next().value;
|
|
154
|
+
_cache.delete(oldest);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Summarize every session under LOG_DIR/<project>/ for the log list (spec §12).
|
|
160
|
+
* Deliberately cheap: journal lines only (small) + a bounded head read of the
|
|
161
|
+
* main conversation's first epoch for the preview — conversation bodies are
|
|
162
|
+
* never loaded. Teammate linkage is surfaced via `leader` so the caller can
|
|
163
|
+
* fold those sessions into their leader's view instead of double-listing.
|
|
164
|
+
*
|
|
165
|
+
* Cached: repeat calls with unchanged journals return instantly (~1-3ms for
|
|
166
|
+
* 100 sessions, vs. seconds for a full rescan).
|
|
167
|
+
*
|
|
168
|
+
* @returns {Array<{sid, dir, startTs, leader, turns, size, preview, discard}>}
|
|
169
|
+
*/
|
|
170
|
+
export function listV2Sessions(projectDir) {
|
|
171
|
+
const sids = listSessionIds(projectDir);
|
|
172
|
+
const sidSet = new Set(sids); // O(1) prune lookups — sids.includes() per cached sid is O(N²)
|
|
173
|
+
let projectCache = _cache.get(projectDir);
|
|
174
|
+
if (!projectCache) {
|
|
175
|
+
projectCache = new Map();
|
|
176
|
+
_cache.set(projectDir, projectCache);
|
|
177
|
+
_evictProjects();
|
|
178
|
+
}
|
|
179
|
+
// Prune deleted/migrated sessions: a sid no longer in the readdir is gone.
|
|
180
|
+
for (const sid of projectCache.keys()) {
|
|
181
|
+
if (!sidSet.has(sid)) projectCache.delete(sid);
|
|
182
|
+
}
|
|
183
|
+
const out = [];
|
|
184
|
+
for (const sid of sids) {
|
|
185
|
+
try {
|
|
186
|
+
const dir = join(projectDir, 'sessions', sid);
|
|
187
|
+
let key;
|
|
188
|
+
try {
|
|
189
|
+
const jst = statSync(join(dir, 'journal.jsonl'));
|
|
190
|
+
// Freshness key spans journal AND prompts.jsonl: the display cache is
|
|
191
|
+
// appended strictly AFTER the journal line (v2-writer.js §5), so a list
|
|
192
|
+
// call landing between the two queue drains — or a crash-resume
|
|
193
|
+
// backfill that appends prompts without any journal write — must not
|
|
194
|
+
// freeze a pre-prompts preview under an unchanged journal key. Absent
|
|
195
|
+
// prompts use a placeholder: their first write always follows a journal
|
|
196
|
+
// append, except exactly that backfill path, which this key detects.
|
|
197
|
+
let pKey = '-:-';
|
|
198
|
+
try {
|
|
199
|
+
const pst = statSync(join(dir, 'prompts.jsonl'));
|
|
200
|
+
pKey = `${pst.size}:${pst.mtimeMs}`;
|
|
201
|
+
} catch { /* prompts.jsonl not written yet */ }
|
|
202
|
+
key = `${jst.size}:${jst.mtimeMs}:${pKey}`;
|
|
203
|
+
} catch {
|
|
204
|
+
continue; // journal unreadable → skip row, never cache
|
|
205
|
+
}
|
|
206
|
+
const cached = projectCache.get(sid);
|
|
207
|
+
if (cached && cached.key === key) {
|
|
208
|
+
out.push(copyRow(cached.row)); // defensive copy — callers must not mutate cache
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const row = summarizeSession(projectDir, sid);
|
|
212
|
+
if (row) {
|
|
213
|
+
projectCache.set(sid, { key, row });
|
|
214
|
+
out.push(copyRow(row));
|
|
215
|
+
}
|
|
216
|
+
// row === null → session skipped (wireFormat gate, sentinel, etc.) — don't cache
|
|
217
|
+
} catch { /* one unreadable session must not break the list */ }
|
|
218
|
+
}
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Copy a row for delivery: nested `preview` (array) and `leader` (object) are
|
|
223
|
+
* re-copied so a caller mutating what it received can never poison the cache
|
|
224
|
+
* entry shared by every viewer of the project. */
|
|
225
|
+
function copyRow(row) {
|
|
226
|
+
return {
|
|
227
|
+
...row,
|
|
228
|
+
...(row.leader ? { leader: { ...row.leader } } : {}),
|
|
229
|
+
preview: [...row.preview],
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Summarize ONE session for the paginated list (server-side paging, 2026-07-31).
|
|
235
|
+
* Same row + same cache as listV2Sessions: a key hit returns the cached row
|
|
236
|
+
* without re-folding the journal; a miss computes and stores it. The caller
|
|
237
|
+
* (listV2LogsPage) has already decided this sid is on the current page, so only
|
|
238
|
+
* these sessions ever pay the summarize cost.
|
|
239
|
+
*
|
|
240
|
+
* @returns {object|null} row {sid, dir, startTs, leader, turns, size, preview, discard}
|
|
241
|
+
* or null if the session should be skipped (wireFormat gate, sentinel, no journal)
|
|
242
|
+
*/
|
|
243
|
+
export function summarizeSessionPage(projectDir, sid) {
|
|
244
|
+
const dir = join(projectDir, 'sessions', sid);
|
|
245
|
+
let key;
|
|
246
|
+
try {
|
|
247
|
+
const jst = statSync(join(dir, 'journal.jsonl'));
|
|
248
|
+
let pKey = '-:-';
|
|
249
|
+
try {
|
|
250
|
+
const pst = statSync(join(dir, 'prompts.jsonl'));
|
|
251
|
+
pKey = `${pst.size}:${pst.mtimeMs}`;
|
|
252
|
+
} catch { /* prompts.jsonl not written yet */ }
|
|
253
|
+
key = `${jst.size}:${jst.mtimeMs}:${pKey}`;
|
|
254
|
+
} catch {
|
|
255
|
+
return null; // journal unreadable → skip, never cache
|
|
256
|
+
}
|
|
257
|
+
let projectCache = _cache.get(projectDir);
|
|
258
|
+
if (!projectCache) {
|
|
259
|
+
projectCache = new Map();
|
|
260
|
+
_cache.set(projectDir, projectCache);
|
|
261
|
+
_evictProjects();
|
|
262
|
+
}
|
|
263
|
+
const cached = projectCache.get(sid);
|
|
264
|
+
if (cached && cached.key === key) return copyRow(cached.row);
|
|
265
|
+
const row = summarizeSession(projectDir, sid);
|
|
266
|
+
if (row) {
|
|
267
|
+
projectCache.set(sid, { key, row });
|
|
268
|
+
return copyRow(row);
|
|
269
|
+
}
|
|
270
|
+
return null; // skipped (wireFormat gate, sentinel, etc.) — don't cache
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/** Test hook: drop all cached rows. */
|
|
274
|
+
export function _resetForTest() {
|
|
275
|
+
_cache.clear();
|
|
276
|
+
}
|
|
@@ -215,10 +215,10 @@ export function sessionHasMainOrTeammateReq(dir) {
|
|
|
215
215
|
* the moment a dir gains its first main req, the predicate flips and every
|
|
216
216
|
* surface picks it up on its next scan/poll.
|
|
217
217
|
*
|
|
218
|
-
* KEEP IN SYNC:
|
|
219
|
-
* inline (hasMainOrTeammate inside its
|
|
220
|
-
*
|
|
221
|
-
*
|
|
218
|
+
* KEEP IN SYNC: session-list.js summarizeSession pre-computes the same verdict
|
|
219
|
+
* inline (hasMainOrTeammate inside its full journal fold — unbounded, vs this
|
|
220
|
+
* predicate's 8MB budget; intentional asymmetry) and then CONFIRMS a discard
|
|
221
|
+
* through this predicate so the error→keep direction is shared.
|
|
222
222
|
* Change the rule here and there together.
|
|
223
223
|
*
|
|
224
224
|
* @param {string} dir - absolute session dir
|
package/server/routes/events.js
CHANGED
|
@@ -320,7 +320,7 @@ async function events(req, res, parsedUrl, isLocal, deps) {
|
|
|
320
320
|
if (!latestContextWindow) {
|
|
321
321
|
const usage = entry.response?.body?.usage;
|
|
322
322
|
if (usage) {
|
|
323
|
-
const contextSize = getContextSizeForModel(entry
|
|
323
|
+
const contextSize = getContextSizeForModel(entry);
|
|
324
324
|
const cw = buildContextWindowEvent(usage, contextSize);
|
|
325
325
|
if (cw) latestContextWindow = cw;
|
|
326
326
|
}
|
package/server/routes/logs.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
// Local log management routes (moved verbatim from server.js handleRequest).
|
|
2
|
-
import { existsSync, realpathSync, statSync, createReadStream, mkdtempSync, rmSync } from 'node:fs';
|
|
2
|
+
import { existsSync, realpathSync, statSync, createReadStream, mkdtempSync, rmSync, readdirSync } from 'node:fs';
|
|
3
3
|
import { join, basename } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
5
|
import AdmZip from 'adm-zip';
|
|
6
6
|
import { LOG_DIR } from '../../findcc.js';
|
|
7
7
|
import { _projectName, _v2Writer } from '../interceptor.js';
|
|
8
|
-
import { listV2Logs, listLocalLogs, countListedV1Files, deleteLogFiles, validateLogPath } from '../lib/log-management.js';
|
|
8
|
+
import { listV2Logs, listV2LogsPage, listLocalLogs, countListedV1Files, deleteLogFiles, validateLogPath } from '../lib/log-management.js';
|
|
9
9
|
import { countLogEntries, streamRawEntriesAsync, readTailEntries } from '../lib/log-stream.js';
|
|
10
10
|
import { sseHead, sseWrite, wireEnd } from '../lib/wire-compress.js';
|
|
11
|
-
import { dirSizeSync } from '../lib/v2/layout.js';
|
|
11
|
+
import { dirSizeSync, sanitizePathComponent } from '../lib/v2/layout.js';
|
|
12
12
|
import { extractV2Zip } from '../lib/log-zip.js';
|
|
13
13
|
import { startConvert, stopConvert, convertStatus } from '../lib/v2/convert-manager.js';
|
|
14
14
|
import { migrationStatus } from '../lib/v2/migrate-prompt.js';
|
|
@@ -32,18 +32,60 @@ async function localLogs(req, res, parsedUrl, isLocal, deps) {
|
|
|
32
32
|
res.end(JSON.stringify(v1));
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
|
-
|
|
35
|
+
// Server-side pagination (2026-07-31): ?page=&pageSize= switches the v2
|
|
36
|
+
// list to a per-project page — only the requested page's sessions are
|
|
37
|
+
// summarized. Response shape becomes {items, total, page, pageSize} plus
|
|
38
|
+
// the same _-prefixed side signals below. No params = legacy grouped shape.
|
|
39
|
+
const pageParam = parsedUrl?.searchParams?.get('page');
|
|
40
|
+
let payload;
|
|
41
|
+
if (pageParam != null) {
|
|
42
|
+
// Optional ?project= views another project's logs (the modal's project
|
|
43
|
+
// switcher). Strict-compare against sanitizePathComponent (same pattern
|
|
44
|
+
// as parseV2Ref) so '..'/ separators can't traverse out of LOG_DIR;
|
|
45
|
+
// absent/empty falls back to the active project.
|
|
46
|
+
const rawProject = parsedUrl.searchParams.get('project');
|
|
47
|
+
let target = _projectName;
|
|
48
|
+
if (rawProject) {
|
|
49
|
+
if (rawProject !== sanitizePathComponent(rawProject)) {
|
|
50
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
51
|
+
res.end(JSON.stringify({ error: 'Invalid project name' }));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
target = rawProject;
|
|
55
|
+
}
|
|
56
|
+
const page = Math.max(1, parseInt(pageParam, 10) || 1);
|
|
57
|
+
const pageSize = Math.min(200, Math.max(1, parseInt(parsedUrl.searchParams.get('pageSize'), 10) || 50));
|
|
58
|
+
payload = listV2LogsPage(LOG_DIR, target, { page, pageSize });
|
|
59
|
+
// _currentProject must stay the ACTIVE project (_projectName), not the
|
|
60
|
+
// viewed one (listV2LogsPage sets it to `target`): the frontend's global
|
|
61
|
+
// currentProject drives the sidebar / migration counts / v1 banner, and
|
|
62
|
+
// the viewed project is carried separately by its own logViewProject
|
|
63
|
+
// state. Overwriting it here would let "viewing project A" leak into
|
|
64
|
+
// global state. _viewedProject tells the client which project this page
|
|
65
|
+
// actually lists.
|
|
66
|
+
payload._currentProject = _projectName || '';
|
|
67
|
+
payload._viewedProject = target || '';
|
|
68
|
+
// Project list for the modal's switcher dropdown. LOG_DIR's top level
|
|
69
|
+
// holds one dir per project (recycle dirs live INSIDE each project, so
|
|
70
|
+
// nothing to filter here) — but skip dot-prefixed hidden dirs.
|
|
71
|
+
try {
|
|
72
|
+
payload._allProjects = readdirSync(LOG_DIR, { withFileTypes: true })
|
|
73
|
+
.filter(e => e.isDirectory() && !e.name.startsWith('.')).map(e => e.name).sort();
|
|
74
|
+
} catch { payload._allProjects = []; }
|
|
75
|
+
} else {
|
|
76
|
+
payload = listV2Logs(LOG_DIR, _projectName);
|
|
77
|
+
}
|
|
36
78
|
// Legacy v1 files are not in this list — surface two distinct signals:
|
|
37
79
|
// - _v1FileCount: v1 files ON DISK (gates the v1-view entry link; the
|
|
38
80
|
// converter never deletes sources, so this outlives a finished migration)
|
|
39
81
|
// - _unmigratedV1Count/Bytes: files still AWAITING migration (gates the
|
|
40
82
|
// migrate button + hint inside the v1 view and the startup prompt)
|
|
41
83
|
const mig = migrationStatus(LOG_DIR, _projectName || '');
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
84
|
+
payload._unmigratedV1Count = mig.files;
|
|
85
|
+
payload._unmigratedV1Bytes = mig.totalBytes;
|
|
86
|
+
payload._v1FileCount = _projectName ? countListedV1Files(join(LOG_DIR, _projectName)) : 0;
|
|
45
87
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
46
|
-
res.end(JSON.stringify(
|
|
88
|
+
res.end(JSON.stringify(payload));
|
|
47
89
|
} catch (err) {
|
|
48
90
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
49
91
|
res.end(JSON.stringify({ error: err.message }));
|
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
* server-reported model in `response.body.model` (authoritative under proxy
|
|
8
8
|
* hot-switch) over the client-supplied `body.model`. Returns null when both
|
|
9
9
|
* are missing — callers should fall back to a sensible default.
|
|
10
|
+
*
|
|
11
|
+
* KEEP IN SYNC: server/lib/context-watcher.js getContextSizeForModel reuses
|
|
12
|
+
* this precedence for its entry path — changing the priority here must be
|
|
13
|
+
* mirrored there (and vice versa).
|
|
10
14
|
*/
|
|
11
15
|
export function getEffectiveModel(request) {
|
|
12
16
|
return request?.response?.body?.model || request?.body?.model || null;
|
package/src/utils/helpers.js
CHANGED
|
@@ -20,8 +20,9 @@ export {
|
|
|
20
20
|
sumCacheCreationTokens,
|
|
21
21
|
sumUsageInputTokens,
|
|
22
22
|
sumUsageContextTokens,
|
|
23
|
+
getCalibrationModel,
|
|
23
24
|
} from '../../server/lib/context-rules.js';
|
|
24
|
-
import { classifyContextWindow, adaptContextWindow } from '../../server/lib/context-rules.js';
|
|
25
|
+
import { classifyContextWindow, adaptContextWindow, getCalibrationModel } from '../../server/lib/context-rules.js';
|
|
25
26
|
|
|
26
27
|
// getEffectiveModel moved to ./effectiveModel.js (pure, node-testable — sessionMerge/sessionManager
|
|
27
28
|
// import it without helpers' Vite-only svg imports); re-exported here to keep import paths stable.
|
|
@@ -57,7 +58,9 @@ const CALIBRATION_TOKEN_MAP = {
|
|
|
57
58
|
export function resolveCalibrationTokens(calibrationModel, lastMainAgent, projectModelHint = null) {
|
|
58
59
|
const direct = CALIBRATION_TOKEN_MAP[calibrationModel];
|
|
59
60
|
if (direct) return direct;
|
|
60
|
-
|
|
61
|
+
// 校准用 getCalibrationModel:请求名带显式 [Nk]/[Nm] 后缀时优先(用户热切换配置的
|
|
62
|
+
// 1M 意图),不被上游响应归一化(如 k3[1m]→裸 k3)覆盖;其余回退 response-first。
|
|
63
|
+
const lastModel = lastMainAgent ? getCalibrationModel(lastMainAgent) : null;
|
|
61
64
|
// 优先用真实 mainAgent 信号;haiku 一律视为 init ping 噪声,跳过
|
|
62
65
|
if (typeof lastModel === 'string' && lastModel && !/haiku/i.test(lastModel)) {
|
|
63
66
|
return classifyContextWindow(lastModel);
|
|
@@ -334,7 +337,7 @@ const MODEL_PROVIDERS = [
|
|
|
334
337
|
match: /kimi|moonshot|^k3$/i,
|
|
335
338
|
name: 'Kimi',
|
|
336
339
|
color: 'var(--bg-model-avatar)',
|
|
337
|
-
svg: '<svg
|
|
340
|
+
svg: '<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M932.096 0a82.048 82.048 0 1 1 0 164.096h-72.352a9.568 9.568 0 0 1-9.664-9.632V82.016A82.048 82.048 0 0 1 932.096 0z" fill="#1783FF"></path><path d="M472.064 477.856l309.76-307.2c5.888-5.792 2.56-17.472-4.96-17.472h-166.72a7.008 7.008 0 0 0-5.056 2.112L271.456 486.24c-5.12 5.12-12.8 0.576-12.8-7.68V162.976c0-5.376-3.584-9.792-7.936-9.792H135.936c-4.352 0-7.936 4.416-7.936 9.792V843.52c0 5.44 3.584 9.824 7.936 9.824h114.784c4.352 0 7.936-4.288 7.936-9.824v-138.656c0-2.912 1.024-5.728 2.88-7.616l103.424-102.656a6.72 6.72 0 0 1 8.8-0.928l276.64 203.616a328.64 328.64 0 0 0 147.296 54.688c4.608 0.512 8.544-4 8.544-9.824V711.68c0-4.96-2.912-9.056-7.008-9.632a214.528 214.528 0 0 1-86.432-34.496l-239.456-173.472c-5.024-3.328-5.632-11.936-1.184-16.224h-0.096z" fill="currentColor"></path></svg>',
|
|
338
341
|
},
|
|
339
342
|
{
|
|
340
343
|
match: /glm|chatglm/i,
|