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.
- package/README.en.md +34 -17
- package/README.md +34 -25
- package/cli/config-health.js +338 -0
- package/cli.js +1570 -839
- package/lib/cli-models-utils.js +186 -27
- package/lib/cli-network-utils.js +117 -101
- package/package.json +8 -1
- package/web-ui/app.js +379 -5754
- package/web-ui/index.html +15 -2079
- package/web-ui/logic.agents-diff.mjs +386 -0
- package/web-ui/logic.claude.mjs +108 -0
- package/web-ui/logic.mjs +5 -793
- package/web-ui/logic.runtime.mjs +124 -0
- package/web-ui/logic.sessions.mjs +263 -0
- package/web-ui/modules/api.mjs +69 -0
- package/web-ui/modules/app.computed.dashboard.mjs +113 -0
- package/web-ui/modules/app.computed.index.mjs +13 -0
- package/web-ui/modules/app.computed.session.mjs +141 -0
- package/web-ui/modules/app.constants.mjs +15 -0
- package/web-ui/modules/app.methods.agents.mjs +493 -0
- package/web-ui/modules/app.methods.claude-config.mjs +174 -0
- package/web-ui/modules/app.methods.codex-config.mjs +640 -0
- package/web-ui/modules/app.methods.index.mjs +86 -0
- package/web-ui/modules/app.methods.install.mjs +157 -0
- package/web-ui/modules/app.methods.navigation.mjs +478 -0
- package/web-ui/modules/app.methods.openclaw-core.mjs +514 -0
- package/web-ui/modules/app.methods.openclaw-editing.mjs +337 -0
- package/web-ui/modules/app.methods.openclaw-persist.mjs +251 -0
- package/web-ui/modules/app.methods.providers.mjs +265 -0
- package/web-ui/modules/app.methods.runtime.mjs +323 -0
- package/web-ui/modules/app.methods.session-actions.mjs +457 -0
- package/web-ui/modules/app.methods.session-browser.mjs +435 -0
- package/web-ui/modules/app.methods.session-timeline.mjs +441 -0
- package/web-ui/modules/app.methods.session-trash.mjs +419 -0
- package/web-ui/modules/app.methods.startup-claude.mjs +406 -0
- package/web-ui/modules/config-mode.computed.mjs +1 -0
- package/web-ui/modules/skills.computed.mjs +26 -1
- package/web-ui/modules/skills.methods.mjs +154 -23
- package/web-ui/partials/index/layout-footer.html +69 -0
- package/web-ui/partials/index/layout-header.html +337 -0
- package/web-ui/partials/index/modal-config-template-agents.html +125 -0
- package/web-ui/partials/index/modal-confirm-toast.html +32 -0
- package/web-ui/partials/index/modal-health-check.html +72 -0
- package/web-ui/partials/index/modal-openclaw-config.html +275 -0
- package/web-ui/partials/index/modal-skills.html +184 -0
- package/web-ui/partials/index/modals-basic.html +196 -0
- package/web-ui/partials/index/panel-config-claude.html +100 -0
- package/web-ui/partials/index/panel-config-codex.html +237 -0
- package/web-ui/partials/index/panel-config-openclaw.html +84 -0
- package/web-ui/partials/index/panel-market.html +174 -0
- package/web-ui/partials/index/panel-sessions.html +387 -0
- package/web-ui/partials/index/panel-settings.html +166 -0
- package/web-ui/session-helpers.mjs +12 -0
- package/web-ui/source-bundle.cjs +233 -0
- package/web-ui/styles/base-theme.css +373 -0
- package/web-ui/styles/controls-forms.css +354 -0
- package/web-ui/styles/feedback.css +108 -0
- package/web-ui/styles/health-check-dialog.css +144 -0
- package/web-ui/styles/layout-shell.css +330 -0
- package/web-ui/styles/modals-core.css +449 -0
- package/web-ui/styles/navigation-panels.css +381 -0
- package/web-ui/styles/openclaw-structured.css +266 -0
- package/web-ui/styles/responsive.css +416 -0
- package/web-ui/styles/sessions-list.css +414 -0
- package/web-ui/styles/sessions-preview.css +405 -0
- package/web-ui/styles/sessions-toolbar-trash.css +243 -0
- package/web-ui/styles/sessions-usage.css +276 -0
- package/web-ui/styles/skills-list.css +298 -0
- package/web-ui/styles/skills-market.css +335 -0
- package/web-ui/styles/titles-cards.css +407 -0
- package/web-ui/styles.css +16 -4499
- package/doc/CHANGELOG.md +0 -32
- 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
|
|
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
|
-
##
|
|
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
|
-
|
|
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 `
|
|
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` | `
|
|
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
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
默认监听 `
|
|
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` | `
|
|
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
|
-
|
|
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
|
+
};
|