@foxden-app/foxclaw 0.3.11 → 0.3.13
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 +4 -1
- package/dist/codex_app/local_usage.d.ts +8 -0
- package/dist/codex_app/local_usage.js +73 -2
- package/dist/controller/controller.d.ts +2 -0
- package/dist/controller/controller.js +36 -1
- package/dist/controller/presentation.d.ts +2 -1
- package/dist/controller/presentation.js +60 -0
- package/dist/i18n.d.ts +8 -0
- package/dist/i18n.js +8 -0
- package/dist/main.js +90 -7
- package/dist/systemd.d.ts +5 -0
- package/dist/systemd.js +58 -0
- package/docs/troubleshooting.md +16 -0
- package/docs/user-manual.md +1 -1
- package/docs/zh/troubleshooting.md +16 -0
- package/docs/zh/user-manual.md +1 -1
- package/package.json +1 -1
- package/skills/foxclaw/SKILL.md +1 -0
package/.env.example
CHANGED
|
@@ -29,12 +29,15 @@ 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
|
|
32
|
+
# Optional: standard HTTP(S) proxy for Telegram and ChatGPT/Codex backend requests.
|
|
33
33
|
# Put these in the same env file that `foxclaw start` installs into systemd/launchd.
|
|
34
|
+
# FoxClaw passes them to the service and enables Node's env proxy support.
|
|
34
35
|
# HTTP_PROXY=http://127.0.0.1:7890
|
|
35
36
|
# HTTPS_PROXY=http://127.0.0.1:7890
|
|
36
37
|
# ALL_PROXY=socks5://127.0.0.1:7891
|
|
37
38
|
# NO_PROXY=127.0.0.1,localhost
|
|
39
|
+
# Optional Linux-only fallback when a service must run through proxychains4.
|
|
40
|
+
# FOXCLAW_PROXYCHAINS_CONF=/absolute/path/to/proxychains.conf
|
|
38
41
|
|
|
39
42
|
# Weixin (iLink): run `foxclaw weixin-login` once, then enable:
|
|
40
43
|
# WX_ENABLED=true
|
|
@@ -11,6 +11,14 @@ export interface CodexLocalUsageStats {
|
|
|
11
11
|
turns: number;
|
|
12
12
|
usageEvents: number;
|
|
13
13
|
totals: CodexLocalUsageTotals;
|
|
14
|
+
outputSpeed: CodexLocalOutputSpeedStats;
|
|
14
15
|
latestSessionMtimeMs: number | null;
|
|
15
16
|
}
|
|
17
|
+
export interface CodexLocalOutputSpeedStats {
|
|
18
|
+
samples: number;
|
|
19
|
+
outputTokens: number;
|
|
20
|
+
seconds: number;
|
|
21
|
+
latestTokensPerSecond: number | null;
|
|
22
|
+
latestSampleAtMs: number | null;
|
|
23
|
+
}
|
|
16
24
|
export declare function readCodexLocalUsageStats(codexHome?: string): Promise<CodexLocalUsageStats>;
|
|
@@ -13,6 +13,7 @@ export async function readCodexLocalUsageStats(codexHome = resolveCodexHome()) {
|
|
|
13
13
|
let sessionsWithUsage = 0;
|
|
14
14
|
let usageEvents = 0;
|
|
15
15
|
let latestSessionMtimeMs = null;
|
|
16
|
+
const outputSpeed = emptyOutputSpeedStats();
|
|
16
17
|
for (const filePath of sessionFiles) {
|
|
17
18
|
const stat = await fs.stat(filePath).catch(() => null);
|
|
18
19
|
if (stat) {
|
|
@@ -20,6 +21,7 @@ export async function readCodexLocalUsageStats(codexHome = resolveCodexHome()) {
|
|
|
20
21
|
}
|
|
21
22
|
const fileUsage = await readSessionUsage(filePath, turnIds);
|
|
22
23
|
usageEvents += fileUsage.usageEvents;
|
|
24
|
+
addOutputSpeed(outputSpeed, fileUsage.outputSpeed);
|
|
23
25
|
if (fileUsage.totalUsage) {
|
|
24
26
|
sessionsWithUsage += 1;
|
|
25
27
|
addUsage(totals, fileUsage.totalUsage);
|
|
@@ -31,6 +33,7 @@ export async function readCodexLocalUsageStats(codexHome = resolveCodexHome()) {
|
|
|
31
33
|
turns: turnIds.size,
|
|
32
34
|
usageEvents,
|
|
33
35
|
totals,
|
|
36
|
+
outputSpeed,
|
|
34
37
|
latestSessionMtimeMs,
|
|
35
38
|
};
|
|
36
39
|
}
|
|
@@ -66,6 +69,8 @@ async function readSessionUsage(filePath, turnIds) {
|
|
|
66
69
|
const reader = readline.createInterface({ input: stream, crlfDelay: Infinity });
|
|
67
70
|
let totalUsage = null;
|
|
68
71
|
let usageEvents = 0;
|
|
72
|
+
let generationStartMs = null;
|
|
73
|
+
const outputSpeed = emptyOutputSpeedStats();
|
|
69
74
|
try {
|
|
70
75
|
for await (const line of reader) {
|
|
71
76
|
if (!line.trim())
|
|
@@ -77,13 +82,22 @@ async function readSessionUsage(filePath, turnIds) {
|
|
|
77
82
|
catch {
|
|
78
83
|
continue;
|
|
79
84
|
}
|
|
85
|
+
const timestampMs = parseTimestampMs(event?.timestamp);
|
|
80
86
|
const turnId = typeof event?.payload?.turn_id === 'string' ? event.payload.turn_id : null;
|
|
81
87
|
if (turnId) {
|
|
82
88
|
turnIds.add(turnId);
|
|
83
89
|
}
|
|
90
|
+
if (timestampMs !== null && isGenerationBoundary(event)) {
|
|
91
|
+
generationStartMs = timestampMs;
|
|
92
|
+
}
|
|
84
93
|
const info = event?.payload?.info;
|
|
85
|
-
|
|
94
|
+
const lastTokenUsage = info?.last_token_usage ?? info?.lastTokenUsage;
|
|
95
|
+
if (lastTokenUsage) {
|
|
86
96
|
usageEvents += 1;
|
|
97
|
+
addOutputSpeedSample(outputSpeed, lastTokenUsage, generationStartMs, timestampMs);
|
|
98
|
+
if (timestampMs !== null) {
|
|
99
|
+
generationStartMs = timestampMs;
|
|
100
|
+
}
|
|
87
101
|
}
|
|
88
102
|
const totalTokenUsage = info?.total_token_usage ?? info?.totalTokenUsage;
|
|
89
103
|
if (totalTokenUsage) {
|
|
@@ -94,7 +108,7 @@ async function readSessionUsage(filePath, turnIds) {
|
|
|
94
108
|
finally {
|
|
95
109
|
reader.close();
|
|
96
110
|
}
|
|
97
|
-
return { usageEvents, totalUsage };
|
|
111
|
+
return { usageEvents, totalUsage, outputSpeed };
|
|
98
112
|
}
|
|
99
113
|
function emptyTotals() {
|
|
100
114
|
return {
|
|
@@ -105,6 +119,15 @@ function emptyTotals() {
|
|
|
105
119
|
totalTokens: 0,
|
|
106
120
|
};
|
|
107
121
|
}
|
|
122
|
+
function emptyOutputSpeedStats() {
|
|
123
|
+
return {
|
|
124
|
+
samples: 0,
|
|
125
|
+
outputTokens: 0,
|
|
126
|
+
seconds: 0,
|
|
127
|
+
latestTokensPerSecond: null,
|
|
128
|
+
latestSampleAtMs: null,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
108
131
|
function addUsage(totals, usage) {
|
|
109
132
|
const inputTokens = numberField(usage, 'input_tokens', 'inputTokens');
|
|
110
133
|
const outputTokens = numberField(usage, 'output_tokens', 'outputTokens');
|
|
@@ -115,9 +138,57 @@ function addUsage(totals, usage) {
|
|
|
115
138
|
totals.reasoningOutputTokens += numberField(usage, 'reasoning_output_tokens', 'reasoningOutputTokens');
|
|
116
139
|
totals.totalTokens += totalTokens || inputTokens + outputTokens;
|
|
117
140
|
}
|
|
141
|
+
function addOutputSpeed(target, source) {
|
|
142
|
+
target.samples += source.samples;
|
|
143
|
+
target.outputTokens += source.outputTokens;
|
|
144
|
+
target.seconds += source.seconds;
|
|
145
|
+
if (source.latestTokensPerSecond !== null
|
|
146
|
+
&& source.latestSampleAtMs !== null
|
|
147
|
+
&& (target.latestSampleAtMs === null || source.latestSampleAtMs > target.latestSampleAtMs)) {
|
|
148
|
+
target.latestTokensPerSecond = source.latestTokensPerSecond;
|
|
149
|
+
target.latestSampleAtMs = source.latestSampleAtMs;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function addOutputSpeedSample(stats, usage, generationStartMs, timestampMs) {
|
|
153
|
+
if (generationStartMs === null || timestampMs === null || timestampMs <= generationStartMs) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const outputTokens = numberField(usage, 'output_tokens', 'outputTokens');
|
|
157
|
+
if (outputTokens <= 0) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const seconds = (timestampMs - generationStartMs) / 1000;
|
|
161
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
stats.samples += 1;
|
|
165
|
+
stats.outputTokens += outputTokens;
|
|
166
|
+
stats.seconds += seconds;
|
|
167
|
+
stats.latestTokensPerSecond = outputTokens / seconds;
|
|
168
|
+
stats.latestSampleAtMs = timestampMs;
|
|
169
|
+
}
|
|
118
170
|
function numberField(source, snakeKey, camelKey) {
|
|
119
171
|
const snakeValue = source[snakeKey];
|
|
120
172
|
const value = snakeValue === undefined || snakeValue === null ? source[camelKey] : snakeValue;
|
|
121
173
|
const numeric = Number(value);
|
|
122
174
|
return Number.isFinite(numeric) ? numeric : 0;
|
|
123
175
|
}
|
|
176
|
+
function parseTimestampMs(value) {
|
|
177
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
const timestamp = Date.parse(value);
|
|
181
|
+
return Number.isFinite(timestamp) ? timestamp : null;
|
|
182
|
+
}
|
|
183
|
+
function isGenerationBoundary(event) {
|
|
184
|
+
const payload = event?.payload;
|
|
185
|
+
if (event?.type === 'response_item' && payload?.type === 'function_call_output') {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
if (event?.type !== 'event_msg') {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
return payload?.type === 'task_started'
|
|
192
|
+
|| payload?.type === 'exec_command_end'
|
|
193
|
+
|| payload?.type === 'user_message';
|
|
194
|
+
}
|
|
@@ -255,8 +255,10 @@ export declare class BridgeSessionCore {
|
|
|
255
255
|
private buildNativeCollaborationMode;
|
|
256
256
|
private buildCodexUsageStatusLines;
|
|
257
257
|
private buildCodexLocalUsageStatusLines;
|
|
258
|
+
private formatCodexLocalOutputSpeedStatusLines;
|
|
258
259
|
private resolveFastStatusLabel;
|
|
259
260
|
private readCachedCodexLocalUsageStats;
|
|
261
|
+
private sendThreadContextSummary;
|
|
260
262
|
private handleModelCommand;
|
|
261
263
|
private handleEffortCommand;
|
|
262
264
|
private handleThreadOpenCallback;
|
|
@@ -4,7 +4,7 @@ import os from 'node:os';
|
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { normalizeLocale, t } from '../i18n.js';
|
|
6
6
|
import { parseCommand } from './commands.js';
|
|
7
|
-
import { buildAccessSettingsKeyboard, buildModelSettingsKeyboard, buildSetupPanelKeyboard, buildThreadListKeyboard, buildThreadsKeyboard, clampEffortToModel, formatAccessPresetLabel, formatActiveTurnMessageModeLabel, formatAccessSettingsMessage, formatApprovalPolicyLabel, formatCollaborationModeLabel, formatModelSettingsMessage, formatSandboxModeLabel, formatServiceTierStatusLabel, formatSetupPanelMessage, formatThreadsMessage, formatWeixinAccessCopyPaste, formatWeixinModelCopyPaste, formatWeixinThreadsCopyPaste, formatWeixinWhereNavCopyPaste, formatWhereMessage, normalizeRequestedEffort, resolveCurrentModel, resolveActiveTurnMessageMode, resolveRequestedModel, } from './presentation.js';
|
|
7
|
+
import { buildAccessSettingsKeyboard, buildModelSettingsKeyboard, buildSetupPanelKeyboard, buildThreadListKeyboard, buildThreadsKeyboard, clampEffortToModel, formatAccessPresetLabel, formatActiveTurnMessageModeLabel, formatAccessSettingsMessage, formatApprovalPolicyLabel, formatCollaborationModeLabel, formatModelSettingsMessage, formatSandboxModeLabel, formatServiceTierStatusLabel, formatSetupPanelMessage, formatThreadContextSummary, formatThreadsMessage, formatWeixinAccessCopyPaste, formatWeixinModelCopyPaste, formatWeixinThreadsCopyPaste, formatWeixinWhereNavCopyPaste, formatWhereMessage, normalizeRequestedEffort, resolveCurrentModel, resolveActiveTurnMessageMode, resolveRequestedModel, } from './presentation.js';
|
|
8
8
|
import { clampServiceTierToModel, resolveFastTierForModel } from './service_tier.js';
|
|
9
9
|
import { TELEGRAM_BOT_API_DOWNLOAD_LIMIT_BYTES, buildAttachmentPrompt, isNativeImageAttachment, planAttachmentStoragePath, summarizeTelegramInput, } from '../telegram/media.js';
|
|
10
10
|
import { TELEGRAM_MESSAGE_LIMIT, chunkTelegramMessage, chunkTelegramStreamMessage, clipTelegramDraftMessage, } from '../telegram/text.js';
|
|
@@ -505,6 +505,7 @@ export class BridgeSessionCore {
|
|
|
505
505
|
lines.push(revealError ? t(locale, 'codex_sync_failed', { error: revealError }) : t(locale, 'opened_in_codex'));
|
|
506
506
|
}
|
|
507
507
|
await this.sendMessage(scopeId, lines.join('\n'));
|
|
508
|
+
await this.sendThreadContextSummary(scopeId, locale, binding.threadId);
|
|
508
509
|
return;
|
|
509
510
|
}
|
|
510
511
|
case 'watch': {
|
|
@@ -831,13 +832,16 @@ export class BridgeSessionCore {
|
|
|
831
832
|
const mode = watch.mode;
|
|
832
833
|
if (mode === 'already') {
|
|
833
834
|
await this.sendMessage(scopeId, t(locale, 'watch_already_enabled', { threadId: watchedThreadId }));
|
|
835
|
+
await this.sendThreadContextSummary(scopeId, locale, watchedThreadId);
|
|
834
836
|
return;
|
|
835
837
|
}
|
|
836
838
|
if (mode === 'active') {
|
|
837
839
|
await this.sendMessage(scopeId, t(locale, 'watch_started_active', { threadId: watchedThreadId }));
|
|
840
|
+
await this.sendThreadContextSummary(scopeId, locale, watchedThreadId);
|
|
838
841
|
return;
|
|
839
842
|
}
|
|
840
843
|
await this.sendMessage(scopeId, t(locale, 'watch_started_idle', { threadId: watchedThreadId }));
|
|
844
|
+
await this.sendThreadContextSummary(scopeId, locale, watchedThreadId);
|
|
841
845
|
}
|
|
842
846
|
async handleThreadArchiveIndexCommand(scopeId, locale, args) {
|
|
843
847
|
const index = Number.parseInt(args[0] || '', 10);
|
|
@@ -4439,6 +4443,7 @@ export class BridgeSessionCore {
|
|
|
4439
4443
|
cached: formatTokenCount(stats.totals.cachedInputTokens),
|
|
4440
4444
|
reasoning: formatTokenCount(stats.totals.reasoningOutputTokens),
|
|
4441
4445
|
}),
|
|
4446
|
+
...this.formatCodexLocalOutputSpeedStatusLines(locale, stats),
|
|
4442
4447
|
];
|
|
4443
4448
|
}
|
|
4444
4449
|
catch (error) {
|
|
@@ -4446,6 +4451,18 @@ export class BridgeSessionCore {
|
|
|
4446
4451
|
return [t(locale, 'status_codex_local_usage_unavailable', { error: formatShortStatusError(error) })];
|
|
4447
4452
|
}
|
|
4448
4453
|
}
|
|
4454
|
+
formatCodexLocalOutputSpeedStatusLines(locale, stats) {
|
|
4455
|
+
const speed = stats.outputSpeed;
|
|
4456
|
+
if (speed.samples === 0 || speed.outputTokens <= 0 || speed.seconds <= 0) {
|
|
4457
|
+
return [];
|
|
4458
|
+
}
|
|
4459
|
+
const avg = speed.outputTokens / speed.seconds;
|
|
4460
|
+
return [t(locale, 'status_codex_local_speed', {
|
|
4461
|
+
avg: formatCompactNumber(avg),
|
|
4462
|
+
latest: speed.latestTokensPerSecond === null ? t(locale, 'unknown') : formatCompactNumber(speed.latestTokensPerSecond),
|
|
4463
|
+
samples: formatTokenCount(speed.samples),
|
|
4464
|
+
})];
|
|
4465
|
+
}
|
|
4449
4466
|
async resolveFastStatusLabel(locale, settings) {
|
|
4450
4467
|
try {
|
|
4451
4468
|
const models = await this.app.listModels();
|
|
@@ -4466,6 +4483,22 @@ export class BridgeSessionCore {
|
|
|
4466
4483
|
this.localUsageCache = { stats, expiresAt: now + CODEX_LOCAL_USAGE_CACHE_MS };
|
|
4467
4484
|
return stats;
|
|
4468
4485
|
}
|
|
4486
|
+
async sendThreadContextSummary(scopeId, locale, threadId) {
|
|
4487
|
+
try {
|
|
4488
|
+
const turns = await this.app.listThreadTurns(threadId, 5);
|
|
4489
|
+
const text = formatThreadContextSummary(locale, turns);
|
|
4490
|
+
if (text) {
|
|
4491
|
+
await this.sendMessage(scopeId, text);
|
|
4492
|
+
}
|
|
4493
|
+
}
|
|
4494
|
+
catch (error) {
|
|
4495
|
+
this.logger.warn('codex.thread_context_summary_failed', {
|
|
4496
|
+
scopeId,
|
|
4497
|
+
threadId,
|
|
4498
|
+
error: formatUserError(error),
|
|
4499
|
+
});
|
|
4500
|
+
}
|
|
4501
|
+
}
|
|
4469
4502
|
async handleModelCommand(event, locale, args) {
|
|
4470
4503
|
const scopeId = event.scopeId;
|
|
4471
4504
|
if (args.length === 0) {
|
|
@@ -4627,6 +4660,7 @@ export class BridgeSessionCore {
|
|
|
4627
4660
|
callbackText = revealError ? t(locale, 'opened_sync_failed_short') : t(locale, 'opened_in_codex_short');
|
|
4628
4661
|
}
|
|
4629
4662
|
await this.messaging.answerCallback(event.callbackQueryId, callbackText);
|
|
4663
|
+
await this.sendThreadContextSummary(scopeId, locale, binding.threadId);
|
|
4630
4664
|
}
|
|
4631
4665
|
async handleThreadActionCallback(event, action, threadId, locale) {
|
|
4632
4666
|
const scopeId = event.scopeId;
|
|
@@ -4673,6 +4707,7 @@ export class BridgeSessionCore {
|
|
|
4673
4707
|
const watch = await this.watchThread(scopeId, target.chatId, target.chatType, target.topicId, binding);
|
|
4674
4708
|
await this.showThreadsPanelFromStoredState(scopeId, event.messageId, locale, false);
|
|
4675
4709
|
await this.messaging.answerCallback(event.callbackQueryId, formatWatchCallbackText(locale, watch.mode, watch.threadId));
|
|
4710
|
+
await this.sendThreadContextSummary(scopeId, locale, watch.threadId);
|
|
4676
4711
|
return;
|
|
4677
4712
|
}
|
|
4678
4713
|
if (action === 'archive') {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AccessPresetValue, ActiveTurnMessageMode, AppLocale, AppThread, ApprovalPolicyValue, ChatSessionSettings, CollaborationModeValue, ModelInfo, ReasoningEffortValue, SandboxModeValue } from '../types.js';
|
|
1
|
+
import type { AccessPresetValue, ActiveTurnMessageMode, AppLocale, AppThread, AppTurnSnapshot, ApprovalPolicyValue, ChatSessionSettings, CollaborationModeValue, ModelInfo, ReasoningEffortValue, SandboxModeValue } from '../types.js';
|
|
2
2
|
import type { ResolvedAccessMode } from './access.js';
|
|
3
3
|
type InlineButton = {
|
|
4
4
|
text: string;
|
|
@@ -60,6 +60,7 @@ pageOffset?: number): string;
|
|
|
60
60
|
export declare function formatWeixinModelCopyPaste(locale: AppLocale, models: ModelInfo[], settings: ChatSessionSettings | null): string;
|
|
61
61
|
export declare function formatWeixinAccessCopyPaste(locale: AppLocale): string;
|
|
62
62
|
export declare function formatWeixinWhereNavCopyPaste(locale: AppLocale, hasBinding: boolean, defaultCwd?: string): string;
|
|
63
|
+
export declare function formatThreadContextSummary(locale: AppLocale, turns: AppTurnSnapshot[]): string | null;
|
|
63
64
|
export declare function formatSetupPanelMessage(locale: AppLocale, ctx: SetupPanelContext): string;
|
|
64
65
|
export declare function buildSetupPanelKeyboard(locale: AppLocale, ctx: SetupPanelContext): InlineButton[][];
|
|
65
66
|
export declare function resolveSetupSummaryLine(ctx: SetupPanelContext, locale?: AppLocale): string;
|
|
@@ -225,6 +225,20 @@ export function formatWeixinWhereNavCopyPaste(locale, hasBinding, defaultCwd) {
|
|
|
225
225
|
}
|
|
226
226
|
return lines.join('\n');
|
|
227
227
|
}
|
|
228
|
+
export function formatThreadContextSummary(locale, turns) {
|
|
229
|
+
const context = selectRecentThreadContext(turns);
|
|
230
|
+
if (!context) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
return [
|
|
234
|
+
t(locale, 'thread_context_title'),
|
|
235
|
+
t(locale, 'thread_context_user'),
|
|
236
|
+
context.userText || t(locale, 'empty'),
|
|
237
|
+
'',
|
|
238
|
+
t(locale, 'thread_context_codex'),
|
|
239
|
+
context.codexText || t(locale, 'empty'),
|
|
240
|
+
].join('\n');
|
|
241
|
+
}
|
|
228
242
|
export function formatSetupPanelMessage(locale, ctx) {
|
|
229
243
|
const currentModel = resolveCurrentModel(ctx.models, ctx.settings?.model ?? null);
|
|
230
244
|
const fastTier = resolveFastTierForModel(currentModel);
|
|
@@ -393,6 +407,52 @@ function formatFastSetupLabel(locale, supported, enabled, tierName) {
|
|
|
393
407
|
function resolveCollaborationMode(mode) {
|
|
394
408
|
return mode === 'plan' ? 'plan' : 'default';
|
|
395
409
|
}
|
|
410
|
+
function selectRecentThreadContext(turns) {
|
|
411
|
+
for (const turn of turns) {
|
|
412
|
+
const userText = lastItemText(turn.items, isUserTurnItem);
|
|
413
|
+
const codexText = lastItemText(turn.items, isCodexTurnItem);
|
|
414
|
+
if (userText || codexText) {
|
|
415
|
+
return { userText, codexText };
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
function lastItemText(items, predicate) {
|
|
421
|
+
for (let index = items.length - 1; index >= 0; index -= 1) {
|
|
422
|
+
const item = items[index];
|
|
423
|
+
if (!predicate(item)) {
|
|
424
|
+
continue;
|
|
425
|
+
}
|
|
426
|
+
const text = normalizeThreadContextText(item.text ?? item.aggregatedOutput ?? item.command ?? '');
|
|
427
|
+
if (text) {
|
|
428
|
+
return text;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return null;
|
|
432
|
+
}
|
|
433
|
+
function isUserTurnItem(item) {
|
|
434
|
+
const type = normalizeTurnItemType(item.type);
|
|
435
|
+
return type === 'user'
|
|
436
|
+
|| type === 'usermessage'
|
|
437
|
+
|| type === 'humanmessage'
|
|
438
|
+
|| type === 'userinput'
|
|
439
|
+
|| type === 'userrequest';
|
|
440
|
+
}
|
|
441
|
+
function isCodexTurnItem(item) {
|
|
442
|
+
const type = normalizeTurnItemType(item.type);
|
|
443
|
+
return type === 'agent'
|
|
444
|
+
|| type === 'assistant'
|
|
445
|
+
|| type === 'agentmessage'
|
|
446
|
+
|| type === 'assistantmessage'
|
|
447
|
+
|| type === 'plan';
|
|
448
|
+
}
|
|
449
|
+
function normalizeTurnItemType(value) {
|
|
450
|
+
return value.replace(/[^a-z]/gi, '').toLowerCase();
|
|
451
|
+
}
|
|
452
|
+
function normalizeThreadContextText(value) {
|
|
453
|
+
const trimmed = value.replace(/\r\n/g, '\n').trim().replace(/\n{3,}/g, '\n\n');
|
|
454
|
+
return truncate(trimmed, 1200);
|
|
455
|
+
}
|
|
396
456
|
function selectedButtonText(selected, label) {
|
|
397
457
|
return `${selected ? '• ' : ''}${label}`;
|
|
398
458
|
}
|
package/dist/i18n.d.ts
CHANGED
|
@@ -111,6 +111,7 @@ declare const MESSAGES: {
|
|
|
111
111
|
readonly status_codex_usage_unavailable: "Codex usage: unavailable ({error})";
|
|
112
112
|
readonly status_codex_local_history: "Codex local history: {sessions} sessions, {turns} turns, {events} usage records";
|
|
113
113
|
readonly status_codex_local_tokens: "Codex local tokens: total {total}; input {input}, output {output}, cached input {cached}, reasoning output {reasoning}";
|
|
114
|
+
readonly status_codex_local_speed: "Codex local output speed: avg {avg} token/s, latest {latest} token/s ({samples} samples)";
|
|
114
115
|
readonly status_codex_local_usage_unavailable: "Codex local history: unavailable ({error})";
|
|
115
116
|
readonly status_codex_credits: "Codex credits: {value}";
|
|
116
117
|
readonly status_codex_limit_reached: "Codex limit: {value}";
|
|
@@ -315,6 +316,9 @@ declare const MESSAGES: {
|
|
|
315
316
|
readonly threads_filter: "Filter: <code>{searchTerm}</code>";
|
|
316
317
|
readonly threads_range: "Showing {start}-{end}";
|
|
317
318
|
readonly threads_filter_cleared_short: "Filter cleared";
|
|
319
|
+
readonly thread_context_title: "Recent context:";
|
|
320
|
+
readonly thread_context_user: "User:";
|
|
321
|
+
readonly thread_context_codex: "Codex:";
|
|
318
322
|
readonly button_prev_page: "⬅️ Prev";
|
|
319
323
|
readonly button_next_page: "➡️ Next";
|
|
320
324
|
readonly button_clear_filter: "🧹 Clear";
|
|
@@ -661,6 +665,7 @@ declare const MESSAGES: {
|
|
|
661
665
|
readonly status_codex_usage_unavailable: "Codex 用量:无法获取({error})";
|
|
662
666
|
readonly status_codex_local_history: "Codex 本地历史:{sessions} 个会话,{turns} 轮,{events} 条用量记录";
|
|
663
667
|
readonly status_codex_local_tokens: "Codex 本地 Token:总计 {total};输入 {input},输出 {output},缓存输入 {cached},推理输出 {reasoning}";
|
|
668
|
+
readonly status_codex_local_speed: "Codex 本地输出速度:平均 {avg} token/s,最近 {latest} token/s({samples} 个样本)";
|
|
664
669
|
readonly status_codex_local_usage_unavailable: "Codex 本地历史:无法获取({error})";
|
|
665
670
|
readonly status_codex_credits: "Codex 额度:{value}";
|
|
666
671
|
readonly status_codex_limit_reached: "Codex 限制:{value}";
|
|
@@ -865,6 +870,9 @@ declare const MESSAGES: {
|
|
|
865
870
|
readonly threads_filter: "筛选:<code>{searchTerm}</code>";
|
|
866
871
|
readonly threads_range: "显示第 {start}-{end} 条";
|
|
867
872
|
readonly threads_filter_cleared_short: "已清除筛选";
|
|
873
|
+
readonly thread_context_title: "最近上下文:";
|
|
874
|
+
readonly thread_context_user: "用户指令:";
|
|
875
|
+
readonly thread_context_codex: "Codex 最后输出:";
|
|
868
876
|
readonly button_prev_page: "⬅️ 上一页";
|
|
869
877
|
readonly button_next_page: "➡️ 下一页";
|
|
870
878
|
readonly button_clear_filter: "🧹 清除";
|
package/dist/i18n.js
CHANGED
|
@@ -109,6 +109,7 @@ const MESSAGES = {
|
|
|
109
109
|
status_codex_usage_unavailable: 'Codex usage: unavailable ({error})',
|
|
110
110
|
status_codex_local_history: 'Codex local history: {sessions} sessions, {turns} turns, {events} usage records',
|
|
111
111
|
status_codex_local_tokens: 'Codex local tokens: total {total}; input {input}, output {output}, cached input {cached}, reasoning output {reasoning}',
|
|
112
|
+
status_codex_local_speed: 'Codex local output speed: avg {avg} token/s, latest {latest} token/s ({samples} samples)',
|
|
112
113
|
status_codex_local_usage_unavailable: 'Codex local history: unavailable ({error})',
|
|
113
114
|
status_codex_credits: 'Codex credits: {value}',
|
|
114
115
|
status_codex_limit_reached: 'Codex limit: {value}',
|
|
@@ -313,6 +314,9 @@ const MESSAGES = {
|
|
|
313
314
|
threads_filter: 'Filter: <code>{searchTerm}</code>',
|
|
314
315
|
threads_range: 'Showing {start}-{end}',
|
|
315
316
|
threads_filter_cleared_short: 'Filter cleared',
|
|
317
|
+
thread_context_title: 'Recent context:',
|
|
318
|
+
thread_context_user: 'User:',
|
|
319
|
+
thread_context_codex: 'Codex:',
|
|
316
320
|
button_prev_page: '⬅️ Prev',
|
|
317
321
|
button_next_page: '➡️ Next',
|
|
318
322
|
button_clear_filter: '🧹 Clear',
|
|
@@ -659,6 +663,7 @@ const MESSAGES = {
|
|
|
659
663
|
status_codex_usage_unavailable: 'Codex 用量:无法获取({error})',
|
|
660
664
|
status_codex_local_history: 'Codex 本地历史:{sessions} 个会话,{turns} 轮,{events} 条用量记录',
|
|
661
665
|
status_codex_local_tokens: 'Codex 本地 Token:总计 {total};输入 {input},输出 {output},缓存输入 {cached},推理输出 {reasoning}',
|
|
666
|
+
status_codex_local_speed: 'Codex 本地输出速度:平均 {avg} token/s,最近 {latest} token/s({samples} 个样本)',
|
|
662
667
|
status_codex_local_usage_unavailable: 'Codex 本地历史:无法获取({error})',
|
|
663
668
|
status_codex_credits: 'Codex 额度:{value}',
|
|
664
669
|
status_codex_limit_reached: 'Codex 限制:{value}',
|
|
@@ -863,6 +868,9 @@ const MESSAGES = {
|
|
|
863
868
|
threads_filter: '筛选:<code>{searchTerm}</code>',
|
|
864
869
|
threads_range: '显示第 {start}-{end} 条',
|
|
865
870
|
threads_filter_cleared_short: '已清除筛选',
|
|
871
|
+
thread_context_title: '最近上下文:',
|
|
872
|
+
thread_context_user: '用户指令:',
|
|
873
|
+
thread_context_codex: 'Codex 最后输出:',
|
|
866
874
|
button_prev_page: '⬅️ 上一页',
|
|
867
875
|
button_next_page: '➡️ 下一页',
|
|
868
876
|
button_clear_filter: '🧹 清除',
|
package/dist/main.js
CHANGED
|
@@ -8,7 +8,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
8
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
|
-
import { refreshFoxclawExecStartDropIns } from './systemd.js';
|
|
11
|
+
import { refreshFoxclawExecStartDropIns, removeFoxclawExecStartDropIns } from './systemd.js';
|
|
12
12
|
const rawCommand = process.argv[2];
|
|
13
13
|
const command = rawCommand || 'serve';
|
|
14
14
|
loadEnv();
|
|
@@ -24,6 +24,7 @@ const PROXY_ENV_KEYS = [
|
|
|
24
24
|
'all_proxy',
|
|
25
25
|
'no_proxy',
|
|
26
26
|
];
|
|
27
|
+
const STANDARD_NODE_PROXY_ENV_KEYS = ['HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy'];
|
|
27
28
|
async function main() {
|
|
28
29
|
if (isVersionCommand(command)) {
|
|
29
30
|
console.log(readPackageVersion());
|
|
@@ -493,9 +494,38 @@ function runDoctorChecks() {
|
|
|
493
494
|
passed = false;
|
|
494
495
|
}
|
|
495
496
|
warnIfProxyEnvMissingFromLoadedEnv();
|
|
497
|
+
warnIfProxyConfigNeedsAttention();
|
|
496
498
|
warnIfInstalledServiceNodeLooksWrong();
|
|
497
499
|
return passed;
|
|
498
500
|
}
|
|
501
|
+
function warnIfProxyConfigNeedsAttention() {
|
|
502
|
+
const proxychainsConf = process.env.FOXCLAW_PROXYCHAINS_CONF?.trim() || '';
|
|
503
|
+
if (proxychainsConf) {
|
|
504
|
+
if (process.platform !== 'linux') {
|
|
505
|
+
console.log('[WARN] FOXCLAW_PROXYCHAINS_CONF is only used by systemd on Linux.');
|
|
506
|
+
}
|
|
507
|
+
else if (!fs.existsSync(proxychainsConf)) {
|
|
508
|
+
console.log(`[WARN] FOXCLAW_PROXYCHAINS_CONF does not exist: ${proxychainsConf}`);
|
|
509
|
+
}
|
|
510
|
+
else if (!hasCommand('proxychains4')) {
|
|
511
|
+
console.log('[WARN] proxychains4 is not available, but FOXCLAW_PROXYCHAINS_CONF is set.');
|
|
512
|
+
}
|
|
513
|
+
else {
|
|
514
|
+
console.log(`[OK] proxychains config exists: ${proxychainsConf}`);
|
|
515
|
+
}
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
const proxyKeys = PROXY_ENV_KEYS.filter((key) => proxyEnvValue(key));
|
|
519
|
+
if (proxyKeys.length === 0) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
if (hasStandardNodeProxyEnv()) {
|
|
523
|
+
console.log(`[OK] service proxy env configured: ${proxyKeys.join(', ')}`);
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
console.log('[WARN] Only ALL_PROXY/all_proxy is configured. Node service proxying works best with HTTP_PROXY/HTTPS_PROXY.');
|
|
527
|
+
console.log('[WARN] For SOCKS-only hosts, set FOXCLAW_PROXYCHAINS_CONF=/absolute/path/to/proxychains.conf and run foxclaw restart.');
|
|
528
|
+
}
|
|
499
529
|
function warnIfProxyEnvMissingFromLoadedEnv() {
|
|
500
530
|
const envPath = serviceEnvPath();
|
|
501
531
|
const proxyUpdates = detectMissingProxyEnv(envPath);
|
|
@@ -519,7 +549,7 @@ function warnIfInstalledServiceNodeLooksWrong() {
|
|
|
519
549
|
return;
|
|
520
550
|
}
|
|
521
551
|
const execStart = text.match(/^ExecStart=(.+)$/m)?.[1]?.trim();
|
|
522
|
-
const nodePath = execStart ?
|
|
552
|
+
const nodePath = execStart ? extractNodePathFromExecStart(execStart) : '';
|
|
523
553
|
if (!nodePath) {
|
|
524
554
|
return;
|
|
525
555
|
}
|
|
@@ -538,6 +568,14 @@ function warnIfInstalledServiceNodeLooksWrong() {
|
|
|
538
568
|
console.log(`[WARN] installed service node is older than 24: ${nodePath}${version ? ` (${version})` : ''}`);
|
|
539
569
|
console.log('[WARN] Run foxclaw start from a Node 24 shell to refresh the service unit.');
|
|
540
570
|
}
|
|
571
|
+
function extractNodePathFromExecStart(execStart) {
|
|
572
|
+
const tokens = execStart.split(/\s+/).map(systemdUnescape).filter(Boolean);
|
|
573
|
+
const directNode = tokens[0] || '';
|
|
574
|
+
if (path.basename(directNode) === 'node') {
|
|
575
|
+
return directNode;
|
|
576
|
+
}
|
|
577
|
+
return tokens.find((token) => path.basename(token) === 'node') || directNode;
|
|
578
|
+
}
|
|
541
579
|
function installSystemd() {
|
|
542
580
|
if (!hasCommand('systemctl')) {
|
|
543
581
|
console.error('systemctl not found (need systemd)');
|
|
@@ -551,6 +589,12 @@ function installSystemd() {
|
|
|
551
589
|
const nodeBin = process.execPath;
|
|
552
590
|
const nodeDir = path.dirname(nodeBin);
|
|
553
591
|
const pathValue = buildServicePath(nodeDir);
|
|
592
|
+
const proxychainsConf = process.env.FOXCLAW_PROXYCHAINS_CONF?.trim() || '';
|
|
593
|
+
const proxychainsBin = proxychainsConf ? resolveCommand('proxychains4') || '/usr/bin/proxychains4' : '';
|
|
594
|
+
const nodeProxyArgs = !proxychainsConf && hasStandardNodeProxyEnv() ? ' --use-env-proxy' : '';
|
|
595
|
+
const execStart = proxychainsConf
|
|
596
|
+
? `${systemdEscape(proxychainsBin)} -f ${systemdEscape(proxychainsConf)} ${systemdEscape(nodeBin)} ${systemdEscape(entryPoint)} serve`
|
|
597
|
+
: `${systemdEscape(nodeBin)}${nodeProxyArgs} ${systemdEscape(entryPoint)} serve`;
|
|
554
598
|
fs.mkdirSync(userSystemdDir, { recursive: true });
|
|
555
599
|
fs.mkdirSync(configDir, { recursive: true });
|
|
556
600
|
fs.mkdirSync(path.join(APP_HOME, 'logs'), { recursive: true });
|
|
@@ -566,12 +610,13 @@ StartLimitBurst=5
|
|
|
566
610
|
[Service]
|
|
567
611
|
Type=simple
|
|
568
612
|
WorkingDirectory=${systemdEscape(configDir)}
|
|
613
|
+
EnvironmentFile=-${systemdEscape(envPath)}
|
|
569
614
|
Environment=HOME=${systemdEscape(process.env.HOME || '')}
|
|
570
615
|
Environment=USER=${systemdEscape(process.env.USER || '')}
|
|
571
616
|
Environment=LOGNAME=${systemdEscape(process.env.LOGNAME || process.env.USER || '')}
|
|
572
617
|
Environment=PATH=${systemdEscape(pathValue)}
|
|
573
618
|
Environment=FOXCLAW_ENV=${systemdEscape(envPath)}
|
|
574
|
-
ExecStart=${
|
|
619
|
+
ExecStart=${execStart}
|
|
575
620
|
Restart=always
|
|
576
621
|
RestartSec=10
|
|
577
622
|
TimeoutStopSec=45
|
|
@@ -580,9 +625,23 @@ KillMode=process
|
|
|
580
625
|
[Install]
|
|
581
626
|
WantedBy=default.target
|
|
582
627
|
`);
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
628
|
+
if (proxychainsConf) {
|
|
629
|
+
console.log(`[OK] systemd proxychains enabled: ${proxychainsConf}`);
|
|
630
|
+
}
|
|
631
|
+
else if (nodeProxyArgs) {
|
|
632
|
+
console.log('[OK] systemd Node env proxy enabled');
|
|
633
|
+
}
|
|
634
|
+
if (proxychainsConf) {
|
|
635
|
+
const dropInUpdates = removeFoxclawExecStartDropIns(userSystemdDir, unitName);
|
|
636
|
+
for (const update of dropInUpdates) {
|
|
637
|
+
console.log(`[OK] removed stale FoxClaw ExecStart override: ${update.path}`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
const dropInUpdates = refreshFoxclawExecStartDropIns(userSystemdDir, unitName, escapedEntryPoint);
|
|
642
|
+
for (const update of dropInUpdates) {
|
|
643
|
+
console.log(`[OK] updated FoxClaw ExecStart override: ${update.path}`);
|
|
644
|
+
}
|
|
586
645
|
}
|
|
587
646
|
spawnChecked('systemctl', ['--user', 'daemon-reload']);
|
|
588
647
|
spawnChecked('systemctl', ['--user', 'enable', unitName]);
|
|
@@ -625,6 +684,9 @@ function installLaunchd() {
|
|
|
625
684
|
const plist = path.join(home, 'Library', 'LaunchAgents', 'app.foxden.foxclaw.plist');
|
|
626
685
|
const envPath = serviceEnvPath();
|
|
627
686
|
const configDir = path.dirname(envPath);
|
|
687
|
+
const nodeProxyArgs = hasStandardNodeProxyEnv() ? ['--use-env-proxy'] : [];
|
|
688
|
+
const nodeProxyArgXml = nodeProxyArgs.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join('\n');
|
|
689
|
+
const proxyEnvXml = buildLaunchdProxyEnvironmentXml();
|
|
628
690
|
fs.mkdirSync(path.dirname(plist), { recursive: true });
|
|
629
691
|
fs.mkdirSync(configDir, { recursive: true });
|
|
630
692
|
fs.mkdirSync(path.join(APP_HOME, 'logs'), { recursive: true });
|
|
@@ -637,7 +699,7 @@ function installLaunchd() {
|
|
|
637
699
|
<key>ProgramArguments</key>
|
|
638
700
|
<array>
|
|
639
701
|
<string>${xmlEscape(process.execPath)}</string>
|
|
640
|
-
<string>${xmlEscape(entryPoint)}</string>
|
|
702
|
+
${nodeProxyArgXml ? `${nodeProxyArgXml}\n` : ''} <string>${xmlEscape(entryPoint)}</string>
|
|
641
703
|
<string>serve</string>
|
|
642
704
|
</array>
|
|
643
705
|
<key>WorkingDirectory</key>
|
|
@@ -654,6 +716,7 @@ function installLaunchd() {
|
|
|
654
716
|
<string>${xmlEscape(process.env.LOGNAME || process.env.USER || '')}</string>
|
|
655
717
|
<key>FOXCLAW_ENV</key>
|
|
656
718
|
<string>${xmlEscape(envPath)}</string>
|
|
719
|
+
${proxyEnvXml}
|
|
657
720
|
</dict>
|
|
658
721
|
<key>RunAtLoad</key>
|
|
659
722
|
<true/>
|
|
@@ -669,6 +732,9 @@ function installLaunchd() {
|
|
|
669
732
|
spawnSync('launchctl', ['unload', plist], { stdio: 'ignore' });
|
|
670
733
|
spawnChecked('launchctl', ['load', plist]);
|
|
671
734
|
console.log(`Installed ${plist}`);
|
|
735
|
+
if (nodeProxyArgs.length > 0) {
|
|
736
|
+
console.log('[OK] launchd Node env proxy enabled');
|
|
737
|
+
}
|
|
672
738
|
}
|
|
673
739
|
function stopLaunchd() {
|
|
674
740
|
if (process.platform !== 'darwin') {
|
|
@@ -699,6 +765,23 @@ function buildServicePath(nodeDir) {
|
|
|
699
765
|
function serviceEnvPath() {
|
|
700
766
|
return path.resolve(process.env.FOXCLAW_ENV?.trim() || getLoadedEnvPath() || DEFAULT_ENV_PATH);
|
|
701
767
|
}
|
|
768
|
+
function hasStandardNodeProxyEnv() {
|
|
769
|
+
return STANDARD_NODE_PROXY_ENV_KEYS.some((key) => Boolean(proxyEnvValue(key)));
|
|
770
|
+
}
|
|
771
|
+
function proxyEnvValue(key) {
|
|
772
|
+
return process.env[key]?.trim() || '';
|
|
773
|
+
}
|
|
774
|
+
function buildLaunchdProxyEnvironmentXml() {
|
|
775
|
+
const entries = [];
|
|
776
|
+
for (const key of PROXY_ENV_KEYS) {
|
|
777
|
+
const value = proxyEnvValue(key);
|
|
778
|
+
if (!value)
|
|
779
|
+
continue;
|
|
780
|
+
entries.push(` <key>${xmlEscape(key)}</key>`);
|
|
781
|
+
entries.push(` <string>${xmlEscape(value)}</string>`);
|
|
782
|
+
}
|
|
783
|
+
return entries.length > 0 ? `${entries.join('\n')}\n` : '';
|
|
784
|
+
}
|
|
702
785
|
function spawnChecked(commandName, args) {
|
|
703
786
|
const result = spawnSync(commandName, args, { stdio: 'inherit' });
|
|
704
787
|
if (result.status !== 0) {
|
package/dist/systemd.d.ts
CHANGED
|
@@ -3,7 +3,12 @@ export interface SystemdDropInUpdate {
|
|
|
3
3
|
replacements: number;
|
|
4
4
|
}
|
|
5
5
|
export declare function refreshFoxclawExecStartDropIns(userSystemdDir: string, unitName: string, escapedEntryPoint: string): SystemdDropInUpdate[];
|
|
6
|
+
export declare function removeFoxclawExecStartDropIns(userSystemdDir: string, unitName: string): SystemdDropInUpdate[];
|
|
6
7
|
export declare function refreshFoxclawExecStartText(text: string, escapedEntryPoint: string): {
|
|
7
8
|
text: string;
|
|
8
9
|
replacements: number;
|
|
9
10
|
};
|
|
11
|
+
export declare function removeFoxclawExecStartText(text: string): {
|
|
12
|
+
text: string;
|
|
13
|
+
replacements: number;
|
|
14
|
+
};
|
package/dist/systemd.js
CHANGED
|
@@ -29,6 +29,39 @@ export function refreshFoxclawExecStartDropIns(userSystemdDir, unitName, escaped
|
|
|
29
29
|
}
|
|
30
30
|
return updates;
|
|
31
31
|
}
|
|
32
|
+
export function removeFoxclawExecStartDropIns(userSystemdDir, unitName) {
|
|
33
|
+
const dropInDir = path.join(userSystemdDir, `${unitName}.d`);
|
|
34
|
+
let names;
|
|
35
|
+
try {
|
|
36
|
+
names = fs.readdirSync(dropInDir).filter((name) => name.endsWith('.conf')).sort();
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
const updates = [];
|
|
42
|
+
for (const name of names) {
|
|
43
|
+
const filePath = path.join(dropInDir, name);
|
|
44
|
+
let before = '';
|
|
45
|
+
try {
|
|
46
|
+
before = fs.readFileSync(filePath, 'utf8');
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
const { text, replacements } = removeFoxclawExecStartText(before);
|
|
52
|
+
if (replacements === 0 || text === before) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (isEmptyServiceDropIn(text)) {
|
|
56
|
+
fs.rmSync(filePath, { force: true });
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
fs.writeFileSync(filePath, text, 'utf8');
|
|
60
|
+
}
|
|
61
|
+
updates.push({ path: filePath, replacements });
|
|
62
|
+
}
|
|
63
|
+
return updates;
|
|
64
|
+
}
|
|
32
65
|
export function refreshFoxclawExecStartText(text, escapedEntryPoint) {
|
|
33
66
|
let replacements = 0;
|
|
34
67
|
const lines = text.split(/(\r?\n)/);
|
|
@@ -43,3 +76,28 @@ export function refreshFoxclawExecStartText(text, escapedEntryPoint) {
|
|
|
43
76
|
});
|
|
44
77
|
return { text: refreshed.join(''), replacements };
|
|
45
78
|
}
|
|
79
|
+
export function removeFoxclawExecStartText(text) {
|
|
80
|
+
const hasFoxclawExecStart = text
|
|
81
|
+
.split(/\r?\n/)
|
|
82
|
+
.some((line) => line.startsWith('ExecStart=') && FOXCLAW_MAIN_PATH_RE.test(line));
|
|
83
|
+
if (!hasFoxclawExecStart) {
|
|
84
|
+
return { text, replacements: 0 };
|
|
85
|
+
}
|
|
86
|
+
let replacements = 0;
|
|
87
|
+
const lines = text.split(/(\r?\n)/);
|
|
88
|
+
const cleaned = lines.map((part) => {
|
|
89
|
+
if (!part.startsWith('ExecStart=')) {
|
|
90
|
+
return part;
|
|
91
|
+
}
|
|
92
|
+
replacements += 1;
|
|
93
|
+
return '';
|
|
94
|
+
});
|
|
95
|
+
return { text: cleaned.join(''), replacements };
|
|
96
|
+
}
|
|
97
|
+
function isEmptyServiceDropIn(text) {
|
|
98
|
+
const meaningfulLines = text
|
|
99
|
+
.split(/\r?\n/)
|
|
100
|
+
.map((line) => line.trim())
|
|
101
|
+
.filter(Boolean);
|
|
102
|
+
return meaningfulLines.length === 0 || (meaningfulLines.length === 1 && meaningfulLines[0] === '[Service]');
|
|
103
|
+
}
|
package/docs/troubleshooting.md
CHANGED
|
@@ -203,12 +203,28 @@ ALL_PROXY=socks5://127.0.0.1:20170
|
|
|
203
203
|
NO_PROXY=127.0.0.1,localhost
|
|
204
204
|
```
|
|
205
205
|
|
|
206
|
+
When `HTTP_PROXY` or `HTTPS_PROXY` is configured, FoxClaw passes those variables to systemd/launchd explicitly and starts Node with `--use-env-proxy`. Do not rely on proxy variables from the current shell; service processes do not inherit them automatically.
|
|
207
|
+
|
|
206
208
|
Restart FoxClaw after editing. The restart also restarts the managed Codex app-server so the new proxy environment takes effect:
|
|
207
209
|
|
|
208
210
|
```bash
|
|
209
211
|
foxclaw restart
|
|
210
212
|
```
|
|
211
213
|
|
|
214
|
+
If a Linux host must use `proxychains4` to reach Telegram or ChatGPT, do not hand-write a systemd drop-in that overrides `ExecStart`. Add this to the FoxClaw env file instead:
|
|
215
|
+
|
|
216
|
+
```dotenv
|
|
217
|
+
FOXCLAW_PROXYCHAINS_CONF=/home/wuya/.proxychains-rt.conf
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Then run:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
foxclaw restart
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
FoxClaw writes proxychains into the main service and removes stale FoxClaw `ExecStart` overrides, so later upgrades still only need the normal `pnpm install -g` and `foxclaw restart`.
|
|
227
|
+
|
|
212
228
|
## Service Starts With The Wrong Node Version
|
|
213
229
|
|
|
214
230
|
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` or any other shell init script. Whether you use nvm, fnm, asdf, mise, Volta, Homebrew, or system Node, run `foxclaw start` from a Node 24+ shell and the service will keep using that Node 24+ path.
|
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. 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
|
|
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. When `HTTP_PROXY` or `HTTPS_PROXY` is configured, FoxClaw passes it to systemd/launchd explicitly and enables Node's env proxy support. Press Enter on any field to skip it, then edit manually if needed:
|
|
104
104
|
|
|
105
105
|
```bash
|
|
106
106
|
$EDITOR ~/.foxclaw/.env
|
|
@@ -204,12 +204,28 @@ ALL_PROXY=socks5://127.0.0.1:20170
|
|
|
204
204
|
NO_PROXY=127.0.0.1,localhost
|
|
205
205
|
```
|
|
206
206
|
|
|
207
|
+
只要配置了 `HTTP_PROXY` 或 `HTTPS_PROXY`,FoxClaw 安装 systemd/launchd 时会把这些变量显式传给服务,并给 Node 加上 `--use-env-proxy`。不要依赖“当前 shell 里有代理变量”,服务进程不会自动继承它们。
|
|
208
|
+
|
|
207
209
|
改完后重启 FoxClaw。重启会同时重启托管的 Codex app-server,让新代理生效:
|
|
208
210
|
|
|
209
211
|
```bash
|
|
210
212
|
foxclaw restart
|
|
211
213
|
```
|
|
212
214
|
|
|
215
|
+
如果这台 Linux 机器必须用 `proxychains4` 才能访问 Telegram 或 ChatGPT,不要手写 systemd drop-in 覆盖 `ExecStart`。在 FoxClaw env 文件里写:
|
|
216
|
+
|
|
217
|
+
```dotenv
|
|
218
|
+
FOXCLAW_PROXYCHAINS_CONF=/home/wuya/.proxychains-rt.conf
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
然后运行:
|
|
222
|
+
|
|
223
|
+
```bash
|
|
224
|
+
foxclaw restart
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
FoxClaw 会把 proxychains 写进主 service,并清理旧的 FoxClaw `ExecStart` 覆盖,后续升级仍然只需要正常 `pnpm install -g` 和 `foxclaw restart`。
|
|
228
|
+
|
|
213
229
|
## 服务用了错误的 Node 版本
|
|
214
230
|
|
|
215
231
|
systemd 安装脚本会记录当时正在运行的 Node 绝对路径,不依赖 systemd 去加载 `nvm.sh` 或其它 shell 初始化脚本。无论你用 nvm、fnm、asdf、mise、Volta、Homebrew 还是系统 Node,原则都是:从 Node 24+ 的 shell 里执行 `foxclaw start`,服务之后就固定使用这个 Node 24+ 路径。
|
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 和默认工作目录。如果当前 shell 里有 `HTTP_PROXY`、`HTTPS_PROXY`、`ALL_PROXY` 等代理变量,它也会询问是否写入 FoxClaw
|
|
103
|
+
`foxclaw init` 会创建默认配置文件 `~/.foxclaw/.env`,并提示填写 Telegram bot token、Telegram 数字用户 ID 和默认工作目录。如果当前 shell 里有 `HTTP_PROXY`、`HTTPS_PROXY`、`ALL_PROXY` 等代理变量,它也会询问是否写入 FoxClaw 配置。配置了 `HTTP_PROXY` 或 `HTTPS_PROXY` 后,FoxClaw 会在安装服务时显式传给 systemd/launchd,并启用 Node 的 env proxy。任何一项都可以直接回车跳过,之后再手动编辑:
|
|
104
104
|
|
|
105
105
|
```bash
|
|
106
106
|
$EDITOR ~/.foxclaw/.env
|
package/package.json
CHANGED
package/skills/foxclaw/SKILL.md
CHANGED
|
@@ -184,6 +184,7 @@ Use this checklist when the user asks for standard closing actions, release wrap
|
|
|
184
184
|
- If the user has a pnpm global FoxClaw install, prefer `pnpm add -g <repo-path>` so the global `foxclaw` points at the local repo.
|
|
185
185
|
- Rebuild before restarting because local linked installs run `dist/main.js`.
|
|
186
186
|
- Refresh systemd with the existing service env path, for example `FOXCLAW_ENV=<existing-env> <node24> dist/main.js install-systemd`. Do not run `install-systemd` from the repo without `FOXCLAW_ENV`, because it may rewrite the service to use the repo `.env`.
|
|
187
|
+
- Do not create systemd drop-ins that override FoxClaw `ExecStart`. For Linux hosts that require proxychains, set `FOXCLAW_PROXYCHAINS_CONF=/absolute/path/to/proxychains.conf` in the FoxClaw env file and rerun `foxclaw restart`.
|
|
187
188
|
- For macOS launchd, use the launchd install/start path from this skill and verify with `node dist/main.js status`.
|
|
188
189
|
- Verify the running service reports the expected FoxClaw version in `status`.
|
|
189
190
|
- If `doctor` fails only because `DEFAULT_CWD` is missing, report that separately; do not treat it as evidence that the service update failed.
|