codexmate 0.0.18 → 0.0.20

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 (73) hide show
  1. package/README.en.md +34 -17
  2. package/README.md +34 -25
  3. package/cli/config-health.js +338 -0
  4. package/cli.js +1570 -839
  5. package/lib/cli-models-utils.js +186 -27
  6. package/lib/cli-network-utils.js +117 -101
  7. package/package.json +8 -1
  8. package/web-ui/app.js +379 -5754
  9. package/web-ui/index.html +15 -2079
  10. package/web-ui/logic.agents-diff.mjs +386 -0
  11. package/web-ui/logic.claude.mjs +108 -0
  12. package/web-ui/logic.mjs +5 -793
  13. package/web-ui/logic.runtime.mjs +124 -0
  14. package/web-ui/logic.sessions.mjs +263 -0
  15. package/web-ui/modules/api.mjs +69 -0
  16. package/web-ui/modules/app.computed.dashboard.mjs +113 -0
  17. package/web-ui/modules/app.computed.index.mjs +13 -0
  18. package/web-ui/modules/app.computed.session.mjs +141 -0
  19. package/web-ui/modules/app.constants.mjs +15 -0
  20. package/web-ui/modules/app.methods.agents.mjs +493 -0
  21. package/web-ui/modules/app.methods.claude-config.mjs +174 -0
  22. package/web-ui/modules/app.methods.codex-config.mjs +640 -0
  23. package/web-ui/modules/app.methods.index.mjs +86 -0
  24. package/web-ui/modules/app.methods.install.mjs +157 -0
  25. package/web-ui/modules/app.methods.navigation.mjs +478 -0
  26. package/web-ui/modules/app.methods.openclaw-core.mjs +514 -0
  27. package/web-ui/modules/app.methods.openclaw-editing.mjs +337 -0
  28. package/web-ui/modules/app.methods.openclaw-persist.mjs +251 -0
  29. package/web-ui/modules/app.methods.providers.mjs +265 -0
  30. package/web-ui/modules/app.methods.runtime.mjs +323 -0
  31. package/web-ui/modules/app.methods.session-actions.mjs +457 -0
  32. package/web-ui/modules/app.methods.session-browser.mjs +435 -0
  33. package/web-ui/modules/app.methods.session-timeline.mjs +441 -0
  34. package/web-ui/modules/app.methods.session-trash.mjs +419 -0
  35. package/web-ui/modules/app.methods.startup-claude.mjs +406 -0
  36. package/web-ui/modules/config-mode.computed.mjs +1 -0
  37. package/web-ui/modules/skills.computed.mjs +26 -1
  38. package/web-ui/modules/skills.methods.mjs +154 -23
  39. package/web-ui/partials/index/layout-footer.html +69 -0
  40. package/web-ui/partials/index/layout-header.html +337 -0
  41. package/web-ui/partials/index/modal-config-template-agents.html +125 -0
  42. package/web-ui/partials/index/modal-confirm-toast.html +32 -0
  43. package/web-ui/partials/index/modal-health-check.html +72 -0
  44. package/web-ui/partials/index/modal-openclaw-config.html +275 -0
  45. package/web-ui/partials/index/modal-skills.html +184 -0
  46. package/web-ui/partials/index/modals-basic.html +196 -0
  47. package/web-ui/partials/index/panel-config-claude.html +100 -0
  48. package/web-ui/partials/index/panel-config-codex.html +237 -0
  49. package/web-ui/partials/index/panel-config-openclaw.html +84 -0
  50. package/web-ui/partials/index/panel-market.html +174 -0
  51. package/web-ui/partials/index/panel-sessions.html +387 -0
  52. package/web-ui/partials/index/panel-settings.html +166 -0
  53. package/web-ui/session-helpers.mjs +12 -0
  54. package/web-ui/source-bundle.cjs +233 -0
  55. package/web-ui/styles/base-theme.css +373 -0
  56. package/web-ui/styles/controls-forms.css +354 -0
  57. package/web-ui/styles/feedback.css +108 -0
  58. package/web-ui/styles/health-check-dialog.css +144 -0
  59. package/web-ui/styles/layout-shell.css +330 -0
  60. package/web-ui/styles/modals-core.css +449 -0
  61. package/web-ui/styles/navigation-panels.css +381 -0
  62. package/web-ui/styles/openclaw-structured.css +266 -0
  63. package/web-ui/styles/responsive.css +416 -0
  64. package/web-ui/styles/sessions-list.css +414 -0
  65. package/web-ui/styles/sessions-preview.css +405 -0
  66. package/web-ui/styles/sessions-toolbar-trash.css +243 -0
  67. package/web-ui/styles/sessions-usage.css +276 -0
  68. package/web-ui/styles/skills-list.css +298 -0
  69. package/web-ui/styles/skills-market.css +335 -0
  70. package/web-ui/styles/titles-cards.css +407 -0
  71. package/web-ui/styles.css +16 -4499
  72. package/doc/CHANGELOG.md +0 -32
  73. package/doc/CHANGELOG.zh-CN.md +0 -34
@@ -82,47 +82,202 @@ function normalizeWireApi(value) {
82
82
  return raw.replace(/[\s\-\/]/g, '_');
83
83
  }
84
84
 
85
+ function buildApiProbeUrlCandidates(baseUrl, pathSuffix) {
86
+ const normalized = normalizeBaseUrl(baseUrl);
87
+ if (!normalized) return [];
88
+
89
+ const safeSuffix = String(pathSuffix || '').replace(/^\/+/g, '');
90
+ if (!safeSuffix) return [normalized];
91
+
92
+ const candidates = [];
93
+ const pushUnique = (value) => {
94
+ if (value && !candidates.includes(value)) {
95
+ candidates.push(value);
96
+ }
97
+ };
98
+
99
+ let pathname = '';
100
+ try {
101
+ pathname = new URL(normalized).pathname.replace(/\/+$/g, '');
102
+ } catch (e) {
103
+ pathname = '';
104
+ }
105
+
106
+ const directUrl = `${normalized}/${safeSuffix}`;
107
+ const versionedUrl = joinApiUrl(normalized, safeSuffix);
108
+ if (/\/v\d+$/i.test(pathname)) {
109
+ pushUnique(directUrl);
110
+ return candidates;
111
+ }
112
+
113
+ if (!pathname || pathname === '/') {
114
+ pushUnique(versionedUrl);
115
+ pushUnique(directUrl);
116
+ return candidates;
117
+ }
118
+
119
+ pushUnique(directUrl);
120
+ pushUnique(versionedUrl);
121
+ return candidates;
122
+ }
123
+
85
124
  function buildModelsProbeUrl(baseUrl) {
86
- return joinApiUrl(baseUrl, 'models');
125
+ return buildApiProbeUrlCandidates(baseUrl, 'models')[0] || '';
87
126
  }
88
127
 
89
- function buildModelProbeSpec(provider, modelName, baseUrl) {
128
+ function buildModelProbeSpecs(provider, modelName, baseUrl) {
90
129
  const model = typeof modelName === 'string' ? modelName.trim() : '';
91
- if (!model) return null;
130
+ if (!model) return [];
92
131
 
93
132
  const wireApi = normalizeWireApi(provider && provider.wire_api);
133
+ let pathSuffix = 'responses';
134
+ let body = {
135
+ model,
136
+ input: 'ping',
137
+ max_output_tokens: 1
138
+ };
139
+
94
140
  if (wireApi === 'chat_completions' || wireApi === 'chat') {
95
- return {
96
- url: joinApiUrl(baseUrl, 'chat/completions'),
97
- body: {
98
- model,
99
- messages: [{ role: 'user', content: 'ping' }],
100
- max_tokens: 1,
101
- temperature: 0
102
- }
141
+ pathSuffix = 'chat/completions';
142
+ body = {
143
+ model,
144
+ messages: [{ role: 'user', content: 'ping' }],
145
+ max_tokens: 1,
146
+ temperature: 0
147
+ };
148
+ } else if (wireApi === 'completions') {
149
+ pathSuffix = 'completions';
150
+ body = {
151
+ model,
152
+ prompt: 'ping',
153
+ max_tokens: 1,
154
+ temperature: 0
103
155
  };
104
156
  }
105
157
 
106
- if (wireApi === 'completions') {
107
- return {
108
- url: joinApiUrl(baseUrl, 'completions'),
109
- body: {
110
- model,
111
- prompt: 'ping',
112
- max_tokens: 1,
113
- temperature: 0
114
- }
158
+ return buildApiProbeUrlCandidates(baseUrl, pathSuffix).map((url) => ({
159
+ url,
160
+ body
161
+ }));
162
+ }
163
+
164
+ function buildModelProbeSpec(provider, modelName, baseUrl) {
165
+ return buildModelProbeSpecs(provider, modelName, baseUrl)[0] || null;
166
+ }
167
+
168
+ function buildModelConversationSpecs(provider, modelName, baseUrl, prompt, options = {}) {
169
+ const model = typeof modelName === 'string' ? modelName.trim() : '';
170
+ const userPrompt = typeof prompt === 'string' ? prompt.trim() : '';
171
+ if (!model || !userPrompt) return [];
172
+
173
+ const wireApi = normalizeWireApi(provider && provider.wire_api);
174
+ const maxOutputTokens = Number.isFinite(options.maxOutputTokens)
175
+ ? Math.max(1, Number(options.maxOutputTokens))
176
+ : 256;
177
+ let pathSuffix = 'responses';
178
+ let body = {
179
+ model,
180
+ input: userPrompt,
181
+ max_output_tokens: maxOutputTokens
182
+ };
183
+
184
+ if (wireApi === 'chat_completions' || wireApi === 'chat') {
185
+ pathSuffix = 'chat/completions';
186
+ body = {
187
+ model,
188
+ messages: [{ role: 'user', content: userPrompt }],
189
+ max_tokens: maxOutputTokens
190
+ };
191
+ } else if (wireApi === 'completions') {
192
+ pathSuffix = 'completions';
193
+ body = {
194
+ model,
195
+ prompt: userPrompt,
196
+ max_tokens: maxOutputTokens
115
197
  };
116
198
  }
117
199
 
118
- return {
119
- url: joinApiUrl(baseUrl, 'responses'),
120
- body: {
121
- model,
122
- input: 'ping',
123
- max_output_tokens: 1
200
+ return buildApiProbeUrlCandidates(baseUrl, pathSuffix).map((url) => ({
201
+ url,
202
+ body,
203
+ wireApi
204
+ }));
205
+ }
206
+
207
+ function collectStructuredText(content, pieces) {
208
+ if (typeof content === 'string') {
209
+ const text = content.trim();
210
+ if (text) pieces.push(text);
211
+ return;
212
+ }
213
+ if (Array.isArray(content)) {
214
+ for (const item of content) {
215
+ collectStructuredText(item, pieces);
124
216
  }
125
- };
217
+ return;
218
+ }
219
+ if (!content || typeof content !== 'object') {
220
+ return;
221
+ }
222
+
223
+ if (typeof content.output_text === 'string' && content.output_text.trim()) {
224
+ pieces.push(content.output_text.trim());
225
+ }
226
+ if (typeof content.text === 'string' && content.text.trim()) {
227
+ pieces.push(content.text.trim());
228
+ }
229
+ if (typeof content.content === 'string' && content.content.trim()) {
230
+ pieces.push(content.content.trim());
231
+ }
232
+
233
+ if (Array.isArray(content.content)) {
234
+ collectStructuredText(content.content, pieces);
235
+ }
236
+ if (content.message) {
237
+ collectStructuredText(content.message, pieces);
238
+ }
239
+ }
240
+
241
+ function extractModelResponseText(payload) {
242
+ if (!payload || typeof payload !== 'object') {
243
+ return '';
244
+ }
245
+
246
+ if (typeof payload.output_text === 'string' && payload.output_text.trim()) {
247
+ return payload.output_text.trim();
248
+ }
249
+
250
+ const pieces = [];
251
+
252
+ if (Array.isArray(payload.output)) {
253
+ for (const item of payload.output) {
254
+ collectStructuredText(item, pieces);
255
+ }
256
+ }
257
+
258
+ if (Array.isArray(payload.choices)) {
259
+ for (const choice of payload.choices) {
260
+ if (!choice || typeof choice !== 'object') continue;
261
+ if (typeof choice.text === 'string' && choice.text.trim()) {
262
+ pieces.push(choice.text.trim());
263
+ }
264
+ if (choice.message) {
265
+ collectStructuredText(choice.message, pieces);
266
+ }
267
+ }
268
+ }
269
+
270
+ if (payload.message) {
271
+ collectStructuredText(payload.message, pieces);
272
+ }
273
+ if (payload.content) {
274
+ collectStructuredText(payload.content, pieces);
275
+ }
276
+ if (typeof payload.text === 'string' && payload.text.trim()) {
277
+ pieces.push(payload.text.trim());
278
+ }
279
+
280
+ return Array.from(new Set(pieces)).join('\n\n').trim();
126
281
  }
127
282
 
128
283
  function hashModelsCacheValue(value) {
@@ -145,8 +300,12 @@ module.exports = {
145
300
  hasModelsListPayload,
146
301
  extractModelIds,
147
302
  normalizeWireApi,
303
+ buildApiProbeUrlCandidates,
148
304
  buildModelsProbeUrl,
305
+ buildModelProbeSpecs,
149
306
  buildModelProbeSpec,
307
+ buildModelConversationSpecs,
308
+ extractModelResponseText,
150
309
  hashModelsCacheValue,
151
310
  buildModelsCacheKey
152
311
  };
@@ -1,58 +1,38 @@
1
1
  const http = require('http');
2
2
  const https = require('https');
3
3
 
4
- function probeUrl(targetUrl, options = {}) {
5
- return new Promise((resolve) => {
6
- let parsed;
7
- try {
8
- parsed = new URL(targetUrl);
9
- } catch (e) {
10
- return resolve({ ok: false, error: 'Invalid URL' });
11
- }
12
-
13
- const protocol = parsed.protocol;
14
- if (protocol !== 'http:' && protocol !== 'https:') {
15
- return resolve({
16
- ok: false,
17
- error: `ERR_INVALID_PROTOCOL: Protocol "${protocol}" not supported. Expected "http:" or "https:"`
18
- });
19
- }
20
-
21
- const transport = protocol === 'https:' ? https : http;
22
- const headers = {
23
- 'User-Agent': 'codexmate-health-check',
24
- 'Accept': 'application/json'
25
- };
26
- if (options.apiKey) {
27
- headers['Authorization'] = `Bearer ${options.apiKey}`;
28
- }
4
+ function shouldRetryWithIpv4(result) {
5
+ if (!result || result.ok || typeof result.error !== 'string') {
6
+ return false;
7
+ }
8
+ return /timeout|timed out|ETIMEDOUT|ENETUNREACH|EHOSTUNREACH|EAI_AGAIN/i.test(result.error);
9
+ }
29
10
 
30
- const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 0;
31
- const maxBytes = Number.isFinite(options.maxBytes) ? options.maxBytes : 256 * 1024;
11
+ function performProbeRequest(transport, parsed, requestOptions, options = {}) {
12
+ return new Promise((resolve) => {
32
13
  const start = Date.now();
33
- const req = transport.request(parsed, { method: 'GET', headers }, (res) => {
14
+ const req = transport.request(parsed, requestOptions, (res) => {
34
15
  const chunks = [];
35
16
  let size = 0;
36
17
  res.on('data', (chunk) => {
37
18
  if (!chunk) return;
38
19
  size += chunk.length;
39
- if (size <= maxBytes) {
20
+ if (size <= options.maxBytes) {
40
21
  chunks.push(chunk);
41
22
  }
42
23
  });
43
24
  res.on('end', () => {
44
- const body = chunks.length > 0 ? Buffer.concat(chunks).toString('utf-8') : '';
45
25
  resolve({
46
26
  ok: true,
47
27
  status: res.statusCode || 0,
48
28
  durationMs: Date.now() - start,
49
- body
29
+ body: chunks.length > 0 ? Buffer.concat(chunks).toString('utf-8') : ''
50
30
  });
51
31
  });
52
32
  });
53
33
 
54
- if (timeoutMs > 0) {
55
- req.setTimeout(timeoutMs, () => {
34
+ if (options.timeoutMs > 0) {
35
+ req.setTimeout(options.timeoutMs, () => {
56
36
  req.destroy(new Error('timeout'));
57
37
  });
58
38
  }
@@ -65,81 +45,117 @@ function probeUrl(targetUrl, options = {}) {
65
45
  });
66
46
  });
67
47
 
48
+ if (options.payload) {
49
+ req.write(options.payload);
50
+ }
68
51
  req.end();
69
52
  });
70
53
  }
71
54
 
72
- function probeJsonPost(targetUrl, body, options = {}) {
73
- return new Promise((resolve) => {
74
- let parsed;
75
- try {
76
- parsed = new URL(targetUrl);
77
- } catch (e) {
78
- return resolve({ ok: false, error: 'Invalid URL' });
79
- }
80
-
81
- const protocol = parsed.protocol;
82
- if (protocol !== 'http:' && protocol !== 'https:') {
83
- return resolve({
84
- ok: false,
85
- error: `ERR_INVALID_PROTOCOL: Protocol "${protocol}" not supported. Expected "http:" or "https:"`
86
- });
87
- }
88
-
89
- const transport = protocol === 'https:' ? https : http;
90
- const headers = {
91
- 'User-Agent': 'codexmate-health-check',
92
- 'Accept': 'application/json',
93
- 'Content-Type': 'application/json'
55
+ async function probeUrl(targetUrl, options = {}) {
56
+ let parsed;
57
+ try {
58
+ parsed = new URL(targetUrl);
59
+ } catch (e) {
60
+ return { ok: false, error: 'Invalid URL' };
61
+ }
62
+
63
+ const protocol = parsed.protocol;
64
+ if (protocol !== 'http:' && protocol !== 'https:') {
65
+ return {
66
+ ok: false,
67
+ error: `ERR_INVALID_PROTOCOL: Protocol "${protocol}" not supported. Expected "http:" or "https:"`
94
68
  };
95
- if (options.apiKey) {
96
- headers['Authorization'] = `Bearer ${options.apiKey}`;
97
- }
98
-
99
- const payload = JSON.stringify(body || {});
100
- headers['Content-Length'] = Buffer.byteLength(payload);
101
-
102
- const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 0;
103
- const maxBytes = Number.isFinite(options.maxBytes) ? options.maxBytes : 256 * 1024;
104
- const start = Date.now();
105
- const req = transport.request(parsed, { method: 'POST', headers }, (res) => {
106
- const chunks = [];
107
- let size = 0;
108
- res.on('data', (chunk) => {
109
- if (!chunk) return;
110
- size += chunk.length;
111
- if (size <= maxBytes) {
112
- chunks.push(chunk);
113
- }
114
- });
115
- res.on('end', () => {
116
- const bodyText = chunks.length > 0 ? Buffer.concat(chunks).toString('utf-8') : '';
117
- resolve({
118
- ok: true,
119
- status: res.statusCode || 0,
120
- durationMs: Date.now() - start,
121
- body: bodyText
122
- });
123
- });
124
- });
125
-
126
- if (timeoutMs > 0) {
127
- req.setTimeout(timeoutMs, () => {
128
- req.destroy(new Error('timeout'));
129
- });
130
- }
131
-
132
- req.on('error', (err) => {
133
- resolve({
134
- ok: false,
135
- error: err.message || 'request failed',
136
- durationMs: Date.now() - start
137
- });
138
- });
69
+ }
70
+
71
+ const transport = protocol === 'https:' ? https : http;
72
+ const headers = {
73
+ 'User-Agent': 'codexmate-health-check',
74
+ 'Accept': 'application/json'
75
+ };
76
+ if (options.apiKey) {
77
+ headers['Authorization'] = `Bearer ${options.apiKey}`;
78
+ }
79
+
80
+ const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 0;
81
+ const maxBytes = Number.isFinite(options.maxBytes) ? options.maxBytes : 256 * 1024;
82
+ const requestOptions = {
83
+ method: 'GET',
84
+ headers
85
+ };
86
+
87
+ const firstResult = await performProbeRequest(transport, parsed, requestOptions, {
88
+ timeoutMs,
89
+ maxBytes
90
+ });
91
+ if (!shouldRetryWithIpv4(firstResult)) {
92
+ return firstResult;
93
+ }
94
+
95
+ const secondResult = await performProbeRequest(transport, parsed, {
96
+ ...requestOptions,
97
+ family: 4
98
+ }, {
99
+ timeoutMs,
100
+ maxBytes
101
+ });
102
+ return secondResult.ok ? secondResult : firstResult;
103
+ }
139
104
 
140
- req.write(payload);
141
- req.end();
105
+ async function probeJsonPost(targetUrl, body, options = {}) {
106
+ let parsed;
107
+ try {
108
+ parsed = new URL(targetUrl);
109
+ } catch (e) {
110
+ return { ok: false, error: 'Invalid URL' };
111
+ }
112
+
113
+ const protocol = parsed.protocol;
114
+ if (protocol !== 'http:' && protocol !== 'https:') {
115
+ return {
116
+ ok: false,
117
+ error: `ERR_INVALID_PROTOCOL: Protocol "${protocol}" not supported. Expected "http:" or "https:"`
118
+ };
119
+ }
120
+
121
+ const transport = protocol === 'https:' ? https : http;
122
+ const headers = {
123
+ 'User-Agent': 'codexmate-health-check',
124
+ 'Accept': 'application/json',
125
+ 'Content-Type': 'application/json'
126
+ };
127
+ if (options.apiKey) {
128
+ headers['Authorization'] = `Bearer ${options.apiKey}`;
129
+ }
130
+
131
+ const payload = JSON.stringify(body || {});
132
+ headers['Content-Length'] = Buffer.byteLength(payload);
133
+
134
+ const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 0;
135
+ const maxBytes = Number.isFinite(options.maxBytes) ? options.maxBytes : 256 * 1024;
136
+ const requestOptions = {
137
+ method: 'POST',
138
+ headers
139
+ };
140
+
141
+ const firstResult = await performProbeRequest(transport, parsed, requestOptions, {
142
+ timeoutMs,
143
+ maxBytes,
144
+ payload
145
+ });
146
+ if (!shouldRetryWithIpv4(firstResult)) {
147
+ return firstResult;
148
+ }
149
+
150
+ const secondResult = await performProbeRequest(transport, parsed, {
151
+ ...requestOptions,
152
+ family: 4
153
+ }, {
154
+ timeoutMs,
155
+ maxBytes,
156
+ payload
142
157
  });
158
+ return secondResult.ok ? secondResult : firstResult;
143
159
  }
144
160
 
145
161
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "description": "Codex/Claude Code 配置与会话管理 CLI + Web 工具",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "files": [
10
10
  "cli.js",
11
+ "cli/",
11
12
  "web-ui.html",
12
13
  "lib/",
13
14
  "web-ui/",
@@ -26,10 +27,16 @@
26
27
  "scripts": {
27
28
  "dev": "node cli.js run",
28
29
  "start": "node cli.js",
30
+ "reset": "node cmd/reset-main.js",
29
31
  "docs:dev": "node ./node_modules/vitepress/dist/node/cli.js dev site",
30
32
  "docs:build": "node ./node_modules/vitepress/dist/node/cli.js build site",
31
33
  "docs:preview": "node ./node_modules/vitepress/dist/node/cli.js preview site",
34
+ "ci:install": "node scripts/run-ci-check.js install",
35
+ "ci:lint": "node scripts/run-ci-check.js lint",
36
+ "ci:test": "node scripts/run-ci-check.js test",
37
+ "lint": "node scripts/lint.js",
32
38
  "test": "npm run test:unit && npm run test:e2e",
39
+ "test:ci": "node scripts/run-ci-check.js all",
33
40
  "test:unit": "node tests/unit/run.mjs",
34
41
  "test:e2e": "node tests/e2e/run.js",
35
42
  "pretest": "node scripts/ensure-test-deps.js"