codexmate 0.0.32 → 0.0.34

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 (148) hide show
  1. package/README.md +147 -363
  2. package/README.zh.md +147 -371
  3. package/cli/agents-files.js +230 -224
  4. package/cli/archive-helpers.js +453 -446
  5. package/cli/auth-profiles.js +375 -375
  6. package/cli/builtin-proxy.js +1725 -1725
  7. package/cli/claude-proxy.js +1022 -1022
  8. package/cli/config-bootstrap.js +402 -402
  9. package/cli/config-health.js +454 -454
  10. package/cli/doctor-core.js +903 -903
  11. package/cli/import-skills-url.js +356 -356
  12. package/cli/local-bridge.js +556 -324
  13. package/cli/openai-bridge.js +1653 -1653
  14. package/cli/openclaw-config.js +629 -629
  15. package/cli/session-convert-args.js +69 -69
  16. package/cli/session-convert-io.js +82 -82
  17. package/cli/session-convert.js +150 -150
  18. package/cli/session-usage.concurrent.js +28 -28
  19. package/cli/session-usage.js +118 -118
  20. package/cli/session-usage.models.js +176 -176
  21. package/cli/skills.js +1141 -1141
  22. package/cli/update.js +171 -0
  23. package/cli/zip-commands.js +510 -510
  24. package/cli.js +16079 -15829
  25. package/lib/automation.js +404 -404
  26. package/lib/cli-file-utils.js +151 -151
  27. package/lib/cli-models-utils.js +440 -440
  28. package/lib/cli-network-utils.js +190 -190
  29. package/lib/cli-path-utils.js +85 -85
  30. package/lib/cli-session-utils.js +121 -121
  31. package/lib/cli-sessions.js +427 -426
  32. package/lib/cli-utils.js +155 -155
  33. package/lib/cli-webhook.js +154 -126
  34. package/lib/download-artifacts.js +92 -92
  35. package/lib/mcp-stdio.js +453 -453
  36. package/lib/task-orchestrator.js +869 -869
  37. package/lib/text-diff.js +303 -303
  38. package/lib/win-tray.js +119 -0
  39. package/lib/workflow-engine.js +340 -340
  40. package/package.json +76 -76
  41. package/plugins/README.md +20 -20
  42. package/plugins/README.zh-CN.md +20 -20
  43. package/plugins/prompt-templates/comment-polish/index.mjs +25 -25
  44. package/plugins/prompt-templates/computed.mjs +253 -253
  45. package/plugins/prompt-templates/index.mjs +8 -8
  46. package/plugins/prompt-templates/manifest.mjs +15 -15
  47. package/plugins/prompt-templates/methods.mjs +553 -553
  48. package/plugins/prompt-templates/overview.mjs +91 -91
  49. package/plugins/prompt-templates/ownership.mjs +19 -19
  50. package/plugins/prompt-templates/rule-ack/index.mjs +21 -21
  51. package/plugins/prompt-templates/storage.mjs +64 -64
  52. package/plugins/registry.mjs +16 -16
  53. package/web-ui/app.js +654 -647
  54. package/web-ui/index.html +37 -36
  55. package/web-ui/logic.agents-diff.mjs +386 -386
  56. package/web-ui/logic.claude.mjs +172 -168
  57. package/web-ui/logic.codex.mjs +69 -69
  58. package/web-ui/logic.mjs +5 -5
  59. package/web-ui/logic.runtime.mjs +128 -128
  60. package/web-ui/logic.session-convert.mjs +70 -70
  61. package/web-ui/logic.sessions.mjs +781 -781
  62. package/web-ui/modules/api.mjs +90 -90
  63. package/web-ui/modules/app.computed.dashboard.mjs +248 -248
  64. package/web-ui/modules/app.computed.index.mjs +17 -17
  65. package/web-ui/modules/app.computed.main-tabs.mjs +205 -205
  66. package/web-ui/modules/app.computed.session.mjs +735 -693
  67. package/web-ui/modules/app.constants.mjs +15 -15
  68. package/web-ui/modules/app.methods.agents.mjs +651 -651
  69. package/web-ui/modules/app.methods.claude-config.mjs +306 -200
  70. package/web-ui/modules/app.methods.codex-config.mjs +869 -861
  71. package/web-ui/modules/app.methods.index.mjs +94 -94
  72. package/web-ui/modules/app.methods.install.mjs +205 -205
  73. package/web-ui/modules/app.methods.navigation.mjs +788 -774
  74. package/web-ui/modules/app.methods.openclaw-core.mjs +814 -814
  75. package/web-ui/modules/app.methods.openclaw-editing.mjs +372 -372
  76. package/web-ui/modules/app.methods.openclaw-persist.mjs +369 -369
  77. package/web-ui/modules/app.methods.providers.mjs +575 -529
  78. package/web-ui/modules/app.methods.runtime.mjs +345 -345
  79. package/web-ui/modules/app.methods.session-actions.mjs +591 -591
  80. package/web-ui/modules/app.methods.session-browser.mjs +1011 -1012
  81. package/web-ui/modules/app.methods.session-timeline.mjs +479 -479
  82. package/web-ui/modules/app.methods.session-trash.mjs +438 -438
  83. package/web-ui/modules/app.methods.startup-claude.mjs +547 -537
  84. package/web-ui/modules/app.methods.task-orchestration.mjs +556 -556
  85. package/web-ui/modules/app.methods.webhook.mjs +87 -79
  86. package/web-ui/modules/config-mode.computed.mjs +124 -124
  87. package/web-ui/modules/config-template-confirm-pref.mjs +33 -33
  88. package/web-ui/modules/i18n.dict.mjs +3195 -3177
  89. package/web-ui/modules/i18n.mjs +62 -62
  90. package/web-ui/modules/plugins.computed.mjs +3 -3
  91. package/web-ui/modules/plugins.methods.mjs +3 -3
  92. package/web-ui/modules/plugins.storage.mjs +11 -11
  93. package/web-ui/modules/provider-url-display.mjs +17 -17
  94. package/web-ui/modules/sessions-filters-url.mjs +85 -85
  95. package/web-ui/modules/skills.computed.mjs +107 -107
  96. package/web-ui/modules/skills.methods.mjs +482 -482
  97. package/web-ui/partials/index/layout-footer.html +13 -13
  98. package/web-ui/partials/index/layout-header.html +499 -503
  99. package/web-ui/partials/index/modal-config-template-agents.html +185 -185
  100. package/web-ui/partials/index/modal-confirm-toast.html +32 -32
  101. package/web-ui/partials/index/modal-health-check.html +45 -45
  102. package/web-ui/partials/index/modal-openclaw-config.html +280 -280
  103. package/web-ui/partials/index/modal-skills.html +200 -200
  104. package/web-ui/partials/index/modal-webhook.html +42 -0
  105. package/web-ui/partials/index/modals-basic.html +223 -162
  106. package/web-ui/partials/index/panel-config-claude.html +155 -136
  107. package/web-ui/partials/index/panel-config-codex.html +176 -196
  108. package/web-ui/partials/index/panel-config-codex.html.bak +337 -0
  109. package/web-ui/partials/index/panel-config-openclaw.html +83 -83
  110. package/web-ui/partials/index/panel-dashboard.html +186 -219
  111. package/web-ui/partials/index/panel-docs.html +114 -114
  112. package/web-ui/partials/index/panel-market.html +177 -177
  113. package/web-ui/partials/index/panel-orchestration.html +391 -391
  114. package/web-ui/partials/index/panel-plugins.html +253 -253
  115. package/web-ui/partials/index/panel-sessions.html +319 -313
  116. package/web-ui/partials/index/panel-settings.html +158 -190
  117. package/web-ui/partials/index/panel-trash.html +82 -82
  118. package/web-ui/partials/index/panel-usage.html +137 -137
  119. package/web-ui/res/json5.min.js +1 -1
  120. package/web-ui/res/vue.global.prod.js +13 -13
  121. package/web-ui/session-helpers.mjs +591 -591
  122. package/web-ui/source-bundle.cjs +233 -233
  123. package/web-ui/styles/base-theme.css +281 -281
  124. package/web-ui/styles/bridge-pool.css +266 -197
  125. package/web-ui/styles/controls-forms.css +433 -433
  126. package/web-ui/styles/dashboard.css +406 -406
  127. package/web-ui/styles/docs-panel.css +245 -245
  128. package/web-ui/styles/feedback.css +108 -108
  129. package/web-ui/styles/health-check-dialog.css +144 -144
  130. package/web-ui/styles/layout-shell.css +628 -638
  131. package/web-ui/styles/modals-core.css +499 -466
  132. package/web-ui/styles/navigation-panels.css +391 -391
  133. package/web-ui/styles/openclaw-structured.css +266 -266
  134. package/web-ui/styles/plugins-panel.css +564 -564
  135. package/web-ui/styles/responsive.css +392 -392
  136. package/web-ui/styles/sessions-list.css +683 -647
  137. package/web-ui/styles/sessions-preview.css +407 -407
  138. package/web-ui/styles/sessions-toolbar-trash.css +518 -518
  139. package/web-ui/styles/sessions-usage.css +588 -588
  140. package/web-ui/styles/settings-panel.css +415 -349
  141. package/web-ui/styles/skills-list.css +305 -305
  142. package/web-ui/styles/skills-market.css +429 -429
  143. package/web-ui/styles/task-orchestration.css +822 -822
  144. package/web-ui/styles/titles-cards.css +472 -472
  145. package/web-ui/styles/trash-panel.css +90 -90
  146. package/web-ui/styles/webhook.css +115 -81
  147. package/web-ui/styles.css +24 -24
  148. package/web-ui.html +17 -17
@@ -1,28 +1,28 @@
1
- function createConcurrencyLimiter(maxConcurrency) {
2
- const max = Number.isFinite(Number(maxConcurrency))
3
- ? Math.max(1, Math.floor(Number(maxConcurrency)))
4
- : 8;
5
- let active = 0;
6
- const queue = [];
7
- const next = () => {
8
- const resolve = queue.shift();
9
- if (resolve) resolve();
10
- };
11
- return async (task) => {
12
- if (active >= max) {
13
- await new Promise((resolve) => queue.push(resolve));
14
- }
15
- active += 1;
16
- try {
17
- return await task();
18
- } finally {
19
- active -= 1;
20
- next();
21
- }
22
- };
23
- }
24
-
25
- module.exports = {
26
- createConcurrencyLimiter
27
- };
28
-
1
+ function createConcurrencyLimiter(maxConcurrency) {
2
+ const max = Number.isFinite(Number(maxConcurrency))
3
+ ? Math.max(1, Math.floor(Number(maxConcurrency)))
4
+ : 8;
5
+ let active = 0;
6
+ const queue = [];
7
+ const next = () => {
8
+ const resolve = queue.shift();
9
+ if (resolve) resolve();
10
+ };
11
+ return async (task) => {
12
+ if (active >= max) {
13
+ await new Promise((resolve) => queue.push(resolve));
14
+ }
15
+ active += 1;
16
+ try {
17
+ return await task();
18
+ } finally {
19
+ active -= 1;
20
+ next();
21
+ }
22
+ };
23
+ }
24
+
25
+ module.exports = {
26
+ createConcurrencyLimiter
27
+ };
28
+
@@ -1,118 +1,118 @@
1
- const { createConcurrencyLimiter } = require('./session-usage.concurrent');
2
- const { normalizeSessionModelList, createSessionModelsFileReader } = require('./session-usage.models');
3
-
4
- async function listSessionUsageCore(params = {}, deps = {}) {
5
- const {
6
- fs,
7
- listSessionBrowse,
8
- parseCodexSessionSummary,
9
- parseClaudeSessionSummary,
10
- parseCodeBuddySessionSummary,
11
- parseGeminiSessionSummary,
12
- MAX_SESSION_USAGE_LIST_SIZE,
13
- SESSION_BROWSE_SUMMARY_READ_BYTES
14
- } = deps;
15
-
16
- const source = params.source === 'codex' || params.source === 'claude' || params.source === 'gemini' || params.source === 'codebuddy'
17
- ? params.source
18
- : 'all';
19
- const rawLimit = Number(params.limit);
20
- const limit = Number.isFinite(rawLimit)
21
- ? Math.max(1, Math.min(rawLimit, MAX_SESSION_USAGE_LIST_SIZE))
22
- : MAX_SESSION_USAGE_LIST_SIZE;
23
-
24
- const sessions = await listSessionBrowse({
25
- source,
26
- limit,
27
- forceRefresh: !!params.forceRefresh
28
- });
29
- if (!Array.isArray(sessions) || sessions.length === 0) {
30
- return [];
31
- }
32
-
33
- const { readSessionModelsFromFile } = createSessionModelsFileReader(fs, {
34
- concurrency: 32,
35
- maxEntries: 1500,
36
- probeHeadBytes: 128 * 1024,
37
- probeTailBytes: 128 * 1024
38
- });
39
-
40
- // CPU/IO 优化策略(面向 2000 会话):
41
- // 1) 优先使用 listSessionBrowse 返回的 model/models(零 I/O)
42
- // 2) 仅当缺少模型名时才读取/解析文件(必要时全文件扫描)
43
- const limitNormalize = createConcurrencyLimiter(64);
44
- const normalizedSessions = await Promise.all(
45
- sessions.map((item) => limitNormalize(async () => {
46
- if (!item || typeof item !== 'object' || Array.isArray(item)) {
47
- return null;
48
- }
49
- const normalized = { ...item };
50
- delete normalized.__messageCountExact;
51
-
52
- const baseModels = normalizeSessionModelList([
53
- ...(Array.isArray(normalized.models) ? normalized.models : []),
54
- normalized.model,
55
- normalized.modelName,
56
- normalized.modelId
57
- ]);
58
- if (baseModels.length > 0) {
59
- normalized.models = baseModels;
60
- normalized.model = baseModels[0];
61
- return normalized;
62
- }
63
-
64
- const filePath = typeof normalized.filePath === 'string' ? normalized.filePath.trim() : '';
65
- if (!filePath) {
66
- return null;
67
- }
68
-
69
- // 快速路径:全文件正则扫描(并发 + 缓存)。只对“缺模型”的会话触发。
70
- const fullFileModels = await readSessionModelsFromFile(filePath);
71
- if (fullFileModels.length > 0) {
72
- normalized.models = fullFileModels;
73
- normalized.model = fullFileModels[0];
74
- return normalized;
75
- }
76
-
77
- // 兜底:摘要解析(可能补 provider 等字段)
78
- const summaryOptions = {
79
- summaryReadBytes: SESSION_BROWSE_SUMMARY_READ_BYTES,
80
- titleReadBytes: SESSION_BROWSE_SUMMARY_READ_BYTES
81
- };
82
- let summary = null;
83
- try {
84
- summary = normalized.source === 'claude'
85
- ? parseClaudeSessionSummary(filePath, summaryOptions)
86
- : (normalized.source === 'gemini'
87
- ? parseGeminiSessionSummary(filePath, summaryOptions)
88
- : (normalized.source === 'codebuddy'
89
- ? parseCodeBuddySessionSummary(filePath, summaryOptions)
90
- : parseCodexSessionSummary(filePath, summaryOptions)));
91
- } catch (_) {
92
- summary = null;
93
- }
94
- if (!summary || typeof summary !== 'object' || Array.isArray(summary)) {
95
- return null;
96
- }
97
- const summaryModels = normalizeSessionModelList([
98
- ...(Array.isArray(summary.models) ? summary.models : []),
99
- summary.model
100
- ]);
101
- if (summaryModels.length === 0) {
102
- return null;
103
- }
104
- normalized.models = summaryModels;
105
- normalized.model = summaryModels[0];
106
- if ((!normalized.provider || !String(normalized.provider).trim()) && typeof summary.provider === 'string' && summary.provider.trim()) {
107
- normalized.provider = summary.provider.trim();
108
- }
109
- return normalized;
110
- }))
111
- );
112
-
113
- return normalizedSessions.filter(Boolean);
114
- }
115
-
116
- module.exports = {
117
- listSessionUsageCore
118
- };
1
+ const { createConcurrencyLimiter } = require('./session-usage.concurrent');
2
+ const { normalizeSessionModelList, createSessionModelsFileReader } = require('./session-usage.models');
3
+
4
+ async function listSessionUsageCore(params = {}, deps = {}) {
5
+ const {
6
+ fs,
7
+ listSessionBrowse,
8
+ parseCodexSessionSummary,
9
+ parseClaudeSessionSummary,
10
+ parseCodeBuddySessionSummary,
11
+ parseGeminiSessionSummary,
12
+ MAX_SESSION_USAGE_LIST_SIZE,
13
+ SESSION_BROWSE_SUMMARY_READ_BYTES
14
+ } = deps;
15
+
16
+ const source = params.source === 'codex' || params.source === 'claude' || params.source === 'gemini' || params.source === 'codebuddy'
17
+ ? params.source
18
+ : 'all';
19
+ const rawLimit = Number(params.limit);
20
+ const limit = Number.isFinite(rawLimit)
21
+ ? Math.max(1, Math.min(rawLimit, MAX_SESSION_USAGE_LIST_SIZE))
22
+ : MAX_SESSION_USAGE_LIST_SIZE;
23
+
24
+ const sessions = await listSessionBrowse({
25
+ source,
26
+ limit,
27
+ forceRefresh: !!params.forceRefresh
28
+ });
29
+ if (!Array.isArray(sessions) || sessions.length === 0) {
30
+ return [];
31
+ }
32
+
33
+ const { readSessionModelsFromFile } = createSessionModelsFileReader(fs, {
34
+ concurrency: 32,
35
+ maxEntries: 1500,
36
+ probeHeadBytes: 128 * 1024,
37
+ probeTailBytes: 128 * 1024
38
+ });
39
+
40
+ // CPU/IO 优化策略(面向 2000 会话):
41
+ // 1) 优先使用 listSessionBrowse 返回的 model/models(零 I/O)
42
+ // 2) 仅当缺少模型名时才读取/解析文件(必要时全文件扫描)
43
+ const limitNormalize = createConcurrencyLimiter(64);
44
+ const normalizedSessions = await Promise.all(
45
+ sessions.map((item) => limitNormalize(async () => {
46
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
47
+ return null;
48
+ }
49
+ const normalized = { ...item };
50
+ delete normalized.__messageCountExact;
51
+
52
+ const baseModels = normalizeSessionModelList([
53
+ ...(Array.isArray(normalized.models) ? normalized.models : []),
54
+ normalized.model,
55
+ normalized.modelName,
56
+ normalized.modelId
57
+ ]);
58
+ if (baseModels.length > 0) {
59
+ normalized.models = baseModels;
60
+ normalized.model = baseModels[0];
61
+ return normalized;
62
+ }
63
+
64
+ const filePath = typeof normalized.filePath === 'string' ? normalized.filePath.trim() : '';
65
+ if (!filePath) {
66
+ return null;
67
+ }
68
+
69
+ // 快速路径:全文件正则扫描(并发 + 缓存)。只对“缺模型”的会话触发。
70
+ const fullFileModels = await readSessionModelsFromFile(filePath);
71
+ if (fullFileModels.length > 0) {
72
+ normalized.models = fullFileModels;
73
+ normalized.model = fullFileModels[0];
74
+ return normalized;
75
+ }
76
+
77
+ // 兜底:摘要解析(可能补 provider 等字段)
78
+ const summaryOptions = {
79
+ summaryReadBytes: SESSION_BROWSE_SUMMARY_READ_BYTES,
80
+ titleReadBytes: SESSION_BROWSE_SUMMARY_READ_BYTES
81
+ };
82
+ let summary = null;
83
+ try {
84
+ summary = normalized.source === 'claude'
85
+ ? parseClaudeSessionSummary(filePath, summaryOptions)
86
+ : (normalized.source === 'gemini'
87
+ ? parseGeminiSessionSummary(filePath, summaryOptions)
88
+ : (normalized.source === 'codebuddy'
89
+ ? parseCodeBuddySessionSummary(filePath, summaryOptions)
90
+ : parseCodexSessionSummary(filePath, summaryOptions)));
91
+ } catch (_) {
92
+ summary = null;
93
+ }
94
+ if (!summary || typeof summary !== 'object' || Array.isArray(summary)) {
95
+ return null;
96
+ }
97
+ const summaryModels = normalizeSessionModelList([
98
+ ...(Array.isArray(summary.models) ? summary.models : []),
99
+ summary.model
100
+ ]);
101
+ if (summaryModels.length === 0) {
102
+ return null;
103
+ }
104
+ normalized.models = summaryModels;
105
+ normalized.model = summaryModels[0];
106
+ if ((!normalized.provider || !String(normalized.provider).trim()) && typeof summary.provider === 'string' && summary.provider.trim()) {
107
+ normalized.provider = summary.provider.trim();
108
+ }
109
+ return normalized;
110
+ }))
111
+ );
112
+
113
+ return normalizedSessions.filter(Boolean);
114
+ }
115
+
116
+ module.exports = {
117
+ listSessionUsageCore
118
+ };
@@ -1,176 +1,176 @@
1
- const { createConcurrencyLimiter } = require('./session-usage.concurrent');
2
-
3
- function isConcreteSessionModelName(value) {
4
- if (typeof value !== 'string') {
5
- return false;
6
- }
7
- const normalized = value.trim();
8
- if (!normalized) {
9
- return false;
10
- }
11
- return normalized.toLowerCase() !== '<synthetic>';
12
- }
13
-
14
- function normalizeSessionModelList(values = []) {
15
- const models = [];
16
- for (const value of values) {
17
- if (!isConcreteSessionModelName(value)) {
18
- continue;
19
- }
20
- const normalized = value.trim();
21
- if (models.includes(normalized)) {
22
- continue;
23
- }
24
- models.push(normalized);
25
- }
26
- return models;
27
- }
28
-
29
- function extractModelsFromJsonlText(content) {
30
- const models = [];
31
- const pushModel = (value) => {
32
- if (!isConcreteSessionModelName(value)) {
33
- return;
34
- }
35
- const normalized = value.trim();
36
- if (models.includes(normalized)) {
37
- return;
38
- }
39
- models.push(normalized);
40
- };
41
-
42
- if (typeof content !== 'string' || !content) {
43
- return models;
44
- }
45
-
46
- // 性能优化:避免逐行 JSON.parse(CPU 重),改为正则扫描提取常见 model 字段。
47
- const modelKeyRegex = /"(?:model|model_name|model_id|modelId|modelName|model_slug|modelSlug)"\s*:\s*"([^"\r\n]+)"/g;
48
- for (const match of content.matchAll(modelKeyRegex)) {
49
- pushModel(match[1]);
50
- }
51
-
52
- const modelsArrayRegex = /"models"\s*:\s*\[([^\]]{0,5000})\]/g;
53
- for (const match of content.matchAll(modelsArrayRegex)) {
54
- const chunk = match[1] || '';
55
- for (const item of chunk.matchAll(/"([^"\r\n]+)"/g)) {
56
- pushModel(item[1]);
57
- }
58
- }
59
-
60
- return models;
61
- }
62
-
63
- function createSessionModelsFileReader(fs, options = {}) {
64
- const cache = new Map();
65
- const maxEntries = Number.isFinite(Number(options.maxEntries))
66
- ? Math.max(50, Math.floor(Number(options.maxEntries)))
67
- : 500;
68
- const maxConcurrency = Number.isFinite(Number(options.concurrency))
69
- ? Math.max(1, Math.floor(Number(options.concurrency)))
70
- : 8;
71
- const probeHeadBytes = Number.isFinite(Number(options.probeHeadBytes))
72
- ? Math.max(1024, Math.floor(Number(options.probeHeadBytes)))
73
- : 128 * 1024;
74
- const probeTailBytes = Number.isFinite(Number(options.probeTailBytes))
75
- ? Math.max(1024, Math.floor(Number(options.probeTailBytes)))
76
- : 128 * 1024;
77
- const limitIo = createConcurrencyLimiter(maxConcurrency);
78
-
79
- async function readSessionModelsFromFile(filePath) {
80
- const targetPath = typeof filePath === 'string' ? filePath.trim() : '';
81
- if (!targetPath) {
82
- return [];
83
- }
84
-
85
- let stat = null;
86
- try {
87
- stat = await fs.promises.stat(targetPath);
88
- } catch (_) {
89
- stat = null;
90
- }
91
- if (!stat) {
92
- return [];
93
- }
94
-
95
- const cacheKey = `${targetPath}:${stat.size}:${stat.mtimeMs}`;
96
- if (cache.has(cacheKey)) {
97
- const cached = cache.get(cacheKey);
98
- try {
99
- const resolved = await Promise.resolve(cached);
100
- return Array.isArray(resolved) ? [...resolved] : [];
101
- } catch (_) {
102
- cache.delete(cacheKey);
103
- return [];
104
- }
105
- }
106
-
107
- const loadPromise = limitIo(async () => {
108
- // I/O 优化:先只读头/尾小片段;命中模型名则不读全文件
109
- let handle = null;
110
- try {
111
- handle = await fs.promises.open(targetPath, 'r');
112
- const size = Number.isFinite(Number(stat.size)) ? Number(stat.size) : 0;
113
- const headSize = Math.min(size, probeHeadBytes);
114
- const tailSize = Math.min(size, probeTailBytes);
115
-
116
- const headBuffer = Buffer.alloc(headSize);
117
- if (headSize > 0) {
118
- await handle.read(headBuffer, 0, headSize, 0);
119
- }
120
-
121
- const tailOffset = Math.max(0, size - tailSize);
122
- const tailBuffer = Buffer.alloc(tailSize);
123
- if (tailSize > 0) {
124
- await handle.read(tailBuffer, 0, tailSize, tailOffset);
125
- }
126
-
127
- const probeText = `${headBuffer.toString('utf8')}\n${tailBuffer.toString('utf8')}`;
128
- const probed = extractModelsFromJsonlText(probeText);
129
- if (probed.length > 0) {
130
- return probed;
131
- }
132
- } catch (_) {
133
- // ignore and fall back
134
- } finally {
135
- if (handle) {
136
- try { await handle.close(); } catch (_) {}
137
- }
138
- }
139
-
140
- // 兜底:读全文件(CPU/IO 重),仅在头/尾未命中时触发
141
- let content = '';
142
- try {
143
- content = await fs.promises.readFile(targetPath, 'utf-8');
144
- } catch (_) {
145
- return [];
146
- }
147
- return extractModelsFromJsonlText(content);
148
- });
149
-
150
- cache.set(cacheKey, loadPromise);
151
- let models = [];
152
- try {
153
- models = await loadPromise;
154
- } catch (_) {
155
- models = [];
156
- }
157
- cache.set(cacheKey, models);
158
- if (cache.size > maxEntries) {
159
- const firstKey = cache.keys().next().value;
160
- if (firstKey) cache.delete(firstKey);
161
- }
162
- return [...models];
163
- }
164
-
165
- return {
166
- readSessionModelsFromFile
167
- };
168
- }
169
-
170
- module.exports = {
171
- isConcreteSessionModelName,
172
- normalizeSessionModelList,
173
- extractModelsFromJsonlText,
174
- createSessionModelsFileReader
175
- };
176
-
1
+ const { createConcurrencyLimiter } = require('./session-usage.concurrent');
2
+
3
+ function isConcreteSessionModelName(value) {
4
+ if (typeof value !== 'string') {
5
+ return false;
6
+ }
7
+ const normalized = value.trim();
8
+ if (!normalized) {
9
+ return false;
10
+ }
11
+ return normalized.toLowerCase() !== '<synthetic>';
12
+ }
13
+
14
+ function normalizeSessionModelList(values = []) {
15
+ const models = [];
16
+ for (const value of values) {
17
+ if (!isConcreteSessionModelName(value)) {
18
+ continue;
19
+ }
20
+ const normalized = value.trim();
21
+ if (models.includes(normalized)) {
22
+ continue;
23
+ }
24
+ models.push(normalized);
25
+ }
26
+ return models;
27
+ }
28
+
29
+ function extractModelsFromJsonlText(content) {
30
+ const models = [];
31
+ const pushModel = (value) => {
32
+ if (!isConcreteSessionModelName(value)) {
33
+ return;
34
+ }
35
+ const normalized = value.trim();
36
+ if (models.includes(normalized)) {
37
+ return;
38
+ }
39
+ models.push(normalized);
40
+ };
41
+
42
+ if (typeof content !== 'string' || !content) {
43
+ return models;
44
+ }
45
+
46
+ // 性能优化:避免逐行 JSON.parse(CPU 重),改为正则扫描提取常见 model 字段。
47
+ const modelKeyRegex = /"(?:model|model_name|model_id|modelId|modelName|model_slug|modelSlug)"\s*:\s*"([^"\r\n]+)"/g;
48
+ for (const match of content.matchAll(modelKeyRegex)) {
49
+ pushModel(match[1]);
50
+ }
51
+
52
+ const modelsArrayRegex = /"models"\s*:\s*\[([^\]]{0,5000})\]/g;
53
+ for (const match of content.matchAll(modelsArrayRegex)) {
54
+ const chunk = match[1] || '';
55
+ for (const item of chunk.matchAll(/"([^"\r\n]+)"/g)) {
56
+ pushModel(item[1]);
57
+ }
58
+ }
59
+
60
+ return models;
61
+ }
62
+
63
+ function createSessionModelsFileReader(fs, options = {}) {
64
+ const cache = new Map();
65
+ const maxEntries = Number.isFinite(Number(options.maxEntries))
66
+ ? Math.max(50, Math.floor(Number(options.maxEntries)))
67
+ : 500;
68
+ const maxConcurrency = Number.isFinite(Number(options.concurrency))
69
+ ? Math.max(1, Math.floor(Number(options.concurrency)))
70
+ : 8;
71
+ const probeHeadBytes = Number.isFinite(Number(options.probeHeadBytes))
72
+ ? Math.max(1024, Math.floor(Number(options.probeHeadBytes)))
73
+ : 128 * 1024;
74
+ const probeTailBytes = Number.isFinite(Number(options.probeTailBytes))
75
+ ? Math.max(1024, Math.floor(Number(options.probeTailBytes)))
76
+ : 128 * 1024;
77
+ const limitIo = createConcurrencyLimiter(maxConcurrency);
78
+
79
+ async function readSessionModelsFromFile(filePath) {
80
+ const targetPath = typeof filePath === 'string' ? filePath.trim() : '';
81
+ if (!targetPath) {
82
+ return [];
83
+ }
84
+
85
+ let stat = null;
86
+ try {
87
+ stat = await fs.promises.stat(targetPath);
88
+ } catch (_) {
89
+ stat = null;
90
+ }
91
+ if (!stat) {
92
+ return [];
93
+ }
94
+
95
+ const cacheKey = `${targetPath}:${stat.size}:${stat.mtimeMs}`;
96
+ if (cache.has(cacheKey)) {
97
+ const cached = cache.get(cacheKey);
98
+ try {
99
+ const resolved = await Promise.resolve(cached);
100
+ return Array.isArray(resolved) ? [...resolved] : [];
101
+ } catch (_) {
102
+ cache.delete(cacheKey);
103
+ return [];
104
+ }
105
+ }
106
+
107
+ const loadPromise = limitIo(async () => {
108
+ // I/O 优化:先只读头/尾小片段;命中模型名则不读全文件
109
+ let handle = null;
110
+ try {
111
+ handle = await fs.promises.open(targetPath, 'r');
112
+ const size = Number.isFinite(Number(stat.size)) ? Number(stat.size) : 0;
113
+ const headSize = Math.min(size, probeHeadBytes);
114
+ const tailSize = Math.min(size, probeTailBytes);
115
+
116
+ const headBuffer = Buffer.alloc(headSize);
117
+ if (headSize > 0) {
118
+ await handle.read(headBuffer, 0, headSize, 0);
119
+ }
120
+
121
+ const tailOffset = Math.max(0, size - tailSize);
122
+ const tailBuffer = Buffer.alloc(tailSize);
123
+ if (tailSize > 0) {
124
+ await handle.read(tailBuffer, 0, tailSize, tailOffset);
125
+ }
126
+
127
+ const probeText = `${headBuffer.toString('utf8')}\n${tailBuffer.toString('utf8')}`;
128
+ const probed = extractModelsFromJsonlText(probeText);
129
+ if (probed.length > 0) {
130
+ return probed;
131
+ }
132
+ } catch (_) {
133
+ // ignore and fall back
134
+ } finally {
135
+ if (handle) {
136
+ try { await handle.close(); } catch (_) {}
137
+ }
138
+ }
139
+
140
+ // 兜底:读全文件(CPU/IO 重),仅在头/尾未命中时触发
141
+ let content = '';
142
+ try {
143
+ content = await fs.promises.readFile(targetPath, 'utf-8');
144
+ } catch (_) {
145
+ return [];
146
+ }
147
+ return extractModelsFromJsonlText(content);
148
+ });
149
+
150
+ cache.set(cacheKey, loadPromise);
151
+ let models = [];
152
+ try {
153
+ models = await loadPromise;
154
+ } catch (_) {
155
+ models = [];
156
+ }
157
+ cache.set(cacheKey, models);
158
+ if (cache.size > maxEntries) {
159
+ const firstKey = cache.keys().next().value;
160
+ if (firstKey) cache.delete(firstKey);
161
+ }
162
+ return [...models];
163
+ }
164
+
165
+ return {
166
+ readSessionModelsFromFile
167
+ };
168
+ }
169
+
170
+ module.exports = {
171
+ isConcreteSessionModelName,
172
+ normalizeSessionModelList,
173
+ extractModelsFromJsonlText,
174
+ createSessionModelsFileReader
175
+ };
176
+