coding-tool-x 3.6.2 → 3.6.3

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,562 @@
1
+ const http = require('http');
2
+ const https = require('https');
3
+ const crypto = require('crypto');
4
+ const { URL } = require('url');
5
+ const { loadUIConfig } = require('./ui-config');
6
+
7
+ const CACHE_TTL_MS = 5 * 60 * 1000;
8
+ const REQUEST_TIMEOUT_MS = 8000;
9
+ const MAX_RESPONSE_BYTES = 1024 * 1024;
10
+ const QUOTA_UNIT = 500000;
11
+ const VELOERA_QUOTA_UNIT = 1000000;
12
+ const VALID_SOURCES = new Set(['claude', 'codex', 'gemini', 'opencode']);
13
+
14
+ const balanceCache = new Map();
15
+
16
+ function nowIso(now = Date.now()) {
17
+ return new Date(now).toISOString();
18
+ }
19
+
20
+ function stripBearer(value) {
21
+ const token = String(value || '').trim();
22
+ return token.toLowerCase().startsWith('bearer ') ? token.slice(7).trim() : token;
23
+ }
24
+
25
+ function buildAuthHeaders(token, extra = {}) {
26
+ return {
27
+ Accept: 'application/json',
28
+ Authorization: `Bearer ${stripBearer(token)}`,
29
+ ...extra
30
+ };
31
+ }
32
+
33
+ function parseFiniteNumber(value) {
34
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
35
+ if (typeof value === 'string') {
36
+ const parsed = Number(value.trim());
37
+ if (Number.isFinite(parsed)) return parsed;
38
+ }
39
+ return null;
40
+ }
41
+
42
+ function roundMoney(value) {
43
+ if (!Number.isFinite(value)) return value;
44
+ return Math.round(value * 1000000) / 1000000;
45
+ }
46
+
47
+ function formatMoney(value) {
48
+ if (!Number.isFinite(value)) return '';
49
+ return `$${value.toFixed(2)}`;
50
+ }
51
+
52
+ function makeHiddenSnapshot(platform = null, { stale = false, updatedAt = nowIso() } = {}) {
53
+ return {
54
+ visible: false,
55
+ platform,
56
+ updatedAt,
57
+ stale
58
+ };
59
+ }
60
+
61
+ function makeVisibleSnapshot(platform, input, { stale = false, updatedAt = nowIso() } = {}) {
62
+ const remaining = parseFiniteNumber(input.remaining);
63
+ const monthlyRemaining = parseFiniteNumber(input.monthlyRemaining);
64
+ if (remaining == null && monthlyRemaining == null) {
65
+ return makeHiddenSnapshot(platform, { stale, updatedAt });
66
+ }
67
+
68
+ const labelValue = remaining != null ? remaining : monthlyRemaining;
69
+ const labelPrefix = remaining != null ? '余额' : '月余';
70
+ const snapshot = {
71
+ visible: true,
72
+ platform,
73
+ label: `${labelPrefix} ${formatMoney(labelValue)}`,
74
+ updatedAt,
75
+ stale
76
+ };
77
+
78
+ if (remaining != null) snapshot.remaining = roundMoney(remaining);
79
+ const used = parseFiniteNumber(input.used);
80
+ if (used != null) snapshot.used = roundMoney(used);
81
+ const total = parseFiniteNumber(input.total);
82
+ if (total != null) snapshot.total = roundMoney(total);
83
+ if (monthlyRemaining != null) snapshot.monthlyRemaining = roundMoney(monthlyRemaining);
84
+ return snapshot;
85
+ }
86
+
87
+ function normalizeBaseUrl(value) {
88
+ const raw = String(value || '').trim();
89
+ if (!raw) return '';
90
+
91
+ let parsed;
92
+ try {
93
+ parsed = new URL(raw);
94
+ } catch {
95
+ return '';
96
+ }
97
+
98
+ parsed.hash = '';
99
+ parsed.search = '';
100
+ let pathname = parsed.pathname || '/';
101
+ pathname = pathname.replace(/\/+$/, '') || '/';
102
+ const lower = pathname.toLowerCase();
103
+ const suffixes = [
104
+ '/v1/chat/completions',
105
+ '/v1/responses',
106
+ '/v1/messages',
107
+ '/v1beta/models',
108
+ '/chat/completions',
109
+ '/responses',
110
+ '/messages',
111
+ '/v1beta',
112
+ '/v1'
113
+ ];
114
+
115
+ for (const suffix of suffixes) {
116
+ if (lower === suffix || lower.endsWith(suffix)) {
117
+ pathname = pathname.slice(0, pathname.length - suffix.length) || '/';
118
+ break;
119
+ }
120
+ }
121
+
122
+ parsed.pathname = pathname.replace(/\/+$/, '') || '/';
123
+ return parsed.toString().replace(/\/$/, '');
124
+ }
125
+
126
+ function buildBaseCandidates(baseUrl) {
127
+ const normalized = normalizeBaseUrl(baseUrl);
128
+ if (!normalized) return [];
129
+
130
+ const candidates = [normalized];
131
+ try {
132
+ const parsed = new URL(normalized);
133
+ const origin = parsed.origin;
134
+ if (origin && !candidates.includes(origin)) {
135
+ candidates.push(origin);
136
+ }
137
+ } catch {}
138
+ return candidates;
139
+ }
140
+
141
+ function joinUrl(baseUrl, endpoint) {
142
+ const base = String(baseUrl || '').replace(/\/+$/, '');
143
+ const path = String(endpoint || '').startsWith('/') ? endpoint : `/${endpoint}`;
144
+ return `${base}${path}`;
145
+ }
146
+
147
+ function requestJson(url, { headers = {}, timeoutMs = REQUEST_TIMEOUT_MS } = {}) {
148
+ return new Promise((resolve, reject) => {
149
+ let parsed;
150
+ try {
151
+ parsed = new URL(url);
152
+ } catch (error) {
153
+ reject(error);
154
+ return;
155
+ }
156
+
157
+ const client = parsed.protocol === 'http:' ? http : https;
158
+ const req = client.request({
159
+ protocol: parsed.protocol,
160
+ hostname: parsed.hostname,
161
+ port: parsed.port,
162
+ path: `${parsed.pathname}${parsed.search}`,
163
+ method: 'GET',
164
+ headers
165
+ }, (res) => {
166
+ const chunks = [];
167
+ let total = 0;
168
+
169
+ res.on('data', (chunk) => {
170
+ total += chunk.length;
171
+ if (total > MAX_RESPONSE_BYTES) {
172
+ req.destroy(new Error('response too large'));
173
+ return;
174
+ }
175
+ chunks.push(chunk);
176
+ });
177
+
178
+ res.on('end', () => {
179
+ const raw = Buffer.concat(chunks).toString('utf8');
180
+ if (res.statusCode < 200 || res.statusCode >= 300) {
181
+ const error = new Error(`HTTP ${res.statusCode}`);
182
+ error.statusCode = res.statusCode;
183
+ reject(error);
184
+ return;
185
+ }
186
+ try {
187
+ resolve(raw ? JSON.parse(raw) : null);
188
+ } catch (error) {
189
+ reject(error);
190
+ }
191
+ });
192
+ });
193
+
194
+ req.setTimeout(timeoutMs, () => {
195
+ req.destroy(new Error('request timeout'));
196
+ });
197
+ req.on('error', reject);
198
+ req.end();
199
+ });
200
+ }
201
+
202
+ async function tryRequestJson(url, options) {
203
+ try {
204
+ return await requestJson(url, options);
205
+ } catch {
206
+ return null;
207
+ }
208
+ }
209
+
210
+ function detectPlatformByUrlHint(baseUrl) {
211
+ const value = String(baseUrl || '').toLowerCase();
212
+ if (value.includes('sub2api')) return 'sub2api';
213
+ if (value.includes('anyrouter')) return 'anyrouter';
214
+ if (value.includes('donehub') || value.includes('done-hub')) return 'done-hub';
215
+ if (value.includes('onehub') || value.includes('one-hub')) return 'one-hub';
216
+ if (value.includes('veloera')) return 'veloera';
217
+ if (value.includes('oneapi') || value.includes('one-api')) return 'one-api';
218
+ if (value.includes('newapi') || value.includes('new-api')) return 'new-api';
219
+ return null;
220
+ }
221
+
222
+ function resolveStatusPlatform(payload, hintedPlatform = null) {
223
+ if (!payload || payload.success !== true) return null;
224
+ const systemName = typeof payload?.data?.system_name === 'string'
225
+ ? payload.data.system_name.toLowerCase()
226
+ : '';
227
+ const version = typeof payload?.data?.version === 'string'
228
+ ? payload.data.version.toLowerCase()
229
+ : '';
230
+
231
+ if (hintedPlatform === 'anyrouter') return 'anyrouter';
232
+ if (hintedPlatform === 'done-hub') return 'done-hub';
233
+ if (hintedPlatform === 'one-hub') return 'one-hub';
234
+ if (systemName.includes('veloera') || version.includes('veloera')) return 'veloera';
235
+ if (systemName) return hintedPlatform === 'one-api' ? 'one-api' : 'new-api';
236
+ return hintedPlatform || 'one-api';
237
+ }
238
+
239
+ function extractSub2ApiData(payload) {
240
+ if (!payload || typeof payload !== 'object') return null;
241
+ if (typeof payload.code === 'number') {
242
+ if (payload.code !== 0 || payload.data === undefined) return null;
243
+ return payload.data;
244
+ }
245
+ if (payload.data && typeof payload.data === 'object') return payload.data;
246
+ return payload;
247
+ }
248
+
249
+ async function detectPlatform(baseUrl, token, hintedPlatform = null) {
250
+ const headers = buildAuthHeaders(token);
251
+
252
+ if (hintedPlatform === 'sub2api') {
253
+ return 'sub2api';
254
+ }
255
+
256
+ const status = await tryRequestJson(joinUrl(baseUrl, '/api/status'), { headers: { Accept: 'application/json' } });
257
+ const statusPlatform = resolveStatusPlatform(status, hintedPlatform);
258
+ if (statusPlatform) return statusPlatform;
259
+
260
+ const sub2Payload = await tryRequestJson(joinUrl(baseUrl, '/api/v1/auth/me'), { headers });
261
+ const sub2Data = extractSub2ApiData(sub2Payload);
262
+ if (sub2Data && Object.prototype.hasOwnProperty.call(sub2Data, 'balance')) {
263
+ return 'sub2api';
264
+ }
265
+
266
+ return hintedPlatform;
267
+ }
268
+
269
+ function extractUserSelfData(payload) {
270
+ if (!payload || typeof payload !== 'object') return null;
271
+ const data = payload.data && typeof payload.data === 'object' ? payload.data : payload;
272
+ if (!data || typeof data !== 'object') return null;
273
+ if (!Object.prototype.hasOwnProperty.call(data, 'quota')) return null;
274
+ return data;
275
+ }
276
+
277
+ function buildHubBalanceSnapshot(platform, payload) {
278
+ const data = extractUserSelfData(payload);
279
+ if (!data) return makeHiddenSnapshot(platform);
280
+
281
+ const unit = platform === 'veloera' ? VELOERA_QUOTA_UNIT : QUOTA_UNIT;
282
+ const quotaRaw = parseFiniteNumber(data.quota);
283
+ const usedRaw = parseFiniteNumber(data.used_quota);
284
+ if (quotaRaw == null) return makeHiddenSnapshot(platform);
285
+
286
+ const quota = quotaRaw / unit;
287
+ const used = (usedRaw || 0) / unit;
288
+ let remaining;
289
+ let total;
290
+
291
+ if (platform === 'new-api' || platform === 'anyrouter' || platform === 'done-hub') {
292
+ remaining = quota;
293
+ total = quota + used;
294
+ } else {
295
+ remaining = quota - used;
296
+ total = quota;
297
+ }
298
+
299
+ return makeVisibleSnapshot(platform, {
300
+ remaining,
301
+ used,
302
+ total
303
+ });
304
+ }
305
+
306
+ function parseSubscriptionItems(payload) {
307
+ const data = extractSub2ApiData(payload);
308
+ const candidates = [
309
+ data?.subscriptions,
310
+ data?.items,
311
+ data?.data,
312
+ payload?.subscriptions,
313
+ payload?.items,
314
+ Array.isArray(data) ? data : null
315
+ ];
316
+ for (const candidate of candidates) {
317
+ if (Array.isArray(candidate)) return candidate;
318
+ }
319
+ return [];
320
+ }
321
+
322
+ function readMoneyField(item, ...keys) {
323
+ for (const key of keys) {
324
+ const value = parseFiniteNumber(item?.[key]);
325
+ if (value != null) return value;
326
+ }
327
+ return null;
328
+ }
329
+
330
+ async function fetchSub2ApiSubscriptionSummary(baseUrl, headers) {
331
+ const endpoints = ['/api/v1/subscriptions/summary', '/api/v1/subscriptions/active'];
332
+ for (const endpoint of endpoints) {
333
+ const payload = await tryRequestJson(joinUrl(baseUrl, endpoint), { headers });
334
+ if (!payload) continue;
335
+ const items = parseSubscriptionItems(payload);
336
+ if (!items.length) continue;
337
+
338
+ let monthlyRemaining = 0;
339
+ let hasMonthlyLimit = false;
340
+ for (const item of items) {
341
+ const used = readMoneyField(item, 'monthlyUsedUsd', 'monthly_used_usd', 'monthly_used', 'used_monthly_usd') || 0;
342
+ const limit = readMoneyField(item, 'monthlyLimitUsd', 'monthly_limit_usd', 'monthly_limit', 'limit_monthly_usd');
343
+ if (limit == null) continue;
344
+ hasMonthlyLimit = true;
345
+ monthlyRemaining += Math.max(0, limit - used);
346
+ }
347
+
348
+ if (hasMonthlyLimit) {
349
+ return { monthlyRemaining };
350
+ }
351
+ }
352
+ return {};
353
+ }
354
+
355
+ async function fetchSub2ApiBalance(baseUrl, token) {
356
+ const headers = buildAuthHeaders(token);
357
+ const payload = await requestJson(joinUrl(baseUrl, '/api/v1/auth/me'), { headers });
358
+ const data = extractSub2ApiData(payload);
359
+ const balance = parseFiniteNumber(data?.balance);
360
+ if (balance == null) return makeHiddenSnapshot('sub2api');
361
+
362
+ const summary = await fetchSub2ApiSubscriptionSummary(baseUrl, headers);
363
+ return makeVisibleSnapshot('sub2api', {
364
+ remaining: balance,
365
+ used: 0,
366
+ total: balance,
367
+ monthlyRemaining: summary.monthlyRemaining
368
+ });
369
+ }
370
+
371
+ async function fetchHubBalance(baseUrl, token, platform) {
372
+ const headers = buildAuthHeaders(token);
373
+ const payload = await requestJson(joinUrl(baseUrl, '/api/user/self'), { headers });
374
+ return buildHubBalanceSnapshot(platform, payload);
375
+ }
376
+
377
+ async function fetchBalanceFromBase(baseUrl, channel) {
378
+ const token = stripBearer(channel.apiKey || channel.token || channel.authToken || '');
379
+ if (!baseUrl || !token) return makeHiddenSnapshot();
380
+
381
+ const hintedPlatform = detectPlatformByUrlHint(channel.baseUrl || baseUrl);
382
+ const platform = await detectPlatform(baseUrl, token, hintedPlatform);
383
+ if (!platform) return makeHiddenSnapshot();
384
+
385
+ if (platform === 'sub2api') {
386
+ return fetchSub2ApiBalance(baseUrl, token);
387
+ }
388
+
389
+ if (['new-api', 'one-api', 'one-hub', 'done-hub', 'veloera', 'anyrouter'].includes(platform)) {
390
+ return fetchHubBalance(baseUrl, token, platform);
391
+ }
392
+
393
+ return makeHiddenSnapshot(platform);
394
+ }
395
+
396
+ function buildCacheKey(source, channel) {
397
+ const token = stripBearer(channel.apiKey || channel.token || channel.authToken || '');
398
+ const tokenHash = token
399
+ ? crypto.createHash('sha256').update(token).digest('hex').slice(0, 16)
400
+ : 'no-token';
401
+ return [
402
+ source,
403
+ channel.id || channel.name || 'unknown',
404
+ normalizeBaseUrl(channel.baseUrl || ''),
405
+ tokenHash
406
+ ].join(':');
407
+ }
408
+
409
+ async function refreshChannelBalanceSnapshot(source, channel, options = {}) {
410
+ const force = options.force === true;
411
+ const currentTime = options.now || Date.now();
412
+ const key = buildCacheKey(source, channel);
413
+ const cached = balanceCache.get(key);
414
+
415
+ if (!force && cached && cached.expiresAt > currentTime) {
416
+ return cached.snapshot;
417
+ }
418
+
419
+ const bases = buildBaseCandidates(channel.baseUrl);
420
+ if (!bases.length || !stripBearer(channel.apiKey || channel.token || channel.authToken || '')) {
421
+ return makeHiddenSnapshot(null, { updatedAt: nowIso(currentTime) });
422
+ }
423
+
424
+ let lastSnapshot = null;
425
+ for (const baseUrl of bases) {
426
+ try {
427
+ const snapshot = await fetchBalanceFromBase(baseUrl, channel);
428
+ if (snapshot?.visible) {
429
+ balanceCache.set(key, {
430
+ snapshot,
431
+ expiresAt: currentTime + CACHE_TTL_MS
432
+ });
433
+ return snapshot;
434
+ }
435
+ lastSnapshot = snapshot || lastSnapshot;
436
+ } catch {
437
+ // Try the next normalized base candidate.
438
+ }
439
+ }
440
+
441
+ if (cached?.snapshot?.visible) {
442
+ const staleSnapshot = {
443
+ ...cached.snapshot,
444
+ stale: true
445
+ };
446
+ balanceCache.set(key, {
447
+ snapshot: staleSnapshot,
448
+ expiresAt: currentTime + CACHE_TTL_MS
449
+ });
450
+ return staleSnapshot;
451
+ }
452
+
453
+ return lastSnapshot || makeHiddenSnapshot(null, { updatedAt: nowIso(currentTime) });
454
+ }
455
+
456
+ function getChannelsForSource(source) {
457
+ if (source === 'claude') {
458
+ return require('./channels').getAllChannels() || [];
459
+ }
460
+ if (source === 'codex') {
461
+ return require('./codex-channels').getChannels().channels || [];
462
+ }
463
+ if (source === 'gemini') {
464
+ return require('./gemini-channels').getChannels().channels || [];
465
+ }
466
+ if (source === 'opencode') {
467
+ return require('./opencode-channels').getChannels().channels || [];
468
+ }
469
+ return [];
470
+ }
471
+
472
+ function isBalanceDisplayEnabled() {
473
+ try {
474
+ return loadUIConfig().channelBalance?.showRemaining === true;
475
+ } catch {
476
+ return false;
477
+ }
478
+ }
479
+
480
+ function validateSource(source) {
481
+ const normalized = String(source || '').trim().toLowerCase();
482
+ if (!VALID_SOURCES.has(normalized)) {
483
+ throw new Error('Invalid channel balance source');
484
+ }
485
+ return normalized;
486
+ }
487
+
488
+ async function mapWithConcurrency(items, limit, worker) {
489
+ const results = new Array(items.length);
490
+ let nextIndex = 0;
491
+ const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
492
+ while (nextIndex < items.length) {
493
+ const index = nextIndex;
494
+ nextIndex += 1;
495
+ results[index] = await worker(items[index], index);
496
+ }
497
+ });
498
+ await Promise.all(workers);
499
+ return results;
500
+ }
501
+
502
+ async function getChannelBalances(source, options = {}) {
503
+ const normalizedSource = validateSource(source);
504
+ if (!isBalanceDisplayEnabled()) {
505
+ return { enabled: false, source: normalizedSource, balances: {} };
506
+ }
507
+
508
+ const channels = getChannelsForSource(normalizedSource);
509
+ const entries = await mapWithConcurrency(channels, 4, async (channel) => {
510
+ const snapshot = await refreshChannelBalanceSnapshot(normalizedSource, channel, options);
511
+ return [channel.id, snapshot];
512
+ });
513
+
514
+ return {
515
+ enabled: true,
516
+ source: normalizedSource,
517
+ balances: Object.fromEntries(entries.filter(([id]) => id))
518
+ };
519
+ }
520
+
521
+ async function refreshChannelBalance(source, channelId) {
522
+ const normalizedSource = validateSource(source);
523
+ if (!isBalanceDisplayEnabled()) {
524
+ return { enabled: false, source: normalizedSource, channelId, balance: makeHiddenSnapshot() };
525
+ }
526
+
527
+ const channels = getChannelsForSource(normalizedSource);
528
+ const channel = channels.find(item => String(item.id) === String(channelId));
529
+ if (!channel) {
530
+ const error = new Error('Channel not found');
531
+ error.statusCode = 404;
532
+ throw error;
533
+ }
534
+
535
+ const balance = await refreshChannelBalanceSnapshot(normalizedSource, channel, { force: true });
536
+ return {
537
+ enabled: true,
538
+ source: normalizedSource,
539
+ channelId,
540
+ balance
541
+ };
542
+ }
543
+
544
+ function clearBalanceCache() {
545
+ balanceCache.clear();
546
+ }
547
+
548
+ module.exports = {
549
+ getChannelBalances,
550
+ refreshChannelBalance,
551
+ _test: {
552
+ clearBalanceCache,
553
+ normalizeBaseUrl,
554
+ buildBaseCandidates,
555
+ buildHubBalanceSnapshot,
556
+ detectPlatformByUrlHint,
557
+ resolveStatusPlatform,
558
+ refreshChannelBalanceSnapshot,
559
+ makeVisibleSnapshot,
560
+ requestJson
561
+ }
562
+ };
@@ -11,6 +11,9 @@ const DEFAULT_UI_CONFIG = {
11
11
  showChannels: true,
12
12
  showLogs: true
13
13
  },
14
+ channelBalance: {
15
+ showRemaining: false
16
+ },
14
17
  channelLocks: {
15
18
  claude: false,
16
19
  codex: false,
@@ -57,6 +60,7 @@ function readUIConfigFromFile() {
57
60
  ...data,
58
61
  theme: data.theme || DEFAULT_UI_CONFIG.theme,
59
62
  panelVisibility: { ...DEFAULT_UI_CONFIG.panelVisibility, ...data.panelVisibility },
63
+ channelBalance: { ...DEFAULT_UI_CONFIG.channelBalance, ...data.channelBalance },
60
64
  channelLocks: { ...DEFAULT_UI_CONFIG.channelLocks, ...data.channelLocks },
61
65
  channelCollapse: { ...DEFAULT_UI_CONFIG.channelCollapse, ...data.channelCollapse },
62
66
  channelOrder: { ...DEFAULT_UI_CONFIG.channelOrder, ...data.channelOrder }
@@ -1 +0,0 @@
1
- import{a8 as e,a5 as a,f as s,r as l,w as t,h as n,K as i,c as o,o as u,a as r,X as d,a0 as c,W as v,Y as f,N as p,p as m,_ as h,F as y,a6 as g,L as k,Z as _,s as w,G as b,y as I,$ as C,v as j}from"./vue-vendor-B5RqjEjQ.js";import{d as z}from"./vendors-DQA4Hw9v.js";import{_ as x,m as N,t as F,C as S,v as $,o as M,p as B,w as U,r as L}from"./index-C2yKfEbU.js";import{X as P,T,ac as A,ax as E,j as K,G as V,a2 as G,ay as H,p as O,h as R,V as q}from"./icons-QFXzEgqM.js";import{N as D,B as W,_ as X,c as Y,a as Z,G as J,q as Q,g as ee,x as ae,s as se,n as le,h as te,p as ne,k as ie}from"./naive-ui-DM897mIP.js";import"./markdown-BNJfEgCA.js";const oe={class:"session-list-container"},ue={class:"header"},re={class:"title-bar"},de={class:"title-section"},ce={class:"title-with-count"},ve={class:"header-actions"},fe={key:0,class:"loading-container"},pe=["onMouseenter","onClick"],me={key:0,class:"drag-handle"},he={class:"session-left"},ye={class:"session-icon"},ge={class:"session-info"},ke={class:"session-header"},_e={class:"session-title-row"},we={class:"session-title"},be={class:"session-meta"},Ie={class:"session-right"},Ce={class:"session-tags-area"},je={key:0,class:"session-actions"},ze={key:0,style:{"max-height":"70vh","overflow-y":"auto"}},xe={class:"search-result-header"},Ne={class:"search-result-title"},Fe="__full__",Se=x({__name:"SessionList",props:{projectName:{type:String,required:!0}},setup(x){const Se=x,$e=e(),Me=a(),Be=N(),{addFavorite:Ue,removeFavorite:Le,isFavorite:Pe}=F(),Te=s(()=>Me.meta.channel||"claude"),Ae=l(Se.projectName),Ee=s(()=>Ae.value||Se.projectName),Ke=l(""),Ve=l(!1),Ge=l(null),He=l(""),Oe=l(null),Re=l([]),qe=l(null),De=l(!1),We=l(null),Xe=l(!1),Ye=l(!1),Ze=l([]),Je=l(!1),Qe=l(!1),ea=l(null),aa=l(""),sa=l(Fe),la=l(!1),ta=l(!1),na=l([]),ia=l(!1),oa=l(""),ua=l(""),ra=l(null),da={group:{name:`${Te.value}-sessions`,pull:!1,put:!1},forceFallback:!0,fallbackOnBody:!1,fallbackTolerance:4,scroll:!0},ca=s(()=>{var e;return(null==(e=Be.currentProjectInfo)?void 0:e.displayName)||Se.projectName}),va=s(()=>{var e;return(null==(e=Be.currentProjectInfo)?void 0:e.fullPath)||Ee.value}),fa=s(()=>new Set(Ze.value)),pa=s(()=>Ze.value.length),ma=s(()=>Re.value.length>0&&Re.value.every(e=>fa.value.has(e.sessionId))),ha=s(()=>pa.value>0&&!ma.value);async function ya(){const e=await async function(){Be.projects.length||await Be.fetchProjects();const e=Be.projects.find(e=>e.name===Se.projectName);if(e)return Ae.value=e.name,e.name;const a=Be.projects.find(e=>e.displayName===Se.projectName||e.fullPath===Se.projectName);return a?(Ae.value=a.name,a.name!==Se.projectName&&await $e.replace({name:`${Te.value}-sessions`,params:{projectName:a.name}}),a.name):(Ae.value=Se.projectName,Se.projectName)}();await Be.fetchSessions(e)}t(()=>Be.sessionsWithAlias,e=>{Re.value=[...e];const a=new Set(e.map(e=>e.sessionId));Ze.value=Ze.value.filter(e=>a.has(e)),Ye.value&&0===e.length&&Ia()},{immediate:!0});const ga=s(()=>{if(!Ke.value)return Re.value;const e=Ke.value.toLowerCase();return Re.value.filter(a=>a.sessionId.toLowerCase().includes(e)||a.alias&&a.alias.toLowerCase().includes(e)||a.firstMessage&&a.firstMessage.toLowerCase().includes(e)||a.gitBranch&&a.gitBranch.toLowerCase().includes(e))});function ka(){const e=Me.meta.channel||"claude";$e.push({name:`${e}-projects`})}async function _a(){if(Ke.value){Xe.value=!0;try{const e=await $(Ee.value,Ke.value,35,Te.value);qe.value=e,De.value=!0}catch(e){M.error("搜索失败: "+e.message)}finally{Xe.value=!1}}}async function wa(){const e=Re.value.map(e=>e.sessionId);await Be.saveSessionOrder(e)}function ba(){Ye.value=!0,Oe.value=null}function Ia(){Ye.value=!1,Ze.value=[],Oe.value=null}function Ca(e){return fa.value.has(e)}function ja(e,a=!Ca(e)){const s=new Set(Ze.value);a?s.add(e):s.delete(e),Ze.value=Array.from(s)}function za(e){Ze.value=e?Re.value.map(e=>e.sessionId):[]}function xa(e){Ye.value?ja(e.sessionId):function(e){oa.value=e.sessionId,ua.value=e.alias||"",ia.value=!0,j(()=>{var e;null==(e=ra.value)||e.open()})}(e)}async function Na(){if(Ge.value)try{const e=Ge.value.sessionId;He.value?(await Be.setAlias(e,He.value),M.success("别名设置成功")):(await Be.deleteAlias(e),M.success("别名已删除")),Ve.value=!1,Ge.value=null,He.value=""}catch(e){M.error("操作失败: "+e.message)}}function Fa(){Qe.value=!1,ea.value=null,aa.value="",sa.value=Fe,na.value=[],la.value=!1,ta.value=!1}async function Sa(){var e;if(ea.value)try{ta.value=!0,await Be.forkSession(ea.value.sessionId,{afterUserMessageNumber:sa.value===Fe?null:sa.value,alias:(null==(e=aa.value)?void 0:e.trim())||void 0}),M.success("Fork 成功!"),Fa()}catch(a){M.error("Fork 失败: "+a.message)}finally{ta.value=!1}}function $a(e){M.error(e)}function Ma(e){const a=new Set((e||[]).filter(Boolean));0!==a.size&&(Ze.value=Ze.value.filter(e=>!a.has(e)),oa.value&&a.has(oa.value)&&(ia.value=!1,oa.value="",ua.value=""))}async function Ba(e){try{const{copyResult:a}=await L(Ee.value,e,Te.value);if("manual"===(null==a?void 0:a.method))return void M.warning("自动复制失败,已弹出手动复制框");M.success("启动命令已复制到剪贴板")}catch(a){M.error("复制失败: "+a.message)}}function Ua(){if(0===pa.value)return void M.warning("请先选择要删除的会话");const e=[...Ze.value];B.warning({title:"批量删除会话",content:`确定要删除选中的 ${e.length} 个会话吗?此操作不可恢复!`,positiveText:"确定删除",negativeText:"取消",onPositiveClick:async()=>{Je.value=!0;try{const a=await Be.deleteSessions(e);if(Ma(a.deletedSessionIds),0===a.failed.length)return M.success(`已删除 ${a.deletedSessionIds.length} 个会话`),void Ia();Ze.value=a.failed.map(e=>e.sessionId),a.deletedSessionIds.length>0?M.warning(`已删除 ${a.deletedSessionIds.length} 个会话,${a.failed.length} 个删除失败`):M.error(`删除失败: ${a.failed[0].error.message}`)}finally{Je.value=!1}}})}function La(e){if(!e)return"";return new Date(e).toLocaleString("zh-CN")}function Pa(e){if(!e)return"0 B";const a=1024;return e<a?e+" B":e<a*a?(e/a).toFixed(1)+" KB":(e/a/a).toFixed(1)+" MB"}function Ta(e,a){if(!a||!e)return e;const s=new RegExp(`(${a})`,"gi");return e.replace(s,'<mark style="background-color: #ffd700; padding: 2px 4px; border-radius: 2px; font-weight: 600;">$1</mark>')}function Aa(e,a=80){return e?e.length>a?e.substring(0,a)+"...":e:""}return t([Te,()=>Se.projectName],([e])=>{Ia(),Be.setChannel(e),ya()},{immediate:!0}),n(()=>{}),i(()=>{}),(e,a)=>(u(),o("div",oe,[r("div",ue,[r("div",re,[d(p(W),{size:"small",onClick:ka,class:"back-button"},{icon:f(()=>[d(p(D),{size:"18"},{default:f(()=>[d(p(P))]),_:1})]),_:1}),r("div",de,[r("div",ce,[d(p(X),null,{default:f(()=>[m(h(ca.value),1)]),_:1}),d(p(Y),{depth:"3",class:"session-count"},{default:f(()=>[m("("+h(p(Be).sessions.length)+" 个对话)",1)]),_:1}),d(p(Z),{size:"small",bordered:!1,type:"info",class:"total-size-tag"},{default:f(()=>[m(h(Pa(p(Be).totalSize)),1)]),_:1}),r("div",ve,[Ye.value?(u(),o(y,{key:0},[d(p(J),{checked:ma.value,indeterminate:ha.value,disabled:0===Re.value.length||Je.value,"onUpdate:checked":za},{default:f(()=>[...a[12]||(a[12]=[m(" 全选 ",-1)])]),_:1},8,["checked","indeterminate","disabled"]),d(p(Y),{depth:"3"},{default:f(()=>[m("已选 "+h(pa.value)+" 个",1)]),_:1}),d(p(W),{size:"small",type:"error",disabled:0===pa.value,loading:Je.value,onClick:Ua},{icon:f(()=>[d(p(D),null,{default:f(()=>[d(p(T))]),_:1})]),default:f(()=>[a[13]||(a[13]=m(" 删除 ",-1))]),_:1},8,["disabled","loading"]),d(p(W),{size:"small",disabled:Je.value,onClick:Ia},{default:f(()=>[...a[14]||(a[14]=[m(" 完成 ",-1)])]),_:1},8,["disabled"])],64)):(u(),c(p(W),{key:1,size:"small",type:"error",secondary:"",onClick:ba},{icon:f(()=>[d(p(D),null,{default:f(()=>[d(p(T))]),_:1})]),default:f(()=>[a[15]||(a[15]=m(" 管理 ",-1))]),_:1}))])]),d(p(Y),{depth:"3",class:"project-path"},{default:f(()=>[m(h(va.value),1)]),_:1})]),d(p(Q),{value:Ke.value,"onUpdate:value":a[0]||(a[0]=e=>Ke.value=e),placeholder:"搜索会话...",clearable:"",class:"search-input",onKeyup:g(_a,["enter"]),disabled:Xe.value},{prefix:f(()=>[d(p(D),null,{default:f(()=>[d(p(A))]),_:1})]),suffix:f(()=>[d(p(W),{text:"",onClick:_a,disabled:!Ke.value||Xe.value,loading:Xe.value},{default:f(()=>[...a[16]||(a[16]=[m(" 搜索 ",-1)])]),_:1},8,["disabled","loading"])]),_:1},8,["value","disabled"])])]),r("div",{class:"content",ref_key:"contentEl",ref:We},[p(Be).loading?(u(),o("div",fe,[d(p(ee),{size:"large"},{description:f(()=>[...a[17]||(a[17]=[m(" 加载会话列表... ",-1)])]),_:1})])):p(Be).error?(u(),c(p(ae),{key:1,type:"error",title:"加载失败",style:{"margin-bottom":"16px"}},{default:f(()=>[m(h(p(Be).error),1)]),_:1})):ga.value.length>0?(u(),c(p(z),I({key:2,modelValue:Re.value,"onUpdate:modelValue":a[3]||(a[3]=e=>Re.value=e),"item-key":"sessionId",class:"sessions-list",handle:".drag-handle",disabled:Ye.value||Je.value},da,{"ghost-class":"ghost","chosen-class":"chosen",animation:"200",onEnd:wa}),{item:f(({element:e})=>[r("div",{class:k(["session-item",{"session-item-selected":Ca(e.sessionId),"session-item-selection-mode":Ye.value}]),onMouseenter:a=>Oe.value=e.sessionId,onMouseleave:a[2]||(a[2]=e=>Oe.value=null),onClick:a=>xa(e)},[Ye.value?(u(),o("div",{key:1,class:"selection-checkbox",onClick:a[1]||(a[1]=_(()=>{},["stop"]))},[d(p(J),{checked:Ca(e.sessionId),"onUpdate:checked":a=>ja(e.sessionId,a)},null,8,["checked","onUpdate:checked"])])):(u(),o("div",me,[d(p(D),{size:"16",color:"#999"},{default:f(()=>[d(p(E))]),_:1})])),r("div",he,[r("div",ye,[d(p(D),{size:"24",color:"#18a058"},{default:f(()=>[d(p(K))]),_:1})]),r("div",ge,[r("div",ke,[r("div",_e,[r("span",we,h(e.alias?`${e.alias} (${e.sessionId.substring(0,8)})`:e.sessionId),1),e.forkedFrom?(u(),c(p(se),{key:0,placement:"top"},{trigger:f(()=>[d(p(Z),{size:"small",type:"warning",bordered:!1,style:{"margin-left":"8px",cursor:"help"}},{icon:f(()=>[d(p(D),null,{default:f(()=>[d(p(V))]),_:1})]),default:f(()=>[a[18]||(a[18]=m(" Fork ",-1))]),_:1})]),default:f(()=>[m(" Fork 自: "+h(e.forkedFrom),1)]),_:2},1024)):v("",!0)])]),r("div",be,[d(p(Y),{depth:"3"},{default:f(()=>[m(h(La(e.mtime)),1)]),_:2},1024),d(p(Y),{depth:"3"},{default:f(()=>[...a[19]||(a[19]=[m("•",-1)])]),_:1}),d(p(Z),{size:"small",bordered:!1},{default:f(()=>[m(h(Pa(e.size)),1)]),_:2},1024)]),e.firstMessage?(u(),c(p(Y),{key:0,depth:"3",class:"session-message"},{default:f(()=>[m(h(Aa(e.firstMessage,80)),1)]),_:2},1024)):e.gitBranch||e.summary?v("",!0):(u(),c(p(Y),{key:1,depth:"3",class:"session-message session-message-empty"},{default:f(()=>[...a[20]||(a[20]=[m(" 暂未读取到对话内容 ",-1)])]),_:1}))])]),r("div",Ie,[r("div",Ce,[e.gitBranch?(u(),c(p(Z),{key:0,size:"small",type:"info",bordered:!1},{icon:f(()=>[d(p(D),null,{default:f(()=>[d(p(V))]),_:1})]),default:f(()=>[m(" "+h(e.gitBranch),1)]),_:2},1024)):v("",!0)]),Ye.value?v("",!0):(u(),o("div",je,[d(p(le),null,{default:f(()=>[w(d(p(W),{size:"small",type:"error",onClick:_(a=>{return s=e.sessionId,void B.warning({title:"删除会话",content:"确定要删除这个会话吗?此操作不可恢复!",positiveText:"确定删除",negativeText:"取消",onPositiveClick:async()=>{try{await Be.deleteSession(s),Ma([s]),M.success("会话已删除")}catch(e){M.error("删除失败: "+e.message)}}});var s},["stop"])},{icon:f(()=>[d(p(D),null,{default:f(()=>[d(p(T))]),_:1})]),default:f(()=>[a[21]||(a[21]=m(" 删除 ",-1))]),_:1},8,["onClick"]),[[b,Oe.value===e.sessionId]]),d(p(W),{size:"small",onClick:_(a=>function(e){Ge.value=e,He.value=e.alias||"",Ve.value=!0}(e),["stop"])},{icon:f(()=>[d(p(D),null,{default:f(()=>[d(p(G))]),_:1})]),default:f(()=>[a[22]||(a[22]=m(" 别名 ",-1))]),_:1},8,["onClick"]),d(p(W),{size:"small",type:p(Pe)(Te.value,Ee.value,e.sessionId)?"warning":"default",onClick:_(a=>async function(e){const a=Te.value,s=Pe(a,Ee.value,e.sessionId);try{if(s)await Le(a,Ee.value,e.sessionId),M.success("已取消收藏");else{const s={sessionId:e.sessionId,projectName:Ee.value,projectDisplayName:ca.value,projectFullPath:va.value,alias:e.alias||"",firstMessage:e.firstMessage||"",mtime:e.mtime,size:e.size,gitBranch:e.gitBranch||"",forkedFrom:e.forkedFrom||""};await Ue(a,s),M.success("已添加到收藏")}}catch(l){M.error("操作失败: "+l.message)}}(e),["stop"])},{icon:f(()=>[d(p(D),null,{default:f(()=>[p(Pe)(Te.value,Ee.value,e.sessionId)?(u(),c(p(H),{key:0})):(u(),c(p(O),{key:1}))]),_:2},1024)]),default:f(()=>[m(" "+h(p(Pe)(Te.value,Ee.value,e.sessionId)?"已收藏":"收藏"),1)]),_:2},1032,["type","onClick"]),"opencode"!==Te.value?(u(),c(p(W),{key:0,size:"small",onClick:_(a=>async function(e){if("opencode"!==Te.value)try{const a=Re.value.find(a=>a.sessionId===e);ea.value=a||{sessionId:e},aa.value="",sa.value=Fe,na.value=[{label:"完整会话末尾(保留全部消息)",value:Fe}],Qe.value=!0,la.value=!0;const s=await U(Ee.value,e,Te.value),l=Array.isArray(null==s?void 0:s.items)?s.items:[];na.value=[{label:"完整会话末尾(保留全部消息)",value:Fe},...l.map(e=>({label:`保留到第 ${e.userMessageNumber} 条用户消息这一轮 · ${e.preview}`,value:e.userMessageNumber}))]}catch(a){M.error("加载 Fork 位置失败: "+a.message),Fa()}finally{la.value=!1}else M.warning("OpenCode 当前不支持该 Fork 操作")}(e.sessionId),["stop"])},{icon:f(()=>[d(p(D),null,{default:f(()=>[d(p(V))]),_:1})]),default:f(()=>[a[23]||(a[23]=m(" Fork ",-1))]),_:1},8,["onClick"])):v("",!0),d(p(W),{size:"small",type:"primary",onClick:_(a=>Ba(e.sessionId),["stop"])},{icon:f(()=>[d(p(D),null,{default:f(()=>[d(p(R))]),_:1})]),default:f(()=>[a[24]||(a[24]=m(" 使用对话 ",-1))]),_:1},8,["onClick"])]),_:2},1024)]))])],42,pe)]),_:1},16,["modelValue","disabled"])):(u(),c(p(te),{key:3,description:"没有找到会话",style:{"margin-top":"60px"}},{icon:f(()=>[d(p(D),null,{default:f(()=>[d(p(q))]),_:1})]),_:1}))],512),d(p(ne),{show:Ve.value,"onUpdate:show":a[6]||(a[6]=e=>Ve.value=e),preset:"dialog",title:"设置别名"},{action:f(()=>[d(p(le),null,{default:f(()=>[d(p(W),{onClick:a[5]||(a[5]=e=>Ve.value=!1)},{default:f(()=>[...a[25]||(a[25]=[m("取消",-1)])]),_:1}),d(p(W),{type:"primary",onClick:Na},{default:f(()=>[...a[26]||(a[26]=[m("确定",-1)])]),_:1})]),_:1})]),default:f(()=>[d(p(Q),{value:He.value,"onUpdate:value":a[4]||(a[4]=e=>He.value=e),placeholder:"输入别名(留空删除)",onKeyup:g(Na,["enter"])},null,8,["value"])]),_:1},8,["show"]),d(p(ne),{show:Qe.value,"onUpdate:show":a[9]||(a[9]=e=>Qe.value=e),preset:"dialog",title:"Fork 对话"},{action:f(()=>[d(p(le),null,{default:f(()=>[d(p(W),{disabled:ta.value,onClick:Fa},{default:f(()=>[...a[28]||(a[28]=[m("取消",-1)])]),_:1},8,["disabled"]),d(p(W),{type:"primary",loading:ta.value,onClick:Sa},{default:f(()=>[...a[29]||(a[29]=[m(" 确认 Fork ",-1)])]),_:1},8,["loading"])]),_:1})]),default:f(()=>[d(p(le),{vertical:"",size:14},{default:f(()=>[d(p(Y),{depth:"3"},{default:f(()=>[...a[27]||(a[27]=[m(" 可选择保留到第几条用户消息所在轮次;会包含这条消息后 AI 的完整输出。 ",-1)])]),_:1}),d(p(ie),{value:sa.value,"onUpdate:value":a[7]||(a[7]=e=>sa.value=e),options:na.value,loading:la.value,clearable:"",placeholder:"选择保留到哪一轮(默认完整会话)"},null,8,["value","options","loading"]),d(p(Q),{value:aa.value,"onUpdate:value":a[8]||(a[8]=e=>aa.value=e),placeholder:"输入新会话别名(可选)",maxlength:"120"},null,8,["value"])]),_:1})]),_:1},8,["show"]),d(p(ne),{show:De.value,"onUpdate:show":a[10]||(a[10]=e=>De.value=e),preset:"card",title:"搜索结果",style:{width:"1200px"}},{default:f(()=>[qe.value?(u(),o("div",ze,[d(p(ae),{type:"info",style:{"margin-bottom":"16px"}},{default:f(()=>[m(' 关键词 "'+h(qe.value.keyword)+'" 共找到 '+h(qe.value.totalMatches)+" 处匹配 ",1)]),_:1}),(u(!0),o(y,null,C(qe.value.sessions,e=>(u(),o("div",{key:e.sessionId,class:"search-result-item"},[r("div",xe,[r("div",Ne,[d(p(Y),{strong:""},{default:f(()=>[m(h(e.alias?`${e.alias} (${e.sessionId.substring(0,8)})`:e.sessionId.substring(0,8)),1)]),_:2},1024),d(p(Z),{size:"small",bordered:!1},{default:f(()=>[m(h(e.matchCount)+" 个匹配",1)]),_:2},1024)]),d(p(W),{size:"small",type:"primary",onClick:a=>Ba(e.sessionId)},{icon:f(()=>[d(p(D),null,{default:f(()=>[d(p(R))]),_:1})]),default:f(()=>[a[30]||(a[30]=m(" 使用对话 ",-1))]),_:1},8,["onClick"])]),(u(!0),o(y,null,C(e.matches,(e,a)=>(u(),o("div",{key:a,class:"search-match"},[d(p(Z),{size:"tiny",type:"user"===e.role?"info":"success",bordered:!1},{default:f(()=>[m(h("user"===e.role?"用户":"助手"),1)]),_:2},1032,["type"]),d(p(Y),{depth:"3",class:"search-match-text",innerHTML:Ta(e.context,qe.value.keyword)},null,8,["innerHTML"])]))),128))]))),128)),0===qe.value.sessions.length?(u(),c(p(te),{key:0,description:"没有找到匹配的内容"})):v("",!0)])):v("",!0)]),_:1},8,["show"]),oa.value?(u(),c(S,{key:0,ref_key:"chatHistoryRef",ref:ra,show:ia.value,"onUpdate:show":a[11]||(a[11]=e=>ia.value=e),"project-name":Ee.value,"session-id":oa.value,"session-alias":ua.value,channel:Te.value,onError:$a},null,8,["show","project-name","session-id","session-alias","channel"])):v("",!0)]))}},[["__scopeId","data-v-528553dd"]]);export{Se as default};