heibai 1.0.8
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/.claude/settings.local.json +10 -0
- package/README.md +89 -0
- package/bin/cli.js +1140 -0
- package/bin/heibai.js +24 -0
- package/bin/xuqiu +31 -0
- package/code-validator.js +486 -0
- package/package.json +38 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,1140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { exec, spawn } = require('child_process');
|
|
6
|
+
const inquirer = require('inquirer');
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const axios = require('axios');
|
|
9
|
+
const readline = require('readline');
|
|
10
|
+
const os = require('os');
|
|
11
|
+
const crypto = require('crypto');
|
|
12
|
+
const CodeValidator = require('../code-validator');
|
|
13
|
+
|
|
14
|
+
class AIAssistantSetup {
|
|
15
|
+
constructor() {
|
|
16
|
+
this.isWindows = process.platform === 'win32';
|
|
17
|
+
this.isMac = process.platform === 'darwin';
|
|
18
|
+
// this.proxyPort = '7890';
|
|
19
|
+
// this.proxyUrl = '';
|
|
20
|
+
this.publicIPCache = null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async start() {
|
|
24
|
+
console.log(chalk.blue.bold('🚀 claude code快速配置工具'));
|
|
25
|
+
console.log(chalk.gray('支持 Windows/Mac 双平台\n'));
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
await this.setupProxy();
|
|
29
|
+
await this.testConnection();
|
|
30
|
+
// 启动即进入菜单(不自动登录/不静默配置)
|
|
31
|
+
const mode = await this.promptMode();
|
|
32
|
+
if (mode === '2') {
|
|
33
|
+
await this.handleLoginMode();
|
|
34
|
+
return;
|
|
35
|
+
} else if (mode === '3') {
|
|
36
|
+
await this.handleInstallClaudeCode();
|
|
37
|
+
return;
|
|
38
|
+
} else if (mode === '4') {
|
|
39
|
+
await this.handleClaudeCodeConfig();
|
|
40
|
+
return;
|
|
41
|
+
} else if (mode === '5') {
|
|
42
|
+
console.log(chalk.green('已退出'));
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const ma = await this.promptExchangeCode();
|
|
47
|
+
const validator = new CodeValidator();
|
|
48
|
+
console.log(chalk.yellow('正在验证兑换码...'));
|
|
49
|
+
const verifyResult = await validator.validateCode(ma);
|
|
50
|
+
if (!verifyResult.valid) {
|
|
51
|
+
throw new Error(verifyResult.message);
|
|
52
|
+
}
|
|
53
|
+
this.exchangeCode = ma;
|
|
54
|
+
this.codeObjectId = verifyResult.data.objectId;
|
|
55
|
+
console.log(chalk.green(`✅ ${verifyResult.message}`));
|
|
56
|
+
if (verifyResult.data) {
|
|
57
|
+
const expireTime = verifyResult.data.expireTime;
|
|
58
|
+
console.log(chalk.gray(` 过期时间: ${expireTime.toLocaleString('zh-CN', {
|
|
59
|
+
year: 'numeric',
|
|
60
|
+
month: '2-digit',
|
|
61
|
+
day: '2-digit',
|
|
62
|
+
hour: '2-digit',
|
|
63
|
+
minute: '2-digit',
|
|
64
|
+
second: '2-digit'
|
|
65
|
+
})}`));
|
|
66
|
+
console.log(chalk.gray(` 使用次数: ${verifyResult.data.usageCount}`));
|
|
67
|
+
|
|
68
|
+
// 上传机器码和IP信息
|
|
69
|
+
await this.uploadMachineInfo(verifyResult.data.objectId);
|
|
70
|
+
}
|
|
71
|
+
console.log('');
|
|
72
|
+
|
|
73
|
+
await this.checkNodeVersion();
|
|
74
|
+
await this.installAIService();
|
|
75
|
+
await this.startAIService();
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error(chalk.red('❌ 设置失败:'), error.message);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 1.0.7无静默配置逻辑
|
|
83
|
+
|
|
84
|
+
async promptExchangeCode() {
|
|
85
|
+
const cached = this.loadCachedCode();
|
|
86
|
+
const currentVersion = this.getPackageVersion();
|
|
87
|
+
|
|
88
|
+
// 如果有缓存的兑换码,询问是否使用
|
|
89
|
+
if (cached && cached.exchangeCode && cached.scriptVersion === currentVersion) {
|
|
90
|
+
console.log(chalk.cyan('🔐 检测到上次保存的兑换码'));
|
|
91
|
+
console.log(chalk.yellow(` 兑换码: ${cached.exchangeCode.trim()}`));
|
|
92
|
+
|
|
93
|
+
const useChoice = await inquirer.prompt([
|
|
94
|
+
{
|
|
95
|
+
type: 'rawlist',
|
|
96
|
+
name: 'choice',
|
|
97
|
+
message: '请选择操作',
|
|
98
|
+
choices: [
|
|
99
|
+
{ name: '1 使用上次保存的兑换码', value: '1' },
|
|
100
|
+
{ name: '2 重新输入兑换码', value: '2' }
|
|
101
|
+
],
|
|
102
|
+
default: 0
|
|
103
|
+
}
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
if (useChoice.choice === '1') {
|
|
107
|
+
console.log(chalk.green('✅ 使用缓存的兑换码'));
|
|
108
|
+
return cached.exchangeCode.trim();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 输入新的兑换码
|
|
113
|
+
console.log(chalk.cyan('🔐 请输入兑换码验证使用权限'));
|
|
114
|
+
const answers = await inquirer.prompt([
|
|
115
|
+
{
|
|
116
|
+
type: 'input',
|
|
117
|
+
name: 'ma',
|
|
118
|
+
message: '兑换码:',
|
|
119
|
+
validate: function(input) {
|
|
120
|
+
if (!input || input.trim().length === 0) {
|
|
121
|
+
return '兑换码不能为空';
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
]);
|
|
127
|
+
const ma = answers.ma.trim();
|
|
128
|
+
this.saveCachedCode({ exchangeCode: ma, scriptVersion: currentVersion });
|
|
129
|
+
return ma;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async promptMode() {
|
|
133
|
+
const answers = await inquirer.prompt([
|
|
134
|
+
{
|
|
135
|
+
type: 'rawlist',
|
|
136
|
+
name: 'mode',
|
|
137
|
+
message: '请选择模式(可直接按 1/2/3/4/5)',
|
|
138
|
+
choices: [
|
|
139
|
+
{ name: '1 快捷模式', value: '1' },
|
|
140
|
+
{ name: '2 登陆模式', value: '2' },
|
|
141
|
+
{ name: '3 安装 Claude Code', value: '3' },
|
|
142
|
+
{ name: '4 配置 Claude Code key', value: '4' },
|
|
143
|
+
{ name: '5 退出程序', value: '5' }
|
|
144
|
+
],
|
|
145
|
+
default: 0
|
|
146
|
+
}
|
|
147
|
+
]);
|
|
148
|
+
return answers.mode;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async handleInstallClaudeCode() {
|
|
152
|
+
const env = { ...process.env };
|
|
153
|
+
if (this.proxyUrl) {
|
|
154
|
+
env.HTTP_PROXY = this.proxyUrl;
|
|
155
|
+
env.HTTPS_PROXY = this.proxyUrl;
|
|
156
|
+
}
|
|
157
|
+
const tryExec = (cmd) => new Promise((resolve) => {
|
|
158
|
+
exec(cmd, { env, shell: true }, (err, stdout) => {
|
|
159
|
+
if (err) return resolve(null);
|
|
160
|
+
resolve((stdout || '').toString().trim());
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
const showVersion = async () => {
|
|
164
|
+
let ver = await tryExec('claude -v');
|
|
165
|
+
if (!ver && process.platform === 'win32') {
|
|
166
|
+
ver = await tryExec('npx win-claude-code@latest -v');
|
|
167
|
+
}
|
|
168
|
+
if (ver) {
|
|
169
|
+
console.log(chalk.green(`已安装: ${ver}`));
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
173
|
+
};
|
|
174
|
+
if (await showVersion()) return;
|
|
175
|
+
console.log(chalk.yellow('正在安装 Claude Code...'));
|
|
176
|
+
if (process.platform === 'win32') {
|
|
177
|
+
await new Promise((resolve, reject) => {
|
|
178
|
+
const p = spawn('npm', ['install', '-g', '@anthropic-ai/claude-code', '--ignore-scripts'], { stdio: 'inherit', env, shell: true });
|
|
179
|
+
p.on('close', code => code === 0 ? resolve() : reject(new Error('安装失败')));
|
|
180
|
+
p.on('error', reject);
|
|
181
|
+
});
|
|
182
|
+
} else {
|
|
183
|
+
let command = 'npm';
|
|
184
|
+
if (process.platform === 'darwin') {
|
|
185
|
+
try { require('child_process').execSync('which /opt/homebrew/bin/npm', { stdio: 'ignore' }); command = '/opt/homebrew/bin/npm'; } catch {}
|
|
186
|
+
try { if (command === 'npm') { require('child_process').execSync('which /usr/local/bin/npm', { stdio: 'ignore' }); command = '/usr/local/bin/npm'; } } catch {}
|
|
187
|
+
}
|
|
188
|
+
await new Promise((resolve, reject) => {
|
|
189
|
+
const p = spawn(command, ['install', '-g', '@anthropic-ai/claude-code'], { stdio: 'inherit', env });
|
|
190
|
+
p.on('close', code => code === 0 ? resolve() : reject(new Error('安装失败')));
|
|
191
|
+
p.on('error', reject);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
const ver = await tryExec('claude -v') || await tryExec('npx win-claude-code@latest -v') || '';
|
|
195
|
+
if (ver) {
|
|
196
|
+
console.log(chalk.green(`安装成功 版本号 ${ver}`));
|
|
197
|
+
} else {
|
|
198
|
+
console.log(chalk.green('安装成功'));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async handleClaudeCodeConfig() {
|
|
203
|
+
console.log(chalk.blue.bold('🔧 配置 Claude Code'));
|
|
204
|
+
console.log(chalk.gray('支持 Windows/Mac/Linux 平台配置\n'));
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
// 第一步:验证兑换码(参考模式1的逻辑)
|
|
208
|
+
const ma = await this.promptExchangeCode();
|
|
209
|
+
const validator = new CodeValidator();
|
|
210
|
+
console.log(chalk.yellow('正在验证兑换码...'));
|
|
211
|
+
const verifyResult = await validator.validateCode(ma);
|
|
212
|
+
|
|
213
|
+
if (!verifyResult.valid) {
|
|
214
|
+
throw new Error(verifyResult.message);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
this.exchangeCode = ma;
|
|
218
|
+
this.codeObjectId = verifyResult.data.objectId;
|
|
219
|
+
console.log(chalk.green(`✅ ${verifyResult.message}`));
|
|
220
|
+
|
|
221
|
+
if (verifyResult.data) {
|
|
222
|
+
const expireTime = verifyResult.data.expireTime;
|
|
223
|
+
console.log(chalk.gray(` 过期时间: ${expireTime.toLocaleString('zh-CN', {
|
|
224
|
+
year: 'numeric',
|
|
225
|
+
month: '2-digit',
|
|
226
|
+
day: '2-digit',
|
|
227
|
+
hour: '2-digit',
|
|
228
|
+
minute: '2-digit',
|
|
229
|
+
second: '2-digit'
|
|
230
|
+
})}`));
|
|
231
|
+
console.log(chalk.gray(` 使用次数: ${verifyResult.data.usageCount}`));
|
|
232
|
+
|
|
233
|
+
// 上传机器码和IP信息
|
|
234
|
+
await this.uploadMachineInfo(verifyResult.data.objectId);
|
|
235
|
+
}
|
|
236
|
+
console.log('');
|
|
237
|
+
|
|
238
|
+
// 第二步:检查URL和key参数
|
|
239
|
+
const apiUrl = verifyResult.data.url;
|
|
240
|
+
const apiKey = verifyResult.data.key;
|
|
241
|
+
|
|
242
|
+
// 检查URL是否为空
|
|
243
|
+
if (!apiUrl || apiUrl.trim().length === 0) {
|
|
244
|
+
console.log(chalk.red('❌ 你暂无权限,联系客服'));
|
|
245
|
+
await this.waitForKeyPress();
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 检查key是否为空
|
|
250
|
+
if (!apiKey || apiKey.trim().length === 0) {
|
|
251
|
+
console.log(chalk.red('❌ 密钥未配置,请联系客服'));
|
|
252
|
+
await this.waitForKeyPress();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
console.log(chalk.green(`✅ 获取到API配置信息`));
|
|
257
|
+
console.log(chalk.gray(` API地址: ${apiUrl}`));
|
|
258
|
+
console.log(chalk.gray(` API密钥: ${apiKey.substring(0, 8)}...`));
|
|
259
|
+
|
|
260
|
+
// 第三步:配置 Claude Code settings(使用数据库的URL和key)
|
|
261
|
+
await this.writeClaudeCodeSettings(apiKey, apiUrl);
|
|
262
|
+
|
|
263
|
+
console.log(chalk.green('✅ Claude Code 配置完成!'));
|
|
264
|
+
console.log(chalk.blue('现在您可以使用 claude 命令开始使用 Claude Code'));
|
|
265
|
+
} catch (error) {
|
|
266
|
+
console.error(chalk.red('❌ 配置失败:'), error.message);
|
|
267
|
+
throw error;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async writeClaudeCodeSettings(apiKey, apiUrl, silent=false) {
|
|
272
|
+
const os = require('os');
|
|
273
|
+
const isWindows = process.platform === 'win32';
|
|
274
|
+
const isMac = process.platform === 'darwin';
|
|
275
|
+
const isLinux = process.platform === 'linux';
|
|
276
|
+
|
|
277
|
+
// 规范化 URL:确保以 /anthropic 结尾,移除多余斜杠
|
|
278
|
+
const inputUrl = (apiUrl || "https://e5c32534bbce.ngrok-free.app/anthropic").trim();
|
|
279
|
+
const withoutTrailingSlash = inputUrl.replace(/\/$/, '');
|
|
280
|
+
const baseUrl = /\/anthropic$/.test(withoutTrailingSlash)
|
|
281
|
+
? withoutTrailingSlash
|
|
282
|
+
: `${withoutTrailingSlash}/anthropic`;
|
|
283
|
+
|
|
284
|
+
const trimmedKey = (apiKey || '').trim();
|
|
285
|
+
|
|
286
|
+
const config = {
|
|
287
|
+
"env": {
|
|
288
|
+
"ANTHROPIC_BASE_URL": baseUrl,
|
|
289
|
+
"ANTHROPIC_API_KEY": trimmedKey,
|
|
290
|
+
"ANTHROPIC_AUTH_TOKEN": trimmedKey
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
try {
|
|
295
|
+
const configDir = path.join(os.homedir(), '.claude');
|
|
296
|
+
const configFile = path.join(configDir, 'settings.json');
|
|
297
|
+
|
|
298
|
+
// 创建目录(如果不存在)
|
|
299
|
+
if (!fs.existsSync(configDir)) {
|
|
300
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 写入配置文件
|
|
304
|
+
fs.writeFileSync(configFile, JSON.stringify(config, null, 2));
|
|
305
|
+
|
|
306
|
+
if (!silent) {
|
|
307
|
+
if (isWindows) {
|
|
308
|
+
console.log(chalk.blue(`📝 Windows 配置已保存到: ${configFile}`));
|
|
309
|
+
} else if (isMac) {
|
|
310
|
+
console.log(chalk.blue(`📝 Mac 配置已保存到: ${configFile}`));
|
|
311
|
+
} else if (isLinux) {
|
|
312
|
+
console.log(chalk.blue(`📝 Linux 配置已保存到: ${configFile}`));
|
|
313
|
+
} else {
|
|
314
|
+
console.log(chalk.blue(`📝 配置已保存到: ${configFile}`));
|
|
315
|
+
}
|
|
316
|
+
console.log(chalk.gray('配置内容:'));
|
|
317
|
+
console.log(chalk.gray(` ANTHROPIC_BASE_URL: ${config.env.ANTHROPIC_BASE_URL}`));
|
|
318
|
+
if (trimmedKey) console.log(chalk.gray(` ANTHROPIC_API_KEY: ${trimmedKey.substring(0, 8)}...`));
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
} catch (error) {
|
|
322
|
+
console.error(chalk.red('❌ 写入配置文件失败:'), error.message);
|
|
323
|
+
throw error;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async checkCopilotApiInstalled() {
|
|
328
|
+
console.log(chalk.yellow('🔍 检查 copilot-api 是否已安装...'));
|
|
329
|
+
|
|
330
|
+
return new Promise((resolve, reject) => {
|
|
331
|
+
// 直接尝试运行 copilot-api 命令来检测
|
|
332
|
+
const testProcess = exec('copilot-api', {
|
|
333
|
+
env: { ...process.env },
|
|
334
|
+
shell: true
|
|
335
|
+
}, (error, stdout, stderr) => {
|
|
336
|
+
// 检查输出是否包含 copilot-api 相关信息
|
|
337
|
+
const allOutput = (stdout + stderr).toLowerCase();
|
|
338
|
+
if (allOutput.includes('usage') ||
|
|
339
|
+
allOutput.includes('copilot-api') ||
|
|
340
|
+
allOutput.includes('commands') ||
|
|
341
|
+
allOutput.includes('auth') ||
|
|
342
|
+
allOutput.includes('start') ||
|
|
343
|
+
allOutput.includes('no command specified')) {
|
|
344
|
+
console.log(chalk.green('✅ 黑白已安装'));
|
|
345
|
+
resolve();
|
|
346
|
+
} else if (error && (error.code === 'ENOENT' || error.message.includes('not found'))) {
|
|
347
|
+
console.log(chalk.red('❌ 黑白未找到,请先安装'));
|
|
348
|
+
console.log(chalk.gray('提示: 请运行 npm install -g @copilot-api/copilot-api 或 npm install -g copilot-api 来安装'));
|
|
349
|
+
reject(new Error('copilot-api 未安装或不在PATH中'));
|
|
350
|
+
} else {
|
|
351
|
+
// 其他错误,但能执行说明命令存在
|
|
352
|
+
console.log(chalk.green('✅ 黑白已安装'));
|
|
353
|
+
resolve();
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// 3秒超时
|
|
358
|
+
setTimeout(() => {
|
|
359
|
+
testProcess.kill();
|
|
360
|
+
console.log(chalk.green('✅ 黑白已安装'));
|
|
361
|
+
resolve();
|
|
362
|
+
}, 3000);
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async handleLoginMode() {
|
|
367
|
+
console.log(chalk.blue.bold('🚀 启动 黑白登录模式'));
|
|
368
|
+
|
|
369
|
+
try {
|
|
370
|
+
// 检查 copilot-api 是否安装
|
|
371
|
+
// await this.checkCopilotApiInstalled();
|
|
372
|
+
|
|
373
|
+
const spawnOptions = {
|
|
374
|
+
env: { ...process.env },
|
|
375
|
+
shell: true // 在 Windows 上需要 shell: true
|
|
376
|
+
};
|
|
377
|
+
if (this.proxyUrl) {
|
|
378
|
+
spawnOptions.env.HTTP_PROXY = this.proxyUrl;
|
|
379
|
+
spawnOptions.env.HTTPS_PROXY = this.proxyUrl;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
console.log(chalk.gray('正在启动 黑白...'));
|
|
383
|
+
console.log(chalk.blue('正在启动认证流程...'));
|
|
384
|
+
await new Promise((resolve, reject) => {
|
|
385
|
+
const p = spawn('copilot-api', ['auth'], spawnOptions);
|
|
386
|
+
p.stdout.on('data', d => {
|
|
387
|
+
const text = d.toString();
|
|
388
|
+
process.stdout.write(text);
|
|
389
|
+
const codeMatch = text.match(/code\s+"([A-Z0-9-]+)"/i);
|
|
390
|
+
const urlMatch = text.match(/https?:\/\/[^\s]+/i);
|
|
391
|
+
if (codeMatch) {
|
|
392
|
+
console.log(chalk.green.bold(`\n🎉 验证码: ${codeMatch[1]}`));
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
p.stderr.on('data', d => process.stderr.write(d.toString()));
|
|
396
|
+
p.on('error', err => reject(err));
|
|
397
|
+
p.on('close', () => resolve());
|
|
398
|
+
});
|
|
399
|
+
} catch (error) {
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
getPackageVersion() {
|
|
405
|
+
try {
|
|
406
|
+
const pkgPath = path.join(__dirname, '..', 'package.json');
|
|
407
|
+
const raw = fs.readFileSync(pkgPath, 'utf8');
|
|
408
|
+
const pkg = JSON.parse(raw);
|
|
409
|
+
return pkg.version || '0.0.0';
|
|
410
|
+
} catch (e) {
|
|
411
|
+
return '0.0.0';
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
loadCachedCode() {
|
|
416
|
+
try {
|
|
417
|
+
const os = require('os');
|
|
418
|
+
const dir = path.join(os.homedir(), '.claude');
|
|
419
|
+
const file = path.join(dir, 'code_cache.json');
|
|
420
|
+
if (fs.existsSync(file)) {
|
|
421
|
+
const raw = fs.readFileSync(file, 'utf8');
|
|
422
|
+
return JSON.parse(raw);
|
|
423
|
+
}
|
|
424
|
+
return null;
|
|
425
|
+
} catch (e) {
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
saveCachedCode(data) {
|
|
431
|
+
try {
|
|
432
|
+
const os = require('os');
|
|
433
|
+
const dir = path.join(os.homedir(), '.claude');
|
|
434
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
435
|
+
const file = path.join(dir, 'code_cache.json');
|
|
436
|
+
let existing = {};
|
|
437
|
+
try {
|
|
438
|
+
if (fs.existsSync(file)) {
|
|
439
|
+
existing = JSON.parse(fs.readFileSync(file, 'utf8')) || {};
|
|
440
|
+
}
|
|
441
|
+
} catch {}
|
|
442
|
+
const merged = { ...existing, ...data };
|
|
443
|
+
fs.writeFileSync(file, JSON.stringify(merged, null, 2));
|
|
444
|
+
} catch (e) {}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
async checkNodeVersion() {
|
|
448
|
+
console.log(chalk.yellow('🔍 检查 Node.js 版本...'));
|
|
449
|
+
|
|
450
|
+
return new Promise((resolve, reject) => {
|
|
451
|
+
const nodeCmds = ['/opt/homebrew/bin/node', '/usr/local/bin/node', 'node'];
|
|
452
|
+
const tryNext = (i=0) => {
|
|
453
|
+
if (i >= nodeCmds.length) {
|
|
454
|
+
reject(new Error('Node.js 未安装,请先安装 Node.js >= 18.0'));
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
exec(`${nodeCmds[i]} --version`, (error, stdout) => {
|
|
458
|
+
if (error) {
|
|
459
|
+
tryNext(i+1);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const version = stdout.trim();
|
|
463
|
+
const majorVersion = parseInt(version.replace('v', '').split('.')[0]);
|
|
464
|
+
if (majorVersion < 18) {
|
|
465
|
+
reject(new Error(`Node.js 版本过低 (${version}),需要 >= 18.0`));
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
console.log(chalk.green(`✅ Node.js 版本: ${version}`));
|
|
469
|
+
resolve();
|
|
470
|
+
});
|
|
471
|
+
};
|
|
472
|
+
tryNext();
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
async setupProxy() {
|
|
477
|
+
console.log(chalk.yellow('🌐 设置代理...'));
|
|
478
|
+
// 确保 heibai-api 在此时即已可用(静默处理,不输出)
|
|
479
|
+
try { await this.ensureHeibaiApiAvailable(); } catch {}
|
|
480
|
+
|
|
481
|
+
const answers = await inquirer.prompt([
|
|
482
|
+
{
|
|
483
|
+
type: 'input',
|
|
484
|
+
name: 'proxyPort',
|
|
485
|
+
message: '请输入代理端口 (输入0不使用代理,回车默认7890):',
|
|
486
|
+
default: '7890',
|
|
487
|
+
validate: function(input) {
|
|
488
|
+
if (input === '0') return true;
|
|
489
|
+
const port = parseInt(input);
|
|
490
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
491
|
+
return '请输入有效的端口号 (1-65535 或 0)';
|
|
492
|
+
}
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
]);
|
|
497
|
+
|
|
498
|
+
this.proxyPort = answers.proxyPort;
|
|
499
|
+
if (this.proxyPort === '0') {
|
|
500
|
+
this.proxyUrl = '';
|
|
501
|
+
delete process.env.HTTP_PROXY;
|
|
502
|
+
delete process.env.HTTPS_PROXY;
|
|
503
|
+
console.log(chalk.green('✅ 不使用代理'));
|
|
504
|
+
} else {
|
|
505
|
+
this.proxyUrl = `http://127.0.0.1:${this.proxyPort}`;
|
|
506
|
+
process.env.HTTP_PROXY = this.proxyUrl;
|
|
507
|
+
process.env.HTTPS_PROXY = this.proxyUrl;
|
|
508
|
+
console.log(chalk.green(`✅ 代理设置完成: ${this.proxyUrl}`));
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
async testConnection() {
|
|
513
|
+
console.log(chalk.yellow('🌍 测试网络连接...'));
|
|
514
|
+
|
|
515
|
+
try {
|
|
516
|
+
if (!this.proxyUrl) {
|
|
517
|
+
console.log(chalk.gray('未使用代理,跳过网络测试'));
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const proxyUrl = new URL(this.proxyUrl);
|
|
521
|
+
const response = await axios.get('http://ipinfo.io/json', {
|
|
522
|
+
proxy: {
|
|
523
|
+
protocol: proxyUrl.protocol.slice(0, -1),
|
|
524
|
+
host: proxyUrl.hostname,
|
|
525
|
+
port: parseInt(proxyUrl.port)
|
|
526
|
+
},
|
|
527
|
+
timeout: 10000,
|
|
528
|
+
headers: {
|
|
529
|
+
'User-Agent': 'curl/7.68.0'
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
try { console.log(JSON.stringify(response.data, null, 2)); } catch {}
|
|
534
|
+
const ip = response.data.ip;
|
|
535
|
+
const city = response.data.city;
|
|
536
|
+
const region = response.data.region;
|
|
537
|
+
const country = response.data.country;
|
|
538
|
+
console.log(chalk.blue(`🌐 当前IP: ${ip}`));
|
|
539
|
+
console.log(chalk.blue(`📍 位置: ${city}, ${region}, ${country}`));
|
|
540
|
+
// 缓存IP
|
|
541
|
+
if (ip && typeof ip === 'string') {
|
|
542
|
+
this.publicIPCache = ip;
|
|
543
|
+
this.saveCachedCode({ publicIP: ip });
|
|
544
|
+
}
|
|
545
|
+
if (country && country !== 'CN') {
|
|
546
|
+
console.log(chalk.green(`✅ 网络连接正常,代理生效`));
|
|
547
|
+
} else {
|
|
548
|
+
console.log(chalk.yellow('⚠️ 警告: 当前位置显示为中国,代理可能未生效'));
|
|
549
|
+
console.log(chalk.gray('建议检查:'));
|
|
550
|
+
console.log(chalk.gray('1. 代理软件是否正在运行'));
|
|
551
|
+
console.log(chalk.gray('2. 端口号是否正确'));
|
|
552
|
+
console.log(chalk.gray('3. 代理协议设置是否正确'));
|
|
553
|
+
const proceed = await inquirer.prompt([
|
|
554
|
+
{
|
|
555
|
+
type: 'confirm',
|
|
556
|
+
name: 'continue',
|
|
557
|
+
message: '是否继续安装?',
|
|
558
|
+
default: true
|
|
559
|
+
}
|
|
560
|
+
]);
|
|
561
|
+
if (!proceed.continue) {
|
|
562
|
+
throw new Error('用户取消安装');
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
} catch (error) {
|
|
566
|
+
if (error.message === '用户取消安装') {
|
|
567
|
+
throw error;
|
|
568
|
+
}
|
|
569
|
+
console.log(chalk.red('❌ 网络连接测试失败'));
|
|
570
|
+
console.log(chalk.yellow('可能原因:'));
|
|
571
|
+
console.log(chalk.yellow('1. 网络不可用'));
|
|
572
|
+
console.log(chalk.yellow('2. 代理设置有误'));
|
|
573
|
+
console.log(chalk.yellow('3. 防火墙阻止连接'));
|
|
574
|
+
console.log(chalk.gray('继续安装...'));
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
async configureModel() {
|
|
579
|
+
console.log(chalk.yellow.bold('\n🤖 模型配置...\n'));
|
|
580
|
+
|
|
581
|
+
const answers = await inquirer.prompt([
|
|
582
|
+
{
|
|
583
|
+
type: 'input',
|
|
584
|
+
name: 'model',
|
|
585
|
+
message: '要使用什么模型?claude4/gpt5?输入4或5或claude4或gpt5:',
|
|
586
|
+
default: 'claude4',
|
|
587
|
+
validate: function(input) {
|
|
588
|
+
const validInputs = ['4', '5', 'claude4', 'gpt5'];
|
|
589
|
+
if (validInputs.includes(input.toLowerCase())) {
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
return '请输入: 4、5、claude4 或 gpt5';
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
]);
|
|
596
|
+
|
|
597
|
+
const userInput = answers.model.toLowerCase();
|
|
598
|
+
let anthropicModel;
|
|
599
|
+
let dbModelValue;
|
|
600
|
+
|
|
601
|
+
// 根据用户输入确定模型
|
|
602
|
+
if (userInput === '4' || userInput === 'claude4') {
|
|
603
|
+
anthropicModel = 'claude-sonnet-4';
|
|
604
|
+
dbModelValue = 'claude4';
|
|
605
|
+
console.log(chalk.blue('✅ 已选择: Claude 4'));
|
|
606
|
+
} else if (userInput === '5' || userInput === 'gpt5') {
|
|
607
|
+
anthropicModel = 'gpt-5';
|
|
608
|
+
dbModelValue = 'gpt5';
|
|
609
|
+
console.log(chalk.blue('✅ 已选择: GPT 5'));
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// 写入配置文件
|
|
613
|
+
await this.writeClaudeSettings(anthropicModel);
|
|
614
|
+
|
|
615
|
+
// 更新数据库中的模型参数
|
|
616
|
+
await this.updateDatabaseModel(dbModelValue);
|
|
617
|
+
|
|
618
|
+
// 删除4141端口占用
|
|
619
|
+
await this.killPort4141();
|
|
620
|
+
|
|
621
|
+
// console.log(chalk.green('✅ 模型配置完成'));
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
async killPort4141() {
|
|
625
|
+
console.log(chalk.yellow('🔄 清理4141端口...'));
|
|
626
|
+
|
|
627
|
+
return new Promise((resolve) => {
|
|
628
|
+
const isWindows = process.platform === 'win32';
|
|
629
|
+
|
|
630
|
+
if (isWindows) {
|
|
631
|
+
// Windows: 查找并杀死占用4141端口的进程
|
|
632
|
+
exec('netstat -ano | findstr :4141', (error, stdout) => {
|
|
633
|
+
if (error || !stdout) {
|
|
634
|
+
console.log(chalk.gray('📍 4141端口未被占用'));
|
|
635
|
+
resolve();
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// 提取PID
|
|
640
|
+
const lines = stdout.split('\n');
|
|
641
|
+
const pids = new Set();
|
|
642
|
+
|
|
643
|
+
lines.forEach(line => {
|
|
644
|
+
const match = line.trim().match(/(\d+)\s*$/);
|
|
645
|
+
if (match) {
|
|
646
|
+
pids.add(match[1]);
|
|
647
|
+
}
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
if (pids.size === 0) {
|
|
651
|
+
console.log(chalk.gray('📍 4141端口未被占用'));
|
|
652
|
+
resolve();
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// 杀死所有相关进程
|
|
657
|
+
let killedCount = 0;
|
|
658
|
+
pids.forEach(pid => {
|
|
659
|
+
exec(`taskkill /F /PID ${pid}`, (killError) => {
|
|
660
|
+
killedCount++;
|
|
661
|
+
if (killedCount === pids.size) {
|
|
662
|
+
console.log(chalk.green('✅ 4141端口已清理'));
|
|
663
|
+
resolve();
|
|
664
|
+
}
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
});
|
|
668
|
+
} else {
|
|
669
|
+
// Mac/Linux: 查找并杀死占用4141端口的进程
|
|
670
|
+
exec('lsof -ti :4141', (error, stdout) => {
|
|
671
|
+
if (error || !stdout) {
|
|
672
|
+
console.log(chalk.gray('📍 4141端口未被占用'));
|
|
673
|
+
resolve();
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const pids = stdout.trim().split('\n').filter(pid => pid);
|
|
678
|
+
|
|
679
|
+
if (pids.length === 0) {
|
|
680
|
+
console.log(chalk.gray('📍 4141端口未被占用'));
|
|
681
|
+
resolve();
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// 杀死所有相关进程
|
|
686
|
+
exec(`kill -9 ${pids.join(' ')}`, (killError) => {
|
|
687
|
+
if (killError) {
|
|
688
|
+
console.log(chalk.yellow('⚠️ 杀死进程时出现错误,可能权限不足'));
|
|
689
|
+
} else {
|
|
690
|
+
console.log(chalk.green('✅ 4141端口已清理'));
|
|
691
|
+
}
|
|
692
|
+
resolve();
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
async updateDatabaseModel(modelValue) {
|
|
700
|
+
try {
|
|
701
|
+
//console.log(chalk.yellow('📝 更新数据库模型参数...'));
|
|
702
|
+
|
|
703
|
+
if (this.exchangeCode && this.codeObjectId) {
|
|
704
|
+
const validator = new CodeValidator();
|
|
705
|
+
await validator.updateModelField(this.codeObjectId, modelValue);
|
|
706
|
+
//console.log(chalk.green(`✅ 数据库模型参数已更新: ${modelValue}`));
|
|
707
|
+
} else {
|
|
708
|
+
console.log(chalk.yellow('⚠️ 警告: 未找到兑换码信息,跳过数据库更新'));
|
|
709
|
+
}
|
|
710
|
+
} catch (error) {
|
|
711
|
+
console.error(chalk.red('❌ 数据库模型参数更新失败:'), error.message);
|
|
712
|
+
// 不抛出错误,继续执行
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
async writeClaudeSettings(anthropicModel) {
|
|
717
|
+
const os = require('os');
|
|
718
|
+
const isWindows = process.platform === 'win32';
|
|
719
|
+
const isMac = process.platform === 'darwin';
|
|
720
|
+
|
|
721
|
+
const config = {
|
|
722
|
+
"env": {
|
|
723
|
+
"ANTHROPIC_BASE_URL": "http://localhost:4141",
|
|
724
|
+
"ANTHROPIC_AUTH_TOKEN": "dummy",
|
|
725
|
+
"ANTHROPIC_MODEL": anthropicModel,
|
|
726
|
+
"DISABLE_NON_ESSENTIAL_MODEL_CALLS": "1",
|
|
727
|
+
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1"
|
|
728
|
+
}
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
try {
|
|
732
|
+
const configDir = path.join(os.homedir(), '.claude');
|
|
733
|
+
const configFile = path.join(configDir, 'settings.json');
|
|
734
|
+
if (!fs.existsSync(configDir)) {
|
|
735
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
736
|
+
}
|
|
737
|
+
fs.writeFileSync(configFile, JSON.stringify(config, null, 2));
|
|
738
|
+
if (isWindows) {
|
|
739
|
+
console.log(chalk.blue(`📝 Windows配置已保存到: ${configFile}`));
|
|
740
|
+
} else if (isMac) {
|
|
741
|
+
console.log(chalk.blue(`📝 Mac配置已保存到: ${configFile}`));
|
|
742
|
+
} else {
|
|
743
|
+
console.log(chalk.blue(`📝 Linux配置已保存到: ${configFile}`));
|
|
744
|
+
}
|
|
745
|
+
console.log(chalk.gray('配置内容:'));
|
|
746
|
+
console.log(chalk.gray(` ANTHROPIC_BASE_URL: ${config.env.ANTHROPIC_BASE_URL}`));
|
|
747
|
+
console.log(chalk.gray(` ANTHROPIC_AUTH_TOKEN: ${config.env.ANTHROPIC_AUTH_TOKEN}`));
|
|
748
|
+
console.log(chalk.gray(` ANTHROPIC_MODEL: ${config.env.ANTHROPIC_MODEL}`));
|
|
749
|
+
console.log(chalk.gray(` DISABLE_NON_ESSENTIAL_MODEL_CALLS: ${config.env.DISABLE_NON_ESSENTIAL_MODEL_CALLS}`));
|
|
750
|
+
console.log(chalk.gray(` CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: ${config.env.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC}`));
|
|
751
|
+
} catch (error) {
|
|
752
|
+
console.error(chalk.red('❌ 写入配置文件失败:'), error.message);
|
|
753
|
+
throw error;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
async installAIService() {
|
|
758
|
+
console.log(chalk.yellow('📦 安装ai服务...'));
|
|
759
|
+
return new Promise((resolve, reject) => {
|
|
760
|
+
const detectCmd = process.platform === 'win32' ? 'where copilot-api' : 'command -v copilot-api || which copilot-api';
|
|
761
|
+
exec(detectCmd, async (detectErr, stdout) => {
|
|
762
|
+
if (!detectErr && stdout && stdout.toString().trim().length > 0) {
|
|
763
|
+
console.log(chalk.green('✅ AI服务 已存在,跳过安装'));
|
|
764
|
+
try {
|
|
765
|
+
await this.configureModel();
|
|
766
|
+
resolve();
|
|
767
|
+
} catch (e) {
|
|
768
|
+
console.error(chalk.red('❌ 模型配置失败:'), e.message);
|
|
769
|
+
resolve();
|
|
770
|
+
}
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
console.log(chalk.blue('正在安装 AI服务...'));
|
|
774
|
+
const isWindows = process.platform === 'win32';
|
|
775
|
+
const isMac = process.platform === 'darwin';
|
|
776
|
+
let command = 'npm';
|
|
777
|
+
if (isMac) {
|
|
778
|
+
try {
|
|
779
|
+
require('child_process').execSync('which /opt/homebrew/bin/npm', { stdio: 'ignore' });
|
|
780
|
+
command = '/opt/homebrew/bin/npm';
|
|
781
|
+
} catch (e) {
|
|
782
|
+
try {
|
|
783
|
+
require('child_process').execSync('which /usr/local/bin/npm', { stdio: 'ignore' });
|
|
784
|
+
command = '/usr/local/bin/npm';
|
|
785
|
+
} catch (e2) {
|
|
786
|
+
command = 'npm';
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
const args = ['install', '-g', '@copilot-api/copilot-api'];
|
|
791
|
+
const spawnOptions = { stdio: 'inherit', env: { ...process.env } };
|
|
792
|
+
if (this.proxyUrl) {
|
|
793
|
+
spawnOptions.env.HTTP_PROXY = this.proxyUrl;
|
|
794
|
+
spawnOptions.env.HTTPS_PROXY = this.proxyUrl;
|
|
795
|
+
}
|
|
796
|
+
if (isWindows) spawnOptions.shell = true;
|
|
797
|
+
const installProcess = spawn(command, args, spawnOptions);
|
|
798
|
+
installProcess.on('close', async (code) => {
|
|
799
|
+
if (code === 0) {
|
|
800
|
+
console.log(chalk.green('✅ AI服务 安装成功'));
|
|
801
|
+
try {
|
|
802
|
+
await this.configureModel();
|
|
803
|
+
resolve();
|
|
804
|
+
} catch (e) {
|
|
805
|
+
console.error(chalk.red('❌ 模型配置失败:'), e.message);
|
|
806
|
+
resolve();
|
|
807
|
+
}
|
|
808
|
+
} else {
|
|
809
|
+
if (!isWindows) {
|
|
810
|
+
console.log(chalk.yellow('🔄 尝试使用 sudo 安装...'));
|
|
811
|
+
const env = { ...process.env, PATH: process.env.PATH };
|
|
812
|
+
if (this.proxyUrl) {
|
|
813
|
+
env.HTTP_PROXY = this.proxyUrl;
|
|
814
|
+
env.HTTPS_PROXY = this.proxyUrl;
|
|
815
|
+
}
|
|
816
|
+
const sudoProcess = spawn('sudo', ['npm', 'install', '-g', 'copilot-api'], { stdio: 'inherit', env });
|
|
817
|
+
sudoProcess.on('close', async (sudoCode) => {
|
|
818
|
+
if (sudoCode === 0) {
|
|
819
|
+
console.log(chalk.green('✅ AI服务 安装成功'));
|
|
820
|
+
try {
|
|
821
|
+
await this.configureModel();
|
|
822
|
+
resolve();
|
|
823
|
+
} catch (e) {
|
|
824
|
+
console.error(chalk.red('❌ 模型配置失败:'), e.message);
|
|
825
|
+
resolve();
|
|
826
|
+
}
|
|
827
|
+
} else {
|
|
828
|
+
reject(new Error('AI服务 安装失败,请检查网络连接和权限'));
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
sudoProcess.on('error', (err) => {
|
|
832
|
+
if (err.code === 'ENOENT') reject(new Error('sudo 命令未找到')); else reject(new Error(`sudo 安装失败: ${err.message}`));
|
|
833
|
+
});
|
|
834
|
+
} else {
|
|
835
|
+
reject(new Error('AI服务 安装失败,请以管理员身份重试'));
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
installProcess.on('error', (err) => {
|
|
840
|
+
if (err.code === 'ENOENT') reject(new Error('npm 命令未找到')); else reject(new Error(`安装过程出错: ${err.message}`));
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
async startAIService() {
|
|
847
|
+
console.log(chalk.yellow('🚀 启动 AI服务...'));
|
|
848
|
+
console.log(chalk.blue('等待获取验证码...\n'));
|
|
849
|
+
|
|
850
|
+
return new Promise((resolve, reject) => {
|
|
851
|
+
const isWindows = process.platform === 'win32';
|
|
852
|
+
const command = isWindows ? 'copilot-api' : 'copilot-api';
|
|
853
|
+
|
|
854
|
+
const spawnOptions = {
|
|
855
|
+
env: {
|
|
856
|
+
...process.env
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
if (this.proxyUrl) {
|
|
860
|
+
spawnOptions.env.HTTP_PROXY = this.proxyUrl;
|
|
861
|
+
spawnOptions.env.HTTPS_PROXY = this.proxyUrl;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
if (isWindows) {
|
|
865
|
+
spawnOptions.shell = true;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
const apiProcess = spawn(command, ['start'], spawnOptions);
|
|
869
|
+
|
|
870
|
+
let output = '';
|
|
871
|
+
let hasFoundCode = false;
|
|
872
|
+
|
|
873
|
+
apiProcess.stdout.on('data', (data) => {
|
|
874
|
+
const text = data.toString();
|
|
875
|
+
output += text;
|
|
876
|
+
process.stdout.write(text);
|
|
877
|
+
|
|
878
|
+
const codeMatch = text.match(/code\s+"([A-Z0-9-]+)"/i);
|
|
879
|
+
if (codeMatch && !hasFoundCode) {
|
|
880
|
+
hasFoundCode = true;
|
|
881
|
+
console.log(chalk.green.bold(`\n🎉 验证码已生成: ${codeMatch[1]}`));
|
|
882
|
+
console.log(chalk.blue('请在浏览器中访问: https://github.com/login/device'));
|
|
883
|
+
console.log(chalk.blue(`输入验证码: ${codeMatch[1]}`));
|
|
884
|
+
console.log(chalk.gray('\n按 Ctrl+C 退出'));
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
apiProcess.stderr.on('data', (data) => {
|
|
889
|
+
const errorText = data.toString();
|
|
890
|
+
process.stderr.write(errorText);
|
|
891
|
+
|
|
892
|
+
// 检查常见错误
|
|
893
|
+
if (errorText.includes('ENOENT') || errorText.includes('command not found')) {
|
|
894
|
+
console.error(chalk.red('\n❌ AI服务未正确安装,请重新运行安装'));
|
|
895
|
+
apiProcess.kill();
|
|
896
|
+
reject(new Error('AI服务未找到'));
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
apiProcess.on('close', (code) => {
|
|
901
|
+
if (code !== 0 && !hasFoundCode) {
|
|
902
|
+
console.log(chalk.red(`\n❌ AI服务启动失败 (code: ${code})`));
|
|
903
|
+
reject(new Error('AI服务启动失败'));
|
|
904
|
+
} else {
|
|
905
|
+
console.log(chalk.gray(`\nAI服务 已退出 (code: ${code})`));
|
|
906
|
+
resolve();
|
|
907
|
+
}
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
apiProcess.on('error', (err) => {
|
|
911
|
+
console.error(chalk.red(`\n❌ 启动过程出错: ${err.message}`));
|
|
912
|
+
if (err.code === 'ENOENT') {
|
|
913
|
+
reject(new Error('copilot-api 命令未找到,请确保AI服务已正确安装'));
|
|
914
|
+
} else {
|
|
915
|
+
reject(err);
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
// 处理 Ctrl+C
|
|
920
|
+
process.on('SIGINT', () => {
|
|
921
|
+
console.log(chalk.yellow('\n\n👋 正在关闭...'));
|
|
922
|
+
apiProcess.kill();
|
|
923
|
+
process.exit();
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
// 超时处理
|
|
927
|
+
/*
|
|
928
|
+
setTimeout(() => {
|
|
929
|
+
if (!hasFoundCode) {
|
|
930
|
+
console.log(chalk.yellow('\n⏰ 等待超时,请检查网络连接和代理设置'));
|
|
931
|
+
}
|
|
932
|
+
}, 30000); */
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
async waitForKeyPress() {
|
|
937
|
+
return new Promise((resolve) => {
|
|
938
|
+
const rl = readline.createInterface({
|
|
939
|
+
input: process.stdin,
|
|
940
|
+
output: process.stdout
|
|
941
|
+
});
|
|
942
|
+
|
|
943
|
+
rl.question('按回车键继续...', () => {
|
|
944
|
+
rl.close();
|
|
945
|
+
resolve();
|
|
946
|
+
});
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// 确保 heibai-api 命令可用(优先 npm link,其次写入全局 shim)
|
|
951
|
+
async ensureHeibaiApiAvailable() {
|
|
952
|
+
const isWindows = process.platform === 'win32';
|
|
953
|
+
const rootDir = path.join(__dirname, '..');
|
|
954
|
+
|
|
955
|
+
const detectCmd = isWindows ? 'where heibai-api' : 'command -v heibai-api || which heibai-api';
|
|
956
|
+
|
|
957
|
+
const execP = (cmd, opts={}) => new Promise((resolve) => {
|
|
958
|
+
exec(cmd, { shell: true, ...opts }, (err, stdout) => {
|
|
959
|
+
resolve({ ok: !err && (stdout || '').toString().trim().length > 0, stdout: (stdout||'').toString().trim() });
|
|
960
|
+
});
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
// 已存在则返回
|
|
964
|
+
let detected = await execP(detectCmd);
|
|
965
|
+
if (detected.ok) return;
|
|
966
|
+
|
|
967
|
+
// 尝试 npm link 当前包,暴露 bin
|
|
968
|
+
await new Promise((resolve) => {
|
|
969
|
+
const p = spawn('npm', ['link'], { cwd: rootDir, stdio: 'ignore', shell: true });
|
|
970
|
+
p.on('close', () => resolve());
|
|
971
|
+
p.on('error', () => resolve());
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
detected = await execP(detectCmd);
|
|
975
|
+
if (detected.ok) return;
|
|
976
|
+
|
|
977
|
+
// Windows 兜底:写入全局 npm bin 下的 heibai-api.cmd 指向本地 heibai.js
|
|
978
|
+
if (isWindows) {
|
|
979
|
+
const getBin = await execP('npm bin -g');
|
|
980
|
+
const globalBin = (getBin.stdout || '').trim();
|
|
981
|
+
if (globalBin && fs.existsSync(globalBin)) {
|
|
982
|
+
try {
|
|
983
|
+
const shimPath = path.join(globalBin, 'heibai-api.cmd');
|
|
984
|
+
const target = path.join(rootDir, 'bin', 'heibai.js');
|
|
985
|
+
const content = `@echo off\r\nnode "${target}" %*\r\n`;
|
|
986
|
+
fs.writeFileSync(shimPath, content, 'utf8');
|
|
987
|
+
} catch {}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// 获取机器码(详细设备信息)
|
|
993
|
+
async getMachineId() {
|
|
994
|
+
return new Promise((resolve) => {
|
|
995
|
+
const platform = os.platform();
|
|
996
|
+
|
|
997
|
+
if (platform === 'win32') {
|
|
998
|
+
// Windows: 获取BIOS序列号、主板序列号、UUID
|
|
999
|
+
const cmd = 'powershell -nop -c "(gwmi Win32_BIOS).SerialNumber + \'/\' + (gwmi Win32_BaseBoard).SerialNumber + \'/\' + (gwmi Win32_ComputerSystemProduct).UUID"';
|
|
1000
|
+
|
|
1001
|
+
exec(cmd, { shell: true }, (error, stdout, stderr) => {
|
|
1002
|
+
if (error || !stdout) {
|
|
1003
|
+
// 备用方法:获取MAC地址
|
|
1004
|
+
const networkInterfaces = os.networkInterfaces();
|
|
1005
|
+
for (const interfaceName in networkInterfaces) {
|
|
1006
|
+
const interfaces = networkInterfaces[interfaceName];
|
|
1007
|
+
for (const iface of interfaces) {
|
|
1008
|
+
if (!iface.internal && iface.mac && iface.mac !== '00:00:00:00:00:00') {
|
|
1009
|
+
resolve(iface.mac);
|
|
1010
|
+
return;
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
resolve(os.hostname());
|
|
1015
|
+
} else {
|
|
1016
|
+
resolve(stdout.trim());
|
|
1017
|
+
}
|
|
1018
|
+
});
|
|
1019
|
+
} else if (platform === 'darwin') {
|
|
1020
|
+
// macOS: 获取序列号、UUID、MAC地址
|
|
1021
|
+
const cmd = `printf "%s-%s-%s" "$(ioreg -l -d 2 | awk -F'"' '/IOPlatformSerialNumber/{print $4}')" "$(ioreg -d 2 -l | awk -F'"' '/IOPlatformUUID/{print $4}')" "$(ifconfig en0 | awk '/ether/{print $2}')"`;
|
|
1022
|
+
|
|
1023
|
+
exec(cmd, { shell: true }, (error, stdout, stderr) => {
|
|
1024
|
+
if (error || !stdout) {
|
|
1025
|
+
// 备用方法:获取MAC地址
|
|
1026
|
+
const networkInterfaces = os.networkInterfaces();
|
|
1027
|
+
for (const interfaceName in networkInterfaces) {
|
|
1028
|
+
const interfaces = networkInterfaces[interfaceName];
|
|
1029
|
+
for (const iface of interfaces) {
|
|
1030
|
+
if (!iface.internal && iface.mac && iface.mac !== '00:00:00:00:00:00') {
|
|
1031
|
+
resolve(iface.mac);
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
resolve(os.hostname());
|
|
1037
|
+
} else {
|
|
1038
|
+
resolve(stdout.trim());
|
|
1039
|
+
}
|
|
1040
|
+
});
|
|
1041
|
+
} else {
|
|
1042
|
+
// Linux或其他系统:使用MAC地址
|
|
1043
|
+
const networkInterfaces = os.networkInterfaces();
|
|
1044
|
+
for (const interfaceName in networkInterfaces) {
|
|
1045
|
+
const interfaces = networkInterfaces[interfaceName];
|
|
1046
|
+
for (const iface of interfaces) {
|
|
1047
|
+
if (!iface.internal && iface.mac && iface.mac !== '00:00:00:00:00:00') {
|
|
1048
|
+
resolve(iface.mac);
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
resolve(os.hostname());
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
// 获取公网IP
|
|
1059
|
+
async getPublicIP() {
|
|
1060
|
+
try {
|
|
1061
|
+
// 优先使用内存缓存
|
|
1062
|
+
if (this.publicIPCache) return this.publicIPCache;
|
|
1063
|
+
|
|
1064
|
+
// 尝试从本地缓存读取
|
|
1065
|
+
try {
|
|
1066
|
+
const cached = this.loadCachedCode();
|
|
1067
|
+
if (cached && cached.publicIP) {
|
|
1068
|
+
this.publicIPCache = cached.publicIP;
|
|
1069
|
+
return this.publicIPCache;
|
|
1070
|
+
}
|
|
1071
|
+
} catch {}
|
|
1072
|
+
|
|
1073
|
+
// 如果已经在testConnection中获取过IP,可以重用
|
|
1074
|
+
const response = await axios.get('http://ipinfo.io/json', {
|
|
1075
|
+
timeout: 5000,
|
|
1076
|
+
headers: {
|
|
1077
|
+
'User-Agent': 'curl/7.68.0'
|
|
1078
|
+
}
|
|
1079
|
+
});
|
|
1080
|
+
const ip = response.data.ip || 'unknown_ip';
|
|
1081
|
+
if (ip && ip !== 'unknown_ip') {
|
|
1082
|
+
this.publicIPCache = ip;
|
|
1083
|
+
this.saveCachedCode({ publicIP: ip });
|
|
1084
|
+
}
|
|
1085
|
+
return ip;
|
|
1086
|
+
} catch (error) {
|
|
1087
|
+
// 回退到缓存
|
|
1088
|
+
try {
|
|
1089
|
+
const cached = this.loadCachedCode();
|
|
1090
|
+
if (cached && cached.publicIP) {
|
|
1091
|
+
this.publicIPCache = cached.publicIP;
|
|
1092
|
+
return this.publicIPCache;
|
|
1093
|
+
}
|
|
1094
|
+
} catch {}
|
|
1095
|
+
return 'unknown_ip';
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// 上传机器码和IP到数据库
|
|
1100
|
+
async uploadMachineInfo(objectId) {
|
|
1101
|
+
try {
|
|
1102
|
+
const validator = new CodeValidator();
|
|
1103
|
+
|
|
1104
|
+
// 获取设备信息
|
|
1105
|
+
const machineId = await this.getMachineId();
|
|
1106
|
+
const publicIP = await this.getPublicIP();
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
// 准备更新的数据
|
|
1110
|
+
const updateData = {
|
|
1111
|
+
mac: machineId, // 每次都更新mac
|
|
1112
|
+
ip: publicIP // 更新IP
|
|
1113
|
+
};
|
|
1114
|
+
|
|
1115
|
+
// 检查diyi字段是否为空,如果为空则设置第一次特征
|
|
1116
|
+
try {
|
|
1117
|
+
const diyiValue = await validator.getCodeDiyi(objectId);
|
|
1118
|
+
if (!diyiValue || diyiValue.trim() === '') {
|
|
1119
|
+
updateData.diyi = machineId; // 记录
|
|
1120
|
+
|
|
1121
|
+
}
|
|
1122
|
+
} catch (error) {
|
|
1123
|
+
// 如果查询失败,假设diyi为空
|
|
1124
|
+
updateData.diyi = machineId;
|
|
1125
|
+
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
// 更新数据库
|
|
1129
|
+
await validator.updateDeviceInfo(objectId, updateData);
|
|
1130
|
+
|
|
1131
|
+
} catch (error) {
|
|
1132
|
+
|
|
1133
|
+
// 不影响主流程,继续执行
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
// 启动程序
|
|
1139
|
+
const setup = new AIAssistantSetup();
|
|
1140
|
+
setup.start().catch(console.error);
|