gx-code-agent 0.1.0 → 0.1.2
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.md +32 -5
- package/bin/gx.js +8 -0
- package/package.json +2 -1
- package/src/adapters/cli/args.js +13 -2
- package/src/adapters/platform/mcpClient.js +1 -1
- package/src/application/doctorCommand.js +381 -0
package/README.md
CHANGED
|
@@ -12,8 +12,8 @@ GX 高代码平台的伴生 CLI。M0 阶段提供 `gx init`:一次性完成 MC
|
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
# 主 CLI
|
|
15
|
-
npm i -g gx-code-agent@0.1.
|
|
16
|
-
npx --yes gx-code-agent@0.1.
|
|
15
|
+
npm i -g gx-code-agent@0.1.2
|
|
16
|
+
npx --yes gx-code-agent@0.1.2 --help
|
|
17
17
|
|
|
18
18
|
# 独立本地 MCP 启动器
|
|
19
19
|
npx --yes gx-mcp-local
|
|
@@ -86,6 +86,7 @@ gx verify [--dir <path>] [--manifest <path>] [--strict]
|
|
|
86
86
|
gx scan [--dir <path>] [--compile] [--report|--no-report] [--source-dir a,b]
|
|
87
87
|
gx audit [--dir <path>] [--compile] [--report|--no-report] [--strict]
|
|
88
88
|
gx chunks [--dir <path>] [--source-dir a,b] [--report|--no-report] [--no-content] [--strict]
|
|
89
|
+
gx doctor [--dir <path>] [--url <url>] [--token <token>] [--skip-mcp] [--skip-local] [--json] [--strict]
|
|
89
90
|
gx version
|
|
90
91
|
gx help
|
|
91
92
|
```
|
|
@@ -99,6 +100,31 @@ gx help
|
|
|
99
100
|
- 非交互模式写入必须带 `--yes`,否则退出码 2。
|
|
100
101
|
- 不支持的客户端会直接报错,不会猜测格式。
|
|
101
102
|
|
|
103
|
+
## 诊断:gx doctor
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# 人类可读
|
|
107
|
+
gx doctor
|
|
108
|
+
|
|
109
|
+
# 只输出 JSON(供 CI / 脚本)
|
|
110
|
+
gx doctor --json
|
|
111
|
+
|
|
112
|
+
# 离线环境跳过远端 MCP / 本地 MCP
|
|
113
|
+
gx doctor --skip-mcp --skip-local
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
检查项:
|
|
117
|
+
|
|
118
|
+
- Node.js 版本(>= 18);
|
|
119
|
+
- JDK / javac 版本(AST 与 `--compile` 需要 JDK 9+,推荐 17);
|
|
120
|
+
- `GX_API_TOKEN` 是否存在、格式是否为 `gxmcp_` + 43 位;
|
|
121
|
+
- `.gx/config.json` 的 planId/templateId;
|
|
122
|
+
- `.gx/manifest.json` 是否存在及文件数;
|
|
123
|
+
- 远端 MCP:`ping` / `tools/list` / `get_current_user`;
|
|
124
|
+
- `gx-local` 本地 stdio MCP 是否能启动并返回工具列表。
|
|
125
|
+
|
|
126
|
+
失败项会给出修复提示;`--strict` 时 warn 也视为失败。退出码:存在 fail(或 strict+warn)为 1。
|
|
127
|
+
|
|
102
128
|
## M3:拉取 / 应用 / 校验
|
|
103
129
|
|
|
104
130
|
```bash
|
|
@@ -235,7 +261,7 @@ CI 变量:
|
|
|
235
261
|
|
|
236
262
|
| 变量 | 说明 |
|
|
237
263
|
|---|---|
|
|
238
|
-
| `GX_AGENT_VERSION` | 固定 CLI 版本,默认 `0.1.
|
|
264
|
+
| `GX_AGENT_VERSION` | 固定 CLI 版本,默认 `0.1.2` |
|
|
239
265
|
| `NPM_REGISTRY` | npm registry;默认 `https://registry.npmjs.org/` |
|
|
240
266
|
| `GX_API_TOKEN` / `GX_PLAN_ID` | MCP 鉴权与方案范围 |
|
|
241
267
|
|
|
@@ -251,7 +277,7 @@ CI 变量:
|
|
|
251
277
|
node --test
|
|
252
278
|
```
|
|
253
279
|
|
|
254
|
-
当前:CLI `
|
|
280
|
+
当前:CLI `44/44`;平台侧 Java MCP `80/80`。
|
|
255
281
|
|
|
256
282
|
## 当前进度与后续
|
|
257
283
|
|
|
@@ -265,12 +291,13 @@ node --test
|
|
|
265
291
|
- P7:审计 compilation facet 合并、向量重建、审计保留、pgVector 健康检查
|
|
266
292
|
- P8:Java AST chunker、混合检索评测集、本地 stdio MCP(`gx-mcp-local`)
|
|
267
293
|
- CLI-02:`gx init` 支持 `--with-local`、timeout、项目绑定、连接自检、`.gx/.gitignore`、`--check`
|
|
294
|
+
- CLI-03:`gx doctor` 诊断 Node/JDK/Token/配置/manifest/MCP/`gx-local`,支持 `--json`
|
|
268
295
|
|
|
269
296
|
待办:
|
|
270
297
|
|
|
271
298
|
- `gx context` / `gx push` / `gx adopt`
|
|
272
299
|
- Agent 外壳(本地 stdio MCP 已提供基础版)
|
|
273
|
-
- 公共 npm
|
|
300
|
+
- 公共 npm 已发布 `gx-code-agent@0.1.2` 与 `gx-mcp-local@0.1.2`
|
|
274
301
|
|
|
275
302
|
## 与主平台的关系
|
|
276
303
|
|
package/bin/gx.js
CHANGED
|
@@ -8,6 +8,7 @@ import { parseArgs, usage } from "../src/adapters/cli/args.js";
|
|
|
8
8
|
import { runApply } from "../src/application/applyWorkflow.js";
|
|
9
9
|
import { runAudit } from "../src/application/auditCommand.js";
|
|
10
10
|
import { runChunks } from "../src/application/chunksCommand.js";
|
|
11
|
+
import { runDoctor } from "../src/application/doctorCommand.js";
|
|
11
12
|
import { runEvalSearch } from "../src/application/evalSearchCommand.js";
|
|
12
13
|
import { runInit } from "../src/application/initWorkflow.js";
|
|
13
14
|
import { runPull } from "../src/application/pullWorkflow.js";
|
|
@@ -85,6 +86,13 @@ async function main() {
|
|
|
85
86
|
}
|
|
86
87
|
return;
|
|
87
88
|
}
|
|
89
|
+
if (command === "doctor") {
|
|
90
|
+
const result = await runDoctor(args);
|
|
91
|
+
if (!result.ok) {
|
|
92
|
+
process.exitCode = 1;
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
88
96
|
|
|
89
97
|
usage();
|
|
90
98
|
process.exitCode = 1;
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gx-code-agent",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "GX 高代码平台伴生 CLI:注册 MCP、注入平台规范、校验生成物、本地 stdio MCP",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"gx": "bin/gx.js",
|
|
8
|
+
"gx-code-agent": "bin/gx.js",
|
|
8
9
|
"gx-mcp-local": "bin/gx-stdio-mcp.js"
|
|
9
10
|
},
|
|
10
11
|
"files": [
|
package/src/adapters/cli/args.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { UsageError } from "../../application/errors.js";
|
|
2
2
|
|
|
3
3
|
export function usage() {
|
|
4
|
-
console.log(`gx-code-agent 0.1.
|
|
4
|
+
console.log(`gx-code-agent 0.1.2
|
|
5
5
|
|
|
6
6
|
用法:
|
|
7
7
|
gx init [选项]
|
|
@@ -35,6 +35,7 @@ export function usage() {
|
|
|
35
35
|
gx audit 上报扩展点并调用 audit_alignment,合并本地/平台 Unknown
|
|
36
36
|
gx chunks 生成 Java/Vue 代码切片并上报 record_code_chunks
|
|
37
37
|
gx eval-search 用评测集计算 search_code 的 HitRate/MRR
|
|
38
|
+
gx doctor 诊断 Node/JDK/Token/配置/manifest/MCP/gx-local
|
|
38
39
|
|
|
39
40
|
gx pull 选项:
|
|
40
41
|
--plan <planId> --template <templateId> [--model-ids a,b] [--url <url>] [--token <token>] [--dry-run]
|
|
@@ -56,6 +57,16 @@ gx chunks 选项:
|
|
|
56
57
|
--no-report 只生成本地 .gx/chunks.json
|
|
57
58
|
--no-content 上报时不上传切片内容(仅元数据)
|
|
58
59
|
|
|
60
|
+
gx doctor 选项:
|
|
61
|
+
--dir <path> 项目目录,默认当前目录
|
|
62
|
+
--url <url> MCP 地址,默认读取 .gx/config.json 或 http://127.0.0.1:4013/mcp
|
|
63
|
+
--token <token> 临时指定 Token;默认读取 GX_API_TOKEN
|
|
64
|
+
--token-env <name> Token 环境变量名,默认 GX_API_TOKEN
|
|
65
|
+
--skip-mcp 跳过远端 MCP 检查
|
|
66
|
+
--skip-local 跳过 gx-local 检查
|
|
67
|
+
--json 输出 JSON
|
|
68
|
+
--strict warn 也视为失败
|
|
69
|
+
|
|
59
70
|
示例:
|
|
60
71
|
GX_API_TOKEN=gxmcp_xxx gx init --client opencode --yes
|
|
61
72
|
gx init --client claude --token gxmcp_xxx --embed-token --yes --url https://example.com/code/mcp
|
|
@@ -79,7 +90,7 @@ export function parseArgs(argv) {
|
|
|
79
90
|
continue;
|
|
80
91
|
}
|
|
81
92
|
const key = item.slice(2);
|
|
82
|
-
if (["help", "dry-run", "force", "embed-token", "yes", "strict", "compile", "report", "no-report", "json", "no-content", "check", "with-local", "skip-verify"].includes(key)) {
|
|
93
|
+
if (["help", "dry-run", "force", "embed-token", "yes", "strict", "compile", "report", "no-report", "json", "no-content", "check", "with-local", "skip-verify", "skip-mcp", "skip-local"].includes(key)) {
|
|
83
94
|
args[key] = true;
|
|
84
95
|
continue;
|
|
85
96
|
}
|
|
@@ -17,7 +17,7 @@ export class McpHttpClient {
|
|
|
17
17
|
this.url = options.url;
|
|
18
18
|
this.token = options.token;
|
|
19
19
|
this.clientName = options.clientName ?? "gx-code-agent";
|
|
20
|
-
this.clientVersion = options.clientVersion ?? "0.1.
|
|
20
|
+
this.clientVersion = options.clientVersion ?? "0.1.2";
|
|
21
21
|
this.protocolVersion = options.protocolVersion ?? "2025-06-18";
|
|
22
22
|
this.timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs > 0 ? options.timeoutMs : 30000;
|
|
23
23
|
this.sessionId = null;
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import { McpHttpClient } from "../adapters/platform/mcpClient.js";
|
|
7
|
+
import { readJson } from "../store/localStore.js";
|
|
8
|
+
|
|
9
|
+
const STDIO_BIN = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../bin/gx-stdio-mcp.js");
|
|
10
|
+
const TOKEN_PATTERN = /^gxmcp_[A-Za-z0-9_-]{43}$/;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 校验 GX MCP API Token 格式。
|
|
14
|
+
*
|
|
15
|
+
* @param {string | undefined} token
|
|
16
|
+
* @returns {{ status: "ok"|"warn"|"fail", message: string, fix?: string }}
|
|
17
|
+
*/
|
|
18
|
+
export function validateToken(token) {
|
|
19
|
+
if (!token) {
|
|
20
|
+
return {
|
|
21
|
+
status: "warn",
|
|
22
|
+
message: "未设置 GX_API_TOKEN",
|
|
23
|
+
fix: "在终端 export GX_API_TOKEN='gxmcp_...' 或写入 ~/.zshrc;见 README『快速开始』"
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (TOKEN_PATTERN.test(token)) {
|
|
27
|
+
return {
|
|
28
|
+
status: "ok",
|
|
29
|
+
message: `格式正确(gxmcp_ + 43 位,共 ${token.length} 字符)`
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
status: "fail",
|
|
34
|
+
message: `格式不正确(长度 ${token.length},应以 gxmcp_ 开头并共 49 字符)`,
|
|
35
|
+
fix: "重新创建 Token,见 README『快速开始』与 plan/MCPServer/opencode-integration.md 2.4"
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 解析 Node 主版本。
|
|
41
|
+
*
|
|
42
|
+
* @param {string} version
|
|
43
|
+
* @returns {number}
|
|
44
|
+
*/
|
|
45
|
+
export function parseNodeMajor(version) {
|
|
46
|
+
return Number(String(version).replace(/^v/u, "").split(".")[0]) || 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 从 java -version / javac -version 输出中解析主版本。
|
|
51
|
+
*
|
|
52
|
+
* @param {string} output
|
|
53
|
+
* @returns {number}
|
|
54
|
+
*/
|
|
55
|
+
export function parseJavaMajor(output) {
|
|
56
|
+
const value = String(output ?? "");
|
|
57
|
+
const quoted = /version\s+"(\d+)(?:\.(\d+))?/u.exec(value);
|
|
58
|
+
if (quoted) {
|
|
59
|
+
const major = Number(quoted[1]);
|
|
60
|
+
return major === 1 ? Number(quoted[2]) : major;
|
|
61
|
+
}
|
|
62
|
+
const plain = /(?:javac|openjdk|java)\s+(\d+)(?:\.(\d+))?/iu.exec(value);
|
|
63
|
+
if (plain) {
|
|
64
|
+
const major = Number(plain[1]);
|
|
65
|
+
return major === 1 ? Number(plain[2]) : major;
|
|
66
|
+
}
|
|
67
|
+
return 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 执行命令并合并 stdout/stderr。
|
|
72
|
+
*
|
|
73
|
+
* @param {string} command
|
|
74
|
+
* @param {string[]} args
|
|
75
|
+
* @returns {{ ok: boolean, output: string, error?: string }}
|
|
76
|
+
*/
|
|
77
|
+
function commandVersion(command, args) {
|
|
78
|
+
const result = spawnSync(command, args, {
|
|
79
|
+
encoding: "utf8",
|
|
80
|
+
timeout: 5000,
|
|
81
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
82
|
+
});
|
|
83
|
+
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
|
|
84
|
+
if (result.error) {
|
|
85
|
+
return { ok: false, output, error: result.error.message };
|
|
86
|
+
}
|
|
87
|
+
return { ok: result.status === 0, output, error: result.status === 0 ? undefined : `exit ${result.status}` };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 查找 java/javac 命令。
|
|
92
|
+
*
|
|
93
|
+
* @returns {{ java: string, javac: string, home?: string }}
|
|
94
|
+
*/
|
|
95
|
+
function javaCommands() {
|
|
96
|
+
const home = process.env.JAVA_HOME;
|
|
97
|
+
if (home) {
|
|
98
|
+
return {
|
|
99
|
+
home,
|
|
100
|
+
java: path.join(home, "bin", "java"),
|
|
101
|
+
javac: path.join(home, "bin", "javac")
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
return { java: "java", javac: "javac" };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 检查本地 stdio MCP。
|
|
109
|
+
*
|
|
110
|
+
* @returns {{ status: "ok"|"warn"|"fail", message: string, tools?: string[] }}
|
|
111
|
+
*/
|
|
112
|
+
function checkLocalMcp() {
|
|
113
|
+
if (!fs.existsSync(STDIO_BIN)) {
|
|
114
|
+
return { status: "fail", message: `找不到 ${STDIO_BIN}` };
|
|
115
|
+
}
|
|
116
|
+
const input = [
|
|
117
|
+
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"gx-doctor","version":"1"}}}',
|
|
118
|
+
'{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}',
|
|
119
|
+
""
|
|
120
|
+
].join("\n");
|
|
121
|
+
const result = spawnSync(process.execPath, [STDIO_BIN], {
|
|
122
|
+
input,
|
|
123
|
+
encoding: "utf8",
|
|
124
|
+
timeout: 5000
|
|
125
|
+
});
|
|
126
|
+
if (result.error) {
|
|
127
|
+
return { status: "warn", message: `gx-local 启动失败:${result.error.message}` };
|
|
128
|
+
}
|
|
129
|
+
if (result.status !== 0) {
|
|
130
|
+
return { status: "warn", message: `gx-local 退出码 ${result.status}` };
|
|
131
|
+
}
|
|
132
|
+
const messages = `${result.stdout ?? ""}`
|
|
133
|
+
.split(/\r?\n/u)
|
|
134
|
+
.map((line) => line.trim())
|
|
135
|
+
.filter(Boolean)
|
|
136
|
+
.map((line) => {
|
|
137
|
+
try {
|
|
138
|
+
return JSON.parse(line);
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
.filter(Boolean);
|
|
144
|
+
const tools = messages.find((message) => message.id === 2)?.result?.tools ?? [];
|
|
145
|
+
if (!Array.isArray(tools) || tools.length === 0) {
|
|
146
|
+
return { status: "warn", message: "gx-local 未返回 tools/list" };
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
status: "ok",
|
|
150
|
+
message: `gx-local 可用(${tools.length} 个工具)`,
|
|
151
|
+
tools: tools.map((item) => item.name).filter(Boolean)
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* 检查远端 MCP 连接。
|
|
157
|
+
*
|
|
158
|
+
* @param {string} url
|
|
159
|
+
* @param {string | undefined} token
|
|
160
|
+
* @returns {Promise<{ status: "ok"|"warn"|"fail"|"skip", message: string, details?: Record<string, unknown> }>}
|
|
161
|
+
*/
|
|
162
|
+
async function checkRemoteMcp(url, token) {
|
|
163
|
+
if (!token) {
|
|
164
|
+
return { status: "skip", message: "未提供 Token,跳过远端 MCP 检查" };
|
|
165
|
+
}
|
|
166
|
+
const client = new McpHttpClient({
|
|
167
|
+
url,
|
|
168
|
+
token,
|
|
169
|
+
clientName: "gx-doctor",
|
|
170
|
+
timeoutMs: 5000
|
|
171
|
+
});
|
|
172
|
+
try {
|
|
173
|
+
await client.initialize();
|
|
174
|
+
const tools = await client.listTools();
|
|
175
|
+
const ping = await client.callTool("ping", { client: "gx doctor" });
|
|
176
|
+
let user = null;
|
|
177
|
+
try {
|
|
178
|
+
user = await client.callTool("get_current_user", {});
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
user = null;
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
status: "ok",
|
|
185
|
+
message: `MCP 可用(协议 ${client.protocolVersion},工具 ${tools.length} 个)`,
|
|
186
|
+
details: { protocolVersion: client.protocolVersion, toolCount: tools.length, ping, user }
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
191
|
+
const httpStatus = Number(/MCP HTTP (\d+)/u.exec(message)?.[1] ?? 0);
|
|
192
|
+
return {
|
|
193
|
+
status: "fail",
|
|
194
|
+
message: httpStatus === 401 ? "MCP 返回 401,Token 无效/过期/已吊销" : `MCP 连接失败:${message}`,
|
|
195
|
+
details: { httpStatus }
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
finally {
|
|
199
|
+
await client.close().catch(() => {});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 执行 gx doctor。
|
|
205
|
+
*
|
|
206
|
+
* @param {Record<string, unknown>} args
|
|
207
|
+
* @param {{ stdout: NodeJS.WriteStream, stderr: NodeJS.WriteStream }} [io]
|
|
208
|
+
* @returns {Promise<Record<string, unknown>>}
|
|
209
|
+
*/
|
|
210
|
+
export async function runDoctor(args, io = { stdout: process.stdout, stderr: process.stderr }) {
|
|
211
|
+
const dir = path.resolve(String(args.dir || process.cwd()));
|
|
212
|
+
const config = readJson(path.join(dir, ".gx", "config.json"), {});
|
|
213
|
+
const url = String(args.url || config.mcpUrl || "http://127.0.0.1:4013/mcp");
|
|
214
|
+
const tokenEnv = String(args["token-env"] || "GX_API_TOKEN");
|
|
215
|
+
const token = typeof args.token === "string" ? args.token : process.env[tokenEnv];
|
|
216
|
+
const skipMcp = Boolean(args["skip-mcp"]);
|
|
217
|
+
const skipLocal = Boolean(args["skip-local"]);
|
|
218
|
+
const strict = Boolean(args.strict);
|
|
219
|
+
const checks = [];
|
|
220
|
+
|
|
221
|
+
const nodeMajor = parseNodeMajor(process.version);
|
|
222
|
+
checks.push({
|
|
223
|
+
id: "node",
|
|
224
|
+
label: "Node.js",
|
|
225
|
+
status: nodeMajor >= 18 ? "ok" : "fail",
|
|
226
|
+
message: `${process.version}${nodeMajor >= 18 ? "" : "(需要 >= 18)"}`,
|
|
227
|
+
...(nodeMajor >= 18 ? {} : { fix: "升级 Node.js 到 18 或更高版本" })
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const java = javaCommands();
|
|
231
|
+
const javaResult = commandVersion(java.java, ["-version"]);
|
|
232
|
+
const javacResult = commandVersion(java.javac, ["-version"]);
|
|
233
|
+
const javaMajor = parseJavaMajor(javaResult.output);
|
|
234
|
+
const javacMajor = parseJavaMajor(javacResult.output);
|
|
235
|
+
const jdkStatus = javaResult.ok && javacResult.ok && javaMajor >= 9 ? "ok" : "warn";
|
|
236
|
+
checks.push({
|
|
237
|
+
id: "jdk",
|
|
238
|
+
label: "JDK",
|
|
239
|
+
status: jdkStatus,
|
|
240
|
+
message: javaResult.ok ? (javaResult.output.split(/\r?\n/u)[0] || `Java ${javaMajor}`) : `java 不可用:${javaResult.error}`,
|
|
241
|
+
...(jdkStatus === "ok" ? {} : { fix: "安装 JDK 17,并设置 JAVA_HOME;见 README 环境要求" })
|
|
242
|
+
});
|
|
243
|
+
checks.push({
|
|
244
|
+
id: "javac",
|
|
245
|
+
label: "javac",
|
|
246
|
+
status: javacResult.ok && javacMajor >= 9 ? "ok" : "warn",
|
|
247
|
+
message: javacResult.ok ? javacResult.output.split(/\r?\n/u)[0] : `javac 不可用:${javacResult.error}`,
|
|
248
|
+
...(javacResult.ok && javacMajor >= 9 ? {} : { fix: "安装 JDK 17;gx scan --compile 需要 javac" })
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const tokenCheck = validateToken(token);
|
|
252
|
+
checks.push({
|
|
253
|
+
id: "token",
|
|
254
|
+
label: "GX_API_TOKEN",
|
|
255
|
+
status: tokenCheck.status,
|
|
256
|
+
message: tokenCheck.message,
|
|
257
|
+
...(tokenCheck.fix ? { fix: tokenCheck.fix } : {})
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const configPath = path.join(dir, ".gx", "config.json");
|
|
261
|
+
if (!fs.existsSync(configPath)) {
|
|
262
|
+
checks.push({
|
|
263
|
+
id: "config",
|
|
264
|
+
label: ".gx/config.json",
|
|
265
|
+
status: "warn",
|
|
266
|
+
message: "未找到;请先执行 gx init",
|
|
267
|
+
fix: "gx init --client opencode --plan <planId> --yes"
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
checks.push({
|
|
272
|
+
id: "config",
|
|
273
|
+
label: ".gx/config.json",
|
|
274
|
+
status: config.planId || config.templateId ? "ok" : "warn",
|
|
275
|
+
message: `planId=${config.planId ?? "-"}, templateId=${config.templateId ?? "-"}`,
|
|
276
|
+
...(config.planId || config.templateId ? {} : { fix: "gx init --plan <planId> --template <templateId> --yes" })
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const manifestPath = path.join(dir, ".gx", "manifest.json");
|
|
281
|
+
if (!fs.existsSync(manifestPath)) {
|
|
282
|
+
checks.push({
|
|
283
|
+
id: "manifest",
|
|
284
|
+
label: ".gx/manifest.json",
|
|
285
|
+
status: "warn",
|
|
286
|
+
message: "未找到;请先执行 gx pull",
|
|
287
|
+
fix: "gx pull --plan <planId> --template <templateId>"
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
try {
|
|
292
|
+
const manifest = readJson(manifestPath, {});
|
|
293
|
+
const fileCount = Array.isArray(manifest.files) ? manifest.files.length : 0;
|
|
294
|
+
checks.push({
|
|
295
|
+
id: "manifest",
|
|
296
|
+
label: ".gx/manifest.json",
|
|
297
|
+
status: "ok",
|
|
298
|
+
message: `${fileCount} 个文件,metadataVersion=${manifest.metadataVersion ?? "-"}`
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
checks.push({
|
|
303
|
+
id: "manifest",
|
|
304
|
+
label: ".gx/manifest.json",
|
|
305
|
+
status: "fail",
|
|
306
|
+
message: error instanceof Error ? error.message : String(error),
|
|
307
|
+
fix: "重新执行 gx pull,不要手工修改 manifest"
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (skipMcp) {
|
|
313
|
+
checks.push({ id: "mcp", label: "MCP", status: "skip", message: "使用 --skip-mcp 跳过" });
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
const mcp = await checkRemoteMcp(url, token);
|
|
317
|
+
checks.push({
|
|
318
|
+
id: "mcp",
|
|
319
|
+
label: "MCP",
|
|
320
|
+
status: mcp.status,
|
|
321
|
+
message: mcp.message,
|
|
322
|
+
...(mcp.details ? { details: mcp.details } : {}),
|
|
323
|
+
...(mcp.status === "fail" ? { fix: "检查 MCP 地址、Token、网络;见 plan/MCPServer/opencode-integration.md 第 6 节" } : {})
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (skipLocal) {
|
|
328
|
+
checks.push({ id: "local", label: "gx-local", status: "skip", message: "使用 --skip-local 跳过" });
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
const local = checkLocalMcp();
|
|
332
|
+
checks.push({
|
|
333
|
+
id: "local",
|
|
334
|
+
label: "gx-local",
|
|
335
|
+
status: local.status,
|
|
336
|
+
message: local.message,
|
|
337
|
+
...(local.tools ? { details: { tools: local.tools } } : {}),
|
|
338
|
+
...(local.status === "fail" ? { fix: "重新安装 gx-code-agent,或检查包内 bin/gx-stdio-mcp.js" } : {})
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const summary = {
|
|
343
|
+
ok: checks.filter((item) => item.status === "ok").length,
|
|
344
|
+
warn: checks.filter((item) => item.status === "warn").length,
|
|
345
|
+
fail: checks.filter((item) => item.status === "fail").length,
|
|
346
|
+
skip: checks.filter((item) => item.status === "skip").length
|
|
347
|
+
};
|
|
348
|
+
const ok = strict ? summary.fail === 0 && summary.warn === 0 : summary.fail === 0;
|
|
349
|
+
const report = {
|
|
350
|
+
ok,
|
|
351
|
+
generatedAt: new Date().toISOString(),
|
|
352
|
+
dir,
|
|
353
|
+
url,
|
|
354
|
+
checks,
|
|
355
|
+
summary
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
if (args.json) {
|
|
359
|
+
io.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
360
|
+
return report;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
io.stdout.write(`GX Doctor\n`);
|
|
364
|
+
io.stdout.write(`- 目录: ${dir}\n`);
|
|
365
|
+
io.stdout.write(`- MCP: ${url}\n\n`);
|
|
366
|
+
for (const item of checks) {
|
|
367
|
+
const marker = item.status === "ok" ? "OK" : item.status.toUpperCase();
|
|
368
|
+
io.stdout.write(`[${marker}] ${item.label}: ${item.message}\n`);
|
|
369
|
+
if (item.fix) {
|
|
370
|
+
io.stdout.write(` 修复: ${item.fix}\n`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
io.stdout.write(`\n汇总: ok=${summary.ok}, warn=${summary.warn}, fail=${summary.fail}, skip=${summary.skip}\n`);
|
|
374
|
+
if (!ok) {
|
|
375
|
+
io.stdout.write("结果: 未通过\n");
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
io.stdout.write("结果: 通过\n");
|
|
379
|
+
}
|
|
380
|
+
return report;
|
|
381
|
+
}
|