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.
@@ -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: adapter.js listV2Sessions pre-computes the same verdict
219
- * inline (hasMainOrTeammate inside its existing full journal fold — unbounded,
220
- * vs this predicate's 8MB budget; intentional asymmetry) and then CONFIRMS a
221
- * discard through this predicate so the error→keep direction is shared.
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
@@ -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.body?.model);
323
+ const contextSize = getContextSizeForModel(entry);
324
324
  const cw = buildContextWindowEvent(usage, contextSize);
325
325
  if (cw) latestContextWindow = cw;
326
326
  }
@@ -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
- const result = listV2Logs(LOG_DIR, _projectName);
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
- result._unmigratedV1Count = mig.files;
43
- result._unmigratedV1Bytes = mig.totalBytes;
44
- result._v1FileCount = _projectName ? countListedV1Files(join(LOG_DIR, _projectName)) : 0;
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(result));
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;
@@ -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
- const lastModel = lastMainAgent ? getEffectiveModel(lastMainAgent) : null;
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 t="1771495664798" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6649" width="200" height="200"><path d="M731.062857 590.262857v318.317714h-148.589714V443.977143a146.285714 146.285714 0 0 1-146.285714 146.285714l-214.491429-0.036571v318.354285H73.142857V165.814857h148.553143v275.858286h180.882286l119.771428-275.858286h167.753143l-67.84 156.269714a255.524571 255.524571 0 0 1-106.678857 119.588572h66.925714a148.553143 148.553143 0 0 1 148.516572 148.589714z m120.758857-473.965714a99.035429 99.035429 0 0 1 0 198.070857h-99.035428V215.332571a99.035429 99.035429 0 0 1 99.035428-99.035428z" fill="currentColor" p-id="6650"></path></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,