codexmate 0.0.20 → 0.0.21

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 (102) hide show
  1. package/README.en.md +349 -259
  2. package/README.md +284 -252
  3. package/cli/agents-files.js +162 -0
  4. package/cli/archive-helpers.js +446 -0
  5. package/cli/auth-profiles.js +359 -0
  6. package/cli/builtin-proxy.js +580 -0
  7. package/cli/claude-proxy.js +998 -0
  8. package/cli/config-bootstrap.js +384 -0
  9. package/cli/config-health.js +338 -338
  10. package/cli/openclaw-config.js +629 -0
  11. package/cli/skills.js +1141 -0
  12. package/cli/zip-commands.js +510 -0
  13. package/cli.js +13101 -13497
  14. package/lib/cli-file-utils.js +151 -151
  15. package/lib/cli-models-utils.js +419 -311
  16. package/lib/cli-network-utils.js +164 -164
  17. package/lib/cli-path-utils.js +69 -0
  18. package/lib/cli-session-utils.js +121 -121
  19. package/lib/cli-sessions.js +386 -0
  20. package/lib/cli-utils.js +155 -155
  21. package/lib/download-artifacts.js +77 -0
  22. package/lib/mcp-stdio.js +440 -440
  23. package/lib/task-orchestrator.js +869 -0
  24. package/lib/text-diff.js +303 -303
  25. package/lib/workflow-engine.js +340 -340
  26. package/package.json +74 -70
  27. package/res/json5.min.js +1 -1
  28. package/res/vue.global.prod.js +13 -0
  29. package/web-ui/app.js +530 -397
  30. package/web-ui/index.html +33 -30
  31. package/web-ui/logic.agents-diff.mjs +386 -386
  32. package/web-ui/logic.claude.mjs +168 -108
  33. package/web-ui/logic.mjs +5 -5
  34. package/web-ui/logic.runtime.mjs +124 -124
  35. package/web-ui/logic.sessions.mjs +581 -263
  36. package/web-ui/modules/api.mjs +90 -69
  37. package/web-ui/modules/app.computed.dashboard.mjs +113 -113
  38. package/web-ui/modules/app.computed.index.mjs +15 -13
  39. package/web-ui/modules/app.computed.main-tabs.mjs +195 -0
  40. package/web-ui/modules/app.computed.session.mjs +507 -141
  41. package/web-ui/modules/app.constants.mjs +15 -15
  42. package/web-ui/modules/app.methods.agents.mjs +493 -493
  43. package/web-ui/modules/app.methods.claude-config.mjs +174 -174
  44. package/web-ui/modules/app.methods.codex-config.mjs +640 -640
  45. package/web-ui/modules/app.methods.index.mjs +88 -86
  46. package/web-ui/modules/app.methods.install.mjs +149 -157
  47. package/web-ui/modules/app.methods.navigation.mjs +619 -478
  48. package/web-ui/modules/app.methods.openclaw-core.mjs +814 -514
  49. package/web-ui/modules/app.methods.openclaw-editing.mjs +372 -337
  50. package/web-ui/modules/app.methods.openclaw-persist.mjs +369 -251
  51. package/web-ui/modules/app.methods.providers.mjs +363 -265
  52. package/web-ui/modules/app.methods.runtime.mjs +323 -323
  53. package/web-ui/modules/app.methods.session-actions.mjs +520 -457
  54. package/web-ui/modules/app.methods.session-browser.mjs +626 -435
  55. package/web-ui/modules/app.methods.session-timeline.mjs +448 -441
  56. package/web-ui/modules/app.methods.session-trash.mjs +422 -419
  57. package/web-ui/modules/app.methods.startup-claude.mjs +412 -406
  58. package/web-ui/modules/app.methods.task-orchestration.mjs +471 -0
  59. package/web-ui/modules/config-mode.computed.mjs +126 -124
  60. package/web-ui/modules/skills.computed.mjs +107 -107
  61. package/web-ui/modules/skills.methods.mjs +481 -481
  62. package/web-ui/partials/index/layout-footer.html +13 -69
  63. package/web-ui/partials/index/layout-header.html +402 -337
  64. package/web-ui/partials/index/modal-config-template-agents.html +125 -125
  65. package/web-ui/partials/index/modal-confirm-toast.html +32 -32
  66. package/web-ui/partials/index/modal-health-check.html +72 -72
  67. package/web-ui/partials/index/modal-openclaw-config.html +280 -275
  68. package/web-ui/partials/index/modal-skills.html +184 -184
  69. package/web-ui/partials/index/modals-basic.html +156 -196
  70. package/web-ui/partials/index/panel-config-claude.html +126 -100
  71. package/web-ui/partials/index/panel-config-codex.html +237 -237
  72. package/web-ui/partials/index/panel-config-openclaw.html +78 -84
  73. package/web-ui/partials/index/panel-docs.html +130 -0
  74. package/web-ui/partials/index/panel-market.html +174 -174
  75. package/web-ui/partials/index/panel-orchestration.html +397 -0
  76. package/web-ui/partials/index/panel-sessions.html +292 -387
  77. package/web-ui/partials/index/panel-settings.html +190 -166
  78. package/web-ui/partials/index/panel-usage.html +213 -0
  79. package/web-ui/session-helpers.mjs +559 -362
  80. package/web-ui/source-bundle.cjs +233 -233
  81. package/web-ui/styles/base-theme.css +271 -373
  82. package/web-ui/styles/controls-forms.css +360 -354
  83. package/web-ui/styles/docs-panel.css +182 -0
  84. package/web-ui/styles/feedback.css +108 -108
  85. package/web-ui/styles/health-check-dialog.css +144 -144
  86. package/web-ui/styles/layout-shell.css +376 -330
  87. package/web-ui/styles/modals-core.css +464 -449
  88. package/web-ui/styles/navigation-panels.css +348 -381
  89. package/web-ui/styles/openclaw-structured.css +266 -266
  90. package/web-ui/styles/responsive.css +450 -416
  91. package/web-ui/styles/sessions-list.css +400 -414
  92. package/web-ui/styles/sessions-preview.css +411 -405
  93. package/web-ui/styles/sessions-toolbar-trash.css +243 -243
  94. package/web-ui/styles/sessions-usage.css +628 -276
  95. package/web-ui/styles/skills-list.css +296 -298
  96. package/web-ui/styles/skills-market.css +335 -335
  97. package/web-ui/styles/task-orchestration.css +776 -0
  98. package/web-ui/styles/titles-cards.css +408 -407
  99. package/web-ui/styles.css +18 -16
  100. package/web-ui.html +17 -17
  101. package/res/screenshot.png +0 -0
  102. package/res/vue.global.js +0 -18552
@@ -1,108 +1,168 @@
1
- export function normalizeClaudeValue(value) {
2
- return typeof value === 'string' ? value.trim() : '';
3
- }
4
-
5
- export function normalizeClaudeConfig(config) {
6
- const safe = config && typeof config === 'object' ? config : {};
7
- const apiKey = normalizeClaudeValue(safe.apiKey);
8
- const authToken = normalizeClaudeValue(safe.authToken);
9
- const useKey = normalizeClaudeValue(safe.useKey);
10
- const externalCredentialType = normalizeClaudeValue(safe.externalCredentialType)
11
- || (apiKey ? '' : (authToken ? 'auth-token' : (useKey ? 'claude-code-use-key' : '')));
12
- return {
13
- apiKey,
14
- baseUrl: normalizeClaudeValue(safe.baseUrl),
15
- model: normalizeClaudeValue(safe.model),
16
- authToken,
17
- useKey,
18
- externalCredentialType
19
- };
20
- }
21
-
22
- export function normalizeClaudeSettingsEnv(env) {
23
- const safe = env && typeof env === 'object' ? env : {};
24
- const apiKey = normalizeClaudeValue(safe.ANTHROPIC_API_KEY);
25
- const authToken = normalizeClaudeValue(safe.ANTHROPIC_AUTH_TOKEN);
26
- const useKey = normalizeClaudeValue(safe.CLAUDE_CODE_USE_KEY);
27
- return {
28
- apiKey,
29
- baseUrl: normalizeClaudeValue(safe.ANTHROPIC_BASE_URL),
30
- model: normalizeClaudeValue(safe.ANTHROPIC_MODEL) || 'glm-4.7',
31
- authToken,
32
- useKey,
33
- externalCredentialType: apiKey
34
- ? ''
35
- : (authToken ? 'auth-token' : (useKey ? 'claude-code-use-key' : ''))
36
- };
37
- }
38
-
39
- function normalizeClaudeComparableUrl(value) {
40
- const trimmed = normalizeClaudeValue(value);
41
- if (!trimmed) return '';
42
- return trimmed.replace(/\/+$/g, '');
43
- }
44
-
45
- function hasClaudeCredential(config = {}) {
46
- return !!(config.apiKey || config.authToken || config.useKey);
47
- }
48
-
49
- export function matchClaudeConfigFromSettings(claudeConfigs = {}, env = {}) {
50
- const normalizedSettings = normalizeClaudeSettingsEnv(env);
51
- if (!normalizedSettings.baseUrl || !normalizedSettings.model || !hasClaudeCredential(normalizedSettings)) {
52
- return '';
53
- }
54
- const comparableSettingsUrl = normalizeClaudeComparableUrl(normalizedSettings.baseUrl);
55
- const entries = Object.entries(claudeConfigs || {});
56
- for (const [name, config] of entries) {
57
- const normalizedConfig = normalizeClaudeConfig(config);
58
- if (!normalizedConfig.baseUrl || !normalizedConfig.model) {
59
- continue;
60
- }
61
- if (normalizeClaudeComparableUrl(normalizedConfig.baseUrl) !== comparableSettingsUrl
62
- || normalizedConfig.model !== normalizedSettings.model) {
63
- continue;
64
- }
65
- if (normalizedSettings.apiKey && normalizedConfig.apiKey === normalizedSettings.apiKey) {
66
- return name;
67
- }
68
- if (!normalizedSettings.apiKey
69
- && normalizedConfig.apiKey === ''
70
- && normalizedConfig.externalCredentialType
71
- && normalizedConfig.externalCredentialType === normalizedSettings.externalCredentialType) {
72
- return name;
73
- }
74
- }
75
- return '';
76
- }
77
-
78
- export function findDuplicateClaudeConfigName(claudeConfigs = {}, config) {
79
- const normalized = normalizeClaudeConfig(config);
80
- if (!normalized.baseUrl || !normalized.model) {
81
- return '';
82
- }
83
- const comparableUrl = normalizeClaudeComparableUrl(normalized.baseUrl);
84
- const isExternal = !normalized.apiKey && !!normalized.externalCredentialType;
85
- if (!normalized.apiKey && !isExternal) {
86
- return '';
87
- }
88
- const entries = Object.entries(claudeConfigs || {});
89
- for (const [name, existing] of entries) {
90
- const normalizedExisting = normalizeClaudeConfig(existing);
91
- if (!normalizedExisting.baseUrl || !normalizedExisting.model) {
92
- continue;
93
- }
94
- if (normalizeClaudeComparableUrl(normalizedExisting.baseUrl) !== comparableUrl
95
- || normalizedExisting.model !== normalized.model) {
96
- continue;
97
- }
98
- if (normalized.apiKey && normalizedExisting.apiKey === normalized.apiKey) {
99
- return name;
100
- }
101
- if (isExternal
102
- && !normalizedExisting.apiKey
103
- && normalizedExisting.externalCredentialType === normalized.externalCredentialType) {
104
- return name;
105
- }
106
- }
107
- return '';
108
- }
1
+ const DEFAULT_CLAUDE_MODEL_CATALOG = Object.freeze([
2
+ 'claude-opus-4-6',
3
+ 'claude-opus-4-1',
4
+ 'claude-opus-4',
5
+ 'claude-sonnet-4-6',
6
+ 'claude-sonnet-4-5',
7
+ 'claude-sonnet-4',
8
+ 'claude-haiku-4-5',
9
+ 'claude-3-7-sonnet',
10
+ 'claude-3-5-sonnet',
11
+ 'claude-3-5-haiku',
12
+ 'claude-3-opus',
13
+ 'claude-3-sonnet',
14
+ 'claude-3-haiku'
15
+ ]);
16
+
17
+ const BIGMODEL_CLAUDE_EXTRA_MODELS = Object.freeze([
18
+ 'glm-3-turbo',
19
+ 'glm-4',
20
+ 'glm-4-0520',
21
+ 'glm-4-plus',
22
+ 'glm-4-air',
23
+ 'glm-4-airx',
24
+ 'glm-4-flash',
25
+ 'glm-4-flashx',
26
+ 'glm-4v',
27
+ 'glm-4v-flash',
28
+ 'glm-4v-plus',
29
+ 'glm-4v-plus-0111',
30
+ 'glm-4.5',
31
+ 'glm-4.5-air',
32
+ 'glm-4.5v',
33
+ 'glm-4.6',
34
+ 'glm-4.6v',
35
+ 'glm-4.7',
36
+ 'glm-4.7-flash',
37
+ 'glm-4.7-flashx',
38
+ 'glm-5',
39
+ 'glm-5-turbo',
40
+ 'glm-5.1',
41
+ 'glm-5v',
42
+ 'glm-5v-turbo',
43
+ 'glm-z1',
44
+ 'glm-z1-air',
45
+ 'glm-coding'
46
+ ]);
47
+
48
+ export function normalizeClaudeValue(value) {
49
+ return typeof value === 'string' ? value.trim() : '';
50
+ }
51
+
52
+ export function getClaudeModelCatalogForBaseUrl(baseUrl) {
53
+ const normalized = normalizeClaudeValue(baseUrl).toLowerCase().replace(/\/+$/g, '');
54
+ const models = [...DEFAULT_CLAUDE_MODEL_CATALOG];
55
+ if (normalized.includes('bigmodel.cn') && normalized.includes('/anthropic')) {
56
+ for (const model of BIGMODEL_CLAUDE_EXTRA_MODELS) {
57
+ if (!models.includes(model)) {
58
+ models.push(model);
59
+ }
60
+ }
61
+ }
62
+ return models;
63
+ }
64
+
65
+ export function normalizeClaudeConfig(config) {
66
+ const safe = config && typeof config === 'object' ? config : {};
67
+ const apiKey = normalizeClaudeValue(safe.apiKey);
68
+ const authToken = normalizeClaudeValue(safe.authToken);
69
+ const useKey = normalizeClaudeValue(safe.useKey);
70
+ const externalCredentialType = normalizeClaudeValue(safe.externalCredentialType)
71
+ || (apiKey ? '' : (authToken ? 'auth-token' : (useKey ? 'claude-code-use-key' : '')));
72
+ return {
73
+ apiKey,
74
+ baseUrl: normalizeClaudeValue(safe.baseUrl),
75
+ model: normalizeClaudeValue(safe.model),
76
+ authToken,
77
+ useKey,
78
+ externalCredentialType
79
+ };
80
+ }
81
+
82
+ export function normalizeClaudeSettingsEnv(env) {
83
+ const safe = env && typeof env === 'object' ? env : {};
84
+ const apiKey = normalizeClaudeValue(safe.ANTHROPIC_API_KEY);
85
+ const authToken = normalizeClaudeValue(safe.ANTHROPIC_AUTH_TOKEN);
86
+ const useKey = normalizeClaudeValue(safe.CLAUDE_CODE_USE_KEY);
87
+ return {
88
+ apiKey,
89
+ baseUrl: normalizeClaudeValue(safe.ANTHROPIC_BASE_URL),
90
+ model: normalizeClaudeValue(safe.ANTHROPIC_MODEL) || 'glm-4.7',
91
+ authToken,
92
+ useKey,
93
+ externalCredentialType: apiKey
94
+ ? ''
95
+ : (authToken ? 'auth-token' : (useKey ? 'claude-code-use-key' : ''))
96
+ };
97
+ }
98
+
99
+ function normalizeClaudeComparableUrl(value) {
100
+ const trimmed = normalizeClaudeValue(value);
101
+ if (!trimmed) return '';
102
+ return trimmed.replace(/\/+$/g, '');
103
+ }
104
+
105
+ function hasClaudeCredential(config = {}) {
106
+ return !!(config.apiKey || config.authToken || config.useKey);
107
+ }
108
+
109
+ export function matchClaudeConfigFromSettings(claudeConfigs = {}, env = {}) {
110
+ const normalizedSettings = normalizeClaudeSettingsEnv(env);
111
+ if (!normalizedSettings.baseUrl || !normalizedSettings.model || !hasClaudeCredential(normalizedSettings)) {
112
+ return '';
113
+ }
114
+ const comparableSettingsUrl = normalizeClaudeComparableUrl(normalizedSettings.baseUrl);
115
+ const entries = Object.entries(claudeConfigs || {});
116
+ for (const [name, config] of entries) {
117
+ const normalizedConfig = normalizeClaudeConfig(config);
118
+ if (!normalizedConfig.baseUrl || !normalizedConfig.model) {
119
+ continue;
120
+ }
121
+ if (normalizeClaudeComparableUrl(normalizedConfig.baseUrl) !== comparableSettingsUrl
122
+ || normalizedConfig.model !== normalizedSettings.model) {
123
+ continue;
124
+ }
125
+ if (normalizedSettings.apiKey && normalizedConfig.apiKey === normalizedSettings.apiKey) {
126
+ return name;
127
+ }
128
+ if (!normalizedSettings.apiKey
129
+ && normalizedConfig.apiKey === ''
130
+ && normalizedConfig.externalCredentialType
131
+ && normalizedConfig.externalCredentialType === normalizedSettings.externalCredentialType) {
132
+ return name;
133
+ }
134
+ }
135
+ return '';
136
+ }
137
+
138
+ export function findDuplicateClaudeConfigName(claudeConfigs = {}, config) {
139
+ const normalized = normalizeClaudeConfig(config);
140
+ if (!normalized.baseUrl || !normalized.model) {
141
+ return '';
142
+ }
143
+ const comparableUrl = normalizeClaudeComparableUrl(normalized.baseUrl);
144
+ const isExternal = !normalized.apiKey && !!normalized.externalCredentialType;
145
+ if (!normalized.apiKey && !isExternal) {
146
+ return '';
147
+ }
148
+ const entries = Object.entries(claudeConfigs || {});
149
+ for (const [name, existing] of entries) {
150
+ const normalizedExisting = normalizeClaudeConfig(existing);
151
+ if (!normalizedExisting.baseUrl || !normalizedExisting.model) {
152
+ continue;
153
+ }
154
+ if (normalizeClaudeComparableUrl(normalizedExisting.baseUrl) !== comparableUrl
155
+ || normalizedExisting.model !== normalized.model) {
156
+ continue;
157
+ }
158
+ if (normalized.apiKey && normalizedExisting.apiKey === normalized.apiKey) {
159
+ return name;
160
+ }
161
+ if (isExternal
162
+ && !normalizedExisting.apiKey
163
+ && normalizedExisting.externalCredentialType === normalized.externalCredentialType) {
164
+ return name;
165
+ }
166
+ }
167
+ return '';
168
+ }
package/web-ui/logic.mjs CHANGED
@@ -1,5 +1,5 @@
1
- // 逻辑纯函数兼容出口:内部按职责拆分,外部保持原有导入路径不变
2
- export * from './logic.agents-diff.mjs';
3
- export * from './logic.claude.mjs';
4
- export * from './logic.runtime.mjs';
5
- export * from './logic.sessions.mjs';
1
+ // 逻辑纯函数兼容出口:内部按职责拆分,外部保持原有导入路径不变
2
+ export * from './logic.agents-diff.mjs';
3
+ export * from './logic.claude.mjs';
4
+ export * from './logic.runtime.mjs';
5
+ export * from './logic.sessions.mjs';
@@ -1,124 +1,124 @@
1
- export function formatLatency(result) {
2
- if (!result) return '';
3
- if (!result.ok) return result.status ? `ERR ${result.status}` : 'ERR';
4
- const ms = (typeof result.durationMs === 'number' && Number.isFinite(result.durationMs))
5
- ? result.durationMs
6
- : 0;
7
- return `${ms}ms`;
8
- }
9
-
10
- export function buildSpeedTestIssue(name, result) {
11
- if (!name || !result) return null;
12
- if (result.error) {
13
- const error = String(result.error || '');
14
- const errorLower = error.toLowerCase();
15
- if (error === 'Provider not found') {
16
- return {
17
- code: 'remote-speedtest-provider-missing',
18
- message: `提供商 ${name} 未找到,无法测速`,
19
- suggestion: '检查配置是否存在该 provider'
20
- };
21
- }
22
- if (error === 'Provider missing URL' || error === 'Missing name or url') {
23
- return {
24
- code: 'remote-speedtest-baseurl-missing',
25
- message: `提供商 ${name} 缺少 base_url`,
26
- suggestion: '补全 base_url 后重试'
27
- };
28
- }
29
- if (errorLower.includes('invalid url')) {
30
- return {
31
- code: 'remote-speedtest-invalid-url',
32
- message: `提供商 ${name} 的 base_url 无效`,
33
- suggestion: '请设置为 http/https 的完整 URL'
34
- };
35
- }
36
- if (errorLower.includes('timeout')) {
37
- return {
38
- code: 'remote-speedtest-timeout',
39
- message: `提供商 ${name} 远程测速超时`,
40
- suggestion: '检查网络或 base_url 是否可达'
41
- };
42
- }
43
- return {
44
- code: 'remote-speedtest-unreachable',
45
- message: `提供商 ${name} 远程测速失败:${error || '无法连接'}`,
46
- suggestion: '检查网络或 base_url 是否可用'
47
- };
48
- }
49
-
50
- const status = typeof result.status === 'number' ? result.status : 0;
51
- if (status === 401 || status === 403) {
52
- return {
53
- code: 'remote-speedtest-auth-failed',
54
- message: `提供商 ${name} 远程测速鉴权失败(401/403)`,
55
- suggestion: '检查 API Key 或认证方式'
56
- };
57
- }
58
- if (status >= 400) {
59
- return {
60
- code: 'remote-speedtest-http-error',
61
- message: `提供商 ${name} 远程测速返回异常状态: ${status}`,
62
- suggestion: '检查 base_url 或服务状态'
63
- };
64
- }
65
- return null;
66
- }
67
-
68
- export async function runLatestOnlyQueue(initialTarget, options = {}) {
69
- const perform = typeof options.perform === 'function'
70
- ? options.perform
71
- : async () => {};
72
- const consumePending = typeof options.consumePending === 'function'
73
- ? options.consumePending
74
- : () => '';
75
- let currentTarget = typeof initialTarget === 'string' ? initialTarget.trim() : '';
76
- let lastError = '';
77
-
78
- while (currentTarget) {
79
- try {
80
- await perform(currentTarget);
81
- lastError = '';
82
- } catch (e) {
83
- lastError = e && e.message ? e.message : 'queue task failed';
84
- }
85
- const queued = String(consumePending() || '').trim();
86
- if (!queued || queued === currentTarget) {
87
- break;
88
- }
89
- currentTarget = queued;
90
- }
91
-
92
- return {
93
- lastTarget: currentTarget,
94
- lastError
95
- };
96
- }
97
-
98
- export function shouldForceCompactLayoutMode(options = {}) {
99
- const viewportWidth = Number(options.viewportWidth || 0);
100
- const screenWidth = Number(options.screenWidth || 0);
101
- const screenHeight = Number(options.screenHeight || 0);
102
- const shortEdge = Number(options.shortEdge || (screenWidth > 0 && screenHeight > 0 ? Math.min(screenWidth, screenHeight) : 0));
103
- const maxTouchPoints = Number(options.maxTouchPoints || 0);
104
- const userAgent = typeof options.userAgent === 'string' ? options.userAgent : '';
105
- const isMobileUa = typeof options.isMobileUa === 'boolean'
106
- ? options.isMobileUa
107
- : /(Android|iPhone|iPad|iPod|Mobile)/i.test(userAgent);
108
- const coarsePointer = !!options.coarsePointer;
109
- const noHover = !!options.noHover;
110
- const isSmallPhysicalScreen = shortEdge > 0 && shortEdge <= 920;
111
- const isNarrowViewport = viewportWidth > 0 && viewportWidth <= 960;
112
- const pointerSuggestsTouchOnly = coarsePointer && noHover;
113
-
114
- if (isMobileUa) {
115
- return isNarrowViewport || isSmallPhysicalScreen;
116
- }
117
- if (!pointerSuggestsTouchOnly) {
118
- return false;
119
- }
120
- if (maxTouchPoints <= 0) {
121
- return false;
122
- }
123
- return isSmallPhysicalScreen;
124
- }
1
+ export function formatLatency(result) {
2
+ if (!result) return '';
3
+ if (!result.ok) return result.status ? `ERR ${result.status}` : 'ERR';
4
+ const ms = (typeof result.durationMs === 'number' && Number.isFinite(result.durationMs))
5
+ ? result.durationMs
6
+ : 0;
7
+ return `${ms}ms`;
8
+ }
9
+
10
+ export function buildSpeedTestIssue(name, result) {
11
+ if (!name || !result) return null;
12
+ if (result.error) {
13
+ const error = String(result.error || '');
14
+ const errorLower = error.toLowerCase();
15
+ if (error === 'Provider not found') {
16
+ return {
17
+ code: 'remote-speedtest-provider-missing',
18
+ message: `提供商 ${name} 未找到,无法测速`,
19
+ suggestion: '检查配置是否存在该 provider'
20
+ };
21
+ }
22
+ if (error === 'Provider missing URL' || error === 'Missing name or url') {
23
+ return {
24
+ code: 'remote-speedtest-baseurl-missing',
25
+ message: `提供商 ${name} 缺少 base_url`,
26
+ suggestion: '补全 base_url 后重试'
27
+ };
28
+ }
29
+ if (errorLower.includes('invalid url')) {
30
+ return {
31
+ code: 'remote-speedtest-invalid-url',
32
+ message: `提供商 ${name} 的 base_url 无效`,
33
+ suggestion: '请设置为 http/https 的完整 URL'
34
+ };
35
+ }
36
+ if (errorLower.includes('timeout')) {
37
+ return {
38
+ code: 'remote-speedtest-timeout',
39
+ message: `提供商 ${name} 远程测速超时`,
40
+ suggestion: '检查网络或 base_url 是否可达'
41
+ };
42
+ }
43
+ return {
44
+ code: 'remote-speedtest-unreachable',
45
+ message: `提供商 ${name} 远程测速失败:${error || '无法连接'}`,
46
+ suggestion: '检查网络或 base_url 是否可用'
47
+ };
48
+ }
49
+
50
+ const status = typeof result.status === 'number' ? result.status : 0;
51
+ if (status === 401 || status === 403) {
52
+ return {
53
+ code: 'remote-speedtest-auth-failed',
54
+ message: `提供商 ${name} 远程测速鉴权失败(401/403)`,
55
+ suggestion: '检查 API Key 或认证方式'
56
+ };
57
+ }
58
+ if (status >= 400) {
59
+ return {
60
+ code: 'remote-speedtest-http-error',
61
+ message: `提供商 ${name} 远程测速返回异常状态: ${status}`,
62
+ suggestion: '检查 base_url 或服务状态'
63
+ };
64
+ }
65
+ return null;
66
+ }
67
+
68
+ export async function runLatestOnlyQueue(initialTarget, options = {}) {
69
+ const perform = typeof options.perform === 'function'
70
+ ? options.perform
71
+ : async () => {};
72
+ const consumePending = typeof options.consumePending === 'function'
73
+ ? options.consumePending
74
+ : () => '';
75
+ let currentTarget = typeof initialTarget === 'string' ? initialTarget.trim() : '';
76
+ let lastError = '';
77
+
78
+ while (currentTarget) {
79
+ try {
80
+ await perform(currentTarget);
81
+ lastError = '';
82
+ } catch (e) {
83
+ lastError = e && e.message ? e.message : 'queue task failed';
84
+ }
85
+ const queued = String(consumePending() || '').trim();
86
+ if (!queued || queued === currentTarget) {
87
+ break;
88
+ }
89
+ currentTarget = queued;
90
+ }
91
+
92
+ return {
93
+ lastTarget: currentTarget,
94
+ lastError
95
+ };
96
+ }
97
+
98
+ export function shouldForceCompactLayoutMode(options = {}) {
99
+ const viewportWidth = Number(options.viewportWidth || 0);
100
+ const screenWidth = Number(options.screenWidth || 0);
101
+ const screenHeight = Number(options.screenHeight || 0);
102
+ const shortEdge = Number(options.shortEdge || (screenWidth > 0 && screenHeight > 0 ? Math.min(screenWidth, screenHeight) : 0));
103
+ const maxTouchPoints = Number(options.maxTouchPoints || 0);
104
+ const userAgent = typeof options.userAgent === 'string' ? options.userAgent : '';
105
+ const isMobileUa = typeof options.isMobileUa === 'boolean'
106
+ ? options.isMobileUa
107
+ : /(Android|iPhone|iPad|iPod|Mobile)/i.test(userAgent);
108
+ const coarsePointer = !!options.coarsePointer;
109
+ const noHover = !!options.noHover;
110
+ const isSmallPhysicalScreen = shortEdge > 0 && shortEdge <= 920;
111
+ const isNarrowViewport = viewportWidth > 0 && viewportWidth <= 960;
112
+ const pointerSuggestsTouchOnly = coarsePointer && noHover;
113
+
114
+ if (isMobileUa) {
115
+ return isNarrowViewport || isSmallPhysicalScreen;
116
+ }
117
+ if (!pointerSuggestsTouchOnly) {
118
+ return false;
119
+ }
120
+ if (maxTouchPoints <= 0) {
121
+ return false;
122
+ }
123
+ return isSmallPhysicalScreen;
124
+ }