@foxden-app/foxclaw 0.3.4 → 0.3.6
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/.env.example +7 -0
- package/README.md +1 -1
- package/README_EN.md +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.js +16 -9
- package/dist/controller/controller.js +74 -4
- package/dist/main.js +133 -13
- package/docs/troubleshooting.md +31 -2
- package/docs/user-manual.md +1 -1
- package/docs/zh/troubleshooting.md +31 -2
- package/docs/zh/user-manual.md +1 -1
- package/package.json +1 -1
package/.env.example
CHANGED
|
@@ -29,6 +29,13 @@ TELEGRAM_PREVIEW_THROTTLE_MS=800
|
|
|
29
29
|
THREAD_LIST_LIMIT=10
|
|
30
30
|
CODEX_CLI_BIN=/absolute/path/to/codex
|
|
31
31
|
|
|
32
|
+
# Optional: proxy for ChatGPT/Codex backend requests when FoxClaw runs as a service.
|
|
33
|
+
# Put these in the same env file that `foxclaw start` installs into systemd/launchd.
|
|
34
|
+
# HTTP_PROXY=http://127.0.0.1:7890
|
|
35
|
+
# HTTPS_PROXY=http://127.0.0.1:7890
|
|
36
|
+
# ALL_PROXY=socks5://127.0.0.1:7891
|
|
37
|
+
# NO_PROXY=127.0.0.1,localhost
|
|
38
|
+
|
|
32
39
|
# Weixin (iLink): run `foxclaw weixin-login` once, then enable:
|
|
33
40
|
# WX_ENABLED=true
|
|
34
41
|
# Comma-separated iLink user ids allowed to chat (see account JSON linkedIlinkUserId)
|
package/README.md
CHANGED
|
@@ -70,7 +70,7 @@ foxclaw doctor
|
|
|
70
70
|
foxclaw start
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
`foxclaw init` 会创建 `~/.foxclaw/.env`,并在终端里提示填写 Telegram bot token、Telegram 数字用户 ID
|
|
73
|
+
`foxclaw init` 会创建 `~/.foxclaw/.env`,并在终端里提示填写 Telegram bot token、Telegram 数字用户 ID 和默认工作目录。如果当前 shell 里有 `HTTP_PROXY`、`HTTPS_PROXY`、`ALL_PROXY` 等代理变量,也会询问是否写入 FoxClaw 配置,避免服务启动后 Codex 走不到同一条网络。任何一项都可以直接回车跳过,之后再用 `$EDITOR ~/.foxclaw/.env` 手动修改。
|
|
74
74
|
|
|
75
75
|
跑 `doctor` 或 `start` 之前先把 `.env` 填好。私聊模式最小配置:
|
|
76
76
|
|
package/README_EN.md
CHANGED
|
@@ -70,7 +70,7 @@ foxclaw doctor
|
|
|
70
70
|
foxclaw start
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
-
`foxclaw init` creates `~/.foxclaw/.env` and prompts for the Telegram bot token, your numeric Telegram user id, and the default workspace. Press Enter on any field to skip it and edit later with `$EDITOR ~/.foxclaw/.env`.
|
|
73
|
+
`foxclaw init` creates `~/.foxclaw/.env` and prompts for the Telegram bot token, your numeric Telegram user id, and the default workspace. If the current shell has proxy variables such as `HTTP_PROXY`, `HTTPS_PROXY`, or `ALL_PROXY`, it also asks whether to save them into the FoxClaw config so the service uses the same network path as your working Codex CLI. Press Enter on any field to skip it and edit later with `$EDITOR ~/.foxclaw/.env`.
|
|
74
74
|
|
|
75
75
|
Fill `.env` before running `doctor` or `start`. Minimum private-chat config:
|
|
76
76
|
|
package/dist/config.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export declare const DEFAULT_LOCK_PATH: string;
|
|
|
8
8
|
export declare const DEFAULT_CODEX_APP_SERVER_STATE_PATH: string;
|
|
9
9
|
export declare const DEFAULT_CODEX_APP_SERVER_LOG_PATH: string;
|
|
10
10
|
export declare const DEFAULT_ENV_PATH: string;
|
|
11
|
+
export declare function resolveEnvPath(): string;
|
|
12
|
+
export declare function getLoadedEnvPath(): string | null;
|
|
11
13
|
export declare function loadEnv(): void;
|
|
12
14
|
export interface AppConfig {
|
|
13
15
|
tgBotToken: string;
|
package/dist/config.js
CHANGED
|
@@ -12,21 +12,28 @@ export const DEFAULT_CODEX_APP_SERVER_STATE_PATH = path.join(APP_HOME, 'runtime'
|
|
|
12
12
|
export const DEFAULT_CODEX_APP_SERVER_LOG_PATH = path.join(APP_HOME, 'logs', 'codex-app-server.log');
|
|
13
13
|
export const DEFAULT_ENV_PATH = path.join(APP_HOME, '.env');
|
|
14
14
|
let envLoaded = false;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return;
|
|
18
|
-
envLoaded = true;
|
|
15
|
+
let loadedEnvPath = null;
|
|
16
|
+
export function resolveEnvPath() {
|
|
19
17
|
const explicitPath = process.env.FOXCLAW_ENV?.trim();
|
|
20
18
|
if (explicitPath) {
|
|
21
|
-
|
|
22
|
-
return;
|
|
19
|
+
return path.resolve(explicitPath);
|
|
23
20
|
}
|
|
24
21
|
const cwdEnvPath = path.join(process.cwd(), '.env');
|
|
25
22
|
if (fs.existsSync(cwdEnvPath)) {
|
|
26
|
-
|
|
27
|
-
return;
|
|
23
|
+
return cwdEnvPath;
|
|
28
24
|
}
|
|
29
|
-
|
|
25
|
+
return DEFAULT_ENV_PATH;
|
|
26
|
+
}
|
|
27
|
+
export function getLoadedEnvPath() {
|
|
28
|
+
return loadedEnvPath;
|
|
29
|
+
}
|
|
30
|
+
export function loadEnv() {
|
|
31
|
+
if (envLoaded)
|
|
32
|
+
return;
|
|
33
|
+
envLoaded = true;
|
|
34
|
+
const envPath = resolveEnvPath();
|
|
35
|
+
loadedEnvPath = envPath;
|
|
36
|
+
dotenv.config({ path: envPath, override: Boolean(process.env.FOXCLAW_ENV?.trim()) });
|
|
30
37
|
}
|
|
31
38
|
export function loadConfig() {
|
|
32
39
|
loadEnv();
|
|
@@ -7065,6 +7065,9 @@ function cloneAuthRetryContext(context) {
|
|
|
7065
7065
|
};
|
|
7066
7066
|
}
|
|
7067
7067
|
function isCodexAuthRotationError(params) {
|
|
7068
|
+
if (isChatGptBackendAccessBlocked(collectCodexErrorText(params))) {
|
|
7069
|
+
return false;
|
|
7070
|
+
}
|
|
7068
7071
|
const code = stringOrNull(params?.error?.codexErrorInfo) ?? stringOrNull(params?.error?.code);
|
|
7069
7072
|
if (code && /usageLimitExceeded|auth|unauthorized|forbidden|login/i.test(code)) {
|
|
7070
7073
|
return true;
|
|
@@ -7073,15 +7076,77 @@ function isCodexAuthRotationError(params) {
|
|
|
7073
7076
|
return /(usage limit|rate limit|not authenticated|unauthorized|forbidden|sign in|log in|login|auth)/i.test(message);
|
|
7074
7077
|
}
|
|
7075
7078
|
function formatCodexNotificationError(params) {
|
|
7079
|
+
const collected = collectCodexErrorText(params);
|
|
7080
|
+
const known = formatKnownCodexAccessError(collected);
|
|
7081
|
+
if (known) {
|
|
7082
|
+
return known;
|
|
7083
|
+
}
|
|
7076
7084
|
const message = stringOrNull(params?.error?.message);
|
|
7077
7085
|
if (message) {
|
|
7078
|
-
return message;
|
|
7086
|
+
return clipUserFacingError(cleanUserFacingError(message));
|
|
7079
7087
|
}
|
|
7080
7088
|
const code = stringOrNull(params?.error?.codexErrorInfo) ?? stringOrNull(params?.error?.code);
|
|
7081
7089
|
if (code) {
|
|
7082
|
-
return code;
|
|
7090
|
+
return clipUserFacingError(cleanUserFacingError(code));
|
|
7091
|
+
}
|
|
7092
|
+
return clipUserFacingError(cleanUserFacingError(JSON.stringify(params?.error ?? params ?? {})));
|
|
7093
|
+
}
|
|
7094
|
+
function collectCodexErrorText(params) {
|
|
7095
|
+
const parts = [
|
|
7096
|
+
stringOrNull(params?.error?.message),
|
|
7097
|
+
stringOrNull(params?.error?.additionalDetails),
|
|
7098
|
+
stringOrNull(params?.additionalDetails),
|
|
7099
|
+
stringOrNull(params?.error?.codexErrorInfo),
|
|
7100
|
+
stringOrNull(params?.error?.code),
|
|
7101
|
+
].filter((part) => part !== null);
|
|
7102
|
+
if (parts.length > 0) {
|
|
7103
|
+
return parts.join(' ');
|
|
7104
|
+
}
|
|
7105
|
+
try {
|
|
7106
|
+
return JSON.stringify(params?.error ?? params ?? {});
|
|
7107
|
+
}
|
|
7108
|
+
catch {
|
|
7109
|
+
return String(params?.error ?? params ?? '');
|
|
7083
7110
|
}
|
|
7084
|
-
|
|
7111
|
+
}
|
|
7112
|
+
function formatKnownCodexAccessError(raw) {
|
|
7113
|
+
if (!isChatGptBackendAccessBlocked(raw)) {
|
|
7114
|
+
return null;
|
|
7115
|
+
}
|
|
7116
|
+
const ray = raw.match(/\bcf-ray:\s*([A-Za-z0-9-]+)/i)?.[1]
|
|
7117
|
+
?? raw.match(/\bRay ID:\s*([A-Za-z0-9-]+)/i)?.[1]
|
|
7118
|
+
?? null;
|
|
7119
|
+
return `ChatGPT backend 403 Forbidden: service network/proxy/IP is blocked, not necessarily auth.json invalid. Check HTTP_PROXY/HTTPS_PROXY in the FoxClaw env file and restart FoxClaw${ray ? ` (cf-ray: ${ray})` : ''}.`;
|
|
7120
|
+
}
|
|
7121
|
+
function isChatGptBackendAccessBlocked(raw) {
|
|
7122
|
+
const value = raw.toLowerCase();
|
|
7123
|
+
const targetsChatGptBackend = value.includes('chatgpt.com/backend-api')
|
|
7124
|
+
|| value.includes('wss://chatgpt.com/backend-api');
|
|
7125
|
+
const isForbidden = value.includes('403 forbidden')
|
|
7126
|
+
|| value.includes('status 403')
|
|
7127
|
+
|| value.includes('httpstatuscode":403')
|
|
7128
|
+
|| value.includes('httpstatuscode:403');
|
|
7129
|
+
const looksLikeHtmlBlock = value.includes('<html')
|
|
7130
|
+
|| value.includes('text/html')
|
|
7131
|
+
|| value.includes('cf-ray')
|
|
7132
|
+
|| value.includes('unable to load site')
|
|
7133
|
+
|| value.includes('if you are using a vpn');
|
|
7134
|
+
return targetsChatGptBackend && isForbidden && looksLikeHtmlBlock;
|
|
7135
|
+
}
|
|
7136
|
+
function cleanUserFacingError(raw) {
|
|
7137
|
+
return raw
|
|
7138
|
+
.replace(/<script\b[\s\S]*?<\/script>/gi, ' ')
|
|
7139
|
+
.replace(/<style\b[\s\S]*?<\/style>/gi, ' ')
|
|
7140
|
+
.replace(/<[^>]+>/g, ' ')
|
|
7141
|
+
.replace(/\s+/g, ' ')
|
|
7142
|
+
.trim();
|
|
7143
|
+
}
|
|
7144
|
+
function clipUserFacingError(raw, limit = 900) {
|
|
7145
|
+
const cleaned = raw.trim();
|
|
7146
|
+
if (cleaned.length <= limit) {
|
|
7147
|
+
return cleaned;
|
|
7148
|
+
}
|
|
7149
|
+
return `${cleaned.slice(0, limit - 3)}...`;
|
|
7085
7150
|
}
|
|
7086
7151
|
function parseUserInputQuestions(params) {
|
|
7087
7152
|
const rawQuestions = Array.isArray(params?.questions)
|
|
@@ -7484,7 +7549,12 @@ function formatLocalTimestamp(seconds) {
|
|
|
7484
7549
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
7485
7550
|
}
|
|
7486
7551
|
function formatShortStatusError(error) {
|
|
7487
|
-
const
|
|
7552
|
+
const raw = formatUserError(error);
|
|
7553
|
+
const known = formatKnownCodexAccessError(raw);
|
|
7554
|
+
if (known) {
|
|
7555
|
+
return known.length > 120 ? `${known.slice(0, 117)}...` : known;
|
|
7556
|
+
}
|
|
7557
|
+
const message = cleanUserFacingError(raw);
|
|
7488
7558
|
return message.length > 120 ? `${message.slice(0, 117)}...` : message;
|
|
7489
7559
|
}
|
|
7490
7560
|
function isThreadNotFoundError(error) {
|
package/dist/main.js
CHANGED
|
@@ -5,13 +5,23 @@ import process from 'node:process';
|
|
|
5
5
|
import { createInterface } from 'node:readline/promises';
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import { APP_HOME, DEFAULT_ENV_PATH, DEFAULT_LOG_PATH, DEFAULT_STATUS_PATH, loadConfig, loadEnv, } from './config.js';
|
|
8
|
+
import { APP_HOME, DEFAULT_ENV_PATH, DEFAULT_LOG_PATH, DEFAULT_STATUS_PATH, getLoadedEnvPath, loadConfig, loadEnv, } from './config.js';
|
|
9
9
|
import { acquireProcessLock, LockHeldError } from './lock.js';
|
|
10
10
|
import { readRuntimeStatus, writeRuntimeStatus } from './runtime.js';
|
|
11
11
|
const command = process.argv[2] || 'serve';
|
|
12
12
|
loadEnv();
|
|
13
13
|
const packageRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
14
14
|
const entryPoint = fileURLToPath(import.meta.url);
|
|
15
|
+
const PROXY_ENV_KEYS = [
|
|
16
|
+
'HTTP_PROXY',
|
|
17
|
+
'HTTPS_PROXY',
|
|
18
|
+
'ALL_PROXY',
|
|
19
|
+
'NO_PROXY',
|
|
20
|
+
'http_proxy',
|
|
21
|
+
'https_proxy',
|
|
22
|
+
'all_proxy',
|
|
23
|
+
'no_proxy',
|
|
24
|
+
];
|
|
15
25
|
async function main() {
|
|
16
26
|
if (command === 'init') {
|
|
17
27
|
await initConfig();
|
|
@@ -130,6 +140,9 @@ async function runServeCli() {
|
|
|
130
140
|
updatedAt: new Date().toISOString(),
|
|
131
141
|
channels: { telegram: false, weixin: false },
|
|
132
142
|
});
|
|
143
|
+
await app.stop({ terminateServer: true }).catch((error) => {
|
|
144
|
+
logger.warn('codex.app-server.stop_failed', { error: serializeError(error) });
|
|
145
|
+
});
|
|
133
146
|
store?.close();
|
|
134
147
|
processLock.release();
|
|
135
148
|
process.exit(0);
|
|
@@ -157,6 +170,7 @@ async function initConfig() {
|
|
|
157
170
|
console.log(`Created ${envPath}`);
|
|
158
171
|
}
|
|
159
172
|
if (!canPromptForInit()) {
|
|
173
|
+
printProxyEnvHint(envPath);
|
|
160
174
|
console.log(`Edit it manually, then run: foxclaw doctor`);
|
|
161
175
|
return;
|
|
162
176
|
}
|
|
@@ -171,6 +185,7 @@ async function configureEnvInteractively(envPath, existed) {
|
|
|
171
185
|
if (existed) {
|
|
172
186
|
const updateExisting = (await rl.question('Update Telegram/workspace setup fields now? [y/N]: ')).trim().toLowerCase();
|
|
173
187
|
if (updateExisting !== 'y' && updateExisting !== 'yes') {
|
|
188
|
+
await maybeSaveProxyEnvFromShell(rl, envPath);
|
|
174
189
|
console.log(`Edit it manually, then run: foxclaw doctor`);
|
|
175
190
|
return;
|
|
176
191
|
}
|
|
@@ -181,6 +196,7 @@ async function configureEnvInteractively(envPath, existed) {
|
|
|
181
196
|
const updates = {};
|
|
182
197
|
const skipped = [];
|
|
183
198
|
const warnings = [];
|
|
199
|
+
Object.assign(updates, await maybeSaveProxyEnvFromShell(rl, envPath));
|
|
184
200
|
const token = sanitizeEnvInput(await rl.question('Telegram bot token (TG_BOT_TOKEN): '));
|
|
185
201
|
if (token) {
|
|
186
202
|
updates.TG_BOT_TOKEN = token;
|
|
@@ -304,6 +320,64 @@ function writeEnvUpdates(envPath, updates) {
|
|
|
304
320
|
}
|
|
305
321
|
fs.writeFileSync(envPath, text);
|
|
306
322
|
}
|
|
323
|
+
async function maybeSaveProxyEnvFromShell(rl, envPath) {
|
|
324
|
+
const proxyUpdates = detectMissingProxyEnv(envPath);
|
|
325
|
+
const keys = Object.keys(proxyUpdates);
|
|
326
|
+
if (keys.length === 0) {
|
|
327
|
+
return {};
|
|
328
|
+
}
|
|
329
|
+
console.log(`Detected proxy env in this shell: ${keys.join(', ')}`);
|
|
330
|
+
const answer = (await rl.question('Save these proxy settings to FoxClaw .env for service use? [Y/n]: ')).trim().toLowerCase();
|
|
331
|
+
if (answer === 'n' || answer === 'no') {
|
|
332
|
+
console.log(`Skipped proxy env. Add it to ${envPath} if ChatGPT access needs a proxy.`);
|
|
333
|
+
return {};
|
|
334
|
+
}
|
|
335
|
+
writeEnvUpdates(envPath, proxyUpdates);
|
|
336
|
+
console.log(`Saved ${keys.join(', ')} to ${envPath}`);
|
|
337
|
+
return proxyUpdates;
|
|
338
|
+
}
|
|
339
|
+
function printProxyEnvHint(envPath) {
|
|
340
|
+
const proxyUpdates = detectMissingProxyEnv(envPath);
|
|
341
|
+
const keys = Object.keys(proxyUpdates);
|
|
342
|
+
if (keys.length === 0) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
console.log(`[WARN] Proxy env detected in this shell but missing from ${envPath}: ${keys.join(', ')}`);
|
|
346
|
+
console.log(`[WARN] Add those proxy variables to ${envPath} if ChatGPT/Codex needs a proxy.`);
|
|
347
|
+
}
|
|
348
|
+
function detectMissingProxyEnv(envPath) {
|
|
349
|
+
const existing = readEnvFileKeys(envPath);
|
|
350
|
+
const existingCanonical = new Set(Array.from(existing, canonicalProxyEnvKey));
|
|
351
|
+
const updates = {};
|
|
352
|
+
for (const key of PROXY_ENV_KEYS) {
|
|
353
|
+
const value = process.env[key]?.trim();
|
|
354
|
+
if (!value || existing.has(key) || existingCanonical.has(canonicalProxyEnvKey(key))) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
updates[key] = value;
|
|
358
|
+
}
|
|
359
|
+
return updates;
|
|
360
|
+
}
|
|
361
|
+
function canonicalProxyEnvKey(key) {
|
|
362
|
+
return key.toUpperCase();
|
|
363
|
+
}
|
|
364
|
+
function readEnvFileKeys(envPath) {
|
|
365
|
+
const keys = new Set();
|
|
366
|
+
let text = '';
|
|
367
|
+
try {
|
|
368
|
+
text = fs.readFileSync(envPath, 'utf8');
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
return keys;
|
|
372
|
+
}
|
|
373
|
+
for (const line of text.split(/\r?\n/)) {
|
|
374
|
+
const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/);
|
|
375
|
+
if (match?.[1]) {
|
|
376
|
+
keys.add(match[1]);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return keys;
|
|
380
|
+
}
|
|
307
381
|
function formatEnvValue(value) {
|
|
308
382
|
const cleaned = value.replace(/[\r\n]/g, '').trim();
|
|
309
383
|
if (!/[\s#"\\]/.test(cleaned))
|
|
@@ -373,8 +447,52 @@ function runDoctorChecks() {
|
|
|
373
447
|
console.log(`[FAIL] default cwd missing: ${cwd}`);
|
|
374
448
|
passed = false;
|
|
375
449
|
}
|
|
450
|
+
warnIfProxyEnvMissingFromLoadedEnv();
|
|
451
|
+
warnIfInstalledServiceNodeLooksWrong();
|
|
376
452
|
return passed;
|
|
377
453
|
}
|
|
454
|
+
function warnIfProxyEnvMissingFromLoadedEnv() {
|
|
455
|
+
const envPath = serviceEnvPath();
|
|
456
|
+
const proxyUpdates = detectMissingProxyEnv(envPath);
|
|
457
|
+
const keys = Object.keys(proxyUpdates);
|
|
458
|
+
if (keys.length === 0) {
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
console.log(`[WARN] proxy env is present in this shell but missing from ${envPath}: ${keys.join(', ')}`);
|
|
462
|
+
console.log(`[WARN] systemd/launchd services do not inherit your shell; add those variables to the FoxClaw env file if Codex needs a proxy.`);
|
|
463
|
+
}
|
|
464
|
+
function warnIfInstalledServiceNodeLooksWrong() {
|
|
465
|
+
if (process.platform !== 'linux') {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const unitPath = path.join(process.env.XDG_CONFIG_HOME || path.join(process.env.HOME || '', '.config'), 'systemd', 'user', 'foxclaw.service');
|
|
469
|
+
let text = '';
|
|
470
|
+
try {
|
|
471
|
+
text = fs.readFileSync(unitPath, 'utf8');
|
|
472
|
+
}
|
|
473
|
+
catch {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
const execStart = text.match(/^ExecStart=(.+)$/m)?.[1]?.trim();
|
|
477
|
+
const nodePath = execStart ? systemdUnescape(execStart.split(/\s+/)[0] ?? '') : '';
|
|
478
|
+
if (!nodePath) {
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (!fs.existsSync(nodePath)) {
|
|
482
|
+
console.log(`[WARN] installed service node is missing: ${nodePath}`);
|
|
483
|
+
console.log('[WARN] Run foxclaw start from a Node 24 shell to refresh the service unit.');
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const result = spawnSync(nodePath, ['-p', 'process.versions.node'], { encoding: 'utf8' });
|
|
487
|
+
const version = result.status === 0 ? result.stdout.trim() : '';
|
|
488
|
+
const major = Number.parseInt(version.split('.')[0] ?? '', 10);
|
|
489
|
+
if (Number.isFinite(major) && major >= 24) {
|
|
490
|
+
console.log(`[OK] service node >= 24: ${nodePath}`);
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
console.log(`[WARN] installed service node is older than 24: ${nodePath}${version ? ` (${version})` : ''}`);
|
|
494
|
+
console.log('[WARN] Run foxclaw start from a Node 24 shell to refresh the service unit.');
|
|
495
|
+
}
|
|
378
496
|
function installSystemd() {
|
|
379
497
|
if (!hasCommand('systemctl')) {
|
|
380
498
|
console.error('systemctl not found (need systemd)');
|
|
@@ -383,16 +501,14 @@ function installSystemd() {
|
|
|
383
501
|
const unitName = 'foxclaw.service';
|
|
384
502
|
const userSystemdDir = path.join(process.env.XDG_CONFIG_HOME || path.join(process.env.HOME || '', '.config'), 'systemd', 'user');
|
|
385
503
|
const unitPath = path.join(userSystemdDir, unitName);
|
|
386
|
-
const
|
|
504
|
+
const envPath = serviceEnvPath();
|
|
505
|
+
const configDir = path.dirname(envPath);
|
|
387
506
|
const nodeBin = process.execPath;
|
|
388
507
|
const nodeDir = path.dirname(nodeBin);
|
|
389
508
|
const pathValue = buildServicePath(nodeDir);
|
|
390
509
|
fs.mkdirSync(userSystemdDir, { recursive: true });
|
|
391
510
|
fs.mkdirSync(configDir, { recursive: true });
|
|
392
511
|
fs.mkdirSync(path.join(APP_HOME, 'logs'), { recursive: true });
|
|
393
|
-
const foxclawEnvLine = process.env.FOXCLAW_ENV?.trim()
|
|
394
|
-
? `Environment=FOXCLAW_ENV=${systemdEscape(process.env.FOXCLAW_ENV.trim())}\n`
|
|
395
|
-
: '';
|
|
396
512
|
fs.writeFileSync(unitPath, `[Unit]
|
|
397
513
|
Description=FoxClaw local Codex execution bridge
|
|
398
514
|
Documentation=https://github.com/foxden-app/foxclaw
|
|
@@ -408,7 +524,8 @@ Environment=HOME=${systemdEscape(process.env.HOME || '')}
|
|
|
408
524
|
Environment=USER=${systemdEscape(process.env.USER || '')}
|
|
409
525
|
Environment=LOGNAME=${systemdEscape(process.env.LOGNAME || process.env.USER || '')}
|
|
410
526
|
Environment=PATH=${systemdEscape(pathValue)}
|
|
411
|
-
|
|
527
|
+
Environment=FOXCLAW_ENV=${systemdEscape(envPath)}
|
|
528
|
+
ExecStart=${systemdEscape(nodeBin)} ${systemdEscape(entryPoint)} serve
|
|
412
529
|
Restart=always
|
|
413
530
|
RestartSec=10
|
|
414
531
|
TimeoutStopSec=45
|
|
@@ -456,15 +573,11 @@ function installLaunchd() {
|
|
|
456
573
|
}
|
|
457
574
|
const home = process.env.HOME || '';
|
|
458
575
|
const plist = path.join(home, 'Library', 'LaunchAgents', 'app.foxden.foxclaw.plist');
|
|
459
|
-
const
|
|
576
|
+
const envPath = serviceEnvPath();
|
|
577
|
+
const configDir = path.dirname(envPath);
|
|
460
578
|
fs.mkdirSync(path.dirname(plist), { recursive: true });
|
|
461
579
|
fs.mkdirSync(configDir, { recursive: true });
|
|
462
580
|
fs.mkdirSync(path.join(APP_HOME, 'logs'), { recursive: true });
|
|
463
|
-
const foxclawEnvXml = process.env.FOXCLAW_ENV?.trim()
|
|
464
|
-
? ` <key>FOXCLAW_ENV</key>
|
|
465
|
-
<string>${xmlEscape(process.env.FOXCLAW_ENV.trim())}</string>
|
|
466
|
-
`
|
|
467
|
-
: '';
|
|
468
581
|
fs.writeFileSync(plist, `<?xml version="1.0" encoding="UTF-8"?>
|
|
469
582
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
470
583
|
<plist version="1.0">
|
|
@@ -489,7 +602,8 @@ function installLaunchd() {
|
|
|
489
602
|
<string>${xmlEscape(process.env.USER || '')}</string>
|
|
490
603
|
<key>LOGNAME</key>
|
|
491
604
|
<string>${xmlEscape(process.env.LOGNAME || process.env.USER || '')}</string>
|
|
492
|
-
|
|
605
|
+
<key>FOXCLAW_ENV</key>
|
|
606
|
+
<string>${xmlEscape(envPath)}</string>
|
|
493
607
|
</dict>
|
|
494
608
|
<key>RunAtLoad</key>
|
|
495
609
|
<true/>
|
|
@@ -532,6 +646,9 @@ function buildServicePath(nodeDir) {
|
|
|
532
646
|
];
|
|
533
647
|
return parts.filter((part, index) => part && parts.indexOf(part) === index).join(':');
|
|
534
648
|
}
|
|
649
|
+
function serviceEnvPath() {
|
|
650
|
+
return path.resolve(process.env.FOXCLAW_ENV?.trim() || getLoadedEnvPath() || DEFAULT_ENV_PATH);
|
|
651
|
+
}
|
|
535
652
|
function spawnChecked(commandName, args) {
|
|
536
653
|
const result = spawnSync(commandName, args, { stdio: 'inherit' });
|
|
537
654
|
if (result.status !== 0) {
|
|
@@ -541,6 +658,9 @@ function spawnChecked(commandName, args) {
|
|
|
541
658
|
function systemdEscape(value) {
|
|
542
659
|
return value.replace(/\\/g, '\\\\').replace(/ /g, '\\x20');
|
|
543
660
|
}
|
|
661
|
+
function systemdUnescape(value) {
|
|
662
|
+
return value.replace(/\\x20/g, ' ').replace(/\\\\/g, '\\');
|
|
663
|
+
}
|
|
544
664
|
function xmlEscape(value) {
|
|
545
665
|
return value
|
|
546
666
|
.replace(/&/g, '&')
|
package/docs/troubleshooting.md
CHANGED
|
@@ -182,9 +182,38 @@ Bridge logs are stored here:
|
|
|
182
182
|
tail -f ~/.foxclaw/logs/service.log
|
|
183
183
|
```
|
|
184
184
|
|
|
185
|
+
## ChatGPT Backend 403 Or Unable To Load Site
|
|
186
|
+
|
|
187
|
+
If Telegram shows `ChatGPT backend 403 Forbidden`, or the app-server log contains `Unable to load site`, `cf-ray`, or `chatgpt.com/backend-api`, the auth file is not necessarily broken. The service process is usually reaching ChatGPT with the wrong network/proxy/IP.
|
|
188
|
+
|
|
189
|
+
A common cause is that your shell or project `.env` has proxy variables, while the systemd/launchd service reads a different env file. Check the env file installed into the service:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
systemctl --user cat foxclaw.service
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
`foxclaw init` detects proxy environment variables in the current shell and asks whether to save them into the FoxClaw `.env`. If you skipped that step, `foxclaw doctor` warns when it sees proxy variables in the shell but not in the FoxClaw env file.
|
|
196
|
+
|
|
197
|
+
Make sure the file referenced by `Environment=FOXCLAW_ENV=...` contains your proxy variables, for example:
|
|
198
|
+
|
|
199
|
+
```dotenv
|
|
200
|
+
HTTP_PROXY=http://127.0.0.1:20171
|
|
201
|
+
HTTPS_PROXY=http://127.0.0.1:20171
|
|
202
|
+
ALL_PROXY=socks5://127.0.0.1:20170
|
|
203
|
+
NO_PROXY=127.0.0.1,localhost
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Restart FoxClaw after editing. The restart also restarts the managed Codex app-server so the new proxy environment takes effect:
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
foxclaw restart
|
|
210
|
+
```
|
|
211
|
+
|
|
185
212
|
## Service Starts With The Wrong Node Version
|
|
186
213
|
|
|
187
|
-
The systemd installer
|
|
214
|
+
The systemd installer records the absolute path of the Node process that is currently running FoxClaw. It does not rely on systemd loading `nvm.sh`. If you manage multiple Node versions with nvm, run `foxclaw start` from a Node 24 shell and the service will keep using that Node 24 path.
|
|
215
|
+
|
|
216
|
+
If you installed the service from a shell using Node 22 or older, reinstall it from a Node 24 shell:
|
|
188
217
|
|
|
189
218
|
```bash
|
|
190
219
|
nvm use 24
|
|
@@ -192,7 +221,7 @@ foxclaw start
|
|
|
192
221
|
systemctl --user status foxclaw.service
|
|
193
222
|
```
|
|
194
223
|
|
|
195
|
-
The status output should show a Node 24 path in `ExecStart`.
|
|
224
|
+
The status output should show a Node 24 path in `ExecStart`. `foxclaw doctor` also checks the installed service Node path and warns if it is missing or older than 24.
|
|
196
225
|
|
|
197
226
|
## Does It Run After Reboot?
|
|
198
227
|
|
package/docs/user-manual.md
CHANGED
|
@@ -100,7 +100,7 @@ Both install the same published npm package. Use one global package manager cons
|
|
|
100
100
|
|
|
101
101
|
### 1.6 Fill In The Config
|
|
102
102
|
|
|
103
|
-
`foxclaw init` creates the default config file at `~/.foxclaw/.env` and prompts for the Telegram bot token, your numeric Telegram user id, and the default workspace. Press Enter on any field to skip it, then edit manually if needed:
|
|
103
|
+
`foxclaw init` creates the default config file at `~/.foxclaw/.env` and prompts for the Telegram bot token, your numeric Telegram user id, and the default workspace. If the current shell has proxy variables such as `HTTP_PROXY`, `HTTPS_PROXY`, or `ALL_PROXY`, it also asks whether to save them into the FoxClaw config so the service-side Codex app-server uses the same network. Press Enter on any field to skip it, then edit manually if needed:
|
|
104
104
|
|
|
105
105
|
```bash
|
|
106
106
|
$EDITOR ~/.foxclaw/.env
|
|
@@ -183,9 +183,38 @@ Bridge 日志默认在:
|
|
|
183
183
|
tail -f ~/.foxclaw/logs/service.log
|
|
184
184
|
```
|
|
185
185
|
|
|
186
|
+
## ChatGPT 后端 403 或 Unable to load site
|
|
187
|
+
|
|
188
|
+
如果 Telegram 里看到 `ChatGPT backend 403 Forbidden`,或者 app-server 日志里出现 `Unable to load site`、`cf-ray`、`chatgpt.com/backend-api`,通常不是 `auth.json` 文件坏了,而是服务进程访问 ChatGPT 后端时没有走正确网络。
|
|
189
|
+
|
|
190
|
+
常见原因是:你在 shell 里配置了代理,或者项目 `.env` 里有代理,但 systemd/launchd 服务实际读的是另一个 env 文件。先看服务用的是哪个 env:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
systemctl --user cat foxclaw.service
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`foxclaw init` 会检测当前 shell 里的代理环境变量,并询问是否保存到 FoxClaw `.env`。如果你跳过了这一步,`foxclaw doctor` 会在发现“shell 有代理,但 FoxClaw env 没有代理”时给出 `[WARN]`。
|
|
197
|
+
|
|
198
|
+
确认 `Environment=FOXCLAW_ENV=...` 指向的文件里有你的代理配置,例如:
|
|
199
|
+
|
|
200
|
+
```dotenv
|
|
201
|
+
HTTP_PROXY=http://127.0.0.1:20171
|
|
202
|
+
HTTPS_PROXY=http://127.0.0.1:20171
|
|
203
|
+
ALL_PROXY=socks5://127.0.0.1:20170
|
|
204
|
+
NO_PROXY=127.0.0.1,localhost
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
改完后重启 FoxClaw。重启会同时重启托管的 Codex app-server,让新代理生效:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
foxclaw restart
|
|
211
|
+
```
|
|
212
|
+
|
|
186
213
|
## 服务用了错误的 Node 版本
|
|
187
214
|
|
|
188
|
-
systemd
|
|
215
|
+
systemd 安装脚本会记录当时正在运行的 Node 绝对路径,不依赖 systemd 去加载 `nvm.sh`。如果你用 nvm 管多个 Node 版本,原则是:从 Node 24 的 shell 里执行 `foxclaw start`,服务之后就固定使用这个 Node 24 路径。
|
|
216
|
+
|
|
217
|
+
如果你从 Node 22 或更旧版本的 shell 里安装过服务,请从 Node 24 的 shell 重新安装:
|
|
189
218
|
|
|
190
219
|
```bash
|
|
191
220
|
nvm use 24
|
|
@@ -193,7 +222,7 @@ foxclaw start
|
|
|
193
222
|
systemctl --user status foxclaw.service
|
|
194
223
|
```
|
|
195
224
|
|
|
196
|
-
状态输出里应该能看到 Node 24
|
|
225
|
+
状态输出里应该能看到 Node 24 的路径。`foxclaw doctor` 也会检查已安装服务里的 Node 路径,如果发现路径不存在或版本低于 24,会提示重新运行 `foxclaw start`。
|
|
197
226
|
|
|
198
227
|
## 重启后是否会自动运行
|
|
199
228
|
|
package/docs/zh/user-manual.md
CHANGED
|
@@ -100,7 +100,7 @@ foxclaw init
|
|
|
100
100
|
|
|
101
101
|
### 1.6 填写配置
|
|
102
102
|
|
|
103
|
-
`foxclaw init` 会创建默认配置文件 `~/.foxclaw/.env`,并提示填写 Telegram bot token、Telegram 数字用户 ID
|
|
103
|
+
`foxclaw init` 会创建默认配置文件 `~/.foxclaw/.env`,并提示填写 Telegram bot token、Telegram 数字用户 ID 和默认工作目录。如果当前 shell 里有 `HTTP_PROXY`、`HTTPS_PROXY`、`ALL_PROXY` 等代理变量,它也会询问是否写入 FoxClaw 配置,让服务里的 Codex app-server 使用同样的网络。任何一项都可以直接回车跳过,之后再手动编辑:
|
|
104
104
|
|
|
105
105
|
```bash
|
|
106
106
|
$EDITOR ~/.foxclaw/.env
|