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
package/README.en.md CHANGED
@@ -23,17 +23,20 @@ Codex Mate is a local-first CLI + Web UI for unified management of:
23
23
  - Codex provider/model switching and config writes
24
24
  - Claude Code profiles (writes to `~/.claude/settings.json`)
25
25
  - OpenClaw JSON5 profiles and workspace `AGENTS.md`
26
- - Local Codex/Claude sessions (list/filter/export/delete)
26
+ - Local skills market for Codex / Claude Code (target switching, local skills management, cross-app import, ZIP distribution)
27
+ - Local Codex/Claude sessions (list/filter/export/delete) with Usage analytics overview
27
28
 
28
- It works on local files directly and does not require cloud hosting.
29
+ It works on local files directly and does not require cloud hosting. The skills market is also local-first: it operates on local directories and does not depend on a remote marketplace.
29
30
 
30
- ## Why Codex Mate?
31
+ ## Comparison
31
32
 
32
33
  | Dimension | Codex Mate | Manual File Editing |
33
34
  | --- | --- | --- |
34
35
  | Multi-tool management | Codex + Claude Code + OpenClaw in one entry | Different files and folders per tool |
35
36
  | Operation mode | CLI + local Web UI | Manual TOML/JSON/JSON5 edits |
36
- | Session handling | Browse/export/batch cleanup | Manual file location and processing |
37
+ | Session handling | Browse/filter/Usage analytics/export/batch cleanup | Manual file location and processing |
38
+ | Skills reuse | Local skills market + cross-app import + ZIP distribution | Manual folder copy and reconciliation |
39
+ | Operational visibility | Unified view of config, sessions, and Usage summaries | Depends on manual file inspection and scattered commands |
37
40
  | Rollback readiness | Backup before first takeover | Easy to overwrite by mistake |
38
41
  | Automation integration | MCP stdio (read-only by default) | Requires custom scripting |
39
42
 
@@ -49,9 +52,16 @@ It works on local files directly and does not require cloud hosting.
49
52
  - Unified Codex + Claude session list
50
53
  - Local session pinning with persistent pinned state and pinned-first ordering
51
54
  - Keyword/source/cwd filters
55
+ - Usage subview with 7d / 30d session trends, message trends, source share, and top paths
52
56
  - Markdown export
53
57
  - Session-level and message-level delete (supports batch)
54
58
 
59
+ **Skills Market**
60
+ - Switch the skills install target between Codex and Claude Code
61
+ - Inspect local installed skills, root paths, and status
62
+ - Scan importable sources from `Codex` / `Claude Code` / `Agents`
63
+ - Support cross-app import, ZIP import/export, and batch delete
64
+
55
65
  **Engineering Utilities**
56
66
  - MCP stdio domains (`tools`, `resources`, `prompts`)
57
67
  - Built-in proxy controls (`proxy`)
@@ -74,15 +84,16 @@ flowchart TB
74
84
  API["Local HTTP API"]
75
85
  MCPS["MCP stdio Server"]
76
86
  PROXY["Built-in Proxy"]
77
- SERVICES["Config / Sessions / Skills / Workflow"]
87
+ SERVICES["Config / Sessions & Usage / Skills Market / Workflow"]
78
88
  CORE["File IO / Network / Diff / Session Utils"]
79
89
  end
80
90
 
81
91
  subgraph Data["Local Files"]
82
- CODEX["~/.codex"]
83
- CLAUDE["~/.claude"]
84
- OPENCLAW["~/.openclaw"]
85
- STATE["sessions / trash / workflow runs"]
92
+ CODEX["~/.codex/config + auth + models"]
93
+ CLAUDE["~/.claude/settings.json"]
94
+ OPENCLAW["~/.openclaw/*.json5 + ~/.openclaw/openclaw.json + workspace/AGENTS.md"]
95
+ SKILLS["~/.codex/skills / ~/.claude/skills / ~/.agents/skills"]
96
+ STATE["sessions / usage aggregates / trash / workflow runs / skill exports"]
86
97
  end
87
98
 
88
99
  CLI --> ENTRY
@@ -100,6 +111,7 @@ flowchart TB
100
111
  CORE --> CODEX
101
112
  CORE --> CLAUDE
102
113
  CORE --> OPENCLAW
114
+ CORE --> SKILLS
103
115
  CORE --> STATE
104
116
  ```
105
117
 
@@ -114,7 +126,9 @@ codexmate status
114
126
  codexmate run
115
127
  ```
116
128
 
117
- Default listen address is `127.0.0.1:3737`, and browser auto-open is enabled by default.
129
+ Default listen address is `0.0.0.0:3737` for LAN access, and browser auto-open is enabled by default.
130
+
131
+ > Safety note: the unauthenticated management UI is exposed to your current LAN by default. Use trusted networks only; for local-only access, set `CODEXMATE_HOST=127.0.0.1` or pass `--host 127.0.0.1`.
118
132
 
119
133
  ### Run from source
120
134
 
@@ -170,8 +184,6 @@ codexmate codex --model gpt-5.3-codex --follow-up "step1" --follow-up "step2"
170
184
  - Provider/model switching
171
185
  - Model list management
172
186
  - `~/.codex/AGENTS.md` editing
173
- - `~/.codex/skills` management (filter, batch delete, cross-app import)
174
-
175
187
 
176
188
  ### Claude Code Mode
177
189
  - Multi-profile management
@@ -185,8 +197,16 @@ codexmate codex --model gpt-5.3-codex --follow-up "step1" --follow-up "step2"
185
197
 
186
198
  ### Sessions Mode
187
199
  - Unified Codex + Claude sessions
200
+ - Browser / Usage subview switching
188
201
  - Local pin/unpin with persistent storage and pinned-first ordering
189
202
  - Search, filter, export, delete, batch cleanup
203
+ - Usage view includes 7d / 30d session trends, message trends, source share, and top paths
204
+
205
+ ### Skills Market Tab
206
+ - Switch the skills install target between `Codex` and `Claude Code`
207
+ - Show the current local skills root, installed items, and importable items
208
+ - Scan importable sources under `Codex` / `Claude Code` / `Agents`
209
+ - Support cross-app import, ZIP import/export, and batch delete
190
210
 
191
211
  ## MCP
192
212
 
@@ -218,7 +238,7 @@ codexmate mcp serve --allow-write
218
238
  | Variable | Default | Description |
219
239
  | --- | --- | --- |
220
240
  | `CODEXMATE_PORT` | `3737` | Web server port |
221
- | `CODEXMATE_HOST` | `127.0.0.1` | Web listen host |
241
+ | `CODEXMATE_HOST` | `0.0.0.0` | Web listen host (set `127.0.0.1` for local-only access) |
222
242
  | `CODEXMATE_NO_BROWSER` | unset | Set `1` to disable browser auto-open |
223
243
  | `CODEXMATE_MCP_ALLOW_WRITE` | unset | Set `1` to allow MCP write tools by default |
224
244
  | `CODEXMATE_FORCE_RESET_EXISTING_CONFIG` | `0` | Set `1` to force bootstrap reset of existing config |
@@ -232,10 +252,7 @@ codexmate mcp serve --allow-write
232
252
 
233
253
  ## Contributing
234
254
 
235
- Issues and pull requests are welcome.
236
-
237
- - English changelog: `doc/CHANGELOG.md`
238
- - Chinese changelog: `doc/CHANGELOG.zh-CN.md`
255
+ Issues and pull requests are accepted.
239
256
 
240
257
  ## License
241
258
 
package/README.md CHANGED
@@ -23,17 +23,20 @@ Codex Mate 提供一套本地优先的 CLI + Web UI,用于统一管理:
23
23
  - Codex 的 provider / model 切换与配置写入
24
24
  - Claude Code 配置方案(写入 `~/.claude/settings.json`)
25
25
  - OpenClaw JSON5 配置与 Workspace `AGENTS.md`
26
- - Codex / Claude 本地会话浏览、筛选、导出、删除
26
+ - Codex / Claude Code Skills 市场(安装目标切换、本地 skills 管理、跨应用导入、ZIP 分发)
27
+ - Codex / Claude 本地会话浏览、筛选、导出、删除与 Usage 统计概览
27
28
 
28
- 项目不依赖云端托管,配置写入你的本地文件,便于审计和回滚。
29
+ 项目不依赖云端托管,配置写入你的本地文件,便于审计和回滚。Skills 市场同样坚持本地优先,只操作本地目录,不依赖远程在线市场。
29
30
 
30
- ## 为什么选择 Codex Mate?
31
+ ## 功能对比
31
32
 
32
33
  | 维度 | Codex Mate | 手动维护配置 |
33
34
  | --- | --- | --- |
34
35
  | 多工具管理 | Codex + Claude Code + OpenClaw 统一入口 | 多文件、多目录分散修改 |
35
36
  | 使用方式 | CLI + 本地 Web UI | 纯手改 TOML / JSON / JSON5 |
36
- | 会话处理 | 支持浏览、导出、批量清理 | 需要手动定位和处理文件 |
37
+ | 会话处理 | 支持浏览、筛选、Usage 统计、导出、批量清理 | 需要手动定位和处理文件 |
38
+ | Skills 复用 | 本地 Skills 市场 + 跨应用导入 + ZIP 分发 | 目录手动复制,容易遗漏 |
39
+ | 使用可见性 | 统一查看配置、会话与 Usage 概览 | 依赖手工翻文件和零散命令 |
37
40
  | 可回滚性 | 首次接管前自动备份 | 易误覆盖、回滚成本高 |
38
41
  | 自动化接入 | 提供 MCP stdio(默认只读) | 需自行封装脚本 |
39
42
 
@@ -49,13 +52,18 @@ Codex Mate 提供一套本地优先的 CLI + Web UI,用于统一管理:
49
52
  - 同页查看 Codex 与 Claude 会话
50
53
  - 支持本地会话置顶,置顶状态持久化保存并优先排序显示
51
54
  - 关键词搜索、来源筛选、cwd 路径筛选
55
+ - Usage 子页:近 7 天 / 近 30 天会话趋势、消息趋势、来源占比、高频路径
52
56
  - 会话导出 Markdown
53
57
  - 会话与消息级删除(支持批量)
54
58
 
59
+ **Skills 市场**
60
+ - 在 Codex 与 Claude Code 之间切换 skills 安装目标
61
+ - 查看本地已安装 skills、根目录与状态
62
+ - 扫描 `Codex` / `Claude Code` / `Agents` 可导入来源
63
+ - 支持跨应用导入、ZIP 导入 / 导出、批量删除
64
+
55
65
  **工程能力**
56
66
  - MCP stdio 能力(tools/resources/prompts)
57
- - 内建代理配置与状态控制(`proxy`)
58
- - 认证档案管理(`auth`)
59
67
  - Zip 压缩/解压(优先系统工具,失败回退 JS 库)
60
68
 
61
69
  ## 架构总览
@@ -66,40 +74,38 @@ flowchart TB
66
74
  CLI["CLI"]
67
75
  WEB["Web UI"]
68
76
  MCP["MCP Client"]
69
- OAI["Codex / OpenAI Client"]
70
77
  end
71
78
 
72
79
  subgraph Runtime["Codex Mate Runtime"]
73
80
  ENTRY["cli.js Entry"]
74
81
  API["Local HTTP API"]
75
82
  MCPS["MCP stdio Server"]
76
- PROXY["Built-in Proxy"]
77
- SERVICES["Config / Sessions / Skills / Workflow"]
83
+ SERVICES["Config / Sessions & Usage / Skills Market / Workflow"]
78
84
  CORE["File IO / Network / Diff / Session Utils"]
79
85
  end
80
86
 
81
87
  subgraph State["Local State"]
82
- CODEX["~/.codex"]
83
- CLAUDE["~/.claude"]
84
- OPENCLAW["~/.openclaw"]
85
- STATE["sessions / trash / workflow runs"]
88
+ CODEX["~/.codex/config + auth + models"]
89
+ CLAUDE["~/.claude/settings.json"]
90
+ OPENCLAW["~/.openclaw/*.json5 + ~/.openclaw/openclaw.json + workspace/AGENTS.md"]
91
+ SKILLS["~/.codex/skills / ~/.claude/skills / ~/.agents/skills"]
92
+ STATE["sessions / usage aggregates / trash / workflow runs / skill exports"]
86
93
  end
87
94
 
88
95
  CLI --> ENTRY
89
96
  WEB -->|GET / + POST /api| API
90
97
  MCP -->|stdio JSON-RPC| MCPS
91
- OAI -->|HTTP /v1| PROXY
92
98
 
93
99
  ENTRY --> SERVICES
94
100
  API --> SERVICES
95
101
  MCPS --> SERVICES
96
- PROXY --> CORE
97
102
 
98
103
  SERVICES --> CORE
99
104
 
100
105
  CORE --> CODEX
101
106
  CORE --> CLAUDE
102
107
  CORE --> OPENCLAW
108
+ CORE --> SKILLS
103
109
  CORE --> STATE
104
110
  ```
105
111
 
@@ -114,7 +120,9 @@ codexmate status
114
120
  codexmate run
115
121
  ```
116
122
 
117
- 默认监听 `127.0.0.1:3737`,并尝试自动打开浏览器。
123
+ 默认监听 `0.0.0.0:3737`,支持局域网访问,并尝试自动打开浏览器。
124
+
125
+ > 安全提示:默认监听会在当前局域网暴露未鉴权的管理界面。若包含 API Key、provider 配置或 skills 管理,请仅在可信网络中使用;如需仅本机访问,可设置 `CODEXMATE_HOST=127.0.0.1` 或启动时传入 `--host 127.0.0.1`。
118
126
 
119
127
  ### 从源码运行
120
128
 
@@ -144,8 +152,6 @@ npm start run --no-browser
144
152
  | `codexmate add <name> <URL> [API_KEY]` | 添加提供商 |
145
153
  | `codexmate delete <name>` | 删除提供商 |
146
154
  | `codexmate claude <BaseURL> <API_KEY> [model]` | 写入 Claude Code 配置 |
147
- | `codexmate auth <list\|import\|switch\|delete\|status>` | 认证档案管理 |
148
- | `codexmate proxy <status\|set\|apply\|enable\|start\|stop>` | 内建代理管理 |
149
155
  | `codexmate workflow <list\|get\|validate\|run\|runs>` | MCP 工作流管理 |
150
156
  | `codexmate codex [args...] [--follow-up <文本> 可重复]` | Codex CLI 透传入口(默认补 `--yolo`,可追加 queued follow-up) |
151
157
  | `codexmate qwen [args...]` | Qwen CLI 透传入口 |
@@ -170,8 +176,6 @@ codexmate codex --model gpt-5.3-codex --follow-up "步骤1" --follow-up "步骤2
170
176
  - provider / model 切换
171
177
  - 模型管理
172
178
  - `~/.codex/AGENTS.md` 编辑
173
- - `~/.codex/skills` 管理(筛选、批量删除、跨应用导入)
174
-
175
179
 
176
180
  ### Claude Code 配置模式
177
181
  - 多配置方案管理
@@ -185,8 +189,16 @@ codexmate codex --model gpt-5.3-codex --follow-up "步骤1" --follow-up "步骤2
185
189
 
186
190
  ### 会话模式
187
191
  - Codex + Claude 会话统一列表
192
+ - Browser / Usage 双子视图切换
188
193
  - 支持本地会话置顶、持久化保存与置顶优先排序
189
194
  - 搜索、筛选、导出、删除、批量清理
195
+ - Usage 视图提供近 7 天 / 近 30 天会话趋势、消息趋势、来源占比与高频路径统计
196
+
197
+ ### Skills 市场标签页
198
+ - 在 `Codex` 与 `Claude Code` 之间切换 skills 安装目标
199
+ - 展示当前目标的本地 skills 根目录、已安装项和可导入项
200
+ - 扫描 `Codex` / `Claude Code` / `Agents` 目录中的可导入来源
201
+ - 支持跨应用导入、ZIP 导入 / 导出、批量删除
190
202
 
191
203
  ## MCP
192
204
 
@@ -219,7 +231,7 @@ codexmate mcp serve --allow-write
219
231
  | 变量 | 默认值 | 说明 |
220
232
  | --- | --- | --- |
221
233
  | `CODEXMATE_PORT` | `3737` | Web 服务端口 |
222
- | `CODEXMATE_HOST` | `127.0.0.1` | Web 服务监听地址 |
234
+ | `CODEXMATE_HOST` | `0.0.0.0` | Web 服务监听地址(如需仅本机访问,显式设为 `127.0.0.1`) |
223
235
  | `CODEXMATE_NO_BROWSER` | 未设置 | 设为 `1` 后不自动打开浏览器 |
224
236
  | `CODEXMATE_MCP_ALLOW_WRITE` | 未设置 | 设为 `1` 后默认允许 MCP 写工具 |
225
237
  | `CODEXMATE_FORCE_RESET_EXISTING_CONFIG` | `0` | 设为 `1` 时首次可强制重建托管配置 |
@@ -233,10 +245,7 @@ codexmate mcp serve --allow-write
233
245
 
234
246
  ## 参与贡献
235
247
 
236
- 欢迎提交 Issue Pull Request
237
-
238
- - 英文更新日志:`doc/CHANGELOG.md`
239
- - 中文更新日志:`doc/CHANGELOG.zh-CN.md`
248
+ Issue Pull Request 可按需提交。
240
249
 
241
250
  ## License
242
251
 
@@ -0,0 +1,338 @@
1
+ const { isValidHttpUrl, normalizeBaseUrl } = require('../lib/cli-utils');
2
+ const { buildModelProbeSpecs } = require('../lib/cli-models-utils');
3
+ const { probeJsonPost } = require('../lib/cli-network-utils');
4
+
5
+ const DEFAULT_TIMEOUT_MS = 6000;
6
+ const JSON_RESPONSE_MAX_BYTES = 256 * 1024;
7
+
8
+ function buildRemoteHealthMessage(issueCode, statusCode, detail) {
9
+ if (!issueCode) {
10
+ return '远程模型探测通过:endpoint、鉴权与模型均可用';
11
+ }
12
+
13
+ if (issueCode === 'remote-model-probe-unreachable') {
14
+ return '远程模型接口不可达,请检查 endpoint、网络或 DNS';
15
+ }
16
+
17
+ if (issueCode === 'remote-model-probe-auth-failed') {
18
+ return '远程模型探测鉴权失败(401/403),请检查 API Key、endpoint 与模型权限';
19
+ }
20
+
21
+ if (issueCode === 'remote-model-probe-not-found') {
22
+ return '远程模型探测返回 404,请检查 base_url、接口路径或模型名';
23
+ }
24
+
25
+ if (issueCode === 'remote-model-probe-error') {
26
+ return detail || '远程模型接口返回错误,请检查模型名与账号权限';
27
+ }
28
+
29
+ if (issueCode === 'remote-model-probe-http-error' && statusCode) {
30
+ return `远程模型探测返回 HTTP ${statusCode},请检查 endpoint 与模型`;
31
+ }
32
+
33
+ return '远程模型探测失败,请检查配置与远端服务状态';
34
+ }
35
+
36
+ function extractPayloadErrorMessage(payload) {
37
+ if (!payload || typeof payload !== 'object') {
38
+ return '';
39
+ }
40
+ if (typeof payload.error === 'string' && payload.error.trim()) {
41
+ return payload.error.trim();
42
+ }
43
+ if (!payload.error || typeof payload.error !== 'object') {
44
+ return '';
45
+ }
46
+ if (typeof payload.error.message === 'string' && payload.error.message.trim()) {
47
+ return payload.error.message.trim();
48
+ }
49
+ if (typeof payload.error.code === 'string' && payload.error.code.trim()) {
50
+ return payload.error.code.trim();
51
+ }
52
+ return '';
53
+ }
54
+
55
+ async function runRemoteHealthCheck(providerName, provider, modelName, options = {}) {
56
+ const issues = [];
57
+ const baseUrl = normalizeBaseUrl(provider && provider.base_url ? provider.base_url : '');
58
+ const summary = {
59
+ type: 'remote-health-check',
60
+ provider: typeof providerName === 'string' ? providerName.trim() : '',
61
+ endpoint: baseUrl,
62
+ ok: false,
63
+ statusCode: null,
64
+ message: '',
65
+ checks: {}
66
+ };
67
+
68
+ if (!baseUrl) {
69
+ issues.push({
70
+ code: 'remote-skip-base-url',
71
+ message: '无法进行远程探测:base_url 为空',
72
+ suggestion: '补全 base_url 或关闭远程探测'
73
+ });
74
+ summary.message = '无法进行远程探测:base_url 为空';
75
+ return { issues, remote: summary };
76
+ }
77
+
78
+ if (!isValidHttpUrl(baseUrl)) {
79
+ issues.push({
80
+ code: 'remote-skip-base-url',
81
+ message: '无法进行远程探测:base_url 无效',
82
+ suggestion: '补全 base_url 或关闭远程探测'
83
+ });
84
+ summary.message = '无法进行远程探测:base_url 无效';
85
+ return { issues, remote: summary };
86
+ }
87
+
88
+ const modelProbeSpecs = buildModelProbeSpecs(provider, modelName, baseUrl);
89
+ if (!modelProbeSpecs.length) {
90
+ issues.push({
91
+ code: 'remote-skip-model',
92
+ message: '无法进行远程探测:当前模型未设置',
93
+ suggestion: '补全 model 后重试'
94
+ });
95
+ summary.message = '无法进行远程探测:当前模型未设置';
96
+ return { issues, remote: summary };
97
+ }
98
+
99
+ const requiresAuth = provider && provider.requires_openai_auth !== false;
100
+ const apiKey = typeof provider.preferred_auth_method === 'string'
101
+ ? provider.preferred_auth_method.trim()
102
+ : '';
103
+ const authValue = requiresAuth ? apiKey : (apiKey || '');
104
+ const timeoutMs = Number.isFinite(options.timeoutMs)
105
+ ? Math.max(1000, Number(options.timeoutMs))
106
+ : DEFAULT_TIMEOUT_MS;
107
+ const runProbeJsonPost = typeof options.probeJsonPost === 'function' ? options.probeJsonPost : probeJsonPost;
108
+
109
+ let modelProbeSpec = modelProbeSpecs[0];
110
+ let modelProbe = null;
111
+ for (let index = 0; index < modelProbeSpecs.length; index += 1) {
112
+ const candidate = modelProbeSpecs[index];
113
+ const probeResult = await runProbeJsonPost(candidate.url, candidate.body, {
114
+ apiKey: authValue,
115
+ timeoutMs,
116
+ maxBytes: JSON_RESPONSE_MAX_BYTES
117
+ });
118
+ modelProbeSpec = candidate;
119
+ modelProbe = probeResult;
120
+ const shouldTryNextCandidate = index < modelProbeSpecs.length - 1
121
+ && (!probeResult.ok || probeResult.status === 404);
122
+ if (!shouldTryNextCandidate) {
123
+ break;
124
+ }
125
+ }
126
+
127
+ summary.checks.modelProbe = {
128
+ url: modelProbeSpec.url,
129
+ ok: !!modelProbe.ok,
130
+ status: Number.isFinite(modelProbe.status) ? modelProbe.status : 0,
131
+ durationMs: Number.isFinite(modelProbe.durationMs) ? modelProbe.durationMs : 0
132
+ };
133
+
134
+ if (!modelProbe.ok) {
135
+ issues.push({
136
+ code: 'remote-model-probe-unreachable',
137
+ message: `模型可用性探测失败:${modelProbe.error || '无法连接'}`,
138
+ suggestion: '检查 endpoint、网络或模型接口是否可用'
139
+ });
140
+ } else if (modelProbe.status === 401 || modelProbe.status === 403) {
141
+ issues.push({
142
+ code: 'remote-model-probe-auth-failed',
143
+ message: '模型可用性探测鉴权失败(401/403)',
144
+ suggestion: '检查 API Key 或认证方式'
145
+ });
146
+ } else if (modelProbe.status === 404) {
147
+ issues.push({
148
+ code: 'remote-model-probe-not-found',
149
+ message: '模型可用性探测返回 404',
150
+ suggestion: '检查 base_url、接口路径或模型名'
151
+ });
152
+ } else if (modelProbe.status >= 400) {
153
+ issues.push({
154
+ code: 'remote-model-probe-http-error',
155
+ message: `模型可用性探测返回异常状态: ${modelProbe.status}`,
156
+ suggestion: '检查 endpoint、模型名或服务状态'
157
+ });
158
+ } else {
159
+ let payload = null;
160
+ try {
161
+ payload = modelProbe.body ? JSON.parse(modelProbe.body) : null;
162
+ } catch (e) {
163
+ payload = null;
164
+ }
165
+ const payloadError = extractPayloadErrorMessage(payload);
166
+ if (payloadError) {
167
+ issues.push({
168
+ code: 'remote-model-probe-error',
169
+ message: `模型可用性探测失败:${payloadError}`,
170
+ suggestion: '检查模型名与权限'
171
+ });
172
+ }
173
+ }
174
+
175
+ const primaryIssue = issues[0] || null;
176
+ summary.ok = issues.length === 0;
177
+ summary.statusCode = Number.isFinite(modelProbe.status) && modelProbe.status > 0
178
+ ? modelProbe.status
179
+ : null;
180
+ summary.message = buildRemoteHealthMessage(
181
+ primaryIssue ? primaryIssue.code : '',
182
+ summary.statusCode,
183
+ primaryIssue ? primaryIssue.message : ''
184
+ );
185
+
186
+ return { issues, remote: summary };
187
+ }
188
+
189
+ async function buildConfigHealthReport(params = {}, deps = {}) {
190
+ const issues = [];
191
+ const {
192
+ readConfigOrVirtualDefault,
193
+ readModels
194
+ } = deps;
195
+
196
+ if (typeof readConfigOrVirtualDefault !== 'function') {
197
+ throw new Error('buildConfigHealthReport 缺少 readConfigOrVirtualDefault 依赖');
198
+ }
199
+ if (typeof readModels !== 'function') {
200
+ throw new Error('buildConfigHealthReport 缺少 readModels 依赖');
201
+ }
202
+
203
+ const status = readConfigOrVirtualDefault();
204
+ const config = status.config || {};
205
+
206
+ if (status.isVirtual) {
207
+ const parseFailed = status.errorType === 'parse';
208
+ const readFailed = status.errorType === 'read';
209
+ issues.push({
210
+ code: parseFailed ? 'config-parse-failed' : (readFailed ? 'config-read-failed' : 'config-missing'),
211
+ message: status.reason || (parseFailed
212
+ ? 'config.toml 解析失败'
213
+ : (readFailed ? '读取 config.toml 失败' : '未检测到 config.toml')),
214
+ suggestion: parseFailed
215
+ ? '修复 config.toml 语法错误后重试'
216
+ : (readFailed ? '检查文件权限后重试' : '在模板编辑器中确认应用配置,生成可用的 config.toml')
217
+ });
218
+ if (parseFailed || readFailed) {
219
+ return {
220
+ ok: false,
221
+ issues,
222
+ summary: {
223
+ currentProvider: '',
224
+ currentModel: ''
225
+ },
226
+ remote: null
227
+ };
228
+ }
229
+ }
230
+
231
+ const providerName = typeof config.model_provider === 'string' ? config.model_provider.trim() : '';
232
+ const modelName = typeof config.model === 'string' ? config.model.trim() : '';
233
+ if (!providerName) {
234
+ issues.push({
235
+ code: 'provider-missing',
236
+ message: '当前 provider 未设置',
237
+ suggestion: '在模板中设置 model_provider'
238
+ });
239
+ }
240
+
241
+ if (!modelName) {
242
+ issues.push({
243
+ code: 'model-missing',
244
+ message: '当前模型未设置',
245
+ suggestion: '在模板中设置 model'
246
+ });
247
+ }
248
+
249
+ const providers = config.model_providers && typeof config.model_providers === 'object'
250
+ ? config.model_providers
251
+ : {};
252
+ const provider = providerName ? providers[providerName] : null;
253
+ if (providerName && !provider) {
254
+ issues.push({
255
+ code: 'provider-not-found',
256
+ message: `当前 provider 未在配置中找到: ${providerName}`,
257
+ suggestion: '检查 model_providers 是否包含该 provider 配置块'
258
+ });
259
+ }
260
+
261
+ if (provider && typeof provider === 'object') {
262
+ const baseUrl = typeof provider.base_url === 'string' ? provider.base_url.trim() : '';
263
+ if (!isValidHttpUrl(baseUrl)) {
264
+ issues.push({
265
+ code: 'base-url-invalid',
266
+ message: '当前 provider 的 base_url 无效',
267
+ suggestion: '请设置为 http/https 的完整 URL'
268
+ });
269
+ }
270
+
271
+ const requiresAuth = provider.requires_openai_auth;
272
+ if (requiresAuth !== false) {
273
+ const apiKey = typeof provider.preferred_auth_method === 'string'
274
+ ? provider.preferred_auth_method.trim()
275
+ : '';
276
+ if (!apiKey) {
277
+ issues.push({
278
+ code: 'api-key-missing',
279
+ message: '当前 provider 未配置 API Key',
280
+ suggestion: '在模板中设置 preferred_auth_method'
281
+ });
282
+ }
283
+ }
284
+ }
285
+
286
+ if (modelName) {
287
+ const models = readModels();
288
+ if (!models.includes(modelName)) {
289
+ issues.push({
290
+ code: 'model-unavailable',
291
+ message: `模型未在可用列表中找到: ${modelName}`,
292
+ suggestion: '在模型列表中添加该模型或切换到已有模型'
293
+ });
294
+ }
295
+ }
296
+
297
+ let remote = null;
298
+ if (params.remote) {
299
+ if (!provider) {
300
+ issues.push({
301
+ code: 'remote-skip-provider',
302
+ message: '无法进行远程探测:provider 未找到',
303
+ suggestion: '检查 model_provider 配置或关闭远程探测'
304
+ });
305
+ remote = {
306
+ type: 'remote-health-check',
307
+ provider: providerName,
308
+ endpoint: '',
309
+ ok: false,
310
+ statusCode: null,
311
+ message: '无法进行远程探测:provider 未找到',
312
+ checks: {}
313
+ };
314
+ } else {
315
+ const remoteReport = await runRemoteHealthCheck(providerName, provider, modelName, {
316
+ timeoutMs: Number.isFinite(params.timeoutMs) ? Number(params.timeoutMs) : undefined,
317
+ probeJsonPost: deps.probeJsonPost
318
+ });
319
+ issues.push(...remoteReport.issues);
320
+ remote = remoteReport.remote;
321
+ }
322
+ }
323
+
324
+ return {
325
+ ok: issues.length === 0,
326
+ issues,
327
+ summary: {
328
+ currentProvider: providerName,
329
+ currentModel: modelName
330
+ },
331
+ remote
332
+ };
333
+ }
334
+
335
+ module.exports = {
336
+ runRemoteHealthCheck,
337
+ buildConfigHealthReport
338
+ };