@autotask/atools-tool 0.1.2 → 0.1.4
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 +42 -8
- package/bin/cli.mjs +5 -11
- package/lib/config-openclaw.mjs +381 -10
- package/lib/install.mjs +195 -47
- package/lib/proxy-server.mjs +88 -57
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -15,20 +15,45 @@ atools-tool <command>
|
|
|
15
15
|
Available command:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
atools-tool
|
|
18
|
+
atools-tool openclaw
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
Interactive flow:
|
|
22
22
|
|
|
23
|
-
1.
|
|
24
|
-
2.
|
|
25
|
-
3. Configure
|
|
26
|
-
4.
|
|
23
|
+
1. Check local proxy status (`127.0.0.1:18888`); if not running, ask whether to start it first
|
|
24
|
+
2. Select default model: `gpt-5.2` / `gpt-5.3-codex` / `gpt-5.4`
|
|
25
|
+
3. Configure reasoning: `off` / `on`
|
|
26
|
+
4. Configure thinking strength: `off|minimal|low|medium|high|xhigh`
|
|
27
|
+
5. Input API key (leave empty to keep current key)
|
|
28
|
+
|
|
29
|
+
Selection UX:
|
|
30
|
+
|
|
31
|
+
- TTY terminal: use `↑/↓` and `Enter`
|
|
32
|
+
- Non-TTY: falls back to number input
|
|
33
|
+
|
|
34
|
+
Rollback to codex-compatible defaults:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
atools-tool openclaw
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Recommended selections:
|
|
41
|
+
|
|
42
|
+
1. `gpt-5.3-codex`
|
|
43
|
+
2. `off`
|
|
44
|
+
3. `low`
|
|
45
|
+
4. Input your sub2api-compatible key
|
|
27
46
|
|
|
28
47
|
## Install (npm)
|
|
29
48
|
|
|
30
49
|
```bash
|
|
31
|
-
npm i -g atools-tool
|
|
50
|
+
npm i -g @autotask/atools-tool
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Default full install (recommended):
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm i -g @autotask/atools-tool && atools-tool-install && atools-tool openclaw
|
|
32
57
|
```
|
|
33
58
|
|
|
34
59
|
Optional installer command:
|
|
@@ -37,6 +62,12 @@ Optional installer command:
|
|
|
37
62
|
atools-tool-install --help
|
|
38
63
|
```
|
|
39
64
|
|
|
65
|
+
`atools-tool-install` service behavior by platform:
|
|
66
|
+
|
|
67
|
+
- Linux: systemd user service
|
|
68
|
+
- macOS: launchd user agent (`~/Library/LaunchAgents`)
|
|
69
|
+
- Windows: Task Scheduler user task (ONLOGON + start now)
|
|
70
|
+
|
|
40
71
|
## Proxy Command
|
|
41
72
|
|
|
42
73
|
```bash
|
|
@@ -62,8 +93,11 @@ curl -fsSL https://raw.githubusercontent.com/aak1247/sub2api-openclaw-proxy/main
|
|
|
62
93
|
- patches `~/.openclaw/openclaw.json`
|
|
63
94
|
- patches `~/.openclaw/agents/main/agent/models.json`
|
|
64
95
|
- patches `~/.openclaw/agents/main/agent/auth-profiles.json` (if API key is available)
|
|
65
|
-
-
|
|
66
|
-
-
|
|
96
|
+
- service install by platform:
|
|
97
|
+
- Linux: creates/enables `~/.config/systemd/user/openclaw-atools-proxy.service`
|
|
98
|
+
- macOS: creates/enables `~/Library/LaunchAgents/openclaw-atools-proxy.plist`
|
|
99
|
+
- Windows: creates/updates scheduled task `openclaw-atools-proxy`
|
|
100
|
+
- restarts `openclaw-gateway.service` only on Linux (unless `--no-restart`)
|
|
67
101
|
|
|
68
102
|
Default proxy log:
|
|
69
103
|
|
package/bin/cli.mjs
CHANGED
|
@@ -8,8 +8,6 @@ function parseArgs(argv) {
|
|
|
8
8
|
openclawHome: undefined,
|
|
9
9
|
port: undefined,
|
|
10
10
|
upstream: undefined,
|
|
11
|
-
fallbackUpstream: undefined,
|
|
12
|
-
fallbackApiKey: undefined,
|
|
13
11
|
maxReqBytes: undefined,
|
|
14
12
|
logFile: undefined,
|
|
15
13
|
retryMax: undefined,
|
|
@@ -27,8 +25,6 @@ function parseArgs(argv) {
|
|
|
27
25
|
if (a === '--port') out.port = Number(next());
|
|
28
26
|
else if (a === '--openclaw-home') out.openclawHome = next();
|
|
29
27
|
else if (a === '--upstream') out.upstream = next();
|
|
30
|
-
else if (a === '--fallback-upstream') out.fallbackUpstream = next();
|
|
31
|
-
else if (a === '--fallback-api-key') out.fallbackApiKey = next();
|
|
32
28
|
else if (a === '--max-req-bytes') out.maxReqBytes = Number(next());
|
|
33
29
|
else if (a === '--log-file') out.logFile = next();
|
|
34
30
|
else if (a === '--retry-max') out.retryMax = Number(next());
|
|
@@ -47,14 +43,13 @@ Usage:
|
|
|
47
43
|
|
|
48
44
|
Commands:
|
|
49
45
|
serve Run proxy service
|
|
50
|
-
|
|
46
|
+
openclaw Interactive OpenClaw setup (sub2api proxy check + model/reasoning/thinking/key)
|
|
47
|
+
config-openclaw Alias of 'openclaw'
|
|
51
48
|
|
|
52
49
|
Options:
|
|
53
50
|
--openclaw-home <path> OpenClaw home dir (default: ~/.openclaw)
|
|
54
51
|
--port <number> Listen port (default: 18888)
|
|
55
52
|
--upstream <url> Upstream base URL (default: https://sub2api.atools.live)
|
|
56
|
-
--fallback-upstream <url> Fallback base URL on primary 5xx
|
|
57
|
-
--fallback-api-key <key> Override Bearer key for fallback upstream
|
|
58
53
|
--max-req-bytes <number> Compact requests larger than this threshold (default: 68000)
|
|
59
54
|
--log-file <path> Log file (default: /tmp/openclaw/atools-compat-proxy.log)
|
|
60
55
|
--retry-max <number> Retry attempts on 5xx for /responses (default: 6)
|
|
@@ -71,7 +66,7 @@ async function main() {
|
|
|
71
66
|
return;
|
|
72
67
|
}
|
|
73
68
|
|
|
74
|
-
if (args.cmd === 'config-openclaw') {
|
|
69
|
+
if (args.cmd === 'config-openclaw' || args.cmd === 'openclaw') {
|
|
75
70
|
await runConfigOpenclaw({ openclawHome: args.openclawHome });
|
|
76
71
|
return;
|
|
77
72
|
}
|
|
@@ -80,9 +75,8 @@ async function main() {
|
|
|
80
75
|
throw new Error(`unsupported command: ${args.cmd}`);
|
|
81
76
|
}
|
|
82
77
|
|
|
83
|
-
const { port, upstream,
|
|
84
|
-
|
|
85
|
-
process.stdout.write(`atools-tool listening on http://127.0.0.1:${port}, upstream=${upstream}${fallbackNote}, log=${logFile}\n`);
|
|
78
|
+
const { port, upstream, logFile } = await createProxyServer(args);
|
|
79
|
+
process.stdout.write(`atools-tool listening on http://127.0.0.1:${port}, upstream=${upstream}, log=${logFile}\n`);
|
|
86
80
|
}
|
|
87
81
|
|
|
88
82
|
main().catch((err) => {
|
package/lib/config-openclaw.mjs
CHANGED
|
@@ -2,12 +2,23 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import process from 'node:process';
|
|
5
|
-
import
|
|
5
|
+
import net from 'node:net';
|
|
6
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import readlinePromises from 'node:readline/promises';
|
|
9
|
+
import readline from 'node:readline';
|
|
6
10
|
|
|
7
11
|
const PROVIDER = 'sub2api';
|
|
8
12
|
const MODEL_OPTIONS = ['gpt-5.2', 'gpt-5.3-codex', 'gpt-5.4'];
|
|
9
13
|
const THINKING_OPTIONS = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh'];
|
|
10
14
|
|
|
15
|
+
const PROXY_HOST = '127.0.0.1';
|
|
16
|
+
const PROXY_PORT = 18888;
|
|
17
|
+
const PROXY_BASE_URL = `http://${PROXY_HOST}:${PROXY_PORT}/v1`;
|
|
18
|
+
const PROXY_UPSTREAM = 'https://sub2api.atools.live';
|
|
19
|
+
const PROXY_LOG_FILE = path.join(os.tmpdir(), 'openclaw', 'atools-compat-proxy.log');
|
|
20
|
+
const PROXY_SERVICE = 'openclaw-atools-proxy.service';
|
|
21
|
+
|
|
11
22
|
function readJson(filePath) {
|
|
12
23
|
if (!fs.existsSync(filePath)) {
|
|
13
24
|
throw new Error(`missing file: ${filePath}`);
|
|
@@ -28,7 +39,19 @@ function backup(filePath) {
|
|
|
28
39
|
fs.copyFileSync(filePath, `${filePath}.bak.${stamp}`);
|
|
29
40
|
}
|
|
30
41
|
|
|
31
|
-
|
|
42
|
+
function sleep(ms) {
|
|
43
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function canUseArrowMenu() {
|
|
47
|
+
return Boolean(
|
|
48
|
+
process.stdin.isTTY
|
|
49
|
+
&& process.stdout.isTTY
|
|
50
|
+
&& typeof process.stdin.setRawMode === 'function'
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function askChoiceNumber(rl, question, options, defaultIndex = 0) {
|
|
32
55
|
while (true) {
|
|
33
56
|
process.stdout.write(`\n${question}\n`);
|
|
34
57
|
options.forEach((item, idx) => {
|
|
@@ -45,14 +68,153 @@ async function askChoice(rl, question, options, defaultIndex = 0) {
|
|
|
45
68
|
}
|
|
46
69
|
}
|
|
47
70
|
|
|
71
|
+
async function askChoiceArrow(question, options, defaultIndex = 0) {
|
|
72
|
+
return new Promise((resolve, reject) => {
|
|
73
|
+
let index = defaultIndex;
|
|
74
|
+
process.stdout.write(`\n${question}\n`);
|
|
75
|
+
|
|
76
|
+
const draw = () => {
|
|
77
|
+
readline.clearLine(process.stdout, 0);
|
|
78
|
+
readline.cursorTo(process.stdout, 0);
|
|
79
|
+
process.stdout.write(`> ${options[index]} (↑/↓, Enter)`);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const cleanup = () => {
|
|
83
|
+
process.stdin.off('keypress', onKeyPress);
|
|
84
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
|
|
85
|
+
process.stdin.setRawMode(false);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const onKeyPress = (str, key = {}) => {
|
|
90
|
+
if (key.ctrl && key.name === 'c') {
|
|
91
|
+
cleanup();
|
|
92
|
+
reject(new Error('aborted by user'));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (key.name === 'up') {
|
|
97
|
+
index = (index - 1 + options.length) % options.length;
|
|
98
|
+
draw();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (key.name === 'down') {
|
|
102
|
+
index = (index + 1) % options.length;
|
|
103
|
+
draw();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
107
|
+
cleanup();
|
|
108
|
+
readline.clearLine(process.stdout, 0);
|
|
109
|
+
readline.cursorTo(process.stdout, 0);
|
|
110
|
+
process.stdout.write(`已选择: ${options[index]}\n`);
|
|
111
|
+
resolve(options[index]);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const n = Number(str);
|
|
116
|
+
if (Number.isInteger(n) && n >= 1 && n <= options.length) {
|
|
117
|
+
index = n - 1;
|
|
118
|
+
draw();
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
readline.emitKeypressEvents(process.stdin);
|
|
124
|
+
process.stdin.setRawMode(true);
|
|
125
|
+
process.stdin.resume();
|
|
126
|
+
process.stdin.on('keypress', onKeyPress);
|
|
127
|
+
draw();
|
|
128
|
+
} catch (err) {
|
|
129
|
+
cleanup();
|
|
130
|
+
reject(err);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function askChoice(rl, question, options, defaultIndex = 0) {
|
|
136
|
+
if (!canUseArrowMenu()) {
|
|
137
|
+
return askChoiceNumber(rl, question, options, defaultIndex);
|
|
138
|
+
}
|
|
139
|
+
return askChoiceArrow(question, options, defaultIndex);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function askInputArrow(defaultValue = '') {
|
|
143
|
+
return new Promise((resolve, reject) => {
|
|
144
|
+
let value = '';
|
|
145
|
+
process.stdout.write('请输入 key: ');
|
|
146
|
+
|
|
147
|
+
const draw = () => {
|
|
148
|
+
readline.clearLine(process.stdout, 1);
|
|
149
|
+
readline.cursorTo(process.stdout, 10);
|
|
150
|
+
process.stdout.write(value);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const cleanup = () => {
|
|
154
|
+
process.stdin.off('keypress', onKeyPress);
|
|
155
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === 'function') {
|
|
156
|
+
process.stdin.setRawMode(false);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const onKeyPress = (str, key = {}) => {
|
|
161
|
+
if (key.ctrl && key.name === 'c') {
|
|
162
|
+
cleanup();
|
|
163
|
+
reject(new Error('aborted by user'));
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (key.name === 'return' || key.name === 'enter') {
|
|
167
|
+
cleanup();
|
|
168
|
+
process.stdout.write('\n');
|
|
169
|
+
resolve(value.trim() || defaultValue || '');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (key.name === 'backspace') {
|
|
173
|
+
value = value.slice(0, -1);
|
|
174
|
+
draw();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (!key.ctrl && !key.meta && typeof str === 'string' && str.length > 0) {
|
|
178
|
+
value += str;
|
|
179
|
+
draw();
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
readline.emitKeypressEvents(process.stdin);
|
|
185
|
+
process.stdin.setRawMode(true);
|
|
186
|
+
process.stdin.resume();
|
|
187
|
+
process.stdin.on('keypress', onKeyPress);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
cleanup();
|
|
190
|
+
reject(err);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
48
195
|
async function askKey(rl, currentKey) {
|
|
49
196
|
const masked = currentKey ? `${currentKey.slice(0, 6)}...${currentKey.slice(-4)}` : '未设置';
|
|
50
|
-
|
|
197
|
+
const question = `4) 输入 API Key(当前: ${masked})`;
|
|
198
|
+
process.stdout.write(`\n${question}\n`);
|
|
51
199
|
process.stdout.write('留空表示保持当前 key\n');
|
|
200
|
+
if (canUseArrowMenu()) {
|
|
201
|
+
return askInputArrow(currentKey);
|
|
202
|
+
}
|
|
52
203
|
const input = (await rl.question('请输入 key: ')).trim();
|
|
53
204
|
return input || currentKey || '';
|
|
54
205
|
}
|
|
55
206
|
|
|
207
|
+
async function askKeyWithGuard(rl, currentKey) {
|
|
208
|
+
let selectedKey = await askKey(rl, currentKey);
|
|
209
|
+
while (/^sk-or-v1-/i.test(selectedKey)) {
|
|
210
|
+
process.stdout.write('\n检测到你输入的是 OpenRouter key(sk-or-v1- 开头),这不适用于 sub2api /v1 代理模式。\n');
|
|
211
|
+
const choice = await askChoice(rl, '是否重新输入 key?', ['重新输入 key(推荐)', '继续使用当前 key'], 0);
|
|
212
|
+
if (choice === '继续使用当前 key') break;
|
|
213
|
+
selectedKey = await askKey(rl, selectedKey);
|
|
214
|
+
}
|
|
215
|
+
return selectedKey;
|
|
216
|
+
}
|
|
217
|
+
|
|
56
218
|
function ensureModel(providerObj, modelId, reasoning) {
|
|
57
219
|
providerObj.models = providerObj.models || [];
|
|
58
220
|
let model = providerObj.models.find((m) => m && m.id === modelId);
|
|
@@ -75,6 +237,186 @@ function ensureModel(providerObj, modelId, reasoning) {
|
|
|
75
237
|
model.api = model.api || 'openai-responses';
|
|
76
238
|
}
|
|
77
239
|
|
|
240
|
+
function escapeRegExp(text) {
|
|
241
|
+
return String(text).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function upsertEnvLine(content, key, value) {
|
|
245
|
+
const line = `Environment=${key}=${value}`;
|
|
246
|
+
const re = new RegExp(`^Environment=${escapeRegExp(key)}=.*$`, 'm');
|
|
247
|
+
if (re.test(content)) {
|
|
248
|
+
return content.replace(re, line);
|
|
249
|
+
}
|
|
250
|
+
const serviceIdx = content.indexOf('[Service]');
|
|
251
|
+
if (serviceIdx === -1) {
|
|
252
|
+
return `${content.trimEnd()}\n${line}\n`;
|
|
253
|
+
}
|
|
254
|
+
const afterServiceHeader = content.indexOf('\n', serviceIdx);
|
|
255
|
+
if (afterServiceHeader < 0) {
|
|
256
|
+
return `${content}\n${line}\n`;
|
|
257
|
+
}
|
|
258
|
+
return `${content.slice(0, afterServiceHeader + 1)}${line}\n${content.slice(afterServiceHeader + 1)}`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function configureLinuxProxyService() {
|
|
262
|
+
const servicePath = path.join(os.homedir(), '.config/systemd/user', PROXY_SERVICE);
|
|
263
|
+
if (!fs.existsSync(servicePath)) {
|
|
264
|
+
return { exists: false, changed: false, servicePath };
|
|
265
|
+
}
|
|
266
|
+
const original = fs.readFileSync(servicePath, 'utf8');
|
|
267
|
+
let next = original;
|
|
268
|
+
next = upsertEnvLine(next, 'SUB2API_UPSTREAM', PROXY_UPSTREAM);
|
|
269
|
+
next = upsertEnvLine(next, 'SUB2API_COMPAT_DROP_TOOLS_ON_COMPACT', '0');
|
|
270
|
+
next = upsertEnvLine(next, 'SUB2API_COMPAT_PORT', String(PROXY_PORT));
|
|
271
|
+
next = upsertEnvLine(next, 'SUB2API_COMPAT_LOG', PROXY_LOG_FILE);
|
|
272
|
+
|
|
273
|
+
if (next === original) {
|
|
274
|
+
return { exists: true, changed: false, servicePath };
|
|
275
|
+
}
|
|
276
|
+
backup(servicePath);
|
|
277
|
+
fs.writeFileSync(servicePath, next, 'utf8');
|
|
278
|
+
return { exists: true, changed: true, servicePath };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function runSystemctl(args) {
|
|
282
|
+
const out = spawnSync('systemctl', ['--user', ...args], { encoding: 'utf8' });
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function isLinuxProxyServiceActive() {
|
|
287
|
+
const out = runSystemctl(['is-active', PROXY_SERVICE]);
|
|
288
|
+
return out.status === 0 && String(out.stdout || '').trim() === 'active';
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function startLinuxProxyService() {
|
|
292
|
+
const reload = runSystemctl(['daemon-reload']);
|
|
293
|
+
if (reload.status !== 0) return false;
|
|
294
|
+
const start = runSystemctl(['enable', '--now', PROXY_SERVICE]);
|
|
295
|
+
return start.status === 0;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function startDetachedProxy() {
|
|
299
|
+
try {
|
|
300
|
+
const cliPath = fileURLToPath(new URL('../bin/cli.mjs', import.meta.url));
|
|
301
|
+
const child = spawn(
|
|
302
|
+
process.execPath,
|
|
303
|
+
[
|
|
304
|
+
cliPath,
|
|
305
|
+
'serve',
|
|
306
|
+
'--port',
|
|
307
|
+
String(PROXY_PORT),
|
|
308
|
+
'--upstream',
|
|
309
|
+
PROXY_UPSTREAM,
|
|
310
|
+
'--log-file',
|
|
311
|
+
PROXY_LOG_FILE
|
|
312
|
+
],
|
|
313
|
+
{
|
|
314
|
+
detached: true,
|
|
315
|
+
stdio: 'ignore',
|
|
316
|
+
env: {
|
|
317
|
+
...process.env,
|
|
318
|
+
SUB2API_UPSTREAM: PROXY_UPSTREAM
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
);
|
|
322
|
+
child.unref();
|
|
323
|
+
return true;
|
|
324
|
+
} catch {
|
|
325
|
+
return false;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async function isPortOpen(host, port, timeoutMs = 900) {
|
|
330
|
+
return new Promise((resolve) => {
|
|
331
|
+
const socket = net.createConnection({ host, port });
|
|
332
|
+
let done = false;
|
|
333
|
+
|
|
334
|
+
const finish = (ok) => {
|
|
335
|
+
if (done) return;
|
|
336
|
+
done = true;
|
|
337
|
+
socket.destroy();
|
|
338
|
+
resolve(ok);
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
socket.on('connect', () => finish(true));
|
|
342
|
+
socket.on('error', () => finish(false));
|
|
343
|
+
socket.setTimeout(timeoutMs, () => finish(false));
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function ensureProxyReady() {
|
|
348
|
+
if (process.platform === 'linux') {
|
|
349
|
+
const service = configureLinuxProxyService();
|
|
350
|
+
if (service.exists && service.changed) {
|
|
351
|
+
// Service file changed: reload + restart so it points back to sub2api.atools.live.
|
|
352
|
+
startLinuxProxyService();
|
|
353
|
+
await sleep(400);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (await isPortOpen(PROXY_HOST, PROXY_PORT)) return true;
|
|
358
|
+
|
|
359
|
+
// Linux retry: service may exist but not running.
|
|
360
|
+
if (process.platform === 'linux') {
|
|
361
|
+
const service = configureLinuxProxyService();
|
|
362
|
+
if (service.exists && isLinuxProxyServiceActive()) {
|
|
363
|
+
if (await isPortOpen(PROXY_HOST, PROXY_PORT)) return true;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
process.stdout.write('\n检测到本地代理未运行(127.0.0.1:18888)。\n');
|
|
368
|
+
const promptRl = readlinePromises.createInterface({
|
|
369
|
+
input: process.stdin,
|
|
370
|
+
output: process.stdout
|
|
371
|
+
});
|
|
372
|
+
let action = '';
|
|
373
|
+
try {
|
|
374
|
+
action = await askChoice(
|
|
375
|
+
promptRl,
|
|
376
|
+
'atools 模型依赖本地代理,是否现在启动代理?',
|
|
377
|
+
['启动代理(推荐)', '取消并退出'],
|
|
378
|
+
0
|
|
379
|
+
);
|
|
380
|
+
} finally {
|
|
381
|
+
promptRl.close();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (action === '取消并退出') {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
let started = false;
|
|
389
|
+
if (process.platform === 'linux') {
|
|
390
|
+
started = startLinuxProxyService();
|
|
391
|
+
}
|
|
392
|
+
if (!started) {
|
|
393
|
+
started = startDetachedProxy();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (!started) return false;
|
|
397
|
+
|
|
398
|
+
for (let i = 0; i < 5; i += 1) {
|
|
399
|
+
await sleep(500);
|
|
400
|
+
if (await isPortOpen(PROXY_HOST, PROXY_PORT)) {
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function printRestartHint() {
|
|
408
|
+
process.stdout.write('\n后续建议:\n');
|
|
409
|
+
if (process.platform === 'linux') {
|
|
410
|
+
process.stdout.write('- Linux(systemd): systemctl --user restart openclaw-gateway.service\n');
|
|
411
|
+
} else if (process.platform === 'darwin') {
|
|
412
|
+
process.stdout.write('- macOS: 重启 OpenClaw 进程,或重新执行你本地的 openclaw 启动命令\n');
|
|
413
|
+
} else if (process.platform === 'win32') {
|
|
414
|
+
process.stdout.write('- Windows: 重启 OpenClaw 进程或服务(若以服务方式运行)\n');
|
|
415
|
+
} else {
|
|
416
|
+
process.stdout.write('- 请重启 OpenClaw 进程,使新配置生效\n');
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
78
420
|
export async function runConfigOpenclaw({ openclawHome } = {}) {
|
|
79
421
|
const home = openclawHome || process.env.OPENCLAW_HOME || path.join(os.homedir(), '.openclaw');
|
|
80
422
|
const openclawJsonPath = path.join(home, 'openclaw.json');
|
|
@@ -83,17 +425,26 @@ export async function runConfigOpenclaw({ openclawHome } = {}) {
|
|
|
83
425
|
|
|
84
426
|
const openclawCfg = readJson(openclawJsonPath);
|
|
85
427
|
const modelsCfg = readJson(modelsJsonPath);
|
|
86
|
-
|
|
428
|
+
const authCfg = fs.existsSync(authProfilesPath)
|
|
87
429
|
? readJson(authProfilesPath)
|
|
88
430
|
: { version: 1, profiles: {}, lastGood: {}, usageStats: {} };
|
|
89
431
|
|
|
90
|
-
|
|
91
|
-
input: process.stdin,
|
|
92
|
-
output: process.stdout
|
|
93
|
-
});
|
|
432
|
+
let rl = null;
|
|
94
433
|
|
|
95
434
|
try {
|
|
96
435
|
process.stdout.write(`OpenClaw Home: ${home}\n`);
|
|
436
|
+
process.stdout.write(`固定代理目标: ${PROXY_UPSTREAM}\n`);
|
|
437
|
+
process.stdout.write(`固定 Provider Base URL: ${PROXY_BASE_URL}\n`);
|
|
438
|
+
|
|
439
|
+
const proxyReady = await ensureProxyReady();
|
|
440
|
+
if (!proxyReady) {
|
|
441
|
+
process.stdout.write('\n已取消:本地代理未就绪,未修改 OpenClaw 模型配置。\n');
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
rl = readlinePromises.createInterface({
|
|
445
|
+
input: process.stdin,
|
|
446
|
+
output: process.stdout
|
|
447
|
+
});
|
|
97
448
|
|
|
98
449
|
const selectedModel = await askChoice(rl, '1) 选择默认模型', MODEL_OPTIONS, 1);
|
|
99
450
|
const reasoningSwitch = await askChoice(rl, '2) 配置推理开关', ['off', 'on'], 0);
|
|
@@ -104,13 +455,17 @@ export async function runConfigOpenclaw({ openclawHome } = {}) {
|
|
|
104
455
|
modelsCfg?.providers?.[PROVIDER]?.apiKey ||
|
|
105
456
|
authCfg?.profiles?.[`${PROVIDER}:default`]?.key ||
|
|
106
457
|
'';
|
|
107
|
-
const selectedKey = await
|
|
458
|
+
const selectedKey = await askKeyWithGuard(rl, currentKey);
|
|
108
459
|
|
|
109
460
|
openclawCfg.models = openclawCfg.models || {};
|
|
110
461
|
openclawCfg.models.providers = openclawCfg.models.providers || {};
|
|
111
462
|
openclawCfg.models.providers[PROVIDER] = openclawCfg.models.providers[PROVIDER] || {};
|
|
463
|
+
openclawCfg.models.providers[PROVIDER].baseUrl = PROXY_BASE_URL;
|
|
112
464
|
openclawCfg.models.providers[PROVIDER].api = 'openai-responses';
|
|
113
465
|
openclawCfg.models.providers[PROVIDER].authHeader = true;
|
|
466
|
+
openclawCfg.models.providers[PROVIDER].headers = openclawCfg.models.providers[PROVIDER].headers || {};
|
|
467
|
+
openclawCfg.models.providers[PROVIDER].headers['User-Agent'] = 'codex_cli_rs/0.101.0 (Ubuntu 24.4.0; x86_64) WindowsTerminal';
|
|
468
|
+
openclawCfg.models.providers[PROVIDER].headers.Accept = 'application/json';
|
|
114
469
|
if (selectedKey) {
|
|
115
470
|
openclawCfg.models.providers[PROVIDER].apiKey = selectedKey;
|
|
116
471
|
openclawCfg.models.providers[PROVIDER].auth = 'api-key';
|
|
@@ -125,11 +480,24 @@ export async function runConfigOpenclaw({ openclawHome } = {}) {
|
|
|
125
480
|
openclawCfg.agents.defaults.models = openclawCfg.agents.defaults.models || {};
|
|
126
481
|
openclawCfg.agents.defaults.models[`${PROVIDER}/${selectedModel}`] =
|
|
127
482
|
openclawCfg.agents.defaults.models[`${PROVIDER}/${selectedModel}`] || { alias: `ATools ${selectedModel}` };
|
|
483
|
+
if (Array.isArray(openclawCfg.agents.list)) {
|
|
484
|
+
openclawCfg.agents.list = openclawCfg.agents.list.map((agent) => {
|
|
485
|
+
if (!agent || typeof agent !== 'object') return agent;
|
|
486
|
+
if (typeof agent.model === 'string' && agent.model.startsWith(`${PROVIDER}/`)) {
|
|
487
|
+
return { ...agent, model: `${PROVIDER}/${selectedModel}` };
|
|
488
|
+
}
|
|
489
|
+
return agent;
|
|
490
|
+
});
|
|
491
|
+
}
|
|
128
492
|
|
|
129
493
|
modelsCfg.providers = modelsCfg.providers || {};
|
|
130
494
|
modelsCfg.providers[PROVIDER] = modelsCfg.providers[PROVIDER] || {};
|
|
495
|
+
modelsCfg.providers[PROVIDER].baseUrl = PROXY_BASE_URL;
|
|
131
496
|
modelsCfg.providers[PROVIDER].api = 'openai-responses';
|
|
132
497
|
modelsCfg.providers[PROVIDER].authHeader = true;
|
|
498
|
+
modelsCfg.providers[PROVIDER].headers = modelsCfg.providers[PROVIDER].headers || {};
|
|
499
|
+
modelsCfg.providers[PROVIDER].headers['User-Agent'] = 'codex_cli_rs/0.101.0 (Ubuntu 24.4.0; x86_64) WindowsTerminal';
|
|
500
|
+
modelsCfg.providers[PROVIDER].headers.Accept = 'application/json';
|
|
133
501
|
if (selectedKey) {
|
|
134
502
|
modelsCfg.providers[PROVIDER].apiKey = selectedKey;
|
|
135
503
|
modelsCfg.providers[PROVIDER].auth = 'api-key';
|
|
@@ -161,6 +529,8 @@ export async function runConfigOpenclaw({ openclawHome } = {}) {
|
|
|
161
529
|
writeJson(authProfilesPath, authCfg);
|
|
162
530
|
|
|
163
531
|
process.stdout.write('\n配置已更新:\n');
|
|
532
|
+
process.stdout.write(`- Proxy Upstream: ${PROXY_UPSTREAM}\n`);
|
|
533
|
+
process.stdout.write(`- Provider Base URL: ${PROXY_BASE_URL}\n`);
|
|
164
534
|
process.stdout.write(`- 默认模型: ${PROVIDER}/${selectedModel}\n`);
|
|
165
535
|
process.stdout.write(`- 推理开关: ${reasoning ? 'on' : 'off'}\n`);
|
|
166
536
|
process.stdout.write(`- 思考强度: ${selectedThinking}\n`);
|
|
@@ -168,7 +538,8 @@ export async function runConfigOpenclaw({ openclawHome } = {}) {
|
|
|
168
538
|
process.stdout.write(`- 已写入: ${openclawJsonPath}\n`);
|
|
169
539
|
process.stdout.write(`- 已写入: ${modelsJsonPath}\n`);
|
|
170
540
|
process.stdout.write(`- 已写入: ${authProfilesPath}\n`);
|
|
541
|
+
printRestartHint();
|
|
171
542
|
} finally {
|
|
172
|
-
rl.close();
|
|
543
|
+
if (rl) rl.close();
|
|
173
544
|
}
|
|
174
545
|
}
|
package/lib/install.mjs
CHANGED
|
@@ -8,8 +8,7 @@ const DEFAULT_PROVIDER = 'sub2api';
|
|
|
8
8
|
const DEFAULT_MODEL = 'gpt-5.3-codex';
|
|
9
9
|
const DEFAULT_PORT = 18888;
|
|
10
10
|
const DEFAULT_UPSTREAM = 'https://sub2api.atools.live';
|
|
11
|
-
const
|
|
12
|
-
const DEFAULT_LOG_FILE = '/tmp/openclaw/atools-compat-proxy.log';
|
|
11
|
+
const DEFAULT_LOG_FILE = path.join(os.tmpdir(), 'openclaw', 'atools-compat-proxy.log');
|
|
13
12
|
const DEFAULT_SERVICE_NAME = 'openclaw-atools-proxy.service';
|
|
14
13
|
const OPENCLAW_GATEWAY_SERVICE = 'openclaw-gateway.service';
|
|
15
14
|
|
|
@@ -47,6 +46,40 @@ function ensureDir(dir) {
|
|
|
47
46
|
fs.mkdirSync(dir, { recursive: true });
|
|
48
47
|
}
|
|
49
48
|
|
|
49
|
+
function defaultServiceId(name = DEFAULT_SERVICE_NAME) {
|
|
50
|
+
return String(name || DEFAULT_SERVICE_NAME).replace(/\.service$/i, '');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildProxyServeArgs({ port, upstream, maxReqBytes, logFile }) {
|
|
54
|
+
const args = [
|
|
55
|
+
'serve',
|
|
56
|
+
'--port',
|
|
57
|
+
String(port),
|
|
58
|
+
'--upstream',
|
|
59
|
+
String(upstream),
|
|
60
|
+
'--log-file',
|
|
61
|
+
String(logFile),
|
|
62
|
+
'--max-req-bytes',
|
|
63
|
+
String(maxReqBytes)
|
|
64
|
+
];
|
|
65
|
+
return args;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function escapeXml(text) {
|
|
69
|
+
return String(text)
|
|
70
|
+
.replace(/&/g, '&')
|
|
71
|
+
.replace(/</g, '<')
|
|
72
|
+
.replace(/>/g, '>')
|
|
73
|
+
.replace(/"/g, '"')
|
|
74
|
+
.replace(/'/g, ''');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function quoteWindowsArg(arg) {
|
|
78
|
+
const s = String(arg ?? '');
|
|
79
|
+
if (!/[\s"]/g.test(s)) return s;
|
|
80
|
+
return `"${s.replace(/"/g, '\\"')}"`;
|
|
81
|
+
}
|
|
82
|
+
|
|
50
83
|
function parseArgs(argv) {
|
|
51
84
|
const out = {
|
|
52
85
|
openclawHome: process.env.OPENCLAW_HOME || path.join(os.homedir(), '.openclaw'),
|
|
@@ -54,12 +87,10 @@ function parseArgs(argv) {
|
|
|
54
87
|
model: DEFAULT_MODEL,
|
|
55
88
|
port: DEFAULT_PORT,
|
|
56
89
|
upstream: DEFAULT_UPSTREAM,
|
|
57
|
-
fallbackUpstream: DEFAULT_FALLBACK_UPSTREAM,
|
|
58
90
|
maxReqBytes: 68000,
|
|
59
91
|
logFile: DEFAULT_LOG_FILE,
|
|
60
92
|
serviceName: DEFAULT_SERVICE_NAME,
|
|
61
93
|
apiKey: process.env.SUB_OAI_KEY || '',
|
|
62
|
-
fallbackApiKey: process.env.FALLBACK_OAI_KEY || '',
|
|
63
94
|
dryRun: false,
|
|
64
95
|
noRestart: false
|
|
65
96
|
};
|
|
@@ -73,12 +104,10 @@ function parseArgs(argv) {
|
|
|
73
104
|
else if (a === '--model') out.model = next();
|
|
74
105
|
else if (a === '--port') out.port = Number(next());
|
|
75
106
|
else if (a === '--upstream') out.upstream = next();
|
|
76
|
-
else if (a === '--fallback-upstream') out.fallbackUpstream = next();
|
|
77
107
|
else if (a === '--max-req-bytes') out.maxReqBytes = Number(next());
|
|
78
108
|
else if (a === '--log-file') out.logFile = next();
|
|
79
109
|
else if (a === '--service-name') out.serviceName = next();
|
|
80
110
|
else if (a === '--api-key') out.apiKey = next();
|
|
81
|
-
else if (a === '--fallback-api-key') out.fallbackApiKey = next();
|
|
82
111
|
else if (a === '--dry-run') out.dryRun = true;
|
|
83
112
|
else if (a === '--no-restart') out.noRestart = true;
|
|
84
113
|
else if (a === '--help' || a === '-h') {
|
|
@@ -95,42 +124,65 @@ function parseArgs(argv) {
|
|
|
95
124
|
}
|
|
96
125
|
|
|
97
126
|
function renderSystemdService({
|
|
98
|
-
serviceName,
|
|
99
127
|
nodeBin,
|
|
100
128
|
cliPath,
|
|
101
|
-
|
|
102
|
-
upstream,
|
|
103
|
-
fallbackUpstream,
|
|
104
|
-
maxReqBytes,
|
|
105
|
-
fallbackApiKey,
|
|
106
|
-
logFile
|
|
129
|
+
serveArgs
|
|
107
130
|
}) {
|
|
108
|
-
const
|
|
109
|
-
? `Environment=SUB2API_FALLBACK_UPSTREAM=${fallbackUpstream}\nEnvironment=SUB2API_FALLBACK_API_KEY=${fallbackApiKey || ''}\n`
|
|
110
|
-
: '';
|
|
131
|
+
const execArgs = [nodeBin, cliPath, ...serveArgs].join(' ');
|
|
111
132
|
return `[Unit]
|
|
112
133
|
Description=OpenClaw ATools Compatibility Proxy
|
|
113
134
|
After=network-online.target
|
|
114
135
|
Wants=network-online.target
|
|
115
136
|
|
|
116
137
|
[Service]
|
|
117
|
-
ExecStart=${
|
|
138
|
+
ExecStart=${execArgs}
|
|
118
139
|
Restart=always
|
|
119
140
|
RestartSec=2
|
|
120
141
|
Environment=HOME=${os.homedir()}
|
|
121
142
|
Environment=TMPDIR=/tmp
|
|
122
|
-
Environment=SUB2API_COMPAT_PORT=${port}
|
|
123
|
-
Environment=SUB2API_UPSTREAM=${upstream}
|
|
124
|
-
Environment=SUB2API_COMPAT_LOG=${logFile}
|
|
125
|
-
Environment=SUB2API_COMPAT_MAX_REQ_BYTES=${maxReqBytes}
|
|
126
143
|
Environment="SUB2API_COMPAT_USER_AGENT=codex_cli_rs/0.101.0 (Ubuntu 24.4.0; x86_64) WindowsTerminal"
|
|
127
|
-
${fallbackEnv}
|
|
128
144
|
|
|
129
145
|
[Install]
|
|
130
146
|
WantedBy=default.target
|
|
131
147
|
`;
|
|
132
148
|
}
|
|
133
149
|
|
|
150
|
+
function renderLaunchdPlist({ label, nodeBin, cliPath, serveArgs, logFile }) {
|
|
151
|
+
const args = [nodeBin, cliPath, ...serveArgs];
|
|
152
|
+
const argsXml = args.map((arg) => ` <string>${escapeXml(arg)}</string>`).join('\n');
|
|
153
|
+
const launchLog = `${logFile}.launchd.log`;
|
|
154
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
155
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
156
|
+
<plist version="1.0">
|
|
157
|
+
<dict>
|
|
158
|
+
<key>Label</key>
|
|
159
|
+
<string>${escapeXml(label)}</string>
|
|
160
|
+
<key>ProgramArguments</key>
|
|
161
|
+
<array>
|
|
162
|
+
${argsXml}
|
|
163
|
+
</array>
|
|
164
|
+
<key>RunAtLoad</key>
|
|
165
|
+
<true/>
|
|
166
|
+
<key>KeepAlive</key>
|
|
167
|
+
<true/>
|
|
168
|
+
<key>WorkingDirectory</key>
|
|
169
|
+
<string>${escapeXml(os.homedir())}</string>
|
|
170
|
+
<key>EnvironmentVariables</key>
|
|
171
|
+
<dict>
|
|
172
|
+
<key>HOME</key>
|
|
173
|
+
<string>${escapeXml(os.homedir())}</string>
|
|
174
|
+
<key>SUB2API_COMPAT_USER_AGENT</key>
|
|
175
|
+
<string>codex_cli_rs/0.101.0 (Ubuntu 24.4.0; x86_64) WindowsTerminal</string>
|
|
176
|
+
</dict>
|
|
177
|
+
<key>StandardOutPath</key>
|
|
178
|
+
<string>${escapeXml(launchLog)}</string>
|
|
179
|
+
<key>StandardErrorPath</key>
|
|
180
|
+
<string>${escapeXml(launchLog)}</string>
|
|
181
|
+
</dict>
|
|
182
|
+
</plist>
|
|
183
|
+
`;
|
|
184
|
+
}
|
|
185
|
+
|
|
134
186
|
function detectApiKey(openclawHome, explicitKey) {
|
|
135
187
|
if (explicitKey) return explicitKey;
|
|
136
188
|
const authPath = path.join(openclawHome, 'agents/main/agent/auth-profiles.json');
|
|
@@ -138,13 +190,6 @@ function detectApiKey(openclawHome, explicitKey) {
|
|
|
138
190
|
return auth?.profiles?.['sub2api:default']?.key || '';
|
|
139
191
|
}
|
|
140
192
|
|
|
141
|
-
function detectFallbackApiKey(openclawHome, explicitKey) {
|
|
142
|
-
if (explicitKey) return explicitKey;
|
|
143
|
-
const cfgPath = path.join(openclawHome, 'openclaw.json');
|
|
144
|
-
const cfg = readJsonIfExists(cfgPath);
|
|
145
|
-
return cfg?.models?.providers?.gmn?.apiKey || '';
|
|
146
|
-
}
|
|
147
|
-
|
|
148
193
|
function patchOpenclawConfig(openclawHome, provider, model, port, dryRun) {
|
|
149
194
|
const cfgPath = path.join(openclawHome, 'openclaw.json');
|
|
150
195
|
const cfg = readJsonIfExists(cfgPath);
|
|
@@ -297,7 +342,7 @@ function runSystemctl(args, { dryRun } = {}) {
|
|
|
297
342
|
execFileSync('systemctl', ['--user', ...args], { stdio: 'inherit' });
|
|
298
343
|
}
|
|
299
344
|
|
|
300
|
-
function
|
|
345
|
+
function installLinuxService({ serviceName, content, dryRun }) {
|
|
301
346
|
const serviceDir = path.join(os.homedir(), '.config/systemd/user');
|
|
302
347
|
const servicePath = path.join(serviceDir, serviceName);
|
|
303
348
|
ensureDir(serviceDir);
|
|
@@ -317,12 +362,105 @@ function installService({ serviceName, content, dryRun }) {
|
|
|
317
362
|
runSystemctl(['enable', '--now', serviceName], { dryRun });
|
|
318
363
|
}
|
|
319
364
|
|
|
365
|
+
function runLaunchctl(args, { dryRun, allowFailure = false } = {}) {
|
|
366
|
+
if (dryRun) {
|
|
367
|
+
info(`dry-run: launchctl ${args.join(' ')}`);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
execFileSync('launchctl', args, { stdio: 'inherit' });
|
|
372
|
+
} catch (err) {
|
|
373
|
+
if (allowFailure) return;
|
|
374
|
+
throw err;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function installMacService({ serviceName, content, dryRun }) {
|
|
379
|
+
const serviceId = defaultServiceId(serviceName);
|
|
380
|
+
const launchAgentDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
|
|
381
|
+
const plistPath = path.join(launchAgentDir, `${serviceId}.plist`);
|
|
382
|
+
const uid = typeof process.getuid === 'function' ? process.getuid() : null;
|
|
383
|
+
if (uid == null) {
|
|
384
|
+
throw new Error('failed to resolve current uid for launchctl');
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
ensureDir(launchAgentDir);
|
|
388
|
+
if (dryRun) {
|
|
389
|
+
info(`dry-run: would write ${plistPath}`);
|
|
390
|
+
} else {
|
|
391
|
+
if (fs.existsSync(plistPath)) {
|
|
392
|
+
const bak = backup(plistPath);
|
|
393
|
+
if (bak) info(`backup created: ${bak}`);
|
|
394
|
+
}
|
|
395
|
+
fs.writeFileSync(plistPath, content, 'utf8');
|
|
396
|
+
info(`wrote: ${plistPath}`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const target = `gui/${uid}/${serviceId}`;
|
|
400
|
+
runLaunchctl(['bootout', target], { dryRun, allowFailure: true });
|
|
401
|
+
runLaunchctl(['bootstrap', `gui/${uid}`, plistPath], { dryRun });
|
|
402
|
+
runLaunchctl(['enable', target], { dryRun, allowFailure: true });
|
|
403
|
+
runLaunchctl(['kickstart', '-k', target], { dryRun, allowFailure: true });
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function runSchtasks(args, { dryRun, allowFailure = false } = {}) {
|
|
407
|
+
if (dryRun) {
|
|
408
|
+
info(`dry-run: schtasks ${args.join(' ')}`);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
try {
|
|
412
|
+
execFileSync('schtasks', args, { stdio: 'inherit' });
|
|
413
|
+
} catch (err) {
|
|
414
|
+
if (allowFailure) return;
|
|
415
|
+
throw err;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function installWindowsService({ serviceName, nodeBin, cliPath, serveArgs, dryRun }) {
|
|
420
|
+
const taskName = defaultServiceId(serviceName);
|
|
421
|
+
const command = [nodeBin, cliPath, ...serveArgs].map(quoteWindowsArg).join(' ');
|
|
422
|
+
runSchtasks(
|
|
423
|
+
['/Create', '/F', '/SC', 'ONLOGON', '/RL', 'LIMITED', '/TN', taskName, '/TR', command],
|
|
424
|
+
{ dryRun }
|
|
425
|
+
);
|
|
426
|
+
runSchtasks(['/Run', '/TN', taskName], { dryRun, allowFailure: true });
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function installService({
|
|
430
|
+
serviceName,
|
|
431
|
+
linuxServiceContent,
|
|
432
|
+
macPlistContent,
|
|
433
|
+
nodeBin,
|
|
434
|
+
cliPath,
|
|
435
|
+
serveArgs,
|
|
436
|
+
dryRun
|
|
437
|
+
}) {
|
|
438
|
+
if (process.platform === 'linux') {
|
|
439
|
+
installLinuxService({ serviceName, content: linuxServiceContent, dryRun });
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (process.platform === 'darwin') {
|
|
443
|
+
installMacService({ serviceName, content: macPlistContent, dryRun });
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (process.platform === 'win32') {
|
|
447
|
+
installWindowsService({ serviceName, nodeBin, cliPath, serveArgs, dryRun });
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
throw new Error(`unsupported platform for service install: ${process.platform}`);
|
|
451
|
+
}
|
|
452
|
+
|
|
320
453
|
function restartOpenclawGateway({ noRestart, dryRun }) {
|
|
321
454
|
if (noRestart) {
|
|
322
455
|
info('skip gateway restart (--no-restart)');
|
|
323
456
|
return;
|
|
324
457
|
}
|
|
325
458
|
|
|
459
|
+
if (process.platform !== 'linux') {
|
|
460
|
+
info(`skip gateway restart on ${process.platform} (please restart OpenClaw manually)`);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
326
464
|
try {
|
|
327
465
|
runSystemctl(['restart', OPENCLAW_GATEWAY_SERVICE], { dryRun });
|
|
328
466
|
} catch {
|
|
@@ -342,12 +480,10 @@ Options:
|
|
|
342
480
|
--model <id> Default: gpt-5.3-codex
|
|
343
481
|
--port <number> Default: 18888
|
|
344
482
|
--upstream <url> Default: https://sub2api.atools.live
|
|
345
|
-
--fallback-upstream <url> Default: https://gmn.chuangzuoli.com/v1
|
|
346
483
|
--max-req-bytes <number> Default: 68000
|
|
347
|
-
--log-file <path> Default:
|
|
348
|
-
--service-name <name>
|
|
349
|
-
--api-key <key> Prefer explicit key;
|
|
350
|
-
--fallback-api-key <key> Prefer explicit key; fallback env FALLBACK_OAI_KEY then openclaw gmn key
|
|
484
|
+
--log-file <path> Default: <tmp>/openclaw/atools-compat-proxy.log
|
|
485
|
+
--service-name <name> Linux: *.service; macOS/Windows: service id/task name
|
|
486
|
+
--api-key <key> Prefer explicit key; otherwise use SUB_OAI_KEY or auth-profiles
|
|
351
487
|
--no-restart Do not restart openclaw-gateway.service
|
|
352
488
|
--dry-run Print actions without changing files
|
|
353
489
|
-h, --help Show help
|
|
@@ -365,32 +501,44 @@ export async function runInstall(rawArgs = process.argv.slice(2)) {
|
|
|
365
501
|
const nodeBin = process.execPath;
|
|
366
502
|
const cliPath = fileURLToPath(new URL('../bin/cli.mjs', import.meta.url));
|
|
367
503
|
const apiKey = detectApiKey(args.openclawHome, args.apiKey);
|
|
368
|
-
const
|
|
504
|
+
const serveArgs = buildProxyServeArgs({
|
|
505
|
+
port: args.port,
|
|
506
|
+
upstream: args.upstream,
|
|
507
|
+
maxReqBytes: args.maxReqBytes,
|
|
508
|
+
logFile: args.logFile
|
|
509
|
+
});
|
|
369
510
|
|
|
370
511
|
info(`openclawHome=${args.openclawHome}`);
|
|
512
|
+
info(`platform=${process.platform}`);
|
|
371
513
|
info(`provider/model=${args.provider}/${args.model}`);
|
|
372
514
|
info(`proxy=http://127.0.0.1:${args.port}/v1 -> ${args.upstream}`);
|
|
373
|
-
if (args.fallbackUpstream) {
|
|
374
|
-
info(`fallback=${args.fallbackUpstream}`);
|
|
375
|
-
}
|
|
376
515
|
|
|
377
516
|
patchOpenclawConfig(args.openclawHome, args.provider, args.model, args.port, args.dryRun);
|
|
378
517
|
patchAgentModels(args.openclawHome, args.provider, args.model, args.port, apiKey, args.dryRun);
|
|
379
518
|
patchAuthProfiles(args.openclawHome, args.provider, apiKey, args.dryRun);
|
|
380
519
|
|
|
381
|
-
const
|
|
382
|
-
serviceName: args.serviceName,
|
|
520
|
+
const linuxServiceContent = renderSystemdService({
|
|
383
521
|
nodeBin,
|
|
384
522
|
cliPath,
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
523
|
+
serveArgs
|
|
524
|
+
});
|
|
525
|
+
const macPlistContent = renderLaunchdPlist({
|
|
526
|
+
label: defaultServiceId(args.serviceName),
|
|
527
|
+
nodeBin,
|
|
528
|
+
cliPath,
|
|
529
|
+
serveArgs,
|
|
390
530
|
logFile: args.logFile
|
|
391
531
|
});
|
|
392
532
|
|
|
393
|
-
installService({
|
|
533
|
+
installService({
|
|
534
|
+
serviceName: args.serviceName,
|
|
535
|
+
linuxServiceContent,
|
|
536
|
+
macPlistContent,
|
|
537
|
+
nodeBin,
|
|
538
|
+
cliPath,
|
|
539
|
+
serveArgs,
|
|
540
|
+
dryRun: args.dryRun
|
|
541
|
+
});
|
|
394
542
|
restartOpenclawGateway({ noRestart: args.noRestart, dryRun: args.dryRun });
|
|
395
543
|
|
|
396
544
|
info('done');
|
package/lib/proxy-server.mjs
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
3
4
|
|
|
4
5
|
const DEFAULT_PORT = 18888;
|
|
5
6
|
const DEFAULT_UPSTREAM = 'https://sub2api.atools.live';
|
|
6
7
|
const DEFAULT_LOG = '/tmp/openclaw/atools-compat-proxy.log';
|
|
7
8
|
const DEFAULT_USER_AGENT = 'codex_cli_rs/0.101.0 (Ubuntu 24.4.0; x86_64) WindowsTerminal';
|
|
8
|
-
const DEFAULT_FALLBACK_UPSTREAM = '';
|
|
9
|
-
const DEFAULT_FALLBACK_API_KEY = '';
|
|
10
9
|
const DEFAULT_MAX_REQ_BYTES = 68000;
|
|
11
10
|
const DEFAULT_DROP_TOOLS_ON_COMPACT = false;
|
|
11
|
+
const DEFAULT_STRIP_PREVIOUS_RESPONSE_ID = false;
|
|
12
12
|
|
|
13
13
|
function ensureParentDir(filePath) {
|
|
14
|
-
const dir =
|
|
14
|
+
const dir = path.dirname(filePath);
|
|
15
15
|
if (dir) {
|
|
16
16
|
fs.mkdirSync(dir, { recursive: true });
|
|
17
17
|
}
|
|
@@ -54,12 +54,13 @@ function normalizeContent(content) {
|
|
|
54
54
|
return [toInputText('')];
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
function normalizeResponsesPayload(payload) {
|
|
57
|
+
function normalizeResponsesPayload(payload, { stripPreviousResponseId = DEFAULT_STRIP_PREVIOUS_RESPONSE_ID } = {}) {
|
|
58
58
|
if (!payload || typeof payload !== 'object') return payload;
|
|
59
59
|
const out = { ...payload };
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
if (stripPreviousResponseId && 'previous_response_id' in out) {
|
|
62
|
+
delete out.previous_response_id;
|
|
63
|
+
}
|
|
63
64
|
|
|
64
65
|
if (typeof out.input === 'string') {
|
|
65
66
|
out.input = [{ role: 'user', content: normalizeContent(out.input) }];
|
|
@@ -84,7 +85,28 @@ function trimTextPart(part, maxChars) {
|
|
|
84
85
|
return { ...part, text: text.length > maxChars ? text.slice(0, maxChars) : text };
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
function
|
|
88
|
+
function compactInputMessages(messages, maxRecentMessages = 8) {
|
|
89
|
+
if (!Array.isArray(messages)) return messages;
|
|
90
|
+
const recent = messages.slice(-Math.max(1, maxRecentMessages));
|
|
91
|
+
let latestSystem = null;
|
|
92
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
93
|
+
const msg = messages[i];
|
|
94
|
+
if (!msg || typeof msg !== 'object') continue;
|
|
95
|
+
if (msg.role === 'system' || msg.role === 'developer') {
|
|
96
|
+
latestSystem = msg;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (latestSystem && !recent.includes(latestSystem)) {
|
|
101
|
+
return [latestSystem, ...recent];
|
|
102
|
+
}
|
|
103
|
+
return recent;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function compactResponsesPayload(payload, {
|
|
107
|
+
dropToolsOnCompact = DEFAULT_DROP_TOOLS_ON_COMPACT,
|
|
108
|
+
maxRecentMessages
|
|
109
|
+
} = {}) {
|
|
88
110
|
if (!payload || typeof payload !== 'object') return payload;
|
|
89
111
|
const out = { ...payload };
|
|
90
112
|
|
|
@@ -104,13 +126,11 @@ function compactResponsesPayload(payload, { dropToolsOnCompact = DEFAULT_DROP_TO
|
|
|
104
126
|
}
|
|
105
127
|
return item;
|
|
106
128
|
});
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
out.input =
|
|
129
|
+
const resolvedMaxRecentMessages = Number.isInteger(maxRecentMessages)
|
|
130
|
+
? Math.max(1, maxRecentMessages)
|
|
131
|
+
: (('previous_response_id' in out) ? 14 : 8);
|
|
132
|
+
out.input = compactInputMessages(normalized, resolvedMaxRecentMessages);
|
|
111
133
|
}
|
|
112
|
-
|
|
113
|
-
if ('previous_response_id' in out) delete out.previous_response_id;
|
|
114
134
|
return out;
|
|
115
135
|
}
|
|
116
136
|
|
|
@@ -127,13 +147,10 @@ async function forward(url, req, headers, body) {
|
|
|
127
147
|
});
|
|
128
148
|
}
|
|
129
149
|
|
|
130
|
-
function buildHeaders(baseHeaders, bodyLen
|
|
150
|
+
function buildHeaders(baseHeaders, bodyLen) {
|
|
131
151
|
const headers = { ...baseHeaders };
|
|
132
152
|
headers['content-type'] = 'application/json';
|
|
133
153
|
headers['content-length'] = String(bodyLen);
|
|
134
|
-
if (apiKeyOverride) {
|
|
135
|
-
headers.authorization = `Bearer ${apiKeyOverride}`;
|
|
136
|
-
}
|
|
137
154
|
return headers;
|
|
138
155
|
}
|
|
139
156
|
|
|
@@ -144,17 +161,16 @@ export async function createProxyServer(options = {}) {
|
|
|
144
161
|
const retryMax = Number(options.retryMax ?? process.env.SUB2API_COMPAT_RETRY_MAX ?? 6);
|
|
145
162
|
const retryBaseMs = Number(options.retryBaseMs ?? process.env.SUB2API_COMPAT_RETRY_BASE_MS ?? 300);
|
|
146
163
|
const userAgent = options.userAgent ?? process.env.SUB2API_COMPAT_USER_AGENT ?? DEFAULT_USER_AGENT;
|
|
147
|
-
const fallbackUpstream = options.fallbackUpstream ?? process.env.SUB2API_FALLBACK_UPSTREAM ?? DEFAULT_FALLBACK_UPSTREAM;
|
|
148
|
-
const fallbackApiKey = options.fallbackApiKey ?? process.env.SUB2API_FALLBACK_API_KEY ?? DEFAULT_FALLBACK_API_KEY;
|
|
149
164
|
const maxReqBytes = Number(options.maxReqBytes ?? process.env.SUB2API_COMPAT_MAX_REQ_BYTES ?? DEFAULT_MAX_REQ_BYTES);
|
|
150
165
|
const dropToolsOnCompact = options.dropToolsOnCompact
|
|
151
166
|
?? ['1', 'true', 'yes', 'on'].includes(String(process.env.SUB2API_COMPAT_DROP_TOOLS_ON_COMPACT || '').toLowerCase());
|
|
167
|
+
const stripPreviousResponseId = options.stripPreviousResponseId
|
|
168
|
+
?? ['1', 'true', 'yes', 'on'].includes(String(process.env.SUB2API_COMPAT_STRIP_PREVIOUS_RESPONSE_ID || '').toLowerCase());
|
|
152
169
|
|
|
153
170
|
const server = http.createServer(async (req, res) => {
|
|
154
171
|
const startAt = Date.now();
|
|
155
172
|
try {
|
|
156
173
|
const url = new URL(req.url || '/', upstream);
|
|
157
|
-
const fallbackUrl = fallbackUpstream ? new URL(req.url || '/', fallbackUpstream) : null;
|
|
158
174
|
const chunks = [];
|
|
159
175
|
for await (const c of req) chunks.push(c);
|
|
160
176
|
const rawBody = Buffer.concat(chunks);
|
|
@@ -167,61 +183,78 @@ export async function createProxyServer(options = {}) {
|
|
|
167
183
|
const isResponsesPath = req.method === 'POST' && url.pathname.endsWith('/responses');
|
|
168
184
|
let body = rawBody;
|
|
169
185
|
let compactBody = null;
|
|
186
|
+
let aggressiveBody = null;
|
|
187
|
+
let minimalBody = null;
|
|
170
188
|
|
|
171
189
|
if (isResponsesPath && rawBody.length > 0) {
|
|
172
190
|
try {
|
|
173
191
|
const parsed = JSON.parse(rawBody.toString('utf8'));
|
|
174
|
-
const normalized = normalizeResponsesPayload(parsed);
|
|
192
|
+
const normalized = normalizeResponsesPayload(parsed, { stripPreviousResponseId });
|
|
175
193
|
body = Buffer.from(JSON.stringify(normalized));
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
194
|
+
if (body.length > maxReqBytes) {
|
|
195
|
+
const compacted = compactResponsesPayload(normalized, { dropToolsOnCompact });
|
|
196
|
+
const compactBuf = Buffer.from(JSON.stringify(compacted));
|
|
197
|
+
if (compactBuf.length + 512 < body.length) {
|
|
198
|
+
compactBody = compactBuf;
|
|
199
|
+
}
|
|
200
|
+
const aggressive = compactResponsesPayload(normalized, {
|
|
201
|
+
dropToolsOnCompact,
|
|
202
|
+
maxRecentMessages: 3
|
|
203
|
+
});
|
|
204
|
+
const aggressiveBuf = Buffer.from(JSON.stringify(aggressive));
|
|
205
|
+
if (aggressiveBuf.length + 256 < body.length) {
|
|
206
|
+
aggressiveBody = aggressiveBuf;
|
|
207
|
+
}
|
|
208
|
+
const minimal = compactResponsesPayload(normalized, {
|
|
209
|
+
dropToolsOnCompact,
|
|
210
|
+
maxRecentMessages: 1
|
|
211
|
+
});
|
|
212
|
+
const minimalBuf = Buffer.from(JSON.stringify(minimal));
|
|
213
|
+
if (minimalBuf.length + 128 < body.length) {
|
|
214
|
+
minimalBody = minimalBuf;
|
|
215
|
+
}
|
|
180
216
|
}
|
|
181
217
|
} catch {
|
|
182
218
|
// Keep original body if parse fails.
|
|
183
219
|
}
|
|
184
220
|
}
|
|
185
221
|
|
|
222
|
+
const requestBodies = [body];
|
|
223
|
+
if (compactBody && !compactBody.equals(body)) {
|
|
224
|
+
requestBodies.push(compactBody);
|
|
225
|
+
}
|
|
226
|
+
if (aggressiveBody && !aggressiveBody.equals(body) && !(compactBody && aggressiveBody.equals(compactBody))) {
|
|
227
|
+
requestBodies.push(aggressiveBody);
|
|
228
|
+
}
|
|
229
|
+
if (
|
|
230
|
+
minimalBody
|
|
231
|
+
&& !minimalBody.equals(body)
|
|
232
|
+
&& !(compactBody && minimalBody.equals(compactBody))
|
|
233
|
+
&& !(aggressiveBody && minimalBody.equals(aggressiveBody))
|
|
234
|
+
) {
|
|
235
|
+
requestBodies.push(minimalBody);
|
|
236
|
+
}
|
|
237
|
+
|
|
186
238
|
let resp;
|
|
187
239
|
let attempts = 0;
|
|
188
|
-
let via = 'primary';
|
|
189
240
|
let primaryStatus = null;
|
|
190
|
-
let fallbackStatus = null;
|
|
191
241
|
let compacted = false;
|
|
242
|
+
let strategy = 'full';
|
|
192
243
|
const maxAttempts = isResponsesPath ? retryMax : 1;
|
|
193
244
|
while (attempts < maxAttempts) {
|
|
194
245
|
attempts += 1;
|
|
195
|
-
const
|
|
196
|
-
const requestBody =
|
|
197
|
-
compacted =
|
|
246
|
+
const strategyIndex = Math.min(requestBodies.length - 1, attempts - 1);
|
|
247
|
+
const requestBody = requestBodies[strategyIndex];
|
|
248
|
+
compacted = strategyIndex > 0;
|
|
249
|
+
strategy = strategyIndex === 0
|
|
250
|
+
? 'full'
|
|
251
|
+
: (strategyIndex === 1 ? 'compact' : (strategyIndex === 2 ? 'aggressive' : 'minimal'));
|
|
198
252
|
const primaryHeaders = isResponsesPath ? buildHeaders(baseHeaders, requestBody.length) : baseHeaders;
|
|
199
|
-
const fallbackHeaders = isResponsesPath
|
|
200
|
-
? buildHeaders(baseHeaders, requestBody.length, fallbackApiKey)
|
|
201
|
-
: (fallbackApiKey ? { ...baseHeaders, authorization: `Bearer ${fallbackApiKey}` } : baseHeaders);
|
|
202
253
|
|
|
203
254
|
const primaryResp = await forward(url, req, primaryHeaders, requestBody);
|
|
204
255
|
primaryStatus = primaryResp.status;
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
via = 'primary';
|
|
208
|
-
break;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (fallbackUrl) {
|
|
212
|
-
const fbResp = await forward(fallbackUrl, req, fallbackHeaders, requestBody);
|
|
213
|
-
fallbackStatus = fbResp.status;
|
|
214
|
-
if (fbResp.status < 500) {
|
|
215
|
-
resp = fbResp;
|
|
216
|
-
via = 'fallback';
|
|
217
|
-
break;
|
|
218
|
-
}
|
|
219
|
-
resp = fbResp;
|
|
220
|
-
via = 'fallback';
|
|
221
|
-
} else {
|
|
222
|
-
resp = primaryResp;
|
|
223
|
-
via = 'primary';
|
|
224
|
-
}
|
|
256
|
+
resp = primaryResp;
|
|
257
|
+
if (primaryResp.status < 500) break;
|
|
225
258
|
|
|
226
259
|
if (attempts < maxAttempts) {
|
|
227
260
|
await sleep(retryBaseMs * attempts);
|
|
@@ -240,12 +273,11 @@ export async function createProxyServer(options = {}) {
|
|
|
240
273
|
path: url.pathname,
|
|
241
274
|
status: resp.status,
|
|
242
275
|
attempts,
|
|
243
|
-
via,
|
|
244
276
|
primaryStatus,
|
|
245
|
-
fallbackStatus,
|
|
246
277
|
compacted,
|
|
278
|
+
strategy,
|
|
247
279
|
reqBytes: rawBody.length,
|
|
248
|
-
upstreamReqBytes:
|
|
280
|
+
upstreamReqBytes: requestBodies[Math.min(requestBodies.length - 1, attempts - 1)].length,
|
|
249
281
|
respBytes: outBuffer.length,
|
|
250
282
|
durationMs: Date.now() - startAt
|
|
251
283
|
});
|
|
@@ -274,7 +306,6 @@ export async function createProxyServer(options = {}) {
|
|
|
274
306
|
server,
|
|
275
307
|
port,
|
|
276
308
|
upstream,
|
|
277
|
-
fallbackUpstream,
|
|
278
309
|
logFile,
|
|
279
310
|
close: () => new Promise((resolve, reject) => server.close((err) => (err ? reject(err) : resolve())))
|
|
280
311
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@autotask/atools-tool",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "ATools CLI for OpenClaw proxy compatibility and interactive model configuration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"start": "node ./bin/cli.mjs serve",
|
|
36
|
-
"config:openclaw": "node ./bin/cli.mjs
|
|
36
|
+
"config:openclaw": "node ./bin/cli.mjs openclaw",
|
|
37
37
|
"install:local": "node ./bin/install.mjs",
|
|
38
38
|
"pack:check": "npm pack"
|
|
39
39
|
},
|