@autotask/atools-tool 0.1.8 → 0.1.10
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 +8 -5
- package/bin/cli.mjs +17 -1
- package/lib/config-codex.mjs +429 -0
- package/lib/config-openclaw.mjs +152 -45
- package/lib/install.mjs +82 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -31,6 +31,8 @@ Selection UX:
|
|
|
31
31
|
- TTY terminal: use `↑/↓` and `Enter`
|
|
32
32
|
- Non-TTY: falls back to number input
|
|
33
33
|
|
|
34
|
+
If proxy startup fails during `atools-tool openclaw`, the CLI now prints environment-aware troubleshooting hints (systemd user bus, permissions, port readiness) and does not silently fail.
|
|
35
|
+
|
|
34
36
|
Rollback to codex-compatible defaults:
|
|
35
37
|
|
|
36
38
|
```bash
|
|
@@ -42,7 +44,7 @@ Recommended selections:
|
|
|
42
44
|
1. `gpt-5.3-codex`
|
|
43
45
|
2. `off`
|
|
44
46
|
3. `low`
|
|
45
|
-
4. Input your
|
|
47
|
+
4. Input your ATools key (same as ATOOLS_OAI_KEY)
|
|
46
48
|
|
|
47
49
|
## Install (npm)
|
|
48
50
|
|
|
@@ -53,18 +55,18 @@ npm i -g @autotask/atools-tool
|
|
|
53
55
|
Default full install (recommended):
|
|
54
56
|
|
|
55
57
|
```bash
|
|
56
|
-
npm i -g @autotask/atools-tool && atools-tool-
|
|
58
|
+
npm i -g @autotask/atools-tool && atools-tool install-proxy && atools-tool openclaw
|
|
57
59
|
```
|
|
58
60
|
|
|
59
61
|
Optional installer command:
|
|
60
62
|
|
|
61
63
|
```bash
|
|
62
|
-
atools-tool-
|
|
64
|
+
atools-tool install-proxy --help
|
|
63
65
|
```
|
|
64
66
|
|
|
65
|
-
`atools-tool-
|
|
67
|
+
`atools-tool install-proxy` service behavior by platform (legacy alias: `atools-tool-install`):
|
|
66
68
|
|
|
67
|
-
- Linux: systemd user service
|
|
69
|
+
- Linux: systemd user service (fallback to detached process if `systemctl --user` is unavailable)
|
|
68
70
|
- macOS: launchd user agent (`~/Library/LaunchAgents`)
|
|
69
71
|
- Windows: Task Scheduler user task (ONLOGON + start now)
|
|
70
72
|
|
|
@@ -98,6 +100,7 @@ curl -fsSL https://raw.githubusercontent.com/aak1247/sub2api-openclaw-proxy/main
|
|
|
98
100
|
- patches `~/.openclaw/agents/main/agent/auth-profiles.json` (if API key is available)
|
|
99
101
|
- service install by platform:
|
|
100
102
|
- Linux: creates/enables `~/.config/systemd/user/openclaw-atools-proxy.service`
|
|
103
|
+
- if user bus/systemd is unavailable (for example container/root without login bus), installer auto-falls back to detached proxy process
|
|
101
104
|
- macOS: creates/enables `~/Library/LaunchAgents/openclaw-atools-proxy.plist`
|
|
102
105
|
- Windows: creates/updates scheduled task `openclaw-atools-proxy`
|
|
103
106
|
- restarts `openclaw-gateway.service` only on Linux (unless `--no-restart`)
|
package/bin/cli.mjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createProxyServer } from '../lib/proxy-server.mjs';
|
|
3
3
|
import { runConfigOpenclaw } from '../lib/config-openclaw.mjs';
|
|
4
|
+
import { runConfigCodex } from '../lib/config-codex.mjs';
|
|
5
|
+
import { runInstall } from '../lib/install.mjs';
|
|
4
6
|
|
|
5
7
|
function parseArgs(argv) {
|
|
6
8
|
const out = {
|
|
@@ -47,6 +49,8 @@ Commands:
|
|
|
47
49
|
serve Run proxy service
|
|
48
50
|
openclaw Interactive OpenClaw setup (sub2api proxy check + model/reasoning/thinking/key)
|
|
49
51
|
config-openclaw Alias of 'openclaw'
|
|
52
|
+
install-proxy Install background proxy service for OpenClaw
|
|
53
|
+
codex Auto-configure Codex CLI to use sub2api (direct upstream)
|
|
50
54
|
|
|
51
55
|
Options:
|
|
52
56
|
--openclaw-home <path> OpenClaw home dir (default: ~/.openclaw)
|
|
@@ -62,13 +66,25 @@ Options:
|
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
async function main() {
|
|
65
|
-
const
|
|
69
|
+
const argv = process.argv.slice(2);
|
|
70
|
+
|
|
71
|
+
if (argv[0] === 'install-proxy') {
|
|
72
|
+
await runInstall(argv.slice(1));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const args = parseArgs(argv);
|
|
66
77
|
|
|
67
78
|
if (args.help) {
|
|
68
79
|
printHelp();
|
|
69
80
|
return;
|
|
70
81
|
}
|
|
71
82
|
|
|
83
|
+
if (args.cmd === 'codex') {
|
|
84
|
+
await runConfigCodex();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
72
88
|
if (args.cmd === 'config-openclaw' || args.cmd === 'openclaw') {
|
|
73
89
|
await runConfigOpenclaw({ openclawHome: args.openclawHome });
|
|
74
90
|
return;
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import readlinePromises from 'node:readline/promises';
|
|
5
|
+
import readline from 'node:readline';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
|
|
8
|
+
const PROVIDER = 'atools';
|
|
9
|
+
const MODEL_OPTIONS = ['gpt-5.2', 'gpt-5.3-codex', 'gpt-5.4'];
|
|
10
|
+
const DEFAULT_MODEL_ID = 'gpt-5.3-codex';
|
|
11
|
+
const UPSTREAM_BASE_URL = 'https://sub2api.atools.live/v1';
|
|
12
|
+
const ENV_KEY = 'ATOOLS_OAI_KEY';
|
|
13
|
+
|
|
14
|
+
const USE_COLOR = Boolean(process.stdout.isTTY && process.env.NO_COLOR !== '1');
|
|
15
|
+
const ANSI = {
|
|
16
|
+
reset: '\x1b[0m',
|
|
17
|
+
bold: '\x1b[1m',
|
|
18
|
+
cyan: '\x1b[36m',
|
|
19
|
+
green: '\x1b[32m',
|
|
20
|
+
yellow: '\x1b[33m'
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function withColor(text, ...codes) {
|
|
24
|
+
if (!USE_COLOR || !codes.length) return text;
|
|
25
|
+
const prefix = codes.join('');
|
|
26
|
+
return `${prefix}${text}${ANSI.reset}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sectionTitle(text) {
|
|
30
|
+
return withColor(text, ANSI.cyan, ANSI.bold);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function ok(text) {
|
|
34
|
+
return withColor(text, ANSI.green);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function warn(text) {
|
|
38
|
+
return withColor(text, ANSI.yellow);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function bold(text) {
|
|
42
|
+
return withColor(text, ANSI.bold);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function nowStamp() {
|
|
46
|
+
const d = new Date();
|
|
47
|
+
const p = (n) => String(n).padStart(2, '0');
|
|
48
|
+
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function backup(filePath) {
|
|
52
|
+
if (!fs.existsSync(filePath)) return null;
|
|
53
|
+
const dest = `${filePath}.bak.${nowStamp()}`;
|
|
54
|
+
fs.copyFileSync(filePath, dest);
|
|
55
|
+
return dest;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function ensureDir(dirPath) {
|
|
59
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function patchCodexConfigText(original, { modelId } = {}) {
|
|
63
|
+
const provider = PROVIDER;
|
|
64
|
+
const model = modelId || DEFAULT_MODEL_ID;
|
|
65
|
+
const baseUrl = UPSTREAM_BASE_URL;
|
|
66
|
+
|
|
67
|
+
if (!original || !original.trim()) {
|
|
68
|
+
return [
|
|
69
|
+
'# Codex config generated by atools-tool',
|
|
70
|
+
`model_provider = "${provider}"`,
|
|
71
|
+
`model = "${model}"`,
|
|
72
|
+
'',
|
|
73
|
+
`[model_providers.${provider}]`,
|
|
74
|
+
'name = "ATools"',
|
|
75
|
+
`base_url = "${baseUrl}"`,
|
|
76
|
+
'wire_api = "responses"',
|
|
77
|
+
'requires_openai_auth = false',
|
|
78
|
+
`env_key = "${ENV_KEY}"`,
|
|
79
|
+
''
|
|
80
|
+
].join('\n');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const lines = original.replace(/\r\n/g, '\n').split('\n');
|
|
84
|
+
let currentSection = '';
|
|
85
|
+
let inProviderSection = false;
|
|
86
|
+
let sawProviderSection = false;
|
|
87
|
+
let sawModelProviderTop = false;
|
|
88
|
+
let sawModelTop = false;
|
|
89
|
+
|
|
90
|
+
const updated = [];
|
|
91
|
+
|
|
92
|
+
for (const rawLine of lines) {
|
|
93
|
+
let line = rawLine;
|
|
94
|
+
const trimmed = rawLine.trim();
|
|
95
|
+
|
|
96
|
+
const sectionMatch = trimmed.match(/^\[(.+?)\]\s*$/);
|
|
97
|
+
if (sectionMatch) {
|
|
98
|
+
currentSection = sectionMatch[1];
|
|
99
|
+
inProviderSection = currentSection === `model_providers.${provider}`;
|
|
100
|
+
if (inProviderSection) sawProviderSection = true;
|
|
101
|
+
updated.push(line);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!currentSection) {
|
|
106
|
+
if (/^model_provider\s*=/.test(trimmed)) {
|
|
107
|
+
line = `model_provider = "${provider}"`;
|
|
108
|
+
sawModelProviderTop = true;
|
|
109
|
+
} else if (/^model\s*=/.test(trimmed)) {
|
|
110
|
+
line = `model = "${model}"`;
|
|
111
|
+
sawModelTop = true;
|
|
112
|
+
}
|
|
113
|
+
updated.push(line);
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (inProviderSection) {
|
|
118
|
+
if (/^name\s*=/.test(trimmed)) {
|
|
119
|
+
line = 'name = "ATools"';
|
|
120
|
+
} else if (/^base_url\s*=/.test(trimmed)) {
|
|
121
|
+
line = `base_url = "${baseUrl}"`;
|
|
122
|
+
} else if (/^wire_api\s*=/.test(trimmed)) {
|
|
123
|
+
line = 'wire_api = "responses"';
|
|
124
|
+
} else if (/^requires_openai_auth\s*=/.test(trimmed)) {
|
|
125
|
+
line = 'requires_openai_auth = false';
|
|
126
|
+
} else if (/^env_key\s*=/.test(trimmed)) {
|
|
127
|
+
line = `env_key = "${ENV_KEY}"`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
updated.push(line);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let resultLines = updated;
|
|
135
|
+
|
|
136
|
+
const topInsert = [];
|
|
137
|
+
if (!sawModelProviderTop) topInsert.push(`model_provider = "${provider}"`);
|
|
138
|
+
if (!sawModelTop) topInsert.push(`model = "${model}"`);
|
|
139
|
+
|
|
140
|
+
if (topInsert.length > 0) {
|
|
141
|
+
const withTop = [];
|
|
142
|
+
let inserted = false;
|
|
143
|
+
for (let i = 0; i < resultLines.length; i += 1) {
|
|
144
|
+
if (!inserted && resultLines[i].trim() && !resultLines[i].trim().startsWith('#')) {
|
|
145
|
+
withTop.push(...topInsert, '');
|
|
146
|
+
inserted = true;
|
|
147
|
+
}
|
|
148
|
+
withTop.push(resultLines[i]);
|
|
149
|
+
}
|
|
150
|
+
if (!inserted) {
|
|
151
|
+
resultLines = [...topInsert, '', ...resultLines];
|
|
152
|
+
} else {
|
|
153
|
+
resultLines = withTop;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!sawProviderSection) {
|
|
158
|
+
if (resultLines.length && resultLines[resultLines.length - 1].trim() !== '') {
|
|
159
|
+
resultLines.push('');
|
|
160
|
+
}
|
|
161
|
+
resultLines.push(`[model_providers.${provider}]`);
|
|
162
|
+
resultLines.push('name = "ATools"');
|
|
163
|
+
resultLines.push(`base_url = "${baseUrl}"`);
|
|
164
|
+
resultLines.push('wire_api = "responses"');
|
|
165
|
+
resultLines.push('requires_openai_auth = false');
|
|
166
|
+
resultLines.push(`env_key = "${ENV_KEY}"`);
|
|
167
|
+
resultLines.push('');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return resultLines.join('\n');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function upsertExportLine(content, key, value) {
|
|
174
|
+
const line = `export ${key}=${value}`;
|
|
175
|
+
const re = new RegExp(`^\s*export\s+${key}=.*$`, 'm');
|
|
176
|
+
if (re.test(content)) {
|
|
177
|
+
return content.replace(re, line);
|
|
178
|
+
}
|
|
179
|
+
if (!content || !content.trim()) {
|
|
180
|
+
return `${line}\n`;
|
|
181
|
+
}
|
|
182
|
+
return `${content.replace(/\s*$/, '')}\n${line}\n`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function detectPosixProfile() {
|
|
186
|
+
const shell = process.env.SHELL || '';
|
|
187
|
+
const home = os.homedir();
|
|
188
|
+
if (shell.includes('zsh')) return path.join(home, '.zshrc');
|
|
189
|
+
if (shell.includes('bash')) return path.join(home, '.bashrc');
|
|
190
|
+
return path.join(home, '.bashrc');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function persistEnvVar(key, value) {
|
|
194
|
+
if (!value) {
|
|
195
|
+
return { changed: false, reason: 'empty', type: '' };
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (process.platform === 'win32') {
|
|
199
|
+
try {
|
|
200
|
+
const out = spawnSync('setx', [key, value], { encoding: 'utf8' });
|
|
201
|
+
if (out.status !== 0) {
|
|
202
|
+
return {
|
|
203
|
+
changed: false,
|
|
204
|
+
type: 'windows',
|
|
205
|
+
reason: String(out.stderr || out.stdout || 'setx failed')
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return { changed: true, type: 'windows', reason: '' };
|
|
209
|
+
} catch (err) {
|
|
210
|
+
return { changed: false, type: 'windows', reason: String(err) };
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const profilePath = detectPosixProfile();
|
|
215
|
+
let content = '';
|
|
216
|
+
if (fs.existsSync(profilePath)) {
|
|
217
|
+
content = fs.readFileSync(profilePath, 'utf8');
|
|
218
|
+
}
|
|
219
|
+
const next = upsertExportLine(content, key, value);
|
|
220
|
+
if (next === content) {
|
|
221
|
+
return { changed: false, type: 'posix', path: profilePath };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (content) {
|
|
225
|
+
backup(profilePath);
|
|
226
|
+
}
|
|
227
|
+
ensureDir(path.dirname(profilePath));
|
|
228
|
+
fs.writeFileSync(profilePath, next, 'utf8');
|
|
229
|
+
return { changed: true, type: 'posix', path: profilePath };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function canUseArrowMenu() {
|
|
233
|
+
return Boolean(
|
|
234
|
+
process.stdin.isTTY
|
|
235
|
+
&& process.stdout.isTTY
|
|
236
|
+
&& typeof process.stdin.setRawMode === 'function'
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function askChoiceArrow(question, options, defaultIndex = 0) {
|
|
241
|
+
return new Promise((resolve, reject) => {
|
|
242
|
+
let index = defaultIndex;
|
|
243
|
+
|
|
244
|
+
const draw = () => {
|
|
245
|
+
readline.clearLine(process.stdout, 0);
|
|
246
|
+
readline.cursorTo(process.stdout, 0);
|
|
247
|
+
const label = `${index + 1}. ${options[index]}`;
|
|
248
|
+
const line = withColor(`> ${label}`, ANSI.cyan, ANSI.bold);
|
|
249
|
+
process.stdout.write(`${line} (↑/↓, Enter; 数字直接跳转)`);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const cleanup = () => {
|
|
253
|
+
process.stdin.off('keypress', onKeyPress);
|
|
254
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
|
|
255
|
+
process.stdin.setRawMode(false);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const onKeyPress = (str, key = {}) => {
|
|
260
|
+
if (key.ctrl && key.name === 'c') {
|
|
261
|
+
cleanup();
|
|
262
|
+
reject(new Error('aborted by user'));
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (key.name === 'up') {
|
|
266
|
+
index = (index - 1 + options.length) % options.length;
|
|
267
|
+
draw();
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (key.name === 'down') {
|
|
271
|
+
index = (index + 1) % options.length;
|
|
272
|
+
draw();
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
276
|
+
cleanup();
|
|
277
|
+
readline.clearLine(process.stdout, 0);
|
|
278
|
+
readline.cursorTo(process.stdout, 0);
|
|
279
|
+
process.stdout.write(`已选择: ${options[index]}\n`);
|
|
280
|
+
resolve(options[index]);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const n = Number(str);
|
|
284
|
+
if (Number.isInteger(n) && n >= 1 && n <= options.length) {
|
|
285
|
+
index = n - 1;
|
|
286
|
+
draw();
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
process.stdout.write(`\n${question}\n`);
|
|
292
|
+
readline.emitKeypressEvents(process.stdin);
|
|
293
|
+
process.stdin.setRawMode(true);
|
|
294
|
+
process.stdin.resume();
|
|
295
|
+
process.stdin.on('keypress', onKeyPress);
|
|
296
|
+
draw();
|
|
297
|
+
} catch (err) {
|
|
298
|
+
cleanup();
|
|
299
|
+
reject(err);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
async function askModel(rl, currentModel = DEFAULT_MODEL_ID) {
|
|
305
|
+
const defaultIndex = Math.max(0, MODEL_OPTIONS.indexOf(currentModel));
|
|
306
|
+
if (canUseArrowMenu()) {
|
|
307
|
+
return askChoiceArrow('1) 选择默认模型', MODEL_OPTIONS, defaultIndex);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
while (true) {
|
|
311
|
+
process.stdout.write('\n1) 选择默认模型\n');
|
|
312
|
+
MODEL_OPTIONS.forEach((m, idx) => {
|
|
313
|
+
const mark = idx === defaultIndex ? ' (default)' : '';
|
|
314
|
+
process.stdout.write(` ${idx + 1}. ${m}${mark}\n`);
|
|
315
|
+
});
|
|
316
|
+
const input = (await rl.question('请选择编号: ')).trim();
|
|
317
|
+
if (!input) return MODEL_OPTIONS[defaultIndex];
|
|
318
|
+
const n = Number(input);
|
|
319
|
+
if (Number.isInteger(n) && n >= 1 && n <= MODEL_OPTIONS.length) {
|
|
320
|
+
return MODEL_OPTIONS[n - 1];
|
|
321
|
+
}
|
|
322
|
+
process.stdout.write('输入无效,请重新输入。\n');
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function askKey(rl, currentKey) {
|
|
327
|
+
const masked = currentKey ? `${currentKey.slice(0, 6)}...${currentKey.slice(-4)}` : '未设置';
|
|
328
|
+
process.stdout.write(`\n2) 输入 ATools API Key(当前: ${masked})\n`);
|
|
329
|
+
process.stdout.write('留空表示保持当前 key\n');
|
|
330
|
+
const input = (await rl.question('请输入 key: ')).trim();
|
|
331
|
+
return input || currentKey || '';
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function askKeyWithGuard(rl, currentKey) {
|
|
335
|
+
let selectedKey = await askKey(rl, currentKey);
|
|
336
|
+
while (/^sk-or-v1-/i.test(selectedKey)) {
|
|
337
|
+
process.stdout.write(`\n${warn('检测到你输入的是 OpenRouter key(sk-or-v1- 开头),这不适用于 ATools /v1 模式。')}\n`);
|
|
338
|
+
const choice = (await rl.question('是否重新输入 ATools key? [Y/n]: ')).trim().toLowerCase();
|
|
339
|
+
if (choice === 'n') break;
|
|
340
|
+
selectedKey = await askKey(rl, selectedKey);
|
|
341
|
+
}
|
|
342
|
+
return selectedKey;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export async function runConfigCodex({ codexHome } = {}) {
|
|
346
|
+
const home = codexHome || process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
|
|
347
|
+
const configPath = path.join(home, 'config.toml');
|
|
348
|
+
|
|
349
|
+
const existing = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : '';
|
|
350
|
+
|
|
351
|
+
process.stdout.write(`${sectionTitle('Codex 配置')}:\n`);
|
|
352
|
+
process.stdout.write(`- Home: ${bold(home)}\n`);
|
|
353
|
+
process.stdout.write(`- Provider: ${bold(PROVIDER)}\n`);
|
|
354
|
+
process.stdout.write(`- Upstream Base URL: ${bold(UPSTREAM_BASE_URL)}\n`);
|
|
355
|
+
process.stdout.write(`- Env Var: ${bold(ENV_KEY)}\n`);
|
|
356
|
+
|
|
357
|
+
const initialKey = process.env[ENV_KEY] || process.env.SUB_OAI_KEY || '';
|
|
358
|
+
|
|
359
|
+
let selectedModel = DEFAULT_MODEL_ID;
|
|
360
|
+
let selectedKey = initialKey;
|
|
361
|
+
|
|
362
|
+
const rl = readlinePromises.createInterface({
|
|
363
|
+
input: process.stdin,
|
|
364
|
+
output: process.stdout
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
selectedModel = await askModel(rl, DEFAULT_MODEL_ID);
|
|
369
|
+
selectedKey = await askKeyWithGuard(rl, initialKey);
|
|
370
|
+
} finally {
|
|
371
|
+
rl.close();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const next = patchCodexConfigText(existing, { modelId: selectedModel });
|
|
375
|
+
|
|
376
|
+
ensureDir(path.dirname(configPath));
|
|
377
|
+
if (existing && next !== existing) {
|
|
378
|
+
const bak = backup(configPath);
|
|
379
|
+
if (bak) {
|
|
380
|
+
process.stdout.write(`${ok(`[atools-tool] backup created: ${bak}`)}\n`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (next !== existing) {
|
|
385
|
+
fs.writeFileSync(configPath, `${next.trimEnd()}\n`, 'utf8');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
let envResult = { changed: false, type: '', reason: '' };
|
|
389
|
+
if (selectedKey) {
|
|
390
|
+
envResult = persistEnvVar(ENV_KEY, selectedKey);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
process.stdout.write(`\n${sectionTitle('[atools-tool] Codex 配置结果')}:\n`);
|
|
394
|
+
process.stdout.write(`- Config: ${bold(configPath)}\n`);
|
|
395
|
+
process.stdout.write(`- model_provider = "${PROVIDER}"\n`);
|
|
396
|
+
process.stdout.write(`- model = "${selectedModel}"\n`);
|
|
397
|
+
process.stdout.write(`- base_url = "${UPSTREAM_BASE_URL}"\n`);
|
|
398
|
+
|
|
399
|
+
process.stdout.write(`\n${sectionTitle('Environment (持久化)')}:\n`);
|
|
400
|
+
if (!selectedKey) {
|
|
401
|
+
process.stdout.write(`${warn('- ATools key 未提供,未更新环境变量。')}\n`);
|
|
402
|
+
} else if (envResult.type === 'windows') {
|
|
403
|
+
if (envResult.changed) {
|
|
404
|
+
process.stdout.write(`${ok(`- Persisted ${ENV_KEY} to Windows user env (setx).`)}\n`);
|
|
405
|
+
process.stdout.write('- 请重新打开终端,使新的环境变量生效。\n');
|
|
406
|
+
} else {
|
|
407
|
+
process.stdout.write(`${warn(`- Failed to persist ${ENV_KEY} via setx: ${envResult.reason}`)}\n`);
|
|
408
|
+
}
|
|
409
|
+
} else if (envResult.type === 'posix') {
|
|
410
|
+
if (envResult.changed) {
|
|
411
|
+
process.stdout.write(`${ok(`- Persisted ${ENV_KEY} to shell profile: ${envResult.path}`)}\n`);
|
|
412
|
+
process.stdout.write('- 请重新打开终端,或手动 source 该文件以生效。\n');
|
|
413
|
+
} else {
|
|
414
|
+
process.stdout.write(`- ${ENV_KEY} 已在 profile 中存在(${envResult.path}),未做修改。\n`);
|
|
415
|
+
}
|
|
416
|
+
} else {
|
|
417
|
+
process.stdout.write(`- Env var not persisted (reason: ${envResult.reason || 'unknown'}).\n`);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (selectedKey) {
|
|
421
|
+
process.stdout.write(`\n${sectionTitle('Environment (当前会话手动生效命令)')}:\n`);
|
|
422
|
+
if (process.platform === 'win32') {
|
|
423
|
+
process.stdout.write(`- CMD: ${bold(`set ${ENV_KEY}=${selectedKey}`)}\n`);
|
|
424
|
+
process.stdout.write(`- PowerShell: ${bold(`$env:${ENV_KEY}="${selectedKey}"`)}\n`);
|
|
425
|
+
} else {
|
|
426
|
+
process.stdout.write(`- bash/zsh: ${bold(`export ${ENV_KEY}=${selectedKey}`)}\n`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
package/lib/config-openclaw.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
8
8
|
import readlinePromises from 'node:readline/promises';
|
|
9
9
|
import readline from 'node:readline';
|
|
10
10
|
|
|
11
|
-
const PROVIDER = '
|
|
11
|
+
const PROVIDER = 'atools';
|
|
12
12
|
const MODEL_OPTIONS = ['gpt-5.2', 'gpt-5.3-codex', 'gpt-5.4'];
|
|
13
13
|
const THINKING_OPTIONS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh'];
|
|
14
14
|
|
|
@@ -19,6 +19,38 @@ const PROXY_UPSTREAM = 'https://sub2api.atools.live';
|
|
|
19
19
|
const PROXY_LOG_FILE = path.join(os.tmpdir(), 'openclaw', 'atools-compat-proxy.log');
|
|
20
20
|
const PROXY_SERVICE = 'openclaw-atools-proxy.service';
|
|
21
21
|
|
|
22
|
+
const USE_COLOR = Boolean(process.stdout.isTTY && process.env.NO_COLOR !== '1');
|
|
23
|
+
const ANSI = {
|
|
24
|
+
reset: '\x1b[0m',
|
|
25
|
+
bold: '\x1b[1m',
|
|
26
|
+
cyan: '\x1b[36m',
|
|
27
|
+
green: '\x1b[32m',
|
|
28
|
+
yellow: '\x1b[33m'
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function withColor(text, ...codes) {
|
|
32
|
+
if (!USE_COLOR || !codes.length) return text;
|
|
33
|
+
const prefix = codes.join("");
|
|
34
|
+
return prefix + text + ANSI.reset;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function sectionTitle(text) {
|
|
38
|
+
return withColor(text, ANSI.cyan, ANSI.bold);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function ok(text) {
|
|
42
|
+
return withColor(text, ANSI.green);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function warn(text) {
|
|
46
|
+
return withColor(text, ANSI.yellow);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function bold(text) {
|
|
50
|
+
return withColor(text, ANSI.bold);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
22
54
|
function readJson(filePath) {
|
|
23
55
|
if (!fs.existsSync(filePath)) {
|
|
24
56
|
throw new Error(`missing file: ${filePath}`);
|
|
@@ -71,59 +103,64 @@ async function askChoiceNumber(rl, question, options, defaultIndex = 0) {
|
|
|
71
103
|
async function askChoiceArrow(question, options, defaultIndex = 0) {
|
|
72
104
|
return new Promise((resolve, reject) => {
|
|
73
105
|
let index = defaultIndex;
|
|
74
|
-
process.stdout.write(
|
|
75
|
-
|
|
106
|
+
process.stdout.write("\n" + question + "\n");
|
|
107
|
+
let firstDraw = true;
|
|
76
108
|
const draw = () => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
109
|
+
if (!firstDraw) {
|
|
110
|
+
readline.moveCursor(process.stdout, 0, -options.length);
|
|
111
|
+
} else {
|
|
112
|
+
firstDraw = false;
|
|
113
|
+
}
|
|
114
|
+
for (let i = 0; i < options.length; i += 1) {
|
|
115
|
+
readline.clearLine(process.stdout, 0);
|
|
116
|
+
const prefix = i === index ? '> ' : ' ';
|
|
117
|
+
const label = String(i + 1) + ". " + String(options[i]);
|
|
118
|
+
let line = prefix + label;
|
|
119
|
+
if (i === index) {
|
|
120
|
+
line = withColor(line, ANSI.cyan, ANSI.bold) + ' (↑/↓, Enter; 数字直接跳转)';
|
|
121
|
+
}
|
|
122
|
+
process.stdout.write(line + "\n");
|
|
123
|
+
}
|
|
80
124
|
};
|
|
81
|
-
|
|
82
125
|
const cleanup = () => {
|
|
83
|
-
process.stdin.off(
|
|
84
|
-
if (process.stdin.isTTY && typeof process.stdin.setRawMode ===
|
|
126
|
+
process.stdin.off("keypress", onKeyPress);
|
|
127
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
85
128
|
process.stdin.setRawMode(false);
|
|
86
129
|
}
|
|
87
130
|
};
|
|
88
|
-
|
|
89
131
|
const onKeyPress = (str, key = {}) => {
|
|
90
|
-
if (key.ctrl && key.name ===
|
|
132
|
+
if (key.ctrl && key.name === "c") {
|
|
91
133
|
cleanup();
|
|
92
|
-
reject(new Error(
|
|
134
|
+
reject(new Error("aborted by user"));
|
|
93
135
|
return;
|
|
94
136
|
}
|
|
95
|
-
|
|
96
|
-
if (key.name === 'up') {
|
|
137
|
+
if (key.name === "up") {
|
|
97
138
|
index = (index - 1 + options.length) % options.length;
|
|
98
139
|
draw();
|
|
99
140
|
return;
|
|
100
141
|
}
|
|
101
|
-
if (key.name ===
|
|
142
|
+
if (key.name === "down") {
|
|
102
143
|
index = (index + 1) % options.length;
|
|
103
144
|
draw();
|
|
104
145
|
return;
|
|
105
146
|
}
|
|
106
|
-
if (key.name ===
|
|
147
|
+
if (key.name === "return" || key.name === "enter") {
|
|
107
148
|
cleanup();
|
|
108
|
-
|
|
109
|
-
readline.cursorTo(process.stdout, 0);
|
|
110
|
-
process.stdout.write(`已选择: ${options[index]}\n`);
|
|
149
|
+
process.stdout.write("已选择: " + String(options[index]) + "\n");
|
|
111
150
|
resolve(options[index]);
|
|
112
151
|
return;
|
|
113
152
|
}
|
|
114
|
-
|
|
115
153
|
const n = Number(str);
|
|
116
154
|
if (Number.isInteger(n) && n >= 1 && n <= options.length) {
|
|
117
155
|
index = n - 1;
|
|
118
156
|
draw();
|
|
119
157
|
}
|
|
120
158
|
};
|
|
121
|
-
|
|
122
159
|
try {
|
|
123
160
|
readline.emitKeypressEvents(process.stdin);
|
|
124
161
|
process.stdin.setRawMode(true);
|
|
125
162
|
process.stdin.resume();
|
|
126
|
-
process.stdin.on(
|
|
163
|
+
process.stdin.on("keypress", onKeyPress);
|
|
127
164
|
draw();
|
|
128
165
|
} catch (err) {
|
|
129
166
|
cleanup();
|
|
@@ -207,7 +244,7 @@ async function askKey(rl, currentKey) {
|
|
|
207
244
|
async function askKeyWithGuard(rl, currentKey) {
|
|
208
245
|
let selectedKey = await askKey(rl, currentKey);
|
|
209
246
|
while (/^sk-or-v1-/i.test(selectedKey)) {
|
|
210
|
-
process.stdout.write('\n检测到你输入的是 OpenRouter key(sk-or-v1- 开头),这不适用于
|
|
247
|
+
process.stdout.write('\n' + warn('检测到你输入的是 OpenRouter key(sk-or-v1- 开头),这不适用于 ATools /v1 模式。') + '\n');
|
|
211
248
|
const choice = await askChoice(rl, '是否重新输入 key?', ['重新输入 key(推荐)', '继续使用当前 key'], 0);
|
|
212
249
|
if (choice === '继续使用当前 key') break;
|
|
213
250
|
selectedKey = await askKey(rl, selectedKey);
|
|
@@ -296,9 +333,22 @@ function isLinuxProxyServiceActive() {
|
|
|
296
333
|
|
|
297
334
|
function startLinuxProxyService() {
|
|
298
335
|
const reload = runSystemctl(['daemon-reload']);
|
|
299
|
-
if (reload.status !== 0)
|
|
336
|
+
if (reload.status !== 0) {
|
|
337
|
+
return {
|
|
338
|
+
ok: false,
|
|
339
|
+
step: 'daemon-reload',
|
|
340
|
+
detail: String(reload.stderr || reload.stdout || '').trim()
|
|
341
|
+
};
|
|
342
|
+
}
|
|
300
343
|
const start = runSystemctl(['enable', '--now', PROXY_SERVICE]);
|
|
301
|
-
|
|
344
|
+
if (start.status !== 0) {
|
|
345
|
+
return {
|
|
346
|
+
ok: false,
|
|
347
|
+
step: 'enable-start',
|
|
348
|
+
detail: String(start.stderr || start.stdout || '').trim()
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
return { ok: true, step: 'enable-start', detail: '' };
|
|
302
352
|
}
|
|
303
353
|
|
|
304
354
|
function startDetachedProxy() {
|
|
@@ -350,6 +400,31 @@ async function isPortOpen(host, port, timeoutMs = 900) {
|
|
|
350
400
|
});
|
|
351
401
|
}
|
|
352
402
|
|
|
403
|
+
function buildProxyTroubleshootingHints({ linuxSystemdError } = {}) {
|
|
404
|
+
const hints = [];
|
|
405
|
+
const err = String(linuxSystemdError || '');
|
|
406
|
+
|
|
407
|
+
if (/Failed to connect to bus|No medium found/i.test(err)) {
|
|
408
|
+
hints.push('当前环境没有可用的 systemd 用户会话(常见于容器、root、无登录会话)。');
|
|
409
|
+
hints.push('建议:直接重新执行 `atools-tool install-proxy`(或兼容别名 `atools-tool-install`),新版会自动降级到非-systemd进程模式。');
|
|
410
|
+
hints.push('如果是容器环境,请确保进程管理策略允许后台常驻进程。');
|
|
411
|
+
return hints;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (/permission denied|access denied/i.test(err)) {
|
|
415
|
+
hints.push('权限不足,无法通过 systemd --user 启动服务。');
|
|
416
|
+
hints.push('建议:使用普通登录用户执行安装与配置,不要混用 root 与普通用户的 OpenClaw 目录。');
|
|
417
|
+
return hints;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (err) {
|
|
421
|
+
hints.push(`systemd 启动失败:${err}`);
|
|
422
|
+
}
|
|
423
|
+
hints.push('建议:检查 OpenClaw Home 路径与 Node 可执行路径是否存在且可访问。');
|
|
424
|
+
hints.push(`建议:确认端口 ${PROXY_PORT} 未被防火墙或策略阻断。`);
|
|
425
|
+
return hints;
|
|
426
|
+
}
|
|
427
|
+
|
|
353
428
|
async function ensureProxyReady() {
|
|
354
429
|
if (process.platform === 'linux') {
|
|
355
430
|
const service = configureLinuxProxyService();
|
|
@@ -360,13 +435,13 @@ async function ensureProxyReady() {
|
|
|
360
435
|
}
|
|
361
436
|
}
|
|
362
437
|
|
|
363
|
-
if (await isPortOpen(PROXY_HOST, PROXY_PORT)) return true;
|
|
438
|
+
if (await isPortOpen(PROXY_HOST, PROXY_PORT)) return { ok: true, mode: 'already-running' };
|
|
364
439
|
|
|
365
440
|
// Linux retry: service may exist but not running.
|
|
366
441
|
if (process.platform === 'linux') {
|
|
367
442
|
const service = configureLinuxProxyService();
|
|
368
443
|
if (service.exists && isLinuxProxyServiceActive()) {
|
|
369
|
-
if (await isPortOpen(PROXY_HOST, PROXY_PORT)) return true;
|
|
444
|
+
if (await isPortOpen(PROXY_HOST, PROXY_PORT)) return { ok: true, mode: 'already-running' };
|
|
370
445
|
}
|
|
371
446
|
}
|
|
372
447
|
|
|
@@ -388,26 +463,45 @@ async function ensureProxyReady() {
|
|
|
388
463
|
}
|
|
389
464
|
|
|
390
465
|
if (action === '取消并退出') {
|
|
391
|
-
return false;
|
|
466
|
+
return { ok: false, reason: 'user-cancelled', hints: [] };
|
|
392
467
|
}
|
|
393
468
|
|
|
394
469
|
let started = false;
|
|
470
|
+
let linuxSystemdError = '';
|
|
395
471
|
if (process.platform === 'linux') {
|
|
396
|
-
|
|
472
|
+
const systemdStart = startLinuxProxyService();
|
|
473
|
+
started = systemdStart.ok;
|
|
474
|
+
linuxSystemdError = systemdStart.detail || '';
|
|
397
475
|
}
|
|
476
|
+
let mode = 'systemd';
|
|
398
477
|
if (!started) {
|
|
399
478
|
started = startDetachedProxy();
|
|
479
|
+
mode = 'detached';
|
|
400
480
|
}
|
|
401
481
|
|
|
402
|
-
if (!started)
|
|
482
|
+
if (!started) {
|
|
483
|
+
return {
|
|
484
|
+
ok: false,
|
|
485
|
+
reason: 'start-failed',
|
|
486
|
+
hints: buildProxyTroubleshootingHints({ linuxSystemdError })
|
|
487
|
+
};
|
|
488
|
+
}
|
|
403
489
|
|
|
404
490
|
for (let i = 0; i < 5; i += 1) {
|
|
405
491
|
await sleep(500);
|
|
406
492
|
if (await isPortOpen(PROXY_HOST, PROXY_PORT)) {
|
|
407
|
-
return true;
|
|
493
|
+
return { ok: true, mode };
|
|
408
494
|
}
|
|
409
495
|
}
|
|
410
|
-
return
|
|
496
|
+
return {
|
|
497
|
+
ok: false,
|
|
498
|
+
reason: 'port-not-open',
|
|
499
|
+
hints: [
|
|
500
|
+
'代理进程启动后端口仍未就绪。',
|
|
501
|
+
`请检查日志:${PROXY_LOG_FILE}`,
|
|
502
|
+
'如为受限环境(容器/最小系统),请确认允许本地监听 127.0.0.1。'
|
|
503
|
+
]
|
|
504
|
+
};
|
|
411
505
|
}
|
|
412
506
|
|
|
413
507
|
function printRestartHint() {
|
|
@@ -438,15 +532,23 @@ export async function runConfigOpenclaw({ openclawHome } = {}) {
|
|
|
438
532
|
let rl = null;
|
|
439
533
|
|
|
440
534
|
try {
|
|
441
|
-
process.stdout.write(
|
|
442
|
-
process.stdout.write(
|
|
443
|
-
process.stdout.write(
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
535
|
+
process.stdout.write(sectionTitle('OpenClaw / ATools 配置') + ':\n');
|
|
536
|
+
process.stdout.write('- OpenClaw Home: ' + bold(home) + '\n');
|
|
537
|
+
process.stdout.write('- 固定代理目标: ' + bold(PROXY_UPSTREAM) + '\n');
|
|
538
|
+
process.stdout.write('- 固定 Provider Base URL: ' + bold(PROXY_BASE_URL) + '\n');
|
|
539
|
+
|
|
540
|
+
const proxyStatus = await ensureProxyReady();
|
|
541
|
+
if (!proxyStatus.ok) {
|
|
542
|
+
process.stdout.write('\n本地代理未就绪,未修改 OpenClaw 模型配置。\n');
|
|
543
|
+
if (Array.isArray(proxyStatus.hints) && proxyStatus.hints.length > 0) {
|
|
544
|
+
process.stdout.write('可行解决方案:\n');
|
|
545
|
+
proxyStatus.hints.forEach((hint) => process.stdout.write(`- ${hint}\n`));
|
|
546
|
+
}
|
|
448
547
|
return;
|
|
449
548
|
}
|
|
549
|
+
if (proxyStatus.mode === 'detached') {
|
|
550
|
+
process.stdout.write('\n已通过非-systemd模式启动代理(兼容无 user bus 环境)。\n');
|
|
551
|
+
}
|
|
450
552
|
rl = readlinePromises.createInterface({
|
|
451
553
|
input: process.stdin,
|
|
452
554
|
output: process.stdout
|
|
@@ -457,10 +559,13 @@ export async function runConfigOpenclaw({ openclawHome } = {}) {
|
|
|
457
559
|
const selectedThinking = await askChoice(rl, '3) 思考强度', THINKING_OPTIONS, 2);
|
|
458
560
|
const reasoning = reasoningSwitch === 'on';
|
|
459
561
|
const currentKey =
|
|
460
|
-
openclawCfg?.models?.providers?.[PROVIDER]?.apiKey
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
562
|
+
openclawCfg?.models?.providers?.[PROVIDER]?.apiKey
|
|
563
|
+
|| openclawCfg?.models?.providers?.sub2api?.apiKey
|
|
564
|
+
|| modelsCfg?.providers?.[PROVIDER]?.apiKey
|
|
565
|
+
|| modelsCfg?.providers?.sub2api?.apiKey
|
|
566
|
+
|| authCfg?.profiles?.[`${PROVIDER}:default`]?.key
|
|
567
|
+
|| authCfg?.profiles?.['sub2api:default']?.key
|
|
568
|
+
|| '';
|
|
464
569
|
const selectedKey = await askKeyWithGuard(rl, currentKey);
|
|
465
570
|
|
|
466
571
|
openclawCfg.models = openclawCfg.models || {};
|
|
@@ -489,8 +594,10 @@ export async function runConfigOpenclaw({ openclawHome } = {}) {
|
|
|
489
594
|
if (Array.isArray(openclawCfg.agents.list)) {
|
|
490
595
|
openclawCfg.agents.list = openclawCfg.agents.list.map((agent) => {
|
|
491
596
|
if (!agent || typeof agent !== 'object') return agent;
|
|
492
|
-
if (typeof agent.model === 'string'
|
|
493
|
-
|
|
597
|
+
if (typeof agent.model === 'string') {
|
|
598
|
+
if (agent.model.startsWith(`${PROVIDER}/`) || agent.model.startsWith('sub2api/')) {
|
|
599
|
+
return { ...agent, model: `${PROVIDER}/${selectedModel}` };
|
|
600
|
+
}
|
|
494
601
|
}
|
|
495
602
|
return agent;
|
|
496
603
|
});
|
package/lib/install.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
|
-
import { execFileSync } from 'node:child_process';
|
|
4
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
|
|
7
|
-
const DEFAULT_PROVIDER = '
|
|
7
|
+
const DEFAULT_PROVIDER = 'atools';
|
|
8
8
|
const DEFAULT_MODEL = 'gpt-5.3-codex';
|
|
9
9
|
const DEFAULT_FORCE_MODEL = '';
|
|
10
10
|
const DEFAULT_PORT = 18888;
|
|
@@ -95,7 +95,7 @@ function parseArgs(argv) {
|
|
|
95
95
|
maxReqBytes: 68000,
|
|
96
96
|
logFile: DEFAULT_LOG_FILE,
|
|
97
97
|
serviceName: DEFAULT_SERVICE_NAME,
|
|
98
|
-
apiKey: process.env.SUB_OAI_KEY || '',
|
|
98
|
+
apiKey: process.env.ATOOLS_OAI_KEY || process.env.SUB_OAI_KEY || '',
|
|
99
99
|
dryRun: false,
|
|
100
100
|
noRestart: false
|
|
101
101
|
};
|
|
@@ -193,7 +193,7 @@ function detectApiKey(openclawHome, explicitKey) {
|
|
|
193
193
|
if (explicitKey) return explicitKey;
|
|
194
194
|
const authPath = path.join(openclawHome, 'agents/main/agent/auth-profiles.json');
|
|
195
195
|
const auth = readJsonIfExists(authPath);
|
|
196
|
-
return auth?.profiles?.['sub2api:default']?.key || '';
|
|
196
|
+
return (auth?.profiles?.['atools:default']?.key || auth?.profiles?.['sub2api:default']?.key || '');
|
|
197
197
|
}
|
|
198
198
|
|
|
199
199
|
function patchOpenclawConfig(openclawHome, provider, model, port, dryRun) {
|
|
@@ -350,7 +350,56 @@ function runSystemctl(args, { dryRun } = {}) {
|
|
|
350
350
|
execFileSync('systemctl', ['--user', ...args], { stdio: 'inherit' });
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
-
function
|
|
353
|
+
function printSystemdFallbackHints(detail) {
|
|
354
|
+
const text = String(detail || '');
|
|
355
|
+
if (/Failed to connect to bus|No medium found/i.test(text)) {
|
|
356
|
+
info('detected: no systemd user bus in current environment');
|
|
357
|
+
info('hint: this is common in containers/root/non-login sessions');
|
|
358
|
+
info('hint: installer will continue with detached process mode');
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (/permission denied|access denied/i.test(text)) {
|
|
362
|
+
info('detected: permission issue for systemd --user');
|
|
363
|
+
info('hint: run install under the same normal user as OpenClaw');
|
|
364
|
+
info('hint: installer will continue with detached process mode');
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
info('detected: systemd --user is not usable in current environment');
|
|
368
|
+
info('hint: installer will continue with detached process mode');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function startDetachedProxyProcess({ nodeBin, cliPath, serveArgs, dryRun }) {
|
|
372
|
+
if (dryRun) {
|
|
373
|
+
info(`dry-run: detached start ${[nodeBin, cliPath, ...serveArgs].join(' ')}`);
|
|
374
|
+
return true;
|
|
375
|
+
}
|
|
376
|
+
try {
|
|
377
|
+
const child = spawn(nodeBin, [cliPath, ...serveArgs], {
|
|
378
|
+
detached: true,
|
|
379
|
+
stdio: 'ignore',
|
|
380
|
+
cwd: os.homedir(),
|
|
381
|
+
env: {
|
|
382
|
+
...process.env,
|
|
383
|
+
HOME: process.env.HOME || os.homedir(),
|
|
384
|
+
TMPDIR: process.env.TMPDIR || '/tmp',
|
|
385
|
+
SUB2API_COMPAT_USER_AGENT: 'codex_cli_rs/0.101.0 (Ubuntu 24.4.0; x86_64) WindowsTerminal'
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
child.unref();
|
|
389
|
+
return true;
|
|
390
|
+
} catch {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function installLinuxService({
|
|
396
|
+
serviceName,
|
|
397
|
+
content,
|
|
398
|
+
nodeBin,
|
|
399
|
+
cliPath,
|
|
400
|
+
serveArgs,
|
|
401
|
+
dryRun
|
|
402
|
+
}) {
|
|
354
403
|
const serviceDir = path.join(os.homedir(), '.config/systemd/user');
|
|
355
404
|
const servicePath = path.join(serviceDir, serviceName);
|
|
356
405
|
ensureDir(serviceDir);
|
|
@@ -366,8 +415,22 @@ function installLinuxService({ serviceName, content, dryRun }) {
|
|
|
366
415
|
info(`wrote: ${servicePath}`);
|
|
367
416
|
}
|
|
368
417
|
|
|
369
|
-
|
|
370
|
-
|
|
418
|
+
try {
|
|
419
|
+
runSystemctl(['daemon-reload'], { dryRun });
|
|
420
|
+
runSystemctl(['enable', '--now', serviceName], { dryRun });
|
|
421
|
+
info(`service started with systemd --user: ${serviceName}`);
|
|
422
|
+
return;
|
|
423
|
+
} catch (err) {
|
|
424
|
+
const detail = String(err?.message || err);
|
|
425
|
+
info(`systemd --user unavailable, fallback to detached process (${detail})`);
|
|
426
|
+
printSystemdFallbackHints(detail);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const started = startDetachedProxyProcess({ nodeBin, cliPath, serveArgs, dryRun });
|
|
430
|
+
if (!started) {
|
|
431
|
+
throw new Error(`failed to start proxy with both systemd --user and detached mode`);
|
|
432
|
+
}
|
|
433
|
+
info('proxy started in detached mode (non-systemd fallback)');
|
|
371
434
|
}
|
|
372
435
|
|
|
373
436
|
function runLaunchctl(args, { dryRun, allowFailure = false } = {}) {
|
|
@@ -444,7 +507,14 @@ function installService({
|
|
|
444
507
|
dryRun
|
|
445
508
|
}) {
|
|
446
509
|
if (process.platform === 'linux') {
|
|
447
|
-
installLinuxService({
|
|
510
|
+
installLinuxService({
|
|
511
|
+
serviceName,
|
|
512
|
+
content: linuxServiceContent,
|
|
513
|
+
nodeBin,
|
|
514
|
+
cliPath,
|
|
515
|
+
serveArgs,
|
|
516
|
+
dryRun
|
|
517
|
+
});
|
|
448
518
|
return;
|
|
449
519
|
}
|
|
450
520
|
if (process.platform === 'darwin') {
|
|
@@ -477,14 +547,14 @@ function restartOpenclawGateway({ noRestart, dryRun }) {
|
|
|
477
547
|
}
|
|
478
548
|
|
|
479
549
|
function printHelp() {
|
|
480
|
-
process.stdout.write(`atools-tool-
|
|
550
|
+
process.stdout.write(`atools-tool install-proxy
|
|
481
551
|
|
|
482
552
|
Usage:
|
|
483
|
-
atools-tool-
|
|
553
|
+
atools-tool install-proxy [options]
|
|
484
554
|
|
|
485
555
|
Options:
|
|
486
556
|
--openclaw-home <path> Default: ~/.openclaw
|
|
487
|
-
--provider <name> Default:
|
|
557
|
+
--provider <name> Default: atools
|
|
488
558
|
--model <id> Default: gpt-5.3-codex
|
|
489
559
|
--force-model <id> Default: disabled (optional hard rewrite)
|
|
490
560
|
--port <number> Default: 18888
|
|
@@ -492,7 +562,7 @@ Options:
|
|
|
492
562
|
--max-req-bytes <number> Default: 68000
|
|
493
563
|
--log-file <path> Default: <tmp>/openclaw/atools-compat-proxy.log
|
|
494
564
|
--service-name <name> Linux: *.service; macOS/Windows: service id/task name
|
|
495
|
-
--api-key <key> Prefer explicit key; otherwise use SUB_OAI_KEY or auth-profiles
|
|
565
|
+
--api-key <key> Prefer explicit key; otherwise use ATOOLS_OAI_KEY (or legacy SUB_OAI_KEY) or auth-profiles
|
|
496
566
|
--no-restart Do not restart openclaw-gateway.service
|
|
497
567
|
--dry-run Print actions without changing files
|
|
498
568
|
-h, --help Show help
|