codexmate 0.0.20 → 0.0.22

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.
Files changed (96) hide show
  1. package/README.md +289 -152
  2. package/README.zh.md +321 -0
  3. package/cli/agents-files.js +224 -0
  4. package/cli/archive-helpers.js +446 -0
  5. package/cli/auth-profiles.js +359 -0
  6. package/cli/builtin-proxy.js +1044 -0
  7. package/cli/claude-proxy.js +998 -0
  8. package/cli/config-bootstrap.js +384 -0
  9. package/cli/openai-bridge.js +950 -0
  10. package/cli/openclaw-config.js +629 -0
  11. package/cli/session-usage.concurrent.js +28 -0
  12. package/cli/session-usage.js +112 -0
  13. package/cli/session-usage.models.js +176 -0
  14. package/cli/skills.js +1141 -0
  15. package/cli/zip-commands.js +510 -0
  16. package/cli.js +9408 -9719
  17. package/lib/cli-models-utils.js +109 -1
  18. package/lib/cli-path-utils.js +69 -0
  19. package/lib/cli-sessions.js +386 -0
  20. package/lib/download-artifacts.js +77 -0
  21. package/lib/task-orchestrator.js +869 -0
  22. package/package.json +14 -10
  23. package/res/logo.png +0 -0
  24. package/res/vue.global.prod.js +13 -0
  25. package/web-ui/app.js +193 -15
  26. package/web-ui/index.html +5 -1
  27. package/web-ui/logic.agents-diff.mjs +1 -1
  28. package/web-ui/logic.claude.mjs +60 -0
  29. package/web-ui/logic.runtime.mjs +11 -7
  30. package/web-ui/logic.sessions.mjs +372 -21
  31. package/web-ui/modules/api.mjs +22 -1
  32. package/web-ui/modules/app.computed.dashboard.mjs +23 -10
  33. package/web-ui/modules/app.computed.index.mjs +4 -0
  34. package/web-ui/modules/app.computed.main-tabs.mjs +198 -0
  35. package/web-ui/modules/app.computed.session.mjs +521 -9
  36. package/web-ui/modules/app.methods.agents.mjs +62 -11
  37. package/web-ui/modules/app.methods.codex-config.mjs +189 -34
  38. package/web-ui/modules/app.methods.index.mjs +7 -1
  39. package/web-ui/modules/app.methods.install.mjs +24 -20
  40. package/web-ui/modules/app.methods.navigation.mjs +142 -1
  41. package/web-ui/modules/app.methods.openclaw-core.mjs +339 -39
  42. package/web-ui/modules/app.methods.openclaw-editing.mjs +39 -4
  43. package/web-ui/modules/app.methods.openclaw-persist.mjs +122 -4
  44. package/web-ui/modules/app.methods.providers.mjs +192 -53
  45. package/web-ui/modules/app.methods.session-actions.mjs +99 -19
  46. package/web-ui/modules/app.methods.session-browser.mjs +196 -5
  47. package/web-ui/modules/app.methods.session-timeline.mjs +22 -15
  48. package/web-ui/modules/app.methods.session-trash.mjs +3 -0
  49. package/web-ui/modules/app.methods.startup-claude.mjs +70 -71
  50. package/web-ui/modules/app.methods.task-orchestration.mjs +471 -0
  51. package/web-ui/modules/config-mode.computed.mjs +2 -0
  52. package/web-ui/modules/config-template-confirm-pref.mjs +33 -0
  53. package/web-ui/modules/i18n.mjs +1609 -0
  54. package/web-ui/modules/plugins.computed.mjs +220 -0
  55. package/web-ui/modules/plugins.methods.mjs +620 -0
  56. package/web-ui/modules/plugins.storage.mjs +37 -0
  57. package/web-ui/partials/index/layout-footer.html +1 -57
  58. package/web-ui/partials/index/layout-header.html +299 -175
  59. package/web-ui/partials/index/modal-config-template-agents.html +79 -29
  60. package/web-ui/partials/index/modal-confirm-toast.html +1 -1
  61. package/web-ui/partials/index/modal-health-check.html +14 -14
  62. package/web-ui/partials/index/modal-openclaw-config.html +47 -42
  63. package/web-ui/partials/index/modal-skills.html +130 -114
  64. package/web-ui/partials/index/modals-basic.html +71 -102
  65. package/web-ui/partials/index/panel-config-claude.html +50 -12
  66. package/web-ui/partials/index/panel-config-codex.html +34 -37
  67. package/web-ui/partials/index/panel-config-openclaw.html +10 -16
  68. package/web-ui/partials/index/panel-docs.html +147 -0
  69. package/web-ui/partials/index/panel-market.html +38 -38
  70. package/web-ui/partials/index/panel-orchestration.html +397 -0
  71. package/web-ui/partials/index/panel-plugins.html +243 -0
  72. package/web-ui/partials/index/panel-sessions.html +51 -146
  73. package/web-ui/partials/index/panel-settings.html +188 -96
  74. package/web-ui/partials/index/panel-usage.html +353 -0
  75. package/web-ui/session-helpers.mjs +221 -10
  76. package/web-ui/styles/base-theme.css +120 -229
  77. package/web-ui/styles/controls-forms.css +59 -51
  78. package/web-ui/styles/docs-panel.css +247 -0
  79. package/web-ui/styles/layout-shell.css +394 -128
  80. package/web-ui/styles/modals-core.css +18 -3
  81. package/web-ui/styles/navigation-panels.css +184 -183
  82. package/web-ui/styles/plugins-panel.css +518 -0
  83. package/web-ui/styles/responsive.css +102 -62
  84. package/web-ui/styles/sessions-list.css +13 -27
  85. package/web-ui/styles/sessions-preview.css +13 -7
  86. package/web-ui/styles/sessions-toolbar-trash.css +25 -0
  87. package/web-ui/styles/sessions-usage.css +581 -6
  88. package/web-ui/styles/settings-panel.css +166 -0
  89. package/web-ui/styles/skills-list.css +16 -11
  90. package/web-ui/styles/skills-market.css +63 -2
  91. package/web-ui/styles/task-orchestration.css +776 -0
  92. package/web-ui/styles/titles-cards.css +67 -66
  93. package/web-ui/styles.css +4 -0
  94. package/README.en.md +0 -259
  95. package/res/screenshot.png +0 -0
  96. package/res/vue.global.js +0 -18552
@@ -1,3 +1,32 @@
1
+ export const DEFAULT_SESSION_LIST_LIMIT = 200;
2
+ export const DEFAULT_SESSION_LIST_FAST_LIMIT = 20;
3
+
4
+ function shouldUseFastSessionBrowseLimit(options = {}) {
5
+ if (options.forceRefresh) {
6
+ return false;
7
+ }
8
+ const normalizedSource = normalizeSessionSource(options.source, 'all');
9
+ if (normalizedSource !== 'all') {
10
+ return false;
11
+ }
12
+ const pathFilter = normalizeSessionPathFilter(options.pathFilter);
13
+ if (pathFilter) {
14
+ return false;
15
+ }
16
+ const query = typeof options.query === 'string' ? options.query.trim() : '';
17
+ if (query) {
18
+ return false;
19
+ }
20
+ const roleFilter = typeof options.roleFilter === 'string' ? options.roleFilter.trim().toLowerCase() : 'all';
21
+ if (roleFilter && roleFilter !== 'all') {
22
+ return false;
23
+ }
24
+ const timeRangePreset = typeof options.timeRangePreset === 'string'
25
+ ? options.timeRangePreset.trim().toLowerCase()
26
+ : 'all';
27
+ return !timeRangePreset || timeRangePreset === 'all';
28
+ }
29
+
1
30
  export function isSessionQueryEnabled(source) {
2
31
  const normalized = normalizeSessionSource(source, '');
3
32
  return normalized === 'codex' || normalized === 'claude' || normalized === 'all';
@@ -17,6 +46,73 @@ export function normalizeSessionPathFilter(pathFilter) {
17
46
  return typeof pathFilter === 'string' ? pathFilter.trim() : '';
18
47
  }
19
48
 
49
+ function isConcreteSessionModelName(value) {
50
+ if (typeof value !== 'string') {
51
+ return false;
52
+ }
53
+ const normalized = value.trim();
54
+ if (!normalized) {
55
+ return false;
56
+ }
57
+ return normalized.toLowerCase() !== '<synthetic>';
58
+ }
59
+
60
+ function collectSessionModelNames(session) {
61
+ if (!session || typeof session !== 'object') {
62
+ return [];
63
+ }
64
+ const values = Array.isArray(session.models)
65
+ ? [...session.models, session.model, session.modelName, session.modelId]
66
+ : [session.model, session.modelName, session.modelId];
67
+ const models = [];
68
+ for (const value of values) {
69
+ if (!isConcreteSessionModelName(value)) {
70
+ continue;
71
+ }
72
+ const normalized = value.trim();
73
+ if (models.includes(normalized)) {
74
+ continue;
75
+ }
76
+ models.push(normalized);
77
+ }
78
+ return models;
79
+ }
80
+
81
+ function readSessionTotalTokens(session) {
82
+ if (!session || typeof session !== 'object') {
83
+ return 0;
84
+ }
85
+
86
+ const rawTotalTokens = Number(session.totalTokens);
87
+ const hasExplicitTotal = Number.isFinite(rawTotalTokens) && rawTotalTokens >= 0;
88
+ const explicitTotal = hasExplicitTotal ? Math.max(0, Math.floor(rawTotalTokens)) : null;
89
+
90
+ const inputTokens = Number.isFinite(Number(session.inputTokens))
91
+ ? Math.max(0, Math.floor(Number(session.inputTokens)))
92
+ : null;
93
+ const outputTokens = Number.isFinite(Number(session.outputTokens))
94
+ ? Math.max(0, Math.floor(Number(session.outputTokens)))
95
+ : null;
96
+ const reasoningOutputTokens = Number.isFinite(Number(session.reasoningOutputTokens))
97
+ ? Math.max(0, Math.floor(Number(session.reasoningOutputTokens)))
98
+ : 0;
99
+
100
+ // 对齐 usage 口径:当总 token 缺失时,使用拆分字段回填(input + output + reasoning)。
101
+ // cachedInputTokens 一般包含在 inputTokens 中,因此不在此重复相加。
102
+ const hasBreakdown = !(inputTokens === null && outputTokens === null && reasoningOutputTokens === 0);
103
+ const breakdownTotal = hasBreakdown
104
+ ? (inputTokens || 0) + (outputTokens || 0) + reasoningOutputTokens
105
+ : 0;
106
+
107
+ if (breakdownTotal > 0) {
108
+ return breakdownTotal;
109
+ }
110
+ if (explicitTotal !== null) {
111
+ return explicitTotal;
112
+ }
113
+ return 0;
114
+ }
115
+
20
116
  export function buildSessionFilterCacheState(source, pathFilter) {
21
117
  return {
22
118
  source: normalizeSessionSource(source, 'all'),
@@ -25,13 +121,17 @@ export function buildSessionFilterCacheState(source, pathFilter) {
25
121
  }
26
122
 
27
123
  export function buildSessionListParams(options = {}) {
124
+ const fallbackLimit = shouldUseFastSessionBrowseLimit(options)
125
+ ? DEFAULT_SESSION_LIST_FAST_LIMIT
126
+ : DEFAULT_SESSION_LIST_LIMIT;
28
127
  const {
29
128
  source = 'all',
30
129
  pathFilter = '',
31
130
  query = '',
32
131
  roleFilter = 'all',
33
132
  timeRangePreset = 'all',
34
- limit = 200
133
+ limit = fallbackLimit,
134
+ forceRefresh = false
35
135
  } = options;
36
136
  const normalizedSource = normalizeSessionSource(source, 'all');
37
137
  const normalizedPathFilter = normalizeSessionPathFilter(pathFilter);
@@ -46,7 +146,7 @@ export function buildSessionListParams(options = {}) {
46
146
  roleFilter,
47
147
  timeRangePreset,
48
148
  limit,
49
- forceRefresh: true
149
+ forceRefresh: !!forceRefresh
50
150
  };
51
151
  }
52
152
 
@@ -88,16 +188,54 @@ export function formatSessionTimelineTimestamp(timestamp) {
88
188
  return value;
89
189
  }
90
190
 
91
- export function buildUsageChartGroups(sessions = [], options = {}) {
92
- const list = Array.isArray(sessions) ? sessions : [];
93
- const range = typeof options.range === 'string' ? options.range.trim().toLowerCase() : '7d';
191
+ function normalizeUsageRange(range) {
192
+ const normalized = typeof range === 'string' ? range.trim().toLowerCase() : '7d';
193
+ if (normalized === '30d' || normalized === 'all') {
194
+ return normalized;
195
+ }
196
+ return '7d';
197
+ }
198
+
199
+ function toUtcDayStartMs(value) {
200
+ const stamp = new Date(value);
201
+ return Date.UTC(stamp.getUTCFullYear(), stamp.getUTCMonth(), stamp.getUTCDate());
202
+ }
203
+
204
+ function formatUtcDayKey(value) {
205
+ const stamp = new Date(value);
206
+ return `${stamp.getUTCFullYear()}-${String(stamp.getUTCMonth() + 1).padStart(2, '0')}-${String(stamp.getUTCDate()).padStart(2, '0')}`;
207
+ }
208
+
209
+ function buildUsageBuckets(normalizedSessions, options = {}) {
210
+ const range = normalizeUsageRange(options.range);
94
211
  const now = Number.isFinite(Number(options.now)) ? Number(options.now) : Date.now();
95
212
  const dayMs = 24 * 60 * 60 * 1000;
96
- const rangeDays = range === '30d' ? 30 : 7;
97
213
  const buckets = [];
214
+
215
+ if (range === 'all') {
216
+ const validDayStarts = normalizedSessions
217
+ .map((session) => toUtcDayStartMs(session.updatedAtMs))
218
+ .filter((value) => Number.isFinite(value));
219
+ const firstDayStart = validDayStarts.length ? Math.min(...validDayStarts) : toUtcDayStartMs(now);
220
+ const lastDayStart = validDayStarts.length ? Math.max(...validDayStarts) : toUtcDayStartMs(now);
221
+ for (let stamp = firstDayStart; stamp <= lastDayStart; stamp += dayMs) {
222
+ const key = formatUtcDayKey(stamp);
223
+ buckets.push({
224
+ key,
225
+ label: key.slice(5),
226
+ codex: 0,
227
+ claude: 0,
228
+ totalMessages: 0,
229
+ totalSessions: 0
230
+ });
231
+ }
232
+ return { range, buckets };
233
+ }
234
+
235
+ const rangeDays = range === '30d' ? 30 : 7;
98
236
  for (let i = rangeDays - 1; i >= 0; i -= 1) {
99
237
  const stamp = new Date(now - (i * dayMs));
100
- const key = `${stamp.getUTCFullYear()}-${String(stamp.getUTCMonth() + 1).padStart(2, '0')}-${String(stamp.getUTCDate()).padStart(2, '0')}`;
238
+ const key = formatUtcDayKey(stamp);
101
239
  buckets.push({
102
240
  key,
103
241
  label: key.slice(5),
@@ -107,25 +245,80 @@ export function buildUsageChartGroups(sessions = [], options = {}) {
107
245
  totalSessions: 0
108
246
  });
109
247
  }
110
- const bucketMap = new Map(buckets.map((bucket) => [bucket.key, bucket]));
111
- let codexTotal = 0;
112
- let claudeTotal = 0;
113
- let messageTotal = 0;
114
- const pathMap = new Map();
248
+ return { range, buckets };
249
+ }
115
250
 
116
- for (const session of list) {
251
+ export function buildUsageChartGroups(sessions = [], options = {}) {
252
+ const list = Array.isArray(sessions) ? sessions : [];
253
+ const normalizedSessions = [];
254
+ for (const [sessionIndex, session] of list.entries()) {
117
255
  if (!session || typeof session !== 'object') continue;
118
256
  const source = normalizeSessionSource(session.source, '');
119
257
  if (source !== 'codex' && source !== 'claude') continue;
120
258
  const updatedAtMs = Date.parse(session.updatedAt || '');
121
259
  if (!Number.isFinite(updatedAtMs)) continue;
260
+ const createdAtMs = Date.parse(session.createdAt || '');
261
+ const sessionStartedAtMs = Number.isFinite(createdAtMs) ? createdAtMs : updatedAtMs;
262
+ const sessionEndedAtMs = Math.max(updatedAtMs, sessionStartedAtMs);
263
+ normalizedSessions.push({
264
+ session,
265
+ sessionIndex,
266
+ source,
267
+ updatedAtMs,
268
+ createdAtMs,
269
+ sessionStartedAtMs,
270
+ sessionEndedAtMs,
271
+ bucketKey: formatUtcDayKey(updatedAtMs)
272
+ });
273
+ }
274
+ const { range, buckets } = buildUsageBuckets(normalizedSessions, options);
275
+ const bucketMap = new Map(buckets.map((bucket) => [bucket.key, bucket]));
276
+ let codexTotal = 0;
277
+ let claudeTotal = 0;
278
+ let messageTotal = 0;
279
+ let totalTokens = 0;
280
+ let totalContextWindow = 0;
281
+ let activeDurationMs = 0;
282
+ let earliestSessionMs = Number.POSITIVE_INFINITY;
283
+ let latestSessionMs = 0;
284
+ const pathMap = new Map();
285
+ const modelMap = new Map();
286
+ const missingModelProviderMap = new Map();
287
+ const missingModelSessionMap = new Map();
288
+ const sourceMessageTotals = { codex: 0, claude: 0 };
289
+ const missingModelSourceTotals = { codex: 0, claude: 0 };
290
+ let missingModelSessions = 0;
291
+ let providerOnlySessions = 0;
292
+ const hourCounts = Array.from({ length: 24 }, (_, hour) => ({
293
+ key: String(hour).padStart(2, '0'),
294
+ label: String(hour).padStart(2, '0'),
295
+ count: 0
296
+ }));
297
+ const weekdayLabels = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
298
+ const weekdayCounts = Array.from({ length: 7 }, (_, index) => ({
299
+ key: String(index),
300
+ label: weekdayLabels[index],
301
+ count: 0
302
+ }));
303
+ const recentSessions = [];
304
+ const topSessionsByMessages = [];
305
+ const filteredSessions = [];
306
+
307
+ for (const normalized of normalizedSessions) {
308
+ const { session, sessionIndex, source, updatedAtMs, sessionStartedAtMs, sessionEndedAtMs, bucketKey } = normalized;
122
309
  const stamp = new Date(updatedAtMs);
123
- const key = `${stamp.getUTCFullYear()}-${String(stamp.getUTCMonth() + 1).padStart(2, '0')}-${String(stamp.getUTCDate()).padStart(2, '0')}`;
124
- const bucket = bucketMap.get(key);
310
+ const bucket = bucketMap.get(bucketKey);
125
311
  if (!bucket) continue;
312
+ const sessionModels = collectSessionModelNames(session);
313
+ if (sessionModels.length === 0) continue;
314
+ filteredSessions.push(session);
126
315
  const messageCount = Number.isFinite(Number(session.messageCount))
127
316
  ? Math.max(0, Math.floor(Number(session.messageCount)))
128
317
  : 0;
318
+ const sessionTotalTokens = readSessionTotalTokens(session);
319
+ const sessionContextWindow = Number.isFinite(Number(session.contextWindow))
320
+ ? Math.max(0, Math.floor(Number(session.contextWindow)))
321
+ : 0;
129
322
  bucket.totalSessions += 1;
130
323
  bucket.totalMessages += messageCount;
131
324
  if (source === 'codex') {
@@ -136,10 +329,71 @@ export function buildUsageChartGroups(sessions = [], options = {}) {
136
329
  claudeTotal += 1;
137
330
  }
138
331
  messageTotal += messageCount;
332
+ totalTokens += sessionTotalTokens;
333
+ totalContextWindow += sessionContextWindow;
334
+ sourceMessageTotals[source] += messageCount;
335
+ activeDurationMs += Math.max(0, sessionEndedAtMs - sessionStartedAtMs);
336
+ earliestSessionMs = Math.min(earliestSessionMs, sessionStartedAtMs);
337
+ latestSessionMs = Math.max(latestSessionMs, sessionEndedAtMs);
338
+
339
+ const utcHour = stamp.getUTCHours();
340
+ if (hourCounts[utcHour]) {
341
+ hourCounts[utcHour].count += 1;
342
+ }
343
+ const dayIndex = (stamp.getUTCDay() + 6) % 7;
344
+ if (weekdayCounts[dayIndex]) {
345
+ weekdayCounts[dayIndex].count += 1;
346
+ }
347
+
139
348
  const cwd = normalizeSessionPathFilter(session.cwd);
140
349
  if (cwd) {
141
- pathMap.set(cwd, (Number(pathMap.get(cwd)) || 0) + 1);
350
+ const prev = pathMap.get(cwd) || { count: 0, messageTotal: 0, updatedAtMs: 0 };
351
+ pathMap.set(cwd, {
352
+ count: prev.count + 1,
353
+ messageTotal: prev.messageTotal + messageCount,
354
+ updatedAtMs: Math.max(prev.updatedAtMs, updatedAtMs)
355
+ });
356
+ }
357
+
358
+ const sourceLabel = source === 'codex' ? 'Codex' : 'Claude Code';
359
+ const normalizedTitle = typeof session.title === 'string' && session.title.trim()
360
+ ? session.title.trim()
361
+ : (typeof session.sessionId === 'string' && session.sessionId.trim() ? session.sessionId.trim() : '未命名会话');
362
+ for (const modelId of sessionModels) {
363
+ const prev = modelMap.get(modelId) || {
364
+ count: 0,
365
+ messageTotal: 0,
366
+ tokenTotal: 0,
367
+ sources: new Set()
368
+ };
369
+ prev.count += 1;
370
+ prev.messageTotal += messageCount;
371
+ prev.tokenTotal += sessionTotalTokens;
372
+ prev.sources.add(source);
373
+ modelMap.set(modelId, prev);
142
374
  }
375
+
376
+ const sessionEntry = {
377
+ key: [
378
+ source,
379
+ session.sessionId || '',
380
+ session.filePath || normalizedTitle,
381
+ String(updatedAtMs),
382
+ String(messageCount),
383
+ String(sessionIndex)
384
+ ].join(':'),
385
+ title: normalizedTitle,
386
+ source,
387
+ sourceLabel,
388
+ cwd,
389
+ messageCount,
390
+ updatedAt: session.updatedAt || '',
391
+ updatedAtMs,
392
+ updatedAtLabel: formatSessionTimelineTimestamp(session.updatedAt || ''),
393
+ hasExactMessageCount: session.__messageCountExact === true
394
+ };
395
+ recentSessions.push(sessionEntry);
396
+ topSessionsByMessages.push({ ...sessionEntry });
143
397
  }
144
398
 
145
399
  const totalSessions = codexTotal + claudeTotal;
@@ -148,31 +402,128 @@ export function buildUsageChartGroups(sessions = [], options = {}) {
148
402
  { key: 'claude', label: 'Claude', value: claudeTotal }
149
403
  ].map((item) => ({
150
404
  ...item,
151
- percent: totalSessions > 0 ? Math.round((item.value / totalSessions) * 100) : 0
405
+ percent: totalSessions > 0 ? Math.round((item.value / totalSessions) * 100) : 0,
406
+ messageTotal: sourceMessageTotals[item.key] || 0,
407
+ messagePercent: messageTotal > 0 ? Math.round(((sourceMessageTotals[item.key] || 0) / messageTotal) * 100) : 0,
408
+ avgMessages: item.value > 0 ? Math.round(((sourceMessageTotals[item.key] || 0) / item.value) * 10) / 10 : 0
152
409
  }));
153
410
 
154
411
  const topPaths = [...pathMap.entries()]
155
- .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0], 'zh-Hans-CN'))
412
+ .sort((a, b) => b[1].count - a[1].count || b[1].messageTotal - a[1].messageTotal || a[0].localeCompare(b[0], 'zh-Hans-CN'))
156
413
  .slice(0, 5)
157
- .map(([pathValue, count]) => ({ path: pathValue, count }));
414
+ .map(([pathValue, meta]) => ({
415
+ path: pathValue,
416
+ count: meta.count,
417
+ messageTotal: meta.messageTotal,
418
+ updatedAtLabel: meta.updatedAtMs ? formatSessionTimelineTimestamp(new Date(meta.updatedAtMs).toISOString()) : ''
419
+ }));
420
+
421
+ const usedModels = [...modelMap.entries()]
422
+ .sort((a, b) => b[1].count - a[1].count)
423
+ .map(([modelId, meta]) => {
424
+ const sourceLabels = [...meta.sources]
425
+ .sort((a, b) => a.localeCompare(b, 'en-US'))
426
+ .map((source) => (source === 'codex' ? 'Codex' : 'Claude Code'));
427
+ return {
428
+ key: modelId,
429
+ model: modelId,
430
+ count: meta.count,
431
+ messageTotal: meta.messageTotal,
432
+ tokenTotal: meta.tokenTotal,
433
+ sourceLabels
434
+ };
435
+ });
436
+
437
+ const sortedRecentSessions = recentSessions
438
+ .sort((a, b) => b.updatedAtMs - a.updatedAtMs || b.messageCount - a.messageCount || a.title.localeCompare(b.title, 'zh-Hans-CN'))
439
+ .slice(0, 6);
440
+
441
+ const missingModelProviders = [...missingModelProviderMap.values()]
442
+ .sort((a, b) => b.count - a.count || a.label.localeCompare(b.label, 'zh-Hans-CN'));
443
+ const missingModelSessionsPreview = [...missingModelSessionMap.values()]
444
+ .sort((a, b) => b.updatedAtMs - a.updatedAtMs || a.title.localeCompare(b.title, 'zh-Hans-CN'))
445
+ .slice(0, 5);
446
+
447
+ const modelCoverage = {
448
+ totalSessions,
449
+ modeledSessions: Math.max(0, totalSessions - missingModelSessions),
450
+ missingModelSessions,
451
+ providerOnlySessions,
452
+ missingModelSourceTotals,
453
+ missingModelProviders,
454
+ missingModelSessionsPreview,
455
+ coveragePercent: totalSessions > 0 ? Math.round(((totalSessions - missingModelSessions) / totalSessions) * 100) : 0
456
+ };
457
+
458
+ const sortedTopSessionsByMessages = topSessionsByMessages
459
+ .sort((a, b) => b.messageCount - a.messageCount || b.updatedAtMs - a.updatedAtMs || a.title.localeCompare(b.title, 'zh-Hans-CN'))
460
+ .slice(0, 6);
158
461
 
159
462
  const maxSessionBucket = buckets.reduce((max, item) => Math.max(max, item.totalSessions), 0);
160
463
  const maxMessageBucket = buckets.reduce((max, item) => Math.max(max, item.totalMessages), 0);
464
+ const maxHourCount = hourCounts.reduce((max, item) => Math.max(max, item.count), 0);
465
+ const maxWeekdayCount = weekdayCounts.reduce((max, item) => Math.max(max, item.count), 0);
466
+ const busiestDay = [...buckets]
467
+ .sort((a, b) => b.totalSessions - a.totalSessions || b.totalMessages - a.totalMessages || a.key.localeCompare(b.key, 'zh-Hans-CN'))[0] || null;
468
+ const busiestHour = [...hourCounts]
469
+ .sort((a, b) => b.count - a.count || a.key.localeCompare(b.key, 'zh-Hans-CN'))[0] || null;
470
+ const activeDays = buckets.filter((item) => item.totalSessions > 0).length;
471
+ const avgMessagesPerSession = totalSessions > 0 ? Math.round((messageTotal / totalSessions) * 10) / 10 : 0;
472
+ const avgSessionsPerActiveDay = activeDays > 0 ? Math.round((totalSessions / activeDays) * 10) / 10 : 0;
473
+ const totalDurationMs = Number.isFinite(earliestSessionMs) && latestSessionMs > 0
474
+ ? Math.max(0, latestSessionMs - earliestSessionMs)
475
+ : 0;
161
476
 
162
477
  return {
163
478
  range,
164
479
  buckets,
480
+ filteredSessions,
165
481
  summary: {
166
482
  totalSessions,
167
483
  totalMessages: messageTotal,
484
+ totalTokens,
485
+ totalContextWindow,
486
+ activeDurationMs,
487
+ totalDurationMs,
168
488
  codexTotal,
169
489
  claudeTotal,
170
- activeDays: buckets.filter((item) => item.totalSessions > 0).length
490
+ activeDays,
491
+ avgMessagesPerSession,
492
+ avgSessionsPerActiveDay,
493
+ busiestDay: busiestDay
494
+ ? {
495
+ key: busiestDay.key,
496
+ label: busiestDay.label,
497
+ totalSessions: busiestDay.totalSessions,
498
+ totalMessages: busiestDay.totalMessages
499
+ }
500
+ : null,
501
+ busiestHour: busiestHour
502
+ ? {
503
+ key: busiestHour.key,
504
+ label: `${busiestHour.label}:00`,
505
+ count: busiestHour.count
506
+ }
507
+ : null
171
508
  },
172
509
  sourceShare,
510
+ usedModels,
511
+ modelCoverage,
173
512
  topPaths,
513
+ recentSessions: sortedRecentSessions,
514
+ topSessionsByMessages: sortedTopSessionsByMessages,
515
+ hourActivity: hourCounts.map((item) => ({
516
+ ...item,
517
+ percent: maxHourCount > 0 ? Math.round((item.count / maxHourCount) * 100) : 0
518
+ })),
519
+ weekdayActivity: weekdayCounts.map((item) => ({
520
+ ...item,
521
+ percent: maxWeekdayCount > 0 ? Math.round((item.count / maxWeekdayCount) * 100) : 0
522
+ })),
174
523
  maxSessionBucket,
175
- maxMessageBucket
524
+ maxMessageBucket,
525
+ maxHourCount,
526
+ maxWeekdayCount
176
527
  };
177
528
  }
178
529
 
@@ -16,6 +16,26 @@ function buildApiResponseContext(action, res, contentType) {
16
16
  return `${action} (${res.status} ${res.statusText}, content-type: ${contentType || 'unknown'})`;
17
17
  }
18
18
 
19
+ function formatUnexpectedApiBodySnippet(body, contentType) {
20
+ const raw = typeof body === 'string' ? body.trim() : '';
21
+ if (!raw) {
22
+ return '';
23
+ }
24
+ const normalizedContentType = String(contentType || '').toLowerCase();
25
+ const looksLikeHtml = normalizedContentType.includes('text/html')
26
+ || /<!doctype\s+html|<html[\s>]|<head[\s>]|<body[\s>]/i.test(raw);
27
+ if (looksLikeHtml) {
28
+ return '';
29
+ }
30
+ const singleLine = raw.replace(/\s+/g, ' ').trim();
31
+ if (!singleLine) {
32
+ return '';
33
+ }
34
+ return singleLine.length > 200
35
+ ? `${singleLine.slice(0, 197)}...`
36
+ : singleLine;
37
+ }
38
+
19
39
  function withPayloadTooLargeErrorCode(res, payload) {
20
40
  if (res.status !== 413 || (payload && typeof payload === 'object' && payload.errorCode)) {
21
41
  return payload;
@@ -29,7 +49,8 @@ export async function api(action, params = {}) {
29
49
  if (contentType && !contentType.includes('application/json')) {
30
50
  const body = await res.text();
31
51
  const errorDetails = buildApiResponseContext(action, res, contentType);
32
- const bodyDetails = body ? `: ${body}` : '';
52
+ const bodySnippet = formatUnexpectedApiBodySnippet(body, contentType);
53
+ const bodyDetails = bodySnippet ? `: ${bodySnippet}` : '';
33
54
  throw new Error(`Unexpected non-JSON API response for ${errorDetails}${bodyDetails}`);
34
55
  }
35
56
  try {
@@ -9,6 +9,15 @@ export function createDashboardComputed() {
9
9
  const removed = Number(stats.removed || 0);
10
10
  return added > 0 || removed > 0;
11
11
  },
12
+ configTemplateDiffHasChanges() {
13
+ const stats = this.configTemplateDiffStats || {};
14
+ const added = Number(stats.added || 0);
15
+ const removed = Number(stats.removed || 0);
16
+ if (this.configTemplateDiffHasChangesValue !== undefined && this.configTemplateDiffHasChangesValue !== null) {
17
+ return !!this.configTemplateDiffHasChangesValue;
18
+ }
19
+ return added > 0 || removed > 0;
20
+ },
12
21
  claudeModelHasList() {
13
22
  return this.claudeModelOptions.length > 0;
14
23
  },
@@ -20,9 +29,6 @@ export function createDashboardComputed() {
20
29
  }
21
30
  return list;
22
31
  },
23
- hasLocalAndProxy() {
24
- return false;
25
- },
26
32
  displayCurrentProvider() {
27
33
  const switching = String(this.providerSwitchDisplayTarget || '').trim();
28
34
  if (switching) return switching;
@@ -38,9 +44,13 @@ export function createDashboardComputed() {
38
44
  const action = this.normalizeInstallAction(this.installCommandAction);
39
45
  return targets.map((target) => {
40
46
  const id = target && typeof target.id === 'string' ? target.id : '';
47
+ const termuxCommand = id === 'codex'
48
+ ? this.getInstallCommand(id, action, 'termux')
49
+ : '';
41
50
  return {
42
51
  ...target,
43
- command: this.getInstallCommand(id, action)
52
+ command: this.getInstallCommand(id, action),
53
+ termuxCommand
44
54
  };
45
55
  });
46
56
  },
@@ -55,6 +65,9 @@ export function createDashboardComputed() {
55
65
  if (this.codexApplying || this.configTemplateApplying || this.openclawApplying) tasks.push('配置应用');
56
66
  if (this.agentsSaving) tasks.push('AGENTS 保存');
57
67
  if (this.skillsLoading || this.skillsDeleting || this.skillsScanningImports || this.skillsImporting || this.skillsZipImporting || this.skillsExporting) tasks.push('Skills 管理');
68
+ if (this.taskOrchestration && (this.taskOrchestration.loading || this.taskOrchestration.planning || this.taskOrchestration.running || this.taskOrchestration.queueAdding || this.taskOrchestration.queueStarting || this.taskOrchestration.retrying || this.taskOrchestration.selectedRunLoading)) {
69
+ tasks.push('任务编排');
70
+ }
58
71
  return tasks.length ? tasks.join(' / ') : '空闲';
59
72
  },
60
73
  inspectorMessageSummary() {
@@ -98,15 +111,15 @@ export function createDashboardComputed() {
98
111
  const platform = this.resolveInstallPlatform();
99
112
  if (platform === 'win32') {
100
113
  return [
101
- 'PowerShell 报权限不足(EACCES/EPERM)时,请以管理员身份执行安装命令。',
102
- '安装后若仍提示找不到命令,重开终端并执行:where codex / where claude。',
103
- '公司网络受限时,可先切换镜像源快捷项(npmmirror / 腾讯云 / 自定义)。'
114
+ this.t('docs.tip.win.1'),
115
+ this.t('docs.tip.win.2'),
116
+ this.t('docs.tip.win.3')
104
117
  ];
105
118
  }
106
119
  return [
107
- '出现 EACCES 权限错误时,优先修复 Node 全局目录权限,不建议直接 sudo npm。',
108
- '安装后若命令未生效,重开终端并执行:which codex / which claude。',
109
- '公司网络受限时,可先切换镜像源快捷项(npmmirror / 腾讯云 / 自定义)。'
120
+ this.t('docs.tip.unix.1'),
121
+ this.t('docs.tip.unix.2'),
122
+ this.t('docs.tip.unix.3')
110
123
  ];
111
124
  }
112
125
  };
@@ -1,13 +1,17 @@
1
1
  import { createDashboardComputed } from './app.computed.dashboard.mjs';
2
+ import { createMainTabsComputed } from './app.computed.main-tabs.mjs';
2
3
  import { createSessionComputed } from './app.computed.session.mjs';
3
4
  import { createConfigModeComputed } from './config-mode.computed.mjs';
4
5
  import { createSkillsComputed } from './skills.computed.mjs';
6
+ import { createPluginsComputed } from './plugins.computed.mjs';
5
7
 
6
8
  export function createAppComputed() {
7
9
  return {
8
10
  ...createSessionComputed(),
9
11
  ...createDashboardComputed(),
12
+ ...createMainTabsComputed(),
10
13
  ...createSkillsComputed(),
14
+ ...createPluginsComputed(),
11
15
  ...createConfigModeComputed()
12
16
  };
13
17
  }