aixxai 1.0.0 → 1.1.0
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/bin/aixx.js +22 -4
- package/package.json +1 -1
- package/src/commands/balance.js +159 -0
- package/src/commands/help.js +11 -2
- package/src/commands/install.js +78 -17
- package/src/commands/models.js +152 -0
- package/src/commands/recharge.js +143 -0
- package/src/utils/agentconfig.js +368 -0
package/bin/aixx.js
CHANGED
|
@@ -5,15 +5,21 @@
|
|
|
5
5
|
* 让你的AI调用万物。
|
|
6
6
|
*
|
|
7
7
|
* 用法:
|
|
8
|
-
* npx
|
|
9
|
-
* npx
|
|
10
|
-
* npx
|
|
11
|
-
* npx
|
|
8
|
+
* npx aixxai install [推荐码] 安装AIXX skill到本地agent(含零配置)
|
|
9
|
+
* npx aixxai config 配置/查看AIXX
|
|
10
|
+
* npx aixxai test 测试AIXX是否可用
|
|
11
|
+
* npx aixxai balance 查询账户余额
|
|
12
|
+
* npx aixxai models 列出可用模型(分组)
|
|
13
|
+
* npx aixxai recharge [金额] 充值引导
|
|
14
|
+
* npx aixxai --version 查看版本
|
|
12
15
|
*/
|
|
13
16
|
|
|
14
17
|
import { install } from '../src/commands/install.js';
|
|
15
18
|
import { config } from '../src/commands/config.js';
|
|
16
19
|
import { test } from '../src/commands/test.js';
|
|
20
|
+
import { balance } from '../src/commands/balance.js';
|
|
21
|
+
import { models } from '../src/commands/models.js';
|
|
22
|
+
import { recharge } from '../src/commands/recharge.js';
|
|
17
23
|
import { showHelp, showVersion } from '../src/commands/help.js';
|
|
18
24
|
|
|
19
25
|
const VERSION = '0.1.0';
|
|
@@ -42,6 +48,18 @@ async function main() {
|
|
|
42
48
|
case 't':
|
|
43
49
|
await test(subArgs);
|
|
44
50
|
break;
|
|
51
|
+
case 'balance':
|
|
52
|
+
case 'b':
|
|
53
|
+
await balance(subArgs);
|
|
54
|
+
break;
|
|
55
|
+
case 'models':
|
|
56
|
+
case 'm':
|
|
57
|
+
await models(subArgs);
|
|
58
|
+
break;
|
|
59
|
+
case 'recharge':
|
|
60
|
+
case 'r':
|
|
61
|
+
await recharge(subArgs);
|
|
62
|
+
break;
|
|
45
63
|
case '--version':
|
|
46
64
|
case '-v':
|
|
47
65
|
showVersion(VERSION);
|
package/package.json
CHANGED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AIXX CLI - balance 命令
|
|
3
|
+
*
|
|
4
|
+
* 查询当前账户余额、已用额度。
|
|
5
|
+
*
|
|
6
|
+
* 数据来源(New-API兼容接口):
|
|
7
|
+
* 1. GET /v1/dashboard/billing/subscription → hard_limit_usd(总额度,美元)
|
|
8
|
+
* 2. GET /v1/dashboard/billing/usage → 已用量(可能多种格式,做兜底)
|
|
9
|
+
*
|
|
10
|
+
* 展示时换算成人民币(按 1 USD ≈ 7 元,只是给用户一个直观感受)。
|
|
11
|
+
* "还剩约N次对话":按每次对话约 ¥0.01 估算(最便宜的deepseek-chat单次几厘到几分)。
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, readFileSync } from 'fs';
|
|
15
|
+
import { homedir } from 'os';
|
|
16
|
+
import { join } from 'path';
|
|
17
|
+
import { DEFAULT_BASE_URL } from '../../bin/aixx.js';
|
|
18
|
+
|
|
19
|
+
// 美元→人民币换算(仅用于展示,给用户一个直观感受)
|
|
20
|
+
const USD_TO_CNY = 7;
|
|
21
|
+
// 估算单次对话成本(元),用于"还能聊多少次"
|
|
22
|
+
const COST_PER_CHAT_CNY = 0.01;
|
|
23
|
+
// 所有对外请求统一 15 秒超时
|
|
24
|
+
const FETCH_TIMEOUT = 15000;
|
|
25
|
+
// 网络/超时异常统一成友好提示
|
|
26
|
+
function friendlyNetErr(err) {
|
|
27
|
+
return err?.name === 'TimeoutError' || err?.name === 'AbortError'
|
|
28
|
+
? '连接超时,请检查网络'
|
|
29
|
+
: err?.message || String(err);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* 从环境变量或本地账号文件读取 apiKey。
|
|
34
|
+
* 优先级:环境变量 > ~/.aixx/account.json
|
|
35
|
+
*/
|
|
36
|
+
function loadApiKey() {
|
|
37
|
+
if (process.env.AIXX_API_KEY) return process.env.AIXX_API_KEY;
|
|
38
|
+
const accountFile = join(homedir(), '.aixx', 'account.json');
|
|
39
|
+
if (existsSync(accountFile)) {
|
|
40
|
+
try {
|
|
41
|
+
const acc = JSON.parse(readFileSync(accountFile, 'utf-8'));
|
|
42
|
+
if (acc.apiKey) return acc.apiKey;
|
|
43
|
+
} catch (e) {
|
|
44
|
+
// 忽略,下面报错
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function balance(subArgs) {
|
|
51
|
+
console.log('╔══════════════════════════════════════════╗');
|
|
52
|
+
console.log('║ AIXX 账户余额 ║');
|
|
53
|
+
console.log('╚══════════════════════════════════════════╝\n');
|
|
54
|
+
|
|
55
|
+
const apiKey = loadApiKey();
|
|
56
|
+
const baseUrl = process.env.AIXX_BASE_URL || DEFAULT_BASE_URL;
|
|
57
|
+
|
|
58
|
+
if (!apiKey) {
|
|
59
|
+
console.error('❌ 未找到AIXX API Key');
|
|
60
|
+
console.error(' 请先运行 aixx install,或设置环境变量 AIXX_API_KEY');
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log(`使用Key: ${apiKey.slice(0, 12)}...${apiKey.slice(-4)}`);
|
|
65
|
+
console.log(`后端地址: ${baseUrl}\n`);
|
|
66
|
+
|
|
67
|
+
// 1. 查总额度(subscription)
|
|
68
|
+
let hardLimitUsd = 0;
|
|
69
|
+
try {
|
|
70
|
+
const resp = await fetch(`${baseUrl}/dashboard/billing/subscription`, {
|
|
71
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
72
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
73
|
+
});
|
|
74
|
+
if (!resp.ok) {
|
|
75
|
+
if (resp.status === 401) {
|
|
76
|
+
console.error('❌ API Key 无效(401),请重新 install 或检查 key');
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
console.error(`❌ 查询余额失败: HTTP ${resp.status}`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
const data = await resp.json();
|
|
83
|
+
// New-API/OpenAI兼容:hard_limit_usd 是总额度
|
|
84
|
+
hardLimitUsd =
|
|
85
|
+
data.hard_limit_usd ??
|
|
86
|
+
data.hard_limit ??
|
|
87
|
+
data.data?.hard_limit_usd ??
|
|
88
|
+
0;
|
|
89
|
+
} catch (err) {
|
|
90
|
+
console.error(`❌ ${friendlyNetErr(err)}`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 2. 查已用量(usage)
|
|
95
|
+
// 不同后端字段不一样,尽量兜底
|
|
96
|
+
let usedUsd = 0;
|
|
97
|
+
try {
|
|
98
|
+
// 用一个宽日期范围,确保能查到所有用量
|
|
99
|
+
const today = new Date();
|
|
100
|
+
const start = `${today.getFullYear()}-01-01`;
|
|
101
|
+
const end = `${today.getFullYear()}-12-31`;
|
|
102
|
+
const resp = await fetch(
|
|
103
|
+
`${baseUrl}/dashboard/billing/usage?start_date=${start}&end_date=${end}`,
|
|
104
|
+
{
|
|
105
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
106
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
if (resp.ok) {
|
|
110
|
+
const data = await resp.json();
|
|
111
|
+
// 常见字段优先级
|
|
112
|
+
usedUsd =
|
|
113
|
+
data.total_usage ??
|
|
114
|
+
data.total_cost ??
|
|
115
|
+
data.data?.total_usage ??
|
|
116
|
+
data.cumulative_usage ??
|
|
117
|
+
0;
|
|
118
|
+
// New-API 的 total_usage 单位有时是"分(美元)",要除以100
|
|
119
|
+
// 这里做个粗判断:如果usedUsd比hardLimit还大100倍,多半是没除100
|
|
120
|
+
if (usedUsd > 0 && hardLimitUsd > 0 && usedUsd > hardLimitUsd * 50) {
|
|
121
|
+
usedUsd = usedUsd / 100;
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
// usage 接口挂了不影响主流程,总额度照常显示
|
|
125
|
+
console.log(' (用量接口未返回数据,仅显示总额度)\n');
|
|
126
|
+
}
|
|
127
|
+
} catch (err) {
|
|
128
|
+
console.log(' (用量查询失败,仅显示总额度)\n');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 3. 计算并展示
|
|
132
|
+
// New-API 的"无限额度"哨兵:hard_limit_usd >= 100000000(1亿)表示这个token不限额。
|
|
133
|
+
// 直接显示真实数字(¥7亿)会吓到用户,所以单独走"无限额度"分支。
|
|
134
|
+
const UNLIMITED_SENTINEL = 100000000;
|
|
135
|
+
const isUnlimited = hardLimitUsd >= UNLIMITED_SENTINEL;
|
|
136
|
+
const usedCny = usedUsd * USD_TO_CNY;
|
|
137
|
+
|
|
138
|
+
console.log('────────────────────────────────────');
|
|
139
|
+
if (isUnlimited) {
|
|
140
|
+
// 无限额度(一般是管理员key或赠送的不限量key)
|
|
141
|
+
console.log(`💰 当前余额:♾️ 无限额度(不限量)`);
|
|
142
|
+
console.log(`📊 已用额度:¥${usedCny.toFixed(2)}`);
|
|
143
|
+
console.log(`💳 账户类型:无限量`);
|
|
144
|
+
} else {
|
|
145
|
+
// 正常有限额度
|
|
146
|
+
const remainingUsd = Math.max(0, hardLimitUsd - usedUsd);
|
|
147
|
+
const totalCny = hardLimitUsd * USD_TO_CNY;
|
|
148
|
+
const remainingCny = remainingUsd * USD_TO_CNY;
|
|
149
|
+
const approxChats = Math.floor(remainingCny / COST_PER_CHAT_CNY);
|
|
150
|
+
console.log(`💰 当前余额:¥${remainingCny.toFixed(2)}(还剩约 ${approxChats.toLocaleString()} 次对话)`);
|
|
151
|
+
console.log(`📊 已用额度:¥${usedCny.toFixed(2)}`);
|
|
152
|
+
console.log(`💳 账户总额度:¥${totalCny.toFixed(2)}($${hardLimitUsd.toFixed(2)})`);
|
|
153
|
+
}
|
|
154
|
+
console.log('────────────────────────────────────');
|
|
155
|
+
console.log('\n💡 提示:');
|
|
156
|
+
console.log(' - 新用户注册送 ¥5 免费额度');
|
|
157
|
+
console.log(' - deepseek-chat 最便宜,省着用能聊很久');
|
|
158
|
+
console.log(' - 余额不足时运行 aixx recharge 查看充值方式\n');
|
|
159
|
+
}
|
package/src/commands/help.js
CHANGED
|
@@ -10,16 +10,25 @@ export function showHelp(version) {
|
|
|
10
10
|
╚══════════════════════════════════════════╝
|
|
11
11
|
|
|
12
12
|
用法:
|
|
13
|
-
aixx install [推荐码] 安装AIXX skill到本地agent
|
|
13
|
+
aixx install [推荐码] 安装AIXX skill到本地agent(含零配置)
|
|
14
14
|
aixx config 配置或查看AIXX设置
|
|
15
15
|
aixx test 测试AIXX是否可用
|
|
16
|
+
aixx balance 查询账户余额(还剩多少钱)
|
|
17
|
+
aixx models 列出可用模型(国产/海外分组)
|
|
18
|
+
aixx recharge [金额] 充值引导(默认$5)
|
|
16
19
|
aixx --version 查看版本
|
|
17
20
|
aixx --help 显示此帮助
|
|
18
21
|
|
|
19
22
|
安装示例:
|
|
20
|
-
npx aixxai install
|
|
23
|
+
npx aixxai install 普通安装(自动注册,送¥5额度)
|
|
21
24
|
npx aixxai install zhangsan 通过张三的推荐码安装(张三获分润)
|
|
22
25
|
|
|
26
|
+
账户示例:
|
|
27
|
+
aixx balance 查看余额
|
|
28
|
+
aixx balance 20 (金额参数对balance无效,仅recharge用)
|
|
29
|
+
aixx models 看有哪些模型可调
|
|
30
|
+
aixx recharge 10 充 $10
|
|
31
|
+
|
|
23
32
|
关于AIXX:
|
|
24
33
|
AIXX = "AI的谷歌"。装一个skill,agent就能调用全世界的AI能力。
|
|
25
34
|
永远不用碰key、参数、文档,一切通过对话。
|
package/src/commands/install.js
CHANGED
|
@@ -18,6 +18,7 @@ import { randomInt } from 'crypto';
|
|
|
18
18
|
import { DEFAULT_BASE_URL } from '../../bin/aixx.js';
|
|
19
19
|
import { downloadSkill } from '../utils/download.js';
|
|
20
20
|
import { setupEnv } from '../utils/env.js';
|
|
21
|
+
import { configureAgents } from '../utils/agentconfig.js';
|
|
21
22
|
|
|
22
23
|
// skill目录检测
|
|
23
24
|
function detectSkillDirs() {
|
|
@@ -52,6 +53,15 @@ function genPassword() {
|
|
|
52
53
|
return pwd;
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
// 所有对外请求统一 15 秒超时(AbortSignal.timeout 在 Node 17.3+ 可用,本 CLI 要求 18+)
|
|
57
|
+
const FETCH_TIMEOUT = 15000;
|
|
58
|
+
// 把网络/超时异常的报错信息统一成友好提示
|
|
59
|
+
function friendlyNetErr(err) {
|
|
60
|
+
return err?.name === 'TimeoutError' || err?.name === 'AbortError'
|
|
61
|
+
? '连接超时,请检查网络'
|
|
62
|
+
: err?.message || String(err);
|
|
63
|
+
}
|
|
64
|
+
|
|
55
65
|
// 自动注册AIXX账号
|
|
56
66
|
async function autoRegister(baseUrl, refCode) {
|
|
57
67
|
const username = genUsername();
|
|
@@ -71,7 +81,8 @@ async function autoRegister(baseUrl, refCode) {
|
|
|
71
81
|
const regResp = await fetch(regUrl, {
|
|
72
82
|
method: 'POST',
|
|
73
83
|
headers: { 'Content-Type': 'application/json' },
|
|
74
|
-
body: JSON.stringify(regBody)
|
|
84
|
+
body: JSON.stringify(regBody),
|
|
85
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
75
86
|
});
|
|
76
87
|
const regData = await regResp.json();
|
|
77
88
|
|
|
@@ -86,7 +97,8 @@ async function autoRegister(baseUrl, refCode) {
|
|
|
86
97
|
const loginResp = await fetch(loginUrl, {
|
|
87
98
|
method: 'POST',
|
|
88
99
|
headers: { 'Content-Type': 'application/json' },
|
|
89
|
-
body: JSON.stringify({ username, password })
|
|
100
|
+
body: JSON.stringify({ username, password }),
|
|
101
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
90
102
|
});
|
|
91
103
|
const loginData = await loginResp.json();
|
|
92
104
|
|
|
@@ -109,7 +121,8 @@ async function autoRegister(baseUrl, refCode) {
|
|
|
109
121
|
name: 'default',
|
|
110
122
|
remain_quota: -1,
|
|
111
123
|
unlimited_quota: true
|
|
112
|
-
})
|
|
124
|
+
}),
|
|
125
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
113
126
|
});
|
|
114
127
|
const tokenData = await tokenResp.json();
|
|
115
128
|
|
|
@@ -121,7 +134,8 @@ async function autoRegister(baseUrl, refCode) {
|
|
|
121
134
|
// 正确做法:先查列表拿 token id,再调 POST /api/token/:id/key 取未脱敏的完整 key。
|
|
122
135
|
const apiBase = baseUrl.replace(/\/v1$/, '');
|
|
123
136
|
const listResp = await fetch(`${apiBase}/api/token/?p=0&page_size=1`, {
|
|
124
|
-
headers: { 'Authorization': `Bearer ${userToken}` }
|
|
137
|
+
headers: { 'Authorization': `Bearer ${userToken}` },
|
|
138
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
125
139
|
});
|
|
126
140
|
const listData = await listResp.json();
|
|
127
141
|
const list = listData.data;
|
|
@@ -135,7 +149,8 @@ async function autoRegister(baseUrl, refCode) {
|
|
|
135
149
|
// 取未脱敏的完整 key(GET 列表会脱敏,必须用这个专用接口)
|
|
136
150
|
const keyResp = await fetch(`${apiBase}/api/token/${tokenId}/key`, {
|
|
137
151
|
method: 'POST',
|
|
138
|
-
headers: { 'Authorization': `Bearer ${userToken}` }
|
|
152
|
+
headers: { 'Authorization': `Bearer ${userToken}` },
|
|
153
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
139
154
|
});
|
|
140
155
|
const keyData = await keyResp.json();
|
|
141
156
|
const apiKey = keyData?.data?.key;
|
|
@@ -147,12 +162,13 @@ async function autoRegister(baseUrl, refCode) {
|
|
|
147
162
|
console.log(`✅ API Key已创建: sk-${apiKey.slice(0, 8)}...${apiKey.slice(-4)}`);
|
|
148
163
|
|
|
149
164
|
// 保存账号信息到本地(方便用户找回)
|
|
165
|
+
// 安全:不存明文密码。注册时密码只用于首次登录拿token,之后用apiKey就够了。
|
|
166
|
+
// 丢了账号可以重新注册(免费),所以不需要保留密码。
|
|
150
167
|
const aixxDir = join(homedir(), '.aixx');
|
|
151
168
|
if (!existsSync(aixxDir)) mkdirSync(aixxDir, { recursive: true });
|
|
152
169
|
const fullKey = `sk-${apiKey}`;
|
|
153
170
|
writeFileSync(join(aixxDir, 'account.json'), JSON.stringify({
|
|
154
171
|
username,
|
|
155
|
-
password,
|
|
156
172
|
apiKey: fullKey, // 存完整key(本地文件,不进git)
|
|
157
173
|
registeredAt: new Date().toISOString(),
|
|
158
174
|
refCode: refCode || null
|
|
@@ -187,7 +203,9 @@ export async function install(subArgs) {
|
|
|
187
203
|
console.log(`✅ 操作系统: ${process.platform}`);
|
|
188
204
|
|
|
189
205
|
// 2. 获取或创建API Key
|
|
206
|
+
// 记录key来源,用于后面给出正确的提示(环境变量 / 本地账号)
|
|
190
207
|
let apiKey = process.env.AIXX_API_KEY;
|
|
208
|
+
let keySource = apiKey ? 'env' : null;
|
|
191
209
|
|
|
192
210
|
if (!apiKey) {
|
|
193
211
|
// 检查本地是否有已保存的账号
|
|
@@ -196,6 +214,7 @@ export async function install(subArgs) {
|
|
|
196
214
|
try {
|
|
197
215
|
const account = JSON.parse(readFileSync(accountFile, 'utf-8'));
|
|
198
216
|
apiKey = account.apiKey;
|
|
217
|
+
keySource = 'account';
|
|
199
218
|
console.log(`\n✅ 发现已保存的账号: ${account.username}`);
|
|
200
219
|
console.log(` 使用已保存的API Key`);
|
|
201
220
|
} catch (e) {
|
|
@@ -213,21 +232,32 @@ export async function install(subArgs) {
|
|
|
213
232
|
// 自动注册
|
|
214
233
|
try {
|
|
215
234
|
apiKey = await autoRegister(DEFAULT_BASE_URL, refCode);
|
|
235
|
+
keySource = 'registered';
|
|
216
236
|
} catch (err) {
|
|
217
|
-
|
|
237
|
+
// 网络/超时类异常给友好提示,业务错误(注册/登录/创建key失败)保留原文
|
|
238
|
+
const msg = /timeout|abort|fetch|network|ECONN|ETIMEDOUT|getaddrinfo/i.test(err.message)
|
|
239
|
+
? friendlyNetErr(err)
|
|
240
|
+
: err.message;
|
|
241
|
+
console.error(`\n❌ 自动注册失败: ${msg}`);
|
|
218
242
|
console.error(' 你可以手动到 AIXX 平台注册后,用已有key重新运行 install。');
|
|
219
243
|
process.exit(1);
|
|
220
244
|
}
|
|
221
245
|
} else if (choice.startsWith('sk-')) {
|
|
222
246
|
// 用户输入已有key
|
|
223
247
|
apiKey = choice;
|
|
248
|
+
keySource = 'manual';
|
|
224
249
|
console.log('✅ 使用你提供的API Key');
|
|
225
250
|
} else {
|
|
226
251
|
console.error('❌ 无效的输入');
|
|
227
252
|
process.exit(1);
|
|
228
253
|
}
|
|
229
254
|
} else {
|
|
230
|
-
|
|
255
|
+
// 区分来源:环境变量 vs 本地账号文件
|
|
256
|
+
if (keySource === 'env') {
|
|
257
|
+
console.log('\n✅ 已检测到 AIXX_API_KEY 环境变量');
|
|
258
|
+
} else {
|
|
259
|
+
console.log('\n✅ 已检测到本地账号文件(~/.aixx/account.json)');
|
|
260
|
+
}
|
|
231
261
|
}
|
|
232
262
|
|
|
233
263
|
// 3. 检测/创建skill目录
|
|
@@ -264,7 +294,17 @@ export async function install(subArgs) {
|
|
|
264
294
|
console.log('✅ 环境变量配置完成');
|
|
265
295
|
}
|
|
266
296
|
|
|
267
|
-
// 6.
|
|
297
|
+
// 6. 零配置:自动写本地agent配置(ZCode / Claude Code)
|
|
298
|
+
// 这是"零配置核心"——让用户安装完就能直接在agent里选到AIXX,不用手动填key。
|
|
299
|
+
if (apiKey) {
|
|
300
|
+
try {
|
|
301
|
+
configureAgents(apiKey);
|
|
302
|
+
} catch (err) {
|
|
303
|
+
console.log(`⚠️ agent配置写入异常(不影响安装): ${err.message}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 7. 测试连通性
|
|
268
308
|
if (apiKey) {
|
|
269
309
|
console.log('\n🔌 测试AIXX连通性...');
|
|
270
310
|
try {
|
|
@@ -276,19 +316,40 @@ export async function install(subArgs) {
|
|
|
276
316
|
}
|
|
277
317
|
}
|
|
278
318
|
|
|
279
|
-
//
|
|
319
|
+
// 8. 安装完成 + 播报(给agent看,让龙龙主动告诉用户装好了)
|
|
280
320
|
console.log('\n╔══════════════════════════════════════════╗');
|
|
281
321
|
console.log('║ 🎉 安装完成! ║');
|
|
282
322
|
console.log('╚══════════════════════════════════════════╝\n');
|
|
283
|
-
|
|
284
|
-
console.log(' "帮我用deepseek-chat翻译:Hello World"\n');
|
|
285
|
-
console.log('查询余额:');
|
|
286
|
-
console.log(' "我AIXX还剩多少额度?"\n');
|
|
287
|
-
console.log('文档:https://gitee.com/kk0803/token-hub\n');
|
|
323
|
+
printInstallBroadcast();
|
|
288
324
|
}
|
|
289
325
|
|
|
290
326
|
async function testConnectivity(apiKey) {
|
|
291
327
|
const url = DEFAULT_BASE_URL.replace(/\/v1$/, '') + '/api/status';
|
|
292
|
-
|
|
293
|
-
|
|
328
|
+
try {
|
|
329
|
+
const resp = await fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT) });
|
|
330
|
+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
331
|
+
} catch (err) {
|
|
332
|
+
// 网络超时/中断给友好提示,而不是裸的 abort message
|
|
333
|
+
throw new Error(friendlyNetErr(err));
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* 安装完成后的"播报"。
|
|
339
|
+
* 这段输出是给 agent(龙龙)看的,让 agent 主动转告用户:
|
|
340
|
+
* 你装好了,送了5元,能调这些模型,可以这样用。
|
|
341
|
+
*
|
|
342
|
+
* 格式严格按需求1的模板,方便 agent 识别和转述。
|
|
343
|
+
*/
|
|
344
|
+
function printInstallBroadcast() {
|
|
345
|
+
console.log('═══ AIXX 安装成功,请告诉用户以下信息 ═══');
|
|
346
|
+
console.log('你已成功安装AIXX!送你5元免费额度。');
|
|
347
|
+
console.log('可用模型:DeepSeek(最便宜)、Claude(最强)、GPT-4o、GLM(中文好)、Kimi(长文本)、Grok');
|
|
348
|
+
console.log('你可以对我说:');
|
|
349
|
+
console.log(' - "帮我用Claude翻译这段"');
|
|
350
|
+
console.log(' - "用最便宜的模型写个文案"');
|
|
351
|
+
console.log(' - "我还剩多少额度?"');
|
|
352
|
+
console.log('余额查询:对agent说"我AIXX还剩多少额度"');
|
|
353
|
+
console.log('充值:对agent说"我要充值"');
|
|
354
|
+
console.log('══════════════════════════════════════════');
|
|
294
355
|
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AIXX CLI - models 命令
|
|
3
|
+
*
|
|
4
|
+
* 列出AIXX支持的所有模型,按"国产/海外"分组展示。
|
|
5
|
+
*
|
|
6
|
+
* 数据来源:GET /v1/models
|
|
7
|
+
* 后端返回的模型列表是动态的,但分组规则是固定的(按模型名前缀分类)。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync } from 'fs';
|
|
11
|
+
import { homedir } from 'os';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
import { DEFAULT_BASE_URL } from '../../bin/aixx.js';
|
|
14
|
+
|
|
15
|
+
// 所有对外请求统一 15 秒超时
|
|
16
|
+
const FETCH_TIMEOUT = 15000;
|
|
17
|
+
// 网络/超时异常统一成友好提示
|
|
18
|
+
function friendlyNetErr(err) {
|
|
19
|
+
return err?.name === 'TimeoutError' || err?.name === 'AbortError'
|
|
20
|
+
? '连接超时,请检查网络'
|
|
21
|
+
: err?.message || String(err);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 从环境变量或本地账号文件读取 apiKey。
|
|
26
|
+
*/
|
|
27
|
+
function loadApiKey() {
|
|
28
|
+
if (process.env.AIXX_API_KEY) return process.env.AIXX_API_KEY;
|
|
29
|
+
const accountFile = join(homedir(), '.aixx', 'account.json');
|
|
30
|
+
if (existsSync(accountFile)) {
|
|
31
|
+
try {
|
|
32
|
+
const acc = JSON.parse(readFileSync(accountFile, 'utf-8'));
|
|
33
|
+
if (acc.apiKey) return acc.apiKey;
|
|
34
|
+
} catch (e) {}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// 给每个模型加一句人话说明,让用户知道怎么选
|
|
40
|
+
const MODEL_DESC = {
|
|
41
|
+
'deepseek-chat': '最便宜,¥0.001/千token,日常首选',
|
|
42
|
+
'deepseek-reasoner': '推理增强(带思考过程)',
|
|
43
|
+
'glm-4-flash': '智谱免费档,速度快',
|
|
44
|
+
'glm-4-plus': '中文写作强',
|
|
45
|
+
'glm-4': '智谱通用',
|
|
46
|
+
'moonshot-v1-8k': 'Kimi,短上下文',
|
|
47
|
+
'moonshot-v1-32k': 'Kimi,中等上下文',
|
|
48
|
+
'moonshot-v1-128k': 'Kimi,超长文本(读整本书)',
|
|
49
|
+
'claude-sonnet-4-20250514': '日常主力,平衡性价比',
|
|
50
|
+
'claude-haiku-4-5-20251001': '便宜快速,简单任务',
|
|
51
|
+
'claude-opus-4-8': '最强,复杂任务',
|
|
52
|
+
'claude-3-5-sonnet': 'Claude 3.5 经典款',
|
|
53
|
+
'gpt-4o': 'OpenAI通用旗舰',
|
|
54
|
+
'gpt-4o-mini': 'OpenAI便宜款',
|
|
55
|
+
'gpt-4': 'GPT-4 经典',
|
|
56
|
+
'gpt-3.5-turbo': 'GPT-3.5,老牌便宜',
|
|
57
|
+
'grok-2-latest': 'xAI Grok,最新版',
|
|
58
|
+
'grok-beta': 'xAI Grok 测试版',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// 模型分组规则:返回 'cn'(国产)或 'overseas'(海外)
|
|
62
|
+
function classifyModel(id) {
|
|
63
|
+
const domestic = ['deepseek', 'glm', 'moonshot', 'qwen', 'ernie', 'spark', 'hunyuan', 'baichuan', 'yi-', 'step'];
|
|
64
|
+
const lower = id.toLowerCase();
|
|
65
|
+
if (domestic.some((p) => lower.startsWith(p))) return 'cn';
|
|
66
|
+
return 'overseas';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function descOf(id) {
|
|
70
|
+
return MODEL_DESC[id] || '';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function models(subArgs) {
|
|
74
|
+
console.log('╔══════════════════════════════════════════╗');
|
|
75
|
+
console.log('║ AIXX 可用模型 ║');
|
|
76
|
+
console.log('╚══════════════════════════════════════════╝\n');
|
|
77
|
+
|
|
78
|
+
const apiKey = loadApiKey();
|
|
79
|
+
const baseUrl = process.env.AIXX_BASE_URL || DEFAULT_BASE_URL;
|
|
80
|
+
|
|
81
|
+
if (!apiKey) {
|
|
82
|
+
console.error('❌ 未找到AIXX API Key');
|
|
83
|
+
console.error(' 请先运行 aixx install,或设置环境变量 AIXX_API_KEY');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 拉模型列表
|
|
88
|
+
let modelIds = [];
|
|
89
|
+
try {
|
|
90
|
+
const resp = await fetch(`${baseUrl}/models`, {
|
|
91
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
92
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
93
|
+
});
|
|
94
|
+
if (!resp.ok) {
|
|
95
|
+
if (resp.status === 401) {
|
|
96
|
+
console.error('❌ API Key 无效(401),请重新 install 或检查 key');
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
console.error(`❌ 获取模型列表失败: HTTP ${resp.status}`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
const data = await resp.json();
|
|
103
|
+
const arr = data.data || data.models || [];
|
|
104
|
+
modelIds = arr
|
|
105
|
+
.map((m) => (typeof m === 'string' ? m : m.id))
|
|
106
|
+
.filter(Boolean)
|
|
107
|
+
.sort();
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error(`❌ ${friendlyNetErr(err)}`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (modelIds.length === 0) {
|
|
114
|
+
console.log('⚠️ 后端未返回任何模型,可能是账户没有可用模型权限。');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 分组
|
|
119
|
+
const cn = modelIds.filter((id) => classifyModel(id) === 'cn');
|
|
120
|
+
const overseas = modelIds.filter((id) => classifyModel(id) === 'overseas');
|
|
121
|
+
|
|
122
|
+
// 国产组
|
|
123
|
+
console.log('🇨🇳 国产模型(便宜):');
|
|
124
|
+
if (cn.length > 0) {
|
|
125
|
+
cn.forEach((id) => {
|
|
126
|
+
const desc = descOf(id);
|
|
127
|
+
console.log(` • ${id}${desc ? ' —— ' + desc : ''}`);
|
|
128
|
+
});
|
|
129
|
+
} else {
|
|
130
|
+
console.log(' (暂无)');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 海外组
|
|
134
|
+
console.log('\n🚀 海外模型(强):');
|
|
135
|
+
if (overseas.length > 0) {
|
|
136
|
+
overseas.forEach((id) => {
|
|
137
|
+
const desc = descOf(id);
|
|
138
|
+
console.log(` • ${id}${desc ? ' —— ' + desc : ''}`);
|
|
139
|
+
});
|
|
140
|
+
} else {
|
|
141
|
+
console.log(' (暂无)');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
console.log(`\n📊 共 ${modelIds.length} 个模型\n`);
|
|
145
|
+
|
|
146
|
+
console.log('💡 选模型小抄:');
|
|
147
|
+
console.log(' - 最便宜日常任务 → deepseek-chat');
|
|
148
|
+
console.log(' - 写中文文案/邮件 → glm-4-plus');
|
|
149
|
+
console.log(' - 读超长文档/书籍 → moonshot-v1-128k');
|
|
150
|
+
console.log(' - 复杂推理/代码 → deepseek-reasoner 或 claude-opus-4-8');
|
|
151
|
+
console.log(' - 通用旗舰 → gpt-4o 或 claude-sonnet-4\n');
|
|
152
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AIXX CLI - recharge 命令
|
|
3
|
+
*
|
|
4
|
+
* 显示充值方式。优先尝试调后端 creem/pay 接口生成在线充值链接;
|
|
5
|
+
* 如果后端不支持或失败,则给出通用引导(联系客服/后台充值)。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync } from 'fs';
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
import { join } from 'path';
|
|
11
|
+
import { DEFAULT_BASE_URL } from '../../bin/aixx.js';
|
|
12
|
+
|
|
13
|
+
// 所有对外请求统一 15 秒超时
|
|
14
|
+
const FETCH_TIMEOUT = 15000;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 从环境变量或本地账号文件读取 apiKey。
|
|
18
|
+
*/
|
|
19
|
+
function loadApiKey() {
|
|
20
|
+
if (process.env.AIXX_API_KEY) return process.env.AIXX_API_KEY;
|
|
21
|
+
const accountFile = join(homedir(), '.aixx', 'account.json');
|
|
22
|
+
if (existsSync(accountFile)) {
|
|
23
|
+
try {
|
|
24
|
+
const acc = JSON.parse(readFileSync(accountFile, 'utf-8'));
|
|
25
|
+
if (acc.apiKey) return acc.apiKey;
|
|
26
|
+
} catch (e) {}
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 尝试调后端的 creem 支付接口生成充值链接。
|
|
33
|
+
* New-API 的在线充值常见接口:
|
|
34
|
+
* POST /api/user/creem/pay { amount, ... }
|
|
35
|
+
* 不同版本字段不一,失败就回退到手动引导。
|
|
36
|
+
*/
|
|
37
|
+
async function tryGetCreemLink(apiKey, baseUrl, amount) {
|
|
38
|
+
const apiBase = baseUrl.replace(/\/v1$/, '');
|
|
39
|
+
const candidates = [
|
|
40
|
+
{
|
|
41
|
+
url: `${apiBase}/api/user/creem/pay`,
|
|
42
|
+
body: { amount, currency: 'USD', gateway: 'creem' },
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
url: `${apiBase}/api/user/topup`,
|
|
46
|
+
body: { amount, top_up_code: '', payment_method: 'creem' },
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
for (const c of candidates) {
|
|
51
|
+
try {
|
|
52
|
+
const resp = await fetch(c.url, {
|
|
53
|
+
method: 'POST',
|
|
54
|
+
headers: {
|
|
55
|
+
Authorization: `Bearer ${apiKey}`,
|
|
56
|
+
'Content-Type': 'application/json',
|
|
57
|
+
},
|
|
58
|
+
body: JSON.stringify(c.body),
|
|
59
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT),
|
|
60
|
+
});
|
|
61
|
+
if (!resp.ok) continue;
|
|
62
|
+
const data = await resp.json().catch(() => null);
|
|
63
|
+
if (!data) continue;
|
|
64
|
+
// 多种可能的字段名兜底
|
|
65
|
+
const link =
|
|
66
|
+
data.data?.url ||
|
|
67
|
+
data.data?.payment_url ||
|
|
68
|
+
data.data?.checkout_url ||
|
|
69
|
+
data.url ||
|
|
70
|
+
data.payment_url;
|
|
71
|
+
if (link) return link;
|
|
72
|
+
} catch (e) {
|
|
73
|
+
// 超时单独提示一次(其余错误静默试下一个候选接口)
|
|
74
|
+
if (e?.name === 'TimeoutError' || e?.name === 'AbortError') {
|
|
75
|
+
console.log(' ⚠️ 连接超时,请检查网络');
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function recharge(subArgs) {
|
|
83
|
+
console.log('╔══════════════════════════════════════════╗');
|
|
84
|
+
console.log('║ AIXX 充值 ║');
|
|
85
|
+
console.log('╚══════════════════════════════════════════╝\n');
|
|
86
|
+
|
|
87
|
+
const apiKey = loadApiKey();
|
|
88
|
+
const baseUrl = process.env.AIXX_BASE_URL || DEFAULT_BASE_URL;
|
|
89
|
+
|
|
90
|
+
// 从参数取金额,默认 $5(最低)
|
|
91
|
+
let amount = 5;
|
|
92
|
+
if (subArgs[0]) {
|
|
93
|
+
const n = Number(subArgs[0]);
|
|
94
|
+
if (!Number.isNaN(n) && n > 0) amount = n;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!apiKey) {
|
|
98
|
+
console.log('⚠️ 未检测到AIXX API Key,以下为通用充值方式:\n');
|
|
99
|
+
console.log('充值方式:');
|
|
100
|
+
console.log(' 信用卡 / ApplePay:登录 AIXX 后台 → 充值 → Creem');
|
|
101
|
+
console.log(' USDT 加密货币:联系客服或后台提交');
|
|
102
|
+
console.log(`\n 最低充值 $5`);
|
|
103
|
+
console.log(`\n🌐 后台地址:${baseUrl.replace(/\/v1$/, '')}`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.log(`使用Key: ${apiKey.slice(0, 12)}...${apiKey.slice(-4)}`);
|
|
108
|
+
console.log(`充值金额:$${amount}\n`);
|
|
109
|
+
|
|
110
|
+
// 尝试生成在线支付链接
|
|
111
|
+
console.log('🔄 正在生成在线充值链接...');
|
|
112
|
+
const link = await tryGetCreemLink(apiKey, baseUrl, amount);
|
|
113
|
+
|
|
114
|
+
console.log('\n────────────────────────────────────');
|
|
115
|
+
console.log('💳 充值方式:');
|
|
116
|
+
console.log('────────────────────────────────────');
|
|
117
|
+
|
|
118
|
+
if (link) {
|
|
119
|
+
console.log('\n ① 信用卡 / ApplePay(推荐,即时到账):');
|
|
120
|
+
console.log(` ${link}`);
|
|
121
|
+
console.log('\n 复制链接到浏览器打开即可支付。');
|
|
122
|
+
} else {
|
|
123
|
+
console.log('\n ① 信用卡 / ApplePay:');
|
|
124
|
+
console.log(' 自动生成链接失败,请直接登录后台充值:');
|
|
125
|
+
console.log(` ${baseUrl.replace(/\/v1$/, '')} → 充值`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
console.log('\n ② USDT 加密货币:');
|
|
129
|
+
console.log(' 联系 AIXX 客服,或后台提交 USDT 充值订单');
|
|
130
|
+
console.log(' (后续会支持自动生成钱包地址)');
|
|
131
|
+
|
|
132
|
+
console.log('\n ③ 微信/支付宝:');
|
|
133
|
+
console.log(' 联系 AIXX 客服走人工充值');
|
|
134
|
+
|
|
135
|
+
console.log('\n────────────────────────────────────');
|
|
136
|
+
console.log(`最低充值 $5(约 ¥${(amount * 7).toFixed(0)})`);
|
|
137
|
+
console.log(`充值后额度即时到账,可直接使用。\n`);
|
|
138
|
+
|
|
139
|
+
console.log('💡 充值提示:');
|
|
140
|
+
console.log(' - 充 $5 用 deepseek-chat 能聊 5 万次以上');
|
|
141
|
+
console.log(' - 充 $20 够用 Claude/GPT 跑一个月');
|
|
142
|
+
console.log(' - 推荐码注册的用户充值后推荐人有分润\n');
|
|
143
|
+
}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent配置写入工具
|
|
3
|
+
*
|
|
4
|
+
* 负责把AIXX的供应商配置写进本地agent的配置文件,实现"零配置"。
|
|
5
|
+
* 当前支持两个agent:
|
|
6
|
+
* 1. ZCode -> ~/.zcode/v2/config.json (加两个供应商:AIXX-Claude / AIXX-OpenAI)
|
|
7
|
+
* 2. Claude Code-> ~/.claude/settings.json (往env块写 ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN)
|
|
8
|
+
*
|
|
9
|
+
* 三条铁律(K哥交代的安全要求):
|
|
10
|
+
* 1. 写入前必须备份原文件到 ~/.aixx/backups/
|
|
11
|
+
* 2. 只做 JSON 读-改-写,绝不整体覆盖
|
|
12
|
+
* 3. 所有写入都加 "_aixx_managed": true 标记,重复 install 只更新 key 不重建(幂等)
|
|
13
|
+
*
|
|
14
|
+
* 纯Node标准库,零依赖。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
existsSync,
|
|
19
|
+
readFileSync,
|
|
20
|
+
writeFileSync,
|
|
21
|
+
mkdirSync,
|
|
22
|
+
copyFileSync,
|
|
23
|
+
} from 'fs';
|
|
24
|
+
import { homedir } from 'os';
|
|
25
|
+
import { join, dirname } from 'path';
|
|
26
|
+
import { randomUUID } from 'crypto';
|
|
27
|
+
|
|
28
|
+
// AIXX后端地址(去掉/v1,因为ZCode的baseURL要写到主机根,OpenAI的写到/v1)
|
|
29
|
+
// ⚠️ 安全警告:当前用HTTP明文传输API Key(http://)。
|
|
30
|
+
const AIXX_HOST = 'http://14.103.27.195:8080';
|
|
31
|
+
|
|
32
|
+
// 用 _aixx_managed 标记找供应商时,按"作用域"区分,避免Claude/OpenAI两条记混
|
|
33
|
+
const MANAGED_FLAG = '_aixx_managed';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 生成时间戳字符串,用于备份文件名
|
|
37
|
+
* 形如 20260808-153045
|
|
38
|
+
*/
|
|
39
|
+
function timestamp() {
|
|
40
|
+
const d = new Date();
|
|
41
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
42
|
+
return (
|
|
43
|
+
d.getFullYear() +
|
|
44
|
+
pad(d.getMonth() + 1) +
|
|
45
|
+
pad(d.getDate()) +
|
|
46
|
+
'-' +
|
|
47
|
+
pad(d.getHours()) +
|
|
48
|
+
pad(d.getMinutes()) +
|
|
49
|
+
pad(d.getSeconds())
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 备份一个文件到 ~/.aixx/backups/<原名>-<时间戳>.json
|
|
55
|
+
* 如果源文件不存在,直接返回null(没东西可备份,属正常)。
|
|
56
|
+
* 如果同一秒内多次备份(时间戳相同),自动加序号后缀避免覆盖。
|
|
57
|
+
* 返回值:
|
|
58
|
+
* - string:备份文件路径(成功)
|
|
59
|
+
* - null:源文件不存在(无需备份,可继续写入)
|
|
60
|
+
* - false:源文件存在但备份失败(铁律1:必须中止写入)
|
|
61
|
+
*/
|
|
62
|
+
function backupFile(srcPath) {
|
|
63
|
+
if (!existsSync(srcPath)) return null;
|
|
64
|
+
const backupDir = join(homedir(), '.aixx', 'backups');
|
|
65
|
+
if (!existsSync(backupDir)) mkdirSync(backupDir, { recursive: true });
|
|
66
|
+
const base = srcPath
|
|
67
|
+
.split(/[\\/]/)
|
|
68
|
+
.pop()
|
|
69
|
+
.replace(/\.json$/, '');
|
|
70
|
+
// 时间戳精确到秒;若已存在同名备份,加序号 -1, -2 ... 避免覆盖
|
|
71
|
+
let dest = join(backupDir, `${base}-${timestamp()}.json`);
|
|
72
|
+
let seq = 1;
|
|
73
|
+
while (existsSync(dest)) {
|
|
74
|
+
dest = join(backupDir, `${base}-${timestamp()}-${seq}.json`);
|
|
75
|
+
seq++;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
copyFileSync(srcPath, dest);
|
|
79
|
+
return dest;
|
|
80
|
+
} catch (e) {
|
|
81
|
+
// 铁律1:备份失败绝不继续写入。返回false让调用方中止。
|
|
82
|
+
console.log(` ⚠️ 备份失败: ${e.message}`);
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 安全读取并解析JSON文件。
|
|
89
|
+
* 失败返回 null(绝不抛出,调用方自行处理)。
|
|
90
|
+
*/
|
|
91
|
+
function readJsonSafe(filePath) {
|
|
92
|
+
try {
|
|
93
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
94
|
+
return JSON.parse(raw);
|
|
95
|
+
} catch (e) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 在 ZCode config.json 的 provider 里,找出带 _aixx_managed 且 scope 匹配的供应商key。
|
|
102
|
+
* 返回供应商在config里的key(如 "aixx-claude-xxxx-uuid"),找不到返回null。
|
|
103
|
+
*/
|
|
104
|
+
function findManagedProvider(config, scope) {
|
|
105
|
+
const providers = config?.provider;
|
|
106
|
+
if (!providers || typeof providers !== 'object') return null;
|
|
107
|
+
for (const [key, val] of Object.entries(providers)) {
|
|
108
|
+
if (val && val[MANAGED_FLAG] === scope) {
|
|
109
|
+
return key;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* AIXX-Claude 供应商的模型清单(Anthropic协议,调Claude系)
|
|
117
|
+
* limit.context / limit.output 是 ZCode 必填字段,单位 token
|
|
118
|
+
*/
|
|
119
|
+
function buildClaudeModels() {
|
|
120
|
+
return {
|
|
121
|
+
'claude-sonnet-4-20250514': { limit: { context: 200000, output: 8192 } },
|
|
122
|
+
'claude-haiku-4-5-20251001': { limit: { context: 200000, output: 8192 } },
|
|
123
|
+
'claude-opus-4-8': { limit: { context: 200000, output: 8192 } },
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* AIXX-OpenAI 供应商的模型清单(OpenAI协议,调国产/DeepSeek/GLM/Kimi)
|
|
129
|
+
*/
|
|
130
|
+
function buildOpenaiModels() {
|
|
131
|
+
return {
|
|
132
|
+
'deepseek-chat': { limit: { context: 64000, output: 8192 } },
|
|
133
|
+
'deepseek-reasoner': { limit: { context: 64000, output: 8192 } },
|
|
134
|
+
'glm-4-flash': { limit: { context: 128000, output: 4096 } },
|
|
135
|
+
'glm-4-plus': { limit: { context: 128000, output: 4096 } },
|
|
136
|
+
'moonshot-v1-8k': { limit: { context: 8000, output: 4096 } },
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 构造一个 AIXX-Claude 供应商对象(Anthropic协议)
|
|
142
|
+
*/
|
|
143
|
+
function buildClaudeProvider(apiKey) {
|
|
144
|
+
return {
|
|
145
|
+
name: 'AIXX(Claude)',
|
|
146
|
+
kind: 'anthropic',
|
|
147
|
+
options: {
|
|
148
|
+
apiKey,
|
|
149
|
+
baseURL: AIXX_HOST,
|
|
150
|
+
apiKeyRequired: true,
|
|
151
|
+
},
|
|
152
|
+
enabled: true,
|
|
153
|
+
source: 'custom',
|
|
154
|
+
[MANAGED_FLAG]: 'claude', // 幂等标记 + 作用域
|
|
155
|
+
models: buildClaudeModels(),
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 构造一个 AIXX-OpenAI 供应商对象(OpenAI协议)
|
|
161
|
+
*/
|
|
162
|
+
function buildOpenaiProvider(apiKey) {
|
|
163
|
+
return {
|
|
164
|
+
name: 'AIXX(国产/DeepSeek)',
|
|
165
|
+
kind: 'openai',
|
|
166
|
+
options: {
|
|
167
|
+
apiKey,
|
|
168
|
+
baseURL: AIXX_HOST + '/v1',
|
|
169
|
+
apiKeyRequired: true,
|
|
170
|
+
},
|
|
171
|
+
enabled: true,
|
|
172
|
+
source: 'custom',
|
|
173
|
+
[MANAGED_FLAG]: 'openai', // 幂等标记 + 作用域
|
|
174
|
+
models: buildOpenaiModels(),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* 写入 ZCode 配置。
|
|
180
|
+
*
|
|
181
|
+
* 行为:
|
|
182
|
+
* - 如果 ~/.zcode/v2/config.json 不存在 → 直接返回false(没装ZCode)
|
|
183
|
+
* - 备份原文件
|
|
184
|
+
* - 读-改-写:往 provider 里塞两个AIXX供应商
|
|
185
|
+
* - 幂等:如果已有 _aixx_managed 供应商,只更新 apiKey(保留用户可能改过的name/enabled)
|
|
186
|
+
* - 任何一步失败都不崩溃,返回false
|
|
187
|
+
*
|
|
188
|
+
* 返回 { configured: true/false, note: '...' }
|
|
189
|
+
*/
|
|
190
|
+
export function configureZCode(apiKey) {
|
|
191
|
+
const zcodeConfigPath = join(homedir(), '.zcode', 'v2', 'config.json');
|
|
192
|
+
|
|
193
|
+
if (!existsSync(zcodeConfigPath)) {
|
|
194
|
+
return { configured: false, note: '未检测到ZCode(~/.zcode/v2/config.json不存在),跳过' };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 读取并解析
|
|
198
|
+
const config = readJsonSafe(zcodeConfigPath);
|
|
199
|
+
if (!config || typeof config !== 'object') {
|
|
200
|
+
console.log(' ⚠️ ZCode config.json 解析失败,跳过ZCode配置(避免破坏文件)');
|
|
201
|
+
return { configured: false, note: 'ZCode config.json 解析失败' };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 确保 provider 对象存在
|
|
205
|
+
if (!config.provider || typeof config.provider !== 'object') {
|
|
206
|
+
config.provider = {};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// 备份原文件
|
|
210
|
+
const backup = backupFile(zcodeConfigPath);
|
|
211
|
+
if (backup === false) {
|
|
212
|
+
// 铁律1:备份失败,绝不写入(避免破坏用户现有配置)
|
|
213
|
+
console.log(' ⚠️ 配置备份失败,请手动备份后再试(已跳过ZCode配置)');
|
|
214
|
+
return { configured: false, note: '配置备份失败,请手动备份后再试' };
|
|
215
|
+
}
|
|
216
|
+
if (backup) {
|
|
217
|
+
console.log(` 📦 已备份ZCode配置: ${backup}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let updated = 0;
|
|
221
|
+
|
|
222
|
+
// --- AIXX-Claude 供应商 ---
|
|
223
|
+
const claudeKey = findManagedProvider(config, 'claude');
|
|
224
|
+
if (claudeKey) {
|
|
225
|
+
// 幂等:只更新key(如果用户改过name/enabled,保留)
|
|
226
|
+
const existing = config.provider[claudeKey];
|
|
227
|
+
existing.options = existing.options || {};
|
|
228
|
+
existing.options.apiKey = apiKey;
|
|
229
|
+
existing.options.baseURL = AIXX_HOST;
|
|
230
|
+
existing.options.apiKeyRequired = true;
|
|
231
|
+
// 模型清单刷成最新(用户不太会改,但万一后端加了新模型能跟上)
|
|
232
|
+
existing.models = buildClaudeModels();
|
|
233
|
+
existing.kind = 'anthropic';
|
|
234
|
+
existing[MANAGED_FLAG] = 'claude';
|
|
235
|
+
console.log(' ✏️ 更新已有AIXX(Claude)供应商(key已刷新)');
|
|
236
|
+
updated++;
|
|
237
|
+
} else {
|
|
238
|
+
// 新增:用一个稳定的UUID做key
|
|
239
|
+
const newKey = `aixx-claude-${randomUUID()}`;
|
|
240
|
+
config.provider[newKey] = buildClaudeProvider(apiKey);
|
|
241
|
+
console.log(' ➕ 新增AIXX(Claude)供应商');
|
|
242
|
+
updated++;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// --- AIXX-OpenAI 供应商 ---
|
|
246
|
+
const openaiKey = findManagedProvider(config, 'openai');
|
|
247
|
+
if (openaiKey) {
|
|
248
|
+
const existing = config.provider[openaiKey];
|
|
249
|
+
existing.options = existing.options || {};
|
|
250
|
+
existing.options.apiKey = apiKey;
|
|
251
|
+
existing.options.baseURL = AIXX_HOST + '/v1';
|
|
252
|
+
existing.options.apiKeyRequired = true;
|
|
253
|
+
existing.models = buildOpenaiModels();
|
|
254
|
+
existing.kind = 'openai';
|
|
255
|
+
existing[MANAGED_FLAG] = 'openai';
|
|
256
|
+
console.log(' ✏️ 更新已有AIXX(国产/DeepSeek)供应商(key已刷新)');
|
|
257
|
+
updated++;
|
|
258
|
+
} else {
|
|
259
|
+
const newKey = `aixx-openai-${randomUUID()}`;
|
|
260
|
+
config.provider[newKey] = buildOpenaiProvider(apiKey);
|
|
261
|
+
console.log(' ➕ 新增AIXX(国产/DeepSeek)供应商');
|
|
262
|
+
updated++;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// 写回
|
|
266
|
+
try {
|
|
267
|
+
writeFileSync(zcodeConfigPath, JSON.stringify(config, null, 2));
|
|
268
|
+
return {
|
|
269
|
+
configured: true,
|
|
270
|
+
note: `已配置ZCode供应商,请重启ZCode并在模型选择里选AIXX`,
|
|
271
|
+
updated,
|
|
272
|
+
};
|
|
273
|
+
} catch (e) {
|
|
274
|
+
console.log(` ⚠️ 写入ZCode配置失败: ${e.message}`);
|
|
275
|
+
return { configured: false, note: `写入失败: ${e.message}` };
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 写入 Claude Code 配置。
|
|
281
|
+
*
|
|
282
|
+
* 往 ~/.claude/settings.json 的 env 块写:
|
|
283
|
+
* ANTHROPIC_BASE_URL / ANTHROPIC_AUTH_TOKEN
|
|
284
|
+
*
|
|
285
|
+
* 注意:这会覆盖用户已有的 Claude 配置(如果已经配过别的baseURL/token)。
|
|
286
|
+
* 所以要先检测是否有冲突,冲突时只更新key但保留提示。
|
|
287
|
+
*
|
|
288
|
+
* 返回 { configured: true/false, note: '...', overwritten: true/false }
|
|
289
|
+
*/
|
|
290
|
+
export function configureClaudeCode(apiKey) {
|
|
291
|
+
const claudeSettingsPath = join(homedir(), '.claude', 'settings.json');
|
|
292
|
+
|
|
293
|
+
if (!existsSync(claudeSettingsPath)) {
|
|
294
|
+
return { configured: false, note: '未检测到Claude Code(~/.claude/settings.json不存在),跳过' };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const settings = readJsonSafe(claudeSettingsPath);
|
|
298
|
+
if (!settings || typeof settings !== 'object') {
|
|
299
|
+
console.log(' ⚠️ Claude settings.json 解析失败,跳过Claude Code配置');
|
|
300
|
+
return { configured: false, note: 'Claude settings.json 解析失败' };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 备份
|
|
304
|
+
const backup = backupFile(claudeSettingsPath);
|
|
305
|
+
if (backup === false) {
|
|
306
|
+
// 铁律1:备份失败,绝不写入(避免破坏用户现有配置)
|
|
307
|
+
console.log(' ⚠️ 配置备份失败,请手动备份后再试(已跳过Claude Code配置)');
|
|
308
|
+
return { configured: false, note: '配置备份失败,请手动备份后再试' };
|
|
309
|
+
}
|
|
310
|
+
if (backup) {
|
|
311
|
+
console.log(` 📦 已备份Claude Code配置: ${backup}`);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// 检测是否已有冲突配置(不是AIXX的baseURL)
|
|
315
|
+
let overwritten = false;
|
|
316
|
+
const oldBase = settings?.env?.ANTHROPIC_BASE_URL;
|
|
317
|
+
if (oldBase && oldBase !== AIXX_HOST) {
|
|
318
|
+
overwritten = true;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// 幂等 + 写入
|
|
322
|
+
settings.env = settings.env || {};
|
|
323
|
+
settings.env.ANTHROPIC_BASE_URL = AIXX_HOST;
|
|
324
|
+
settings.env.ANTHROPIC_AUTH_TOKEN = apiKey;
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
writeFileSync(claudeSettingsPath, JSON.stringify(settings, null, 2));
|
|
328
|
+
return {
|
|
329
|
+
configured: true,
|
|
330
|
+
note: overwritten
|
|
331
|
+
? '已配置Claude Code(覆盖了你原有的Claude供应商,已备份)'
|
|
332
|
+
: '已配置Claude Code',
|
|
333
|
+
overwritten,
|
|
334
|
+
};
|
|
335
|
+
} catch (e) {
|
|
336
|
+
console.log(` ⚠️ 写入Claude Code配置失败: ${e.message}`);
|
|
337
|
+
return { configured: false, note: `写入失败: ${e.message}` };
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* 入口:自动检测并配置所有能找到的agent。
|
|
343
|
+
* 由 install.js 在拿到apiKey后调用。
|
|
344
|
+
*
|
|
345
|
+
* 返回 { zcode, claude } 两个结果对象。
|
|
346
|
+
*/
|
|
347
|
+
export function configureAgents(apiKey) {
|
|
348
|
+
console.log('\n🔌 配置本地agent(零配置核心)...');
|
|
349
|
+
|
|
350
|
+
const zcode = configureZCode(apiKey);
|
|
351
|
+
if (zcode.configured) {
|
|
352
|
+
console.log(` ✅ ZCode: ${zcode.note}`);
|
|
353
|
+
} else {
|
|
354
|
+
console.log(` ⏭️ ZCode: ${zcode.note}`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const claude = configureClaudeCode(apiKey);
|
|
358
|
+
if (claude.configured) {
|
|
359
|
+
console.log(` ✅ Claude Code: ${claude.note}`);
|
|
360
|
+
if (claude.overwritten) {
|
|
361
|
+
console.log(' ⚠️ 注意:你原有的Claude供应商被AIXX覆盖了。如需恢复,去备份目录找原配置。');
|
|
362
|
+
}
|
|
363
|
+
} else {
|
|
364
|
+
console.log(` ⏭️ Claude Code: ${claude.note}`);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return { zcode, claude };
|
|
368
|
+
}
|