@foxden-app/foxclaw 0.3.12 → 0.3.14
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 +4 -2
- package/README_EN.md +4 -2
- 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 +11 -1
- package/dist/controller/controller.js +116 -2
- package/dist/controller/presentation.d.ts +2 -1
- package/dist/controller/presentation.js +60 -0
- package/dist/i18n.d.ts +22 -0
- package/dist/i18n.js +23 -0
- package/dist/main.js +27 -1
- package/dist/update.d.ts +49 -0
- package/dist/update.js +203 -0
- package/docs/install-for-beginners.md +3 -2
- package/docs/troubleshooting.md +1 -1
- package/docs/user-manual.md +4 -1
- package/docs/zh/install-for-beginners.md +3 -2
- package/docs/zh/troubleshooting.md +1 -1
- package/docs/zh/user-manual.md +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,7 +84,7 @@ DEFAULT_SANDBOX_MODE=workspace-write
|
|
|
84
84
|
|
|
85
85
|
配置文件默认在 `~/.foxclaw/.env`。想放别处的话设 `FOXCLAW_ENV=/path/to/.env`。
|
|
86
86
|
|
|
87
|
-
`foxclaw start`
|
|
87
|
+
`foxclaw start` 会自动检查环境并安装/重启后台服务。后续升级直接运行 `foxclaw update`,它会沿用当前的 npm/pnpm 全局安装方式,完成安装、自检和服务重启。
|
|
88
88
|
|
|
89
89
|
FoxClaw 只响应 `TG_ALLOWED_USER_ID` 的消息——把机器人拉进群不代表群里所有人都能用。
|
|
90
90
|
|
|
@@ -150,6 +150,7 @@ journalctl --user -u foxclaw.service -f
|
|
|
150
150
|
```bash
|
|
151
151
|
foxclaw status
|
|
152
152
|
foxclaw restart
|
|
153
|
+
foxclaw update
|
|
153
154
|
foxclaw stop
|
|
154
155
|
```
|
|
155
156
|
|
|
@@ -238,7 +239,7 @@ FoxClaw 会把 `codex app-server` 作为 detached 子进程启动,记录其 pi
|
|
|
238
239
|
- `/setup` — 统一设置面板
|
|
239
240
|
- `/fast <on|off|toggle>`
|
|
240
241
|
- `/active <steer|queue>`
|
|
241
|
-
- `/status`、`/account`、`/quota`
|
|
242
|
+
- `/status`、`/account`、`/quota`、`/update`
|
|
242
243
|
- `/quota_nudge <credits|usage_limit> confirm`
|
|
243
244
|
- `/login_device`、`/login_cancel [id]`、`/logout confirm`
|
|
244
245
|
- `/auth [list|use <n>|enable <n>|disable <n>|reload|add <name>]`
|
|
@@ -290,6 +291,7 @@ foxclaw doctor
|
|
|
290
291
|
foxclaw status
|
|
291
292
|
foxclaw start
|
|
292
293
|
foxclaw restart
|
|
294
|
+
foxclaw update
|
|
293
295
|
foxclaw stop
|
|
294
296
|
foxclaw uninstall-systemd
|
|
295
297
|
```
|
package/README_EN.md
CHANGED
|
@@ -84,7 +84,7 @@ DEFAULT_SANDBOX_MODE=workspace-write
|
|
|
84
84
|
|
|
85
85
|
The default config file is `~/.foxclaw/.env`. Set `FOXCLAW_ENV=/path/to/.env` if you want to keep it somewhere else.
|
|
86
86
|
|
|
87
|
-
`foxclaw start` runs checks and installs or restarts the background service.
|
|
87
|
+
`foxclaw start` runs checks and installs or restarts the background service. For later upgrades, run `foxclaw update`; it preserves the current npm/pnpm global-install method, runs checks, and restarts the service.
|
|
88
88
|
|
|
89
89
|
FoxClaw accepts messages only from `TG_ALLOWED_USER_ID`. Putting the bot in a group does not make it available to every group member.
|
|
90
90
|
|
|
@@ -150,6 +150,7 @@ You can also use the wrapper commands:
|
|
|
150
150
|
```bash
|
|
151
151
|
foxclaw status
|
|
152
152
|
foxclaw restart
|
|
153
|
+
foxclaw update
|
|
153
154
|
foxclaw stop
|
|
154
155
|
```
|
|
155
156
|
|
|
@@ -238,7 +239,7 @@ No static Codex app-server port is required in normal installs.
|
|
|
238
239
|
- `/setup` — unified preference panel
|
|
239
240
|
- `/fast <on|off|toggle>`
|
|
240
241
|
- `/active <steer|queue>`
|
|
241
|
-
- `/status`, `/account`, `/quota`
|
|
242
|
+
- `/status`, `/account`, `/quota`, `/update`
|
|
242
243
|
- `/quota_nudge <credits|usage_limit> confirm`
|
|
243
244
|
- `/login_device`, `/login_cancel [id]`, `/logout confirm`
|
|
244
245
|
- `/auth [list|use <n>|enable <n>|disable <n>|reload|add <name>]`
|
|
@@ -290,6 +291,7 @@ foxclaw doctor
|
|
|
290
291
|
foxclaw status
|
|
291
292
|
foxclaw start
|
|
292
293
|
foxclaw restart
|
|
294
|
+
foxclaw update
|
|
293
295
|
foxclaw stop
|
|
294
296
|
foxclaw uninstall-systemd
|
|
295
297
|
```
|
|
@@ -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
|
+
}
|
|
@@ -5,12 +5,14 @@ import type { RuntimeStatus } from '../types.js';
|
|
|
5
5
|
import type { TelegramGateway, TelegramTextEvent } from '../telegram/gateway.js';
|
|
6
6
|
import { BridgeMessagingRouter } from '../channels/bridge_messaging_router.js';
|
|
7
7
|
import type { CodexAppClient } from '../codex_app/client.js';
|
|
8
|
+
import type { SelfUpdateRuntime } from '../update.js';
|
|
8
9
|
export declare class BridgeSessionCore {
|
|
9
10
|
private readonly config;
|
|
10
11
|
private readonly store;
|
|
11
12
|
private readonly logger;
|
|
12
13
|
private readonly bot;
|
|
13
14
|
private readonly app;
|
|
15
|
+
private readonly selfUpdater;
|
|
14
16
|
private activeTurns;
|
|
15
17
|
private activeTurnsByTurnId;
|
|
16
18
|
private observedThreadWatchers;
|
|
@@ -38,13 +40,14 @@ export declare class BridgeSessionCore {
|
|
|
38
40
|
private locks;
|
|
39
41
|
private approvalTimers;
|
|
40
42
|
private submittedUserInputTimers;
|
|
43
|
+
private selfUpdatePollTimer;
|
|
41
44
|
private attachedThreads;
|
|
42
45
|
private botUsername;
|
|
43
46
|
private lastError;
|
|
44
47
|
/** Last threads-panel pagination state per scope (Telegram inline nav + /open index alignment). */
|
|
45
48
|
private threadListPresentationState;
|
|
46
49
|
private readonly messaging;
|
|
47
|
-
constructor(config: AppConfig, store: BridgeStore, logger: Logger, bot: TelegramGateway, app: CodexAppClient, outbound: BridgeMessagingRouter);
|
|
50
|
+
constructor(config: AppConfig, store: BridgeStore, logger: Logger, bot: TelegramGateway, app: CodexAppClient, outbound: BridgeMessagingRouter, selfUpdater?: SelfUpdateRuntime | null);
|
|
48
51
|
/** Wire Telegram inbound events. Call before {@link startCodexApp}. */
|
|
49
52
|
registerTelegramInboundHandlers(): void;
|
|
50
53
|
/**
|
|
@@ -203,6 +206,11 @@ export declare class BridgeSessionCore {
|
|
|
203
206
|
private handleFastCommand;
|
|
204
207
|
private setCollaborationMode;
|
|
205
208
|
private handleAuthReloadCommand;
|
|
209
|
+
private handleSelfUpdateCommand;
|
|
210
|
+
private scheduleSelfUpdateStatusPoll;
|
|
211
|
+
private clearSelfUpdateStatusPoll;
|
|
212
|
+
private pollSelfUpdateStatus;
|
|
213
|
+
private formatSelfUpdateResult;
|
|
206
214
|
private handleAuthCommand;
|
|
207
215
|
private handleAuthUseCommand;
|
|
208
216
|
private handleAuthToggleCommand;
|
|
@@ -255,8 +263,10 @@ export declare class BridgeSessionCore {
|
|
|
255
263
|
private buildNativeCollaborationMode;
|
|
256
264
|
private buildCodexUsageStatusLines;
|
|
257
265
|
private buildCodexLocalUsageStatusLines;
|
|
266
|
+
private formatCodexLocalOutputSpeedStatusLines;
|
|
258
267
|
private resolveFastStatusLabel;
|
|
259
268
|
private readCachedCodexLocalUsageStats;
|
|
269
|
+
private sendThreadContextSummary;
|
|
260
270
|
private handleModelCommand;
|
|
261
271
|
private handleEffortCommand;
|
|
262
272
|
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';
|
|
@@ -25,6 +25,7 @@ const OBSERVED_CLI_USER_LABEL = 'codex-cli-user';
|
|
|
25
25
|
const DEFAULT_COLLABORATION_MODE = 'default';
|
|
26
26
|
const CODEX_LOCAL_USAGE_CACHE_MS = 30_000;
|
|
27
27
|
const USER_INPUT_SUBMITTED_NOTICE_MS = 90_000;
|
|
28
|
+
const SELF_UPDATE_STATUS_POLL_MS = 1000;
|
|
28
29
|
const PLAN_IMPLEMENTATION_CODING_MESSAGE = 'Implement the plan.';
|
|
29
30
|
const PLAN_IMPLEMENTATION_CLEAR_CONTEXT_PREFIX = 'A previous agent produced the plan below to accomplish the user\'s task. Implement the plan in a fresh context. Treat the plan as the source of user intent, re-read files as needed, and carry the work through implementation and verification.';
|
|
30
31
|
const PINNED_HELP_COMMANDS = [
|
|
@@ -39,6 +40,7 @@ const DYNAMIC_HELP_COMMANDS = [
|
|
|
39
40
|
{ key: 'active', line: '/active <steer|queue>' },
|
|
40
41
|
{ key: 'account', line: '/account' },
|
|
41
42
|
{ key: 'quota', line: '/quota' },
|
|
43
|
+
{ key: 'update', line: '/update' },
|
|
42
44
|
{ key: 'login_device', line: '/login_device' },
|
|
43
45
|
{ key: 'threads_archived', line: '/threads archived [query]' },
|
|
44
46
|
{ key: 'open', line: '/open <n>' },
|
|
@@ -88,6 +90,7 @@ export class BridgeSessionCore {
|
|
|
88
90
|
logger;
|
|
89
91
|
bot;
|
|
90
92
|
app;
|
|
93
|
+
selfUpdater;
|
|
91
94
|
activeTurns = new Map();
|
|
92
95
|
activeTurnsByTurnId = new Map();
|
|
93
96
|
observedThreadWatchers = new Map();
|
|
@@ -115,18 +118,20 @@ export class BridgeSessionCore {
|
|
|
115
118
|
locks = new Map();
|
|
116
119
|
approvalTimers = new Map();
|
|
117
120
|
submittedUserInputTimers = new Map();
|
|
121
|
+
selfUpdatePollTimer = null;
|
|
118
122
|
attachedThreads = new Set();
|
|
119
123
|
botUsername = null;
|
|
120
124
|
lastError = null;
|
|
121
125
|
/** Last threads-panel pagination state per scope (Telegram inline nav + /open index alignment). */
|
|
122
126
|
threadListPresentationState = new Map();
|
|
123
127
|
messaging;
|
|
124
|
-
constructor(config, store, logger, bot, app, outbound) {
|
|
128
|
+
constructor(config, store, logger, bot, app, outbound, selfUpdater = null) {
|
|
125
129
|
this.config = config;
|
|
126
130
|
this.store = store;
|
|
127
131
|
this.logger = logger;
|
|
128
132
|
this.bot = bot;
|
|
129
133
|
this.app = app;
|
|
134
|
+
this.selfUpdater = selfUpdater;
|
|
130
135
|
this.messaging = outbound;
|
|
131
136
|
}
|
|
132
137
|
/** Wire Telegram inbound events. Call before {@link startCodexApp}. */
|
|
@@ -188,6 +193,7 @@ export class BridgeSessionCore {
|
|
|
188
193
|
await this.bot.start();
|
|
189
194
|
this.botUsername = this.bot.username;
|
|
190
195
|
this.updateStatus();
|
|
196
|
+
this.scheduleSelfUpdateStatusPoll(0);
|
|
191
197
|
}
|
|
192
198
|
/** Telegram-only default startup (single channel). */
|
|
193
199
|
async start() {
|
|
@@ -223,6 +229,7 @@ export class BridgeSessionCore {
|
|
|
223
229
|
clearTimeout(timer);
|
|
224
230
|
}
|
|
225
231
|
this.submittedUserInputTimers.clear();
|
|
232
|
+
this.clearSelfUpdateStatusPoll();
|
|
226
233
|
await this.app.stop({ terminateServer: false });
|
|
227
234
|
this.updateStatus();
|
|
228
235
|
}
|
|
@@ -382,6 +389,10 @@ export class BridgeSessionCore {
|
|
|
382
389
|
await this.handleQuotaCommand(scopeId, locale);
|
|
383
390
|
return;
|
|
384
391
|
}
|
|
392
|
+
case 'update': {
|
|
393
|
+
await this.handleSelfUpdateCommand(scopeId, locale);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
385
396
|
case 'quota_nudge': {
|
|
386
397
|
await this.handleQuotaNudgeCommand(scopeId, locale, args);
|
|
387
398
|
return;
|
|
@@ -505,6 +516,7 @@ export class BridgeSessionCore {
|
|
|
505
516
|
lines.push(revealError ? t(locale, 'codex_sync_failed', { error: revealError }) : t(locale, 'opened_in_codex'));
|
|
506
517
|
}
|
|
507
518
|
await this.sendMessage(scopeId, lines.join('\n'));
|
|
519
|
+
await this.sendThreadContextSummary(scopeId, locale, binding.threadId);
|
|
508
520
|
return;
|
|
509
521
|
}
|
|
510
522
|
case 'watch': {
|
|
@@ -831,13 +843,16 @@ export class BridgeSessionCore {
|
|
|
831
843
|
const mode = watch.mode;
|
|
832
844
|
if (mode === 'already') {
|
|
833
845
|
await this.sendMessage(scopeId, t(locale, 'watch_already_enabled', { threadId: watchedThreadId }));
|
|
846
|
+
await this.sendThreadContextSummary(scopeId, locale, watchedThreadId);
|
|
834
847
|
return;
|
|
835
848
|
}
|
|
836
849
|
if (mode === 'active') {
|
|
837
850
|
await this.sendMessage(scopeId, t(locale, 'watch_started_active', { threadId: watchedThreadId }));
|
|
851
|
+
await this.sendThreadContextSummary(scopeId, locale, watchedThreadId);
|
|
838
852
|
return;
|
|
839
853
|
}
|
|
840
854
|
await this.sendMessage(scopeId, t(locale, 'watch_started_idle', { threadId: watchedThreadId }));
|
|
855
|
+
await this.sendThreadContextSummary(scopeId, locale, watchedThreadId);
|
|
841
856
|
}
|
|
842
857
|
async handleThreadArchiveIndexCommand(scopeId, locale, args) {
|
|
843
858
|
const index = Number.parseInt(args[0] || '', 10);
|
|
@@ -3637,6 +3652,74 @@ export class BridgeSessionCore {
|
|
|
3637
3652
|
lines.push(...await this.buildCodexUsageStatusLines(locale));
|
|
3638
3653
|
await this.sendMessage(scopeId, lines.join('\n'));
|
|
3639
3654
|
}
|
|
3655
|
+
async handleSelfUpdateCommand(scopeId, locale) {
|
|
3656
|
+
if (!this.selfUpdater) {
|
|
3657
|
+
await this.sendMessage(scopeId, t(locale, 'update_unavailable'));
|
|
3658
|
+
return;
|
|
3659
|
+
}
|
|
3660
|
+
if (this.activeTurns.size > 0 || this.store.countPendingApprovals() > 0 || this.pendingUserInputs.size > 0 || this.pendingMcpElicitations.size > 0) {
|
|
3661
|
+
await this.sendMessage(scopeId, t(locale, 'update_blocked_active'));
|
|
3662
|
+
return;
|
|
3663
|
+
}
|
|
3664
|
+
const status = await this.selfUpdater.readStatus();
|
|
3665
|
+
if (status?.state === 'pending') {
|
|
3666
|
+
await this.sendMessage(scopeId, t(locale, 'update_already_running'));
|
|
3667
|
+
this.scheduleSelfUpdateStatusPoll();
|
|
3668
|
+
return;
|
|
3669
|
+
}
|
|
3670
|
+
if (status) {
|
|
3671
|
+
await this.selfUpdater.clearStatus();
|
|
3672
|
+
}
|
|
3673
|
+
await this.sendMessage(scopeId, t(locale, 'update_started'));
|
|
3674
|
+
try {
|
|
3675
|
+
await this.selfUpdater.launch(scopeId, locale);
|
|
3676
|
+
this.scheduleSelfUpdateStatusPoll();
|
|
3677
|
+
}
|
|
3678
|
+
catch (error) {
|
|
3679
|
+
await this.selfUpdater.clearStatus().catch(() => undefined);
|
|
3680
|
+
await this.sendMessage(scopeId, t(locale, 'update_failed', { error: formatUserError(error) }));
|
|
3681
|
+
}
|
|
3682
|
+
}
|
|
3683
|
+
scheduleSelfUpdateStatusPoll(delay = SELF_UPDATE_STATUS_POLL_MS) {
|
|
3684
|
+
if (!this.selfUpdater || this.selfUpdatePollTimer) {
|
|
3685
|
+
return;
|
|
3686
|
+
}
|
|
3687
|
+
this.selfUpdatePollTimer = setTimeout(() => {
|
|
3688
|
+
this.selfUpdatePollTimer = null;
|
|
3689
|
+
void this.pollSelfUpdateStatus().catch((error) => {
|
|
3690
|
+
this.logger.error('self_update.poll_failed', { error: toErrorMeta(error) });
|
|
3691
|
+
this.scheduleSelfUpdateStatusPoll();
|
|
3692
|
+
});
|
|
3693
|
+
}, delay);
|
|
3694
|
+
}
|
|
3695
|
+
clearSelfUpdateStatusPoll() {
|
|
3696
|
+
if (!this.selfUpdatePollTimer) {
|
|
3697
|
+
return;
|
|
3698
|
+
}
|
|
3699
|
+
clearTimeout(this.selfUpdatePollTimer);
|
|
3700
|
+
this.selfUpdatePollTimer = null;
|
|
3701
|
+
}
|
|
3702
|
+
async pollSelfUpdateStatus() {
|
|
3703
|
+
const status = await this.selfUpdater?.readStatus();
|
|
3704
|
+
if (!status) {
|
|
3705
|
+
return;
|
|
3706
|
+
}
|
|
3707
|
+
if (status.state === 'pending') {
|
|
3708
|
+
this.scheduleSelfUpdateStatusPoll();
|
|
3709
|
+
return;
|
|
3710
|
+
}
|
|
3711
|
+
await this.sendMessage(status.scopeId, this.formatSelfUpdateResult(status));
|
|
3712
|
+
await this.selfUpdater?.clearStatus();
|
|
3713
|
+
}
|
|
3714
|
+
formatSelfUpdateResult(status) {
|
|
3715
|
+
if (status.state === 'succeeded') {
|
|
3716
|
+
return t(status.locale, 'update_succeeded', {
|
|
3717
|
+
from: status.fromVersion,
|
|
3718
|
+
to: status.toVersion ?? t(status.locale, 'unknown'),
|
|
3719
|
+
});
|
|
3720
|
+
}
|
|
3721
|
+
return t(status.locale, 'update_failed', { error: status.error ?? t(status.locale, 'unknown') });
|
|
3722
|
+
}
|
|
3640
3723
|
async handleAuthCommand(scopeId, locale, args) {
|
|
3641
3724
|
const action = args[0]?.toLowerCase() ?? 'list';
|
|
3642
3725
|
if (action === 'reload' || action === 'restart') {
|
|
@@ -4439,6 +4522,7 @@ export class BridgeSessionCore {
|
|
|
4439
4522
|
cached: formatTokenCount(stats.totals.cachedInputTokens),
|
|
4440
4523
|
reasoning: formatTokenCount(stats.totals.reasoningOutputTokens),
|
|
4441
4524
|
}),
|
|
4525
|
+
...this.formatCodexLocalOutputSpeedStatusLines(locale, stats),
|
|
4442
4526
|
];
|
|
4443
4527
|
}
|
|
4444
4528
|
catch (error) {
|
|
@@ -4446,6 +4530,18 @@ export class BridgeSessionCore {
|
|
|
4446
4530
|
return [t(locale, 'status_codex_local_usage_unavailable', { error: formatShortStatusError(error) })];
|
|
4447
4531
|
}
|
|
4448
4532
|
}
|
|
4533
|
+
formatCodexLocalOutputSpeedStatusLines(locale, stats) {
|
|
4534
|
+
const speed = stats.outputSpeed;
|
|
4535
|
+
if (speed.samples === 0 || speed.outputTokens <= 0 || speed.seconds <= 0) {
|
|
4536
|
+
return [];
|
|
4537
|
+
}
|
|
4538
|
+
const avg = speed.outputTokens / speed.seconds;
|
|
4539
|
+
return [t(locale, 'status_codex_local_speed', {
|
|
4540
|
+
avg: formatCompactNumber(avg),
|
|
4541
|
+
latest: speed.latestTokensPerSecond === null ? t(locale, 'unknown') : formatCompactNumber(speed.latestTokensPerSecond),
|
|
4542
|
+
samples: formatTokenCount(speed.samples),
|
|
4543
|
+
})];
|
|
4544
|
+
}
|
|
4449
4545
|
async resolveFastStatusLabel(locale, settings) {
|
|
4450
4546
|
try {
|
|
4451
4547
|
const models = await this.app.listModels();
|
|
@@ -4466,6 +4562,22 @@ export class BridgeSessionCore {
|
|
|
4466
4562
|
this.localUsageCache = { stats, expiresAt: now + CODEX_LOCAL_USAGE_CACHE_MS };
|
|
4467
4563
|
return stats;
|
|
4468
4564
|
}
|
|
4565
|
+
async sendThreadContextSummary(scopeId, locale, threadId) {
|
|
4566
|
+
try {
|
|
4567
|
+
const turns = await this.app.listThreadTurns(threadId, 5);
|
|
4568
|
+
const text = formatThreadContextSummary(locale, turns);
|
|
4569
|
+
if (text) {
|
|
4570
|
+
await this.sendMessage(scopeId, text);
|
|
4571
|
+
}
|
|
4572
|
+
}
|
|
4573
|
+
catch (error) {
|
|
4574
|
+
this.logger.warn('codex.thread_context_summary_failed', {
|
|
4575
|
+
scopeId,
|
|
4576
|
+
threadId,
|
|
4577
|
+
error: formatUserError(error),
|
|
4578
|
+
});
|
|
4579
|
+
}
|
|
4580
|
+
}
|
|
4469
4581
|
async handleModelCommand(event, locale, args) {
|
|
4470
4582
|
const scopeId = event.scopeId;
|
|
4471
4583
|
if (args.length === 0) {
|
|
@@ -4627,6 +4739,7 @@ export class BridgeSessionCore {
|
|
|
4627
4739
|
callbackText = revealError ? t(locale, 'opened_sync_failed_short') : t(locale, 'opened_in_codex_short');
|
|
4628
4740
|
}
|
|
4629
4741
|
await this.messaging.answerCallback(event.callbackQueryId, callbackText);
|
|
4742
|
+
await this.sendThreadContextSummary(scopeId, locale, binding.threadId);
|
|
4630
4743
|
}
|
|
4631
4744
|
async handleThreadActionCallback(event, action, threadId, locale) {
|
|
4632
4745
|
const scopeId = event.scopeId;
|
|
@@ -4673,6 +4786,7 @@ export class BridgeSessionCore {
|
|
|
4673
4786
|
const watch = await this.watchThread(scopeId, target.chatId, target.chatType, target.topicId, binding);
|
|
4674
4787
|
await this.showThreadsPanelFromStoredState(scopeId, event.messageId, locale, false);
|
|
4675
4788
|
await this.messaging.answerCallback(event.callbackQueryId, formatWatchCallbackText(locale, watch.mode, watch.threadId));
|
|
4789
|
+
await this.sendThreadContextSummary(scopeId, locale, watch.threadId);
|
|
4676
4790
|
return;
|
|
4677
4791
|
}
|
|
4678
4792
|
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
|
@@ -46,6 +46,7 @@ declare const MESSAGES: {
|
|
|
46
46
|
readonly cmd_desc_fast: "Toggle Fast mode";
|
|
47
47
|
readonly cmd_desc_active: "Active-turn message behavior";
|
|
48
48
|
readonly cmd_desc_status: "Bridge status";
|
|
49
|
+
readonly cmd_desc_update: "Update and restart FoxClaw";
|
|
49
50
|
readonly cmd_desc_account: "Codex account";
|
|
50
51
|
readonly cmd_desc_quota: "Codex quota";
|
|
51
52
|
readonly cmd_desc_login_device: "ChatGPT device login";
|
|
@@ -111,9 +112,16 @@ declare const MESSAGES: {
|
|
|
111
112
|
readonly status_codex_usage_unavailable: "Codex usage: unavailable ({error})";
|
|
112
113
|
readonly status_codex_local_history: "Codex local history: {sessions} sessions, {turns} turns, {events} usage records";
|
|
113
114
|
readonly status_codex_local_tokens: "Codex local tokens: total {total}; input {input}, output {output}, cached input {cached}, reasoning output {reasoning}";
|
|
115
|
+
readonly status_codex_local_speed: "Codex local output speed: avg {avg} token/s, latest {latest} token/s ({samples} samples)";
|
|
114
116
|
readonly status_codex_local_usage_unavailable: "Codex local history: unavailable ({error})";
|
|
115
117
|
readonly status_codex_credits: "Codex credits: {value}";
|
|
116
118
|
readonly status_codex_limit_reached: "Codex limit: {value}";
|
|
119
|
+
readonly update_started: "FoxClaw update started. I will report here after installation, checks, and service restart complete.";
|
|
120
|
+
readonly update_succeeded: "FoxClaw updated and restarted: {from} -> {to}.";
|
|
121
|
+
readonly update_failed: "FoxClaw update failed: {error}\nRun foxclaw update in a terminal for details.";
|
|
122
|
+
readonly update_unavailable: "Self-update is unavailable in this runtime. Run foxclaw update in a terminal.";
|
|
123
|
+
readonly update_already_running: "A FoxClaw update is already running. I will report here when it finishes.";
|
|
124
|
+
readonly update_blocked_active: "Cannot update FoxClaw while a turn, approval, or question is active. Wait or use /interrupt first.";
|
|
117
125
|
readonly auth_reload_restarting: "Restarting Codex app-server to reload auth...";
|
|
118
126
|
readonly auth_reload_done: "Codex app-server restarted. Current auth has been reloaded.";
|
|
119
127
|
readonly auth_reload_blocked_active: "Cannot reload Codex auth while a turn, approval, or question is active. Wait or use /interrupt first.";
|
|
@@ -315,6 +323,9 @@ declare const MESSAGES: {
|
|
|
315
323
|
readonly threads_filter: "Filter: <code>{searchTerm}</code>";
|
|
316
324
|
readonly threads_range: "Showing {start}-{end}";
|
|
317
325
|
readonly threads_filter_cleared_short: "Filter cleared";
|
|
326
|
+
readonly thread_context_title: "Recent context:";
|
|
327
|
+
readonly thread_context_user: "User:";
|
|
328
|
+
readonly thread_context_codex: "Codex:";
|
|
318
329
|
readonly button_prev_page: "⬅️ Prev";
|
|
319
330
|
readonly button_next_page: "➡️ Next";
|
|
320
331
|
readonly button_clear_filter: "🧹 Clear";
|
|
@@ -596,6 +607,7 @@ declare const MESSAGES: {
|
|
|
596
607
|
readonly cmd_desc_fast: "切换 Fast 模式";
|
|
597
608
|
readonly cmd_desc_active: "运行中新消息处理方式";
|
|
598
609
|
readonly cmd_desc_status: "查看桥接状态";
|
|
610
|
+
readonly cmd_desc_update: "升级并重启 FoxClaw";
|
|
599
611
|
readonly cmd_desc_account: "Codex 账号";
|
|
600
612
|
readonly cmd_desc_quota: "Codex 用量";
|
|
601
613
|
readonly cmd_desc_login_device: "ChatGPT 设备登录";
|
|
@@ -661,9 +673,16 @@ declare const MESSAGES: {
|
|
|
661
673
|
readonly status_codex_usage_unavailable: "Codex 用量:无法获取({error})";
|
|
662
674
|
readonly status_codex_local_history: "Codex 本地历史:{sessions} 个会话,{turns} 轮,{events} 条用量记录";
|
|
663
675
|
readonly status_codex_local_tokens: "Codex 本地 Token:总计 {total};输入 {input},输出 {output},缓存输入 {cached},推理输出 {reasoning}";
|
|
676
|
+
readonly status_codex_local_speed: "Codex 本地输出速度:平均 {avg} token/s,最近 {latest} token/s({samples} 个样本)";
|
|
664
677
|
readonly status_codex_local_usage_unavailable: "Codex 本地历史:无法获取({error})";
|
|
665
678
|
readonly status_codex_credits: "Codex 额度:{value}";
|
|
666
679
|
readonly status_codex_limit_reached: "Codex 限制:{value}";
|
|
680
|
+
readonly update_started: "已开始升级 FoxClaw。安装、自检和服务重启完成后,我会在这里回报结果。";
|
|
681
|
+
readonly update_succeeded: "FoxClaw 已升级并重启:{from} -> {to}。";
|
|
682
|
+
readonly update_failed: "FoxClaw 升级失败:{error}\n请在终端运行 foxclaw update 查看详情。";
|
|
683
|
+
readonly update_unavailable: "当前运行方式不支持自升级,请在终端运行 foxclaw update。";
|
|
684
|
+
readonly update_already_running: "FoxClaw 升级已经在进行中,结束后我会在这里回报结果。";
|
|
685
|
+
readonly update_blocked_active: "当前有回复、审批或问题在进行中,不能升级 FoxClaw。请先等待,或使用 /interrupt。";
|
|
667
686
|
readonly auth_reload_restarting: "正在重启 Codex app-server 以重新读取 auth...";
|
|
668
687
|
readonly auth_reload_done: "Codex app-server 已重启,当前 auth 已重新读取。";
|
|
669
688
|
readonly auth_reload_blocked_active: "当前有回复、审批或问题在进行中,不能重载 Codex auth。请先等待,或使用 /interrupt。";
|
|
@@ -865,6 +884,9 @@ declare const MESSAGES: {
|
|
|
865
884
|
readonly threads_filter: "筛选:<code>{searchTerm}</code>";
|
|
866
885
|
readonly threads_range: "显示第 {start}-{end} 条";
|
|
867
886
|
readonly threads_filter_cleared_short: "已清除筛选";
|
|
887
|
+
readonly thread_context_title: "最近上下文:";
|
|
888
|
+
readonly thread_context_user: "用户指令:";
|
|
889
|
+
readonly thread_context_codex: "Codex 最后输出:";
|
|
868
890
|
readonly button_prev_page: "⬅️ 上一页";
|
|
869
891
|
readonly button_next_page: "➡️ 下一页";
|
|
870
892
|
readonly button_clear_filter: "🧹 清除";
|
package/dist/i18n.js
CHANGED
|
@@ -44,6 +44,7 @@ const MESSAGES = {
|
|
|
44
44
|
cmd_desc_fast: 'Toggle Fast mode',
|
|
45
45
|
cmd_desc_active: 'Active-turn message behavior',
|
|
46
46
|
cmd_desc_status: 'Bridge status',
|
|
47
|
+
cmd_desc_update: 'Update and restart FoxClaw',
|
|
47
48
|
cmd_desc_account: 'Codex account',
|
|
48
49
|
cmd_desc_quota: 'Codex quota',
|
|
49
50
|
cmd_desc_login_device: 'ChatGPT device login',
|
|
@@ -109,9 +110,16 @@ const MESSAGES = {
|
|
|
109
110
|
status_codex_usage_unavailable: 'Codex usage: unavailable ({error})',
|
|
110
111
|
status_codex_local_history: 'Codex local history: {sessions} sessions, {turns} turns, {events} usage records',
|
|
111
112
|
status_codex_local_tokens: 'Codex local tokens: total {total}; input {input}, output {output}, cached input {cached}, reasoning output {reasoning}',
|
|
113
|
+
status_codex_local_speed: 'Codex local output speed: avg {avg} token/s, latest {latest} token/s ({samples} samples)',
|
|
112
114
|
status_codex_local_usage_unavailable: 'Codex local history: unavailable ({error})',
|
|
113
115
|
status_codex_credits: 'Codex credits: {value}',
|
|
114
116
|
status_codex_limit_reached: 'Codex limit: {value}',
|
|
117
|
+
update_started: 'FoxClaw update started. I will report here after installation, checks, and service restart complete.',
|
|
118
|
+
update_succeeded: 'FoxClaw updated and restarted: {from} -> {to}.',
|
|
119
|
+
update_failed: 'FoxClaw update failed: {error}\nRun foxclaw update in a terminal for details.',
|
|
120
|
+
update_unavailable: 'Self-update is unavailable in this runtime. Run foxclaw update in a terminal.',
|
|
121
|
+
update_already_running: 'A FoxClaw update is already running. I will report here when it finishes.',
|
|
122
|
+
update_blocked_active: 'Cannot update FoxClaw while a turn, approval, or question is active. Wait or use /interrupt first.',
|
|
115
123
|
auth_reload_restarting: 'Restarting Codex app-server to reload auth...',
|
|
116
124
|
auth_reload_done: 'Codex app-server restarted. Current auth has been reloaded.',
|
|
117
125
|
auth_reload_blocked_active: 'Cannot reload Codex auth while a turn, approval, or question is active. Wait or use /interrupt first.',
|
|
@@ -313,6 +321,9 @@ const MESSAGES = {
|
|
|
313
321
|
threads_filter: 'Filter: <code>{searchTerm}</code>',
|
|
314
322
|
threads_range: 'Showing {start}-{end}',
|
|
315
323
|
threads_filter_cleared_short: 'Filter cleared',
|
|
324
|
+
thread_context_title: 'Recent context:',
|
|
325
|
+
thread_context_user: 'User:',
|
|
326
|
+
thread_context_codex: 'Codex:',
|
|
316
327
|
button_prev_page: '⬅️ Prev',
|
|
317
328
|
button_next_page: '➡️ Next',
|
|
318
329
|
button_clear_filter: '🧹 Clear',
|
|
@@ -594,6 +605,7 @@ const MESSAGES = {
|
|
|
594
605
|
cmd_desc_fast: '切换 Fast 模式',
|
|
595
606
|
cmd_desc_active: '运行中新消息处理方式',
|
|
596
607
|
cmd_desc_status: '查看桥接状态',
|
|
608
|
+
cmd_desc_update: '升级并重启 FoxClaw',
|
|
597
609
|
cmd_desc_account: 'Codex 账号',
|
|
598
610
|
cmd_desc_quota: 'Codex 用量',
|
|
599
611
|
cmd_desc_login_device: 'ChatGPT 设备登录',
|
|
@@ -659,9 +671,16 @@ const MESSAGES = {
|
|
|
659
671
|
status_codex_usage_unavailable: 'Codex 用量:无法获取({error})',
|
|
660
672
|
status_codex_local_history: 'Codex 本地历史:{sessions} 个会话,{turns} 轮,{events} 条用量记录',
|
|
661
673
|
status_codex_local_tokens: 'Codex 本地 Token:总计 {total};输入 {input},输出 {output},缓存输入 {cached},推理输出 {reasoning}',
|
|
674
|
+
status_codex_local_speed: 'Codex 本地输出速度:平均 {avg} token/s,最近 {latest} token/s({samples} 个样本)',
|
|
662
675
|
status_codex_local_usage_unavailable: 'Codex 本地历史:无法获取({error})',
|
|
663
676
|
status_codex_credits: 'Codex 额度:{value}',
|
|
664
677
|
status_codex_limit_reached: 'Codex 限制:{value}',
|
|
678
|
+
update_started: '已开始升级 FoxClaw。安装、自检和服务重启完成后,我会在这里回报结果。',
|
|
679
|
+
update_succeeded: 'FoxClaw 已升级并重启:{from} -> {to}。',
|
|
680
|
+
update_failed: 'FoxClaw 升级失败:{error}\n请在终端运行 foxclaw update 查看详情。',
|
|
681
|
+
update_unavailable: '当前运行方式不支持自升级,请在终端运行 foxclaw update。',
|
|
682
|
+
update_already_running: 'FoxClaw 升级已经在进行中,结束后我会在这里回报结果。',
|
|
683
|
+
update_blocked_active: '当前有回复、审批或问题在进行中,不能升级 FoxClaw。请先等待,或使用 /interrupt。',
|
|
665
684
|
auth_reload_restarting: '正在重启 Codex app-server 以重新读取 auth...',
|
|
666
685
|
auth_reload_done: 'Codex app-server 已重启,当前 auth 已重新读取。',
|
|
667
686
|
auth_reload_blocked_active: '当前有回复、审批或问题在进行中,不能重载 Codex auth。请先等待,或使用 /interrupt。',
|
|
@@ -863,6 +882,9 @@ const MESSAGES = {
|
|
|
863
882
|
threads_filter: '筛选:<code>{searchTerm}</code>',
|
|
864
883
|
threads_range: '显示第 {start}-{end} 条',
|
|
865
884
|
threads_filter_cleared_short: '已清除筛选',
|
|
885
|
+
thread_context_title: '最近上下文:',
|
|
886
|
+
thread_context_user: '用户指令:',
|
|
887
|
+
thread_context_codex: 'Codex 最后输出:',
|
|
866
888
|
button_prev_page: '⬅️ 上一页',
|
|
867
889
|
button_next_page: '➡️ 下一页',
|
|
868
890
|
button_clear_filter: '🧹 清除',
|
|
@@ -1113,6 +1135,7 @@ export function getTelegramCommands(locale) {
|
|
|
1113
1135
|
{ command: 'help', description: t(locale, 'cmd_desc_help') },
|
|
1114
1136
|
{ command: 'setup', description: t(locale, 'cmd_desc_setup') },
|
|
1115
1137
|
{ command: 'status', description: t(locale, 'cmd_desc_status') },
|
|
1138
|
+
{ command: 'update', description: t(locale, 'cmd_desc_update') },
|
|
1116
1139
|
{ command: 'threads', description: t(locale, 'cmd_desc_threads') },
|
|
1117
1140
|
{ command: 'auth', description: t(locale, 'cmd_desc_auth') },
|
|
1118
1141
|
{ command: 'fast', description: t(locale, 'cmd_desc_fast') },
|
package/dist/main.js
CHANGED
|
@@ -9,6 +9,7 @@ import { APP_HOME, DEFAULT_ENV_PATH, DEFAULT_LOG_PATH, DEFAULT_STATUS_PATH, getL
|
|
|
9
9
|
import { acquireProcessLock, LockHeldError } from './lock.js';
|
|
10
10
|
import { readRuntimeStatus, writeRuntimeStatus } from './runtime.js';
|
|
11
11
|
import { refreshFoxclawExecStartDropIns, removeFoxclawExecStartDropIns } from './systemd.js';
|
|
12
|
+
import { createSelfUpdateRuntime, performSelfUpdate } from './update.js';
|
|
12
13
|
const rawCommand = process.argv[2];
|
|
13
14
|
const command = rawCommand || 'serve';
|
|
14
15
|
loadEnv();
|
|
@@ -53,6 +54,18 @@ async function main() {
|
|
|
53
54
|
startService('restart');
|
|
54
55
|
return;
|
|
55
56
|
}
|
|
57
|
+
if (command === 'update') {
|
|
58
|
+
requireNode24(command);
|
|
59
|
+
const notificationFile = readOptionValue('--notification-file');
|
|
60
|
+
const options = {
|
|
61
|
+
entryPoint,
|
|
62
|
+
nodePath: process.execPath,
|
|
63
|
+
version: readPackageVersion(),
|
|
64
|
+
...(notificationFile ? { notificationFile } : {}),
|
|
65
|
+
};
|
|
66
|
+
const outcome = performSelfUpdate(options);
|
|
67
|
+
process.exit(outcome.ok ? 0 : 1);
|
|
68
|
+
}
|
|
56
69
|
if (command === 'stop') {
|
|
57
70
|
stopService();
|
|
58
71
|
return;
|
|
@@ -116,6 +129,7 @@ Usage:
|
|
|
116
129
|
foxclaw doctor
|
|
117
130
|
foxclaw status
|
|
118
131
|
foxclaw start|restart|stop
|
|
132
|
+
foxclaw update
|
|
119
133
|
foxclaw install-systemd|uninstall-systemd
|
|
120
134
|
foxclaw install-launchd
|
|
121
135
|
foxclaw weixin-login [account-id]
|
|
@@ -152,7 +166,14 @@ async function runServeCli() {
|
|
|
152
166
|
? new WeixinMessagingPort(store, (id) => loadWeixinAccount(config.weixinAccountsDir, id))
|
|
153
167
|
: null;
|
|
154
168
|
const outbound = new BridgeMessagingRouter(telegramMessaging, weixinMessaging);
|
|
155
|
-
const
|
|
169
|
+
const selfUpdater = createSelfUpdateRuntime({
|
|
170
|
+
entryPoint,
|
|
171
|
+
nodePath: process.execPath,
|
|
172
|
+
version: readPackageVersion(),
|
|
173
|
+
statusPath: config.statusPath,
|
|
174
|
+
logPath: path.join(APP_HOME, 'logs', 'update.log'),
|
|
175
|
+
});
|
|
176
|
+
const core = new BridgeSessionCore(config, store, logger, bot, app, outbound, selfUpdater);
|
|
156
177
|
const telegram = new TelegramChannelAdapter(core);
|
|
157
178
|
if (config.wxEnabled) {
|
|
158
179
|
weixinAdapter = new WeixinChannelAdapter(core, store, config, logger);
|
|
@@ -436,6 +457,11 @@ function escapeRegExp(value) {
|
|
|
436
457
|
function editorCommand(envPath) {
|
|
437
458
|
return `${process.env.EDITOR?.trim() || '$EDITOR'} ${envPath}`;
|
|
438
459
|
}
|
|
460
|
+
function readOptionValue(option) {
|
|
461
|
+
const index = process.argv.indexOf(option);
|
|
462
|
+
const value = index >= 0 ? process.argv[index + 1] : undefined;
|
|
463
|
+
return value && !value.startsWith('--') ? value : undefined;
|
|
464
|
+
}
|
|
439
465
|
function startService(action) {
|
|
440
466
|
if (!runDoctorChecks()) {
|
|
441
467
|
console.error('');
|
package/dist/update.d.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { AppLocale } from './types.js';
|
|
2
|
+
export type SelfUpdateState = 'pending' | 'succeeded' | 'failed';
|
|
3
|
+
export interface SelfUpdateStatus {
|
|
4
|
+
state: SelfUpdateState;
|
|
5
|
+
scopeId: string;
|
|
6
|
+
locale: AppLocale;
|
|
7
|
+
fromVersion: string;
|
|
8
|
+
toVersion: string | null;
|
|
9
|
+
error: string | null;
|
|
10
|
+
updatedAt: string;
|
|
11
|
+
}
|
|
12
|
+
export interface SelfUpdateRuntime {
|
|
13
|
+
launch(scopeId: string, locale: AppLocale): Promise<void>;
|
|
14
|
+
readStatus(): Promise<SelfUpdateStatus | null>;
|
|
15
|
+
clearStatus(): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export interface SelfUpdateInstaller {
|
|
18
|
+
manager: 'npm' | 'pnpm';
|
|
19
|
+
command: string;
|
|
20
|
+
installArgs: string[];
|
|
21
|
+
rootArgs: string[];
|
|
22
|
+
}
|
|
23
|
+
interface CreateSelfUpdateRuntimeOptions {
|
|
24
|
+
entryPoint: string;
|
|
25
|
+
nodePath: string;
|
|
26
|
+
version: string;
|
|
27
|
+
statusPath: string;
|
|
28
|
+
logPath: string;
|
|
29
|
+
}
|
|
30
|
+
interface PerformSelfUpdateOptions {
|
|
31
|
+
entryPoint: string;
|
|
32
|
+
nodePath: string;
|
|
33
|
+
version: string;
|
|
34
|
+
notificationFile?: string;
|
|
35
|
+
env?: NodeJS.ProcessEnv;
|
|
36
|
+
}
|
|
37
|
+
export interface SelfUpdateOutcome {
|
|
38
|
+
ok: boolean;
|
|
39
|
+
fromVersion: string;
|
|
40
|
+
toVersion: string | null;
|
|
41
|
+
error: string | null;
|
|
42
|
+
}
|
|
43
|
+
export declare function selfUpdateStatusPath(statusPath: string): string;
|
|
44
|
+
export declare function resolveSelfUpdateInstaller(entryPoint: string, nodePath?: string, exists?: (target: string) => boolean): SelfUpdateInstaller;
|
|
45
|
+
export declare function readSelfUpdateStatus(statusFile: string): SelfUpdateStatus | null;
|
|
46
|
+
export declare function writeSelfUpdateStatus(statusFile: string, status: SelfUpdateStatus): void;
|
|
47
|
+
export declare function createSelfUpdateRuntime(options: CreateSelfUpdateRuntimeOptions): SelfUpdateRuntime;
|
|
48
|
+
export declare function performSelfUpdate(options: PerformSelfUpdateOptions): SelfUpdateOutcome;
|
|
49
|
+
export {};
|
package/dist/update.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
5
|
+
const PACKAGE_SPEC = '@foxden-app/foxclaw@latest';
|
|
6
|
+
const UPDATE_STATUS_FILENAME = 'self-update.json';
|
|
7
|
+
export function selfUpdateStatusPath(statusPath) {
|
|
8
|
+
return path.join(path.dirname(statusPath), UPDATE_STATUS_FILENAME);
|
|
9
|
+
}
|
|
10
|
+
export function resolveSelfUpdateInstaller(entryPoint, nodePath = process.execPath, exists = fs.existsSync) {
|
|
11
|
+
const normalizedEntryPoint = entryPoint.replace(/\\/g, '/');
|
|
12
|
+
const globalMarker = '/global/';
|
|
13
|
+
const globalIndex = normalizedEntryPoint.indexOf(globalMarker);
|
|
14
|
+
if (globalIndex > 0 && normalizedEntryPoint.includes('/.pnpm/')) {
|
|
15
|
+
const pnpmHome = normalizedEntryPoint.slice(0, globalIndex);
|
|
16
|
+
const pnpmCommand = path.join(pnpmHome, process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm');
|
|
17
|
+
if (!exists(pnpmCommand)) {
|
|
18
|
+
throw new Error(`Current installation is managed by pnpm, but pnpm was not found at ${pnpmCommand}.`);
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
manager: 'pnpm',
|
|
22
|
+
command: pnpmCommand,
|
|
23
|
+
installArgs: ['add', '--global', PACKAGE_SPEC],
|
|
24
|
+
rootArgs: ['root', '--global'],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const adjacentNpm = path.join(path.dirname(nodePath), process.platform === 'win32' ? 'npm.cmd' : 'npm');
|
|
28
|
+
return {
|
|
29
|
+
manager: 'npm',
|
|
30
|
+
command: exists(adjacentNpm) ? adjacentNpm : (process.platform === 'win32' ? 'npm.cmd' : 'npm'),
|
|
31
|
+
installArgs: ['install', '--global', PACKAGE_SPEC],
|
|
32
|
+
rootArgs: ['root', '--global'],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export function readSelfUpdateStatus(statusFile) {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = JSON.parse(fs.readFileSync(statusFile, 'utf8'));
|
|
38
|
+
if ((parsed.state !== 'pending' && parsed.state !== 'succeeded' && parsed.state !== 'failed')
|
|
39
|
+
|| typeof parsed.scopeId !== 'string'
|
|
40
|
+
|| (parsed.locale !== 'en' && parsed.locale !== 'zh')
|
|
41
|
+
|| typeof parsed.fromVersion !== 'string'
|
|
42
|
+
|| typeof parsed.updatedAt !== 'string') {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
state: parsed.state,
|
|
47
|
+
scopeId: parsed.scopeId,
|
|
48
|
+
locale: parsed.locale,
|
|
49
|
+
fromVersion: parsed.fromVersion,
|
|
50
|
+
toVersion: typeof parsed.toVersion === 'string' ? parsed.toVersion : null,
|
|
51
|
+
error: typeof parsed.error === 'string' ? parsed.error : null,
|
|
52
|
+
updatedAt: parsed.updatedAt,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function writeSelfUpdateStatus(statusFile, status) {
|
|
60
|
+
fs.mkdirSync(path.dirname(statusFile), { recursive: true });
|
|
61
|
+
const temporaryFile = `${statusFile}.${process.pid}.tmp`;
|
|
62
|
+
fs.writeFileSync(temporaryFile, `${JSON.stringify(status, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
63
|
+
fs.renameSync(temporaryFile, statusFile);
|
|
64
|
+
}
|
|
65
|
+
export function createSelfUpdateRuntime(options) {
|
|
66
|
+
const statusFile = selfUpdateStatusPath(options.statusPath);
|
|
67
|
+
return {
|
|
68
|
+
async launch(scopeId, locale) {
|
|
69
|
+
const current = readSelfUpdateStatus(statusFile);
|
|
70
|
+
if (current?.state === 'pending') {
|
|
71
|
+
throw new Error('A FoxClaw update is already running.');
|
|
72
|
+
}
|
|
73
|
+
writeSelfUpdateStatus(statusFile, {
|
|
74
|
+
state: 'pending',
|
|
75
|
+
scopeId,
|
|
76
|
+
locale,
|
|
77
|
+
fromVersion: options.version,
|
|
78
|
+
toVersion: null,
|
|
79
|
+
error: null,
|
|
80
|
+
updatedAt: new Date().toISOString(),
|
|
81
|
+
});
|
|
82
|
+
fs.mkdirSync(path.dirname(options.logPath), { recursive: true });
|
|
83
|
+
const logFd = fs.openSync(options.logPath, 'a', 0o600);
|
|
84
|
+
try {
|
|
85
|
+
const child = spawn(options.nodePath, [options.entryPoint, 'update', '--notification-file', statusFile], {
|
|
86
|
+
detached: true,
|
|
87
|
+
stdio: ['ignore', logFd, logFd],
|
|
88
|
+
env: process.env,
|
|
89
|
+
});
|
|
90
|
+
child.unref();
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
writeSelfUpdateStatus(statusFile, {
|
|
94
|
+
state: 'failed',
|
|
95
|
+
scopeId,
|
|
96
|
+
locale,
|
|
97
|
+
fromVersion: options.version,
|
|
98
|
+
toVersion: null,
|
|
99
|
+
error: formatError(error),
|
|
100
|
+
updatedAt: new Date().toISOString(),
|
|
101
|
+
});
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
fs.closeSync(logFd);
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
async readStatus() {
|
|
109
|
+
return readSelfUpdateStatus(statusFile);
|
|
110
|
+
},
|
|
111
|
+
async clearStatus() {
|
|
112
|
+
fs.rmSync(statusFile, { force: true });
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
export function performSelfUpdate(options) {
|
|
117
|
+
const env = options.env ?? process.env;
|
|
118
|
+
let toVersion = null;
|
|
119
|
+
try {
|
|
120
|
+
const installer = resolveSelfUpdateInstaller(options.entryPoint, options.nodePath);
|
|
121
|
+
console.log(`[UPDATE] Installing ${PACKAGE_SPEC} with ${installer.manager}...`);
|
|
122
|
+
runInherited(installer.command, installer.installArgs, env);
|
|
123
|
+
const updatedEntryPoint = resolveUpdatedEntryPoint(installer, env);
|
|
124
|
+
toVersion = readInstalledPackageVersion(updatedEntryPoint);
|
|
125
|
+
console.log('[UPDATE] Running checks and restarting the FoxClaw service...');
|
|
126
|
+
runInherited(options.nodePath, [updatedEntryPoint, 'start'], env);
|
|
127
|
+
completeNotification(options.notificationFile, 'succeeded', toVersion, null);
|
|
128
|
+
console.log(`[OK] FoxClaw updated and restarted: ${options.version} -> ${toVersion}`);
|
|
129
|
+
return {
|
|
130
|
+
ok: true,
|
|
131
|
+
fromVersion: options.version,
|
|
132
|
+
toVersion,
|
|
133
|
+
error: null,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
const message = formatError(error);
|
|
138
|
+
completeNotification(options.notificationFile, 'failed', toVersion, message);
|
|
139
|
+
console.error(`[FAIL] FoxClaw update failed: ${message}`);
|
|
140
|
+
return {
|
|
141
|
+
ok: false,
|
|
142
|
+
fromVersion: options.version,
|
|
143
|
+
toVersion,
|
|
144
|
+
error: message,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function runInherited(command, args, env) {
|
|
149
|
+
const result = spawnSync(command, args, { stdio: 'inherit', env });
|
|
150
|
+
if (result.error) {
|
|
151
|
+
throw result.error;
|
|
152
|
+
}
|
|
153
|
+
if (result.status !== 0) {
|
|
154
|
+
throw new Error(`${command} ${args.join(' ')} exited with status ${result.status ?? 'unknown'}.`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function resolveUpdatedEntryPoint(installer, env) {
|
|
158
|
+
const result = spawnSync(installer.command, installer.rootArgs, { encoding: 'utf8', env });
|
|
159
|
+
if (result.error) {
|
|
160
|
+
throw result.error;
|
|
161
|
+
}
|
|
162
|
+
if (result.status !== 0) {
|
|
163
|
+
throw new Error(`Could not locate the updated global package root using ${installer.manager}.`);
|
|
164
|
+
}
|
|
165
|
+
const globalRoot = result.stdout.trim().split(/\r?\n/).filter(Boolean).at(-1);
|
|
166
|
+
if (!globalRoot) {
|
|
167
|
+
throw new Error(`Could not locate the updated global package root using ${installer.manager}.`);
|
|
168
|
+
}
|
|
169
|
+
const updatedEntryPoint = path.join(globalRoot, '@foxden-app', 'foxclaw', 'dist', 'main.js');
|
|
170
|
+
if (!fs.existsSync(updatedEntryPoint)) {
|
|
171
|
+
throw new Error(`Updated FoxClaw entry point was not found at ${updatedEntryPoint}.`);
|
|
172
|
+
}
|
|
173
|
+
return updatedEntryPoint;
|
|
174
|
+
}
|
|
175
|
+
function readInstalledPackageVersion(updatedEntryPoint) {
|
|
176
|
+
try {
|
|
177
|
+
const packageFile = path.resolve(path.dirname(updatedEntryPoint), '..', 'package.json');
|
|
178
|
+
const pkg = JSON.parse(fs.readFileSync(packageFile, 'utf8'));
|
|
179
|
+
return typeof pkg.version === 'string' && pkg.version.trim() ? pkg.version : 'unknown';
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return 'unknown';
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function completeNotification(notificationFile, state, toVersion, error) {
|
|
186
|
+
if (!notificationFile) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const pending = readSelfUpdateStatus(notificationFile);
|
|
190
|
+
if (!pending) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
writeSelfUpdateStatus(notificationFile, {
|
|
194
|
+
...pending,
|
|
195
|
+
state,
|
|
196
|
+
toVersion,
|
|
197
|
+
error,
|
|
198
|
+
updatedAt: new Date().toISOString(),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
function formatError(error) {
|
|
202
|
+
return error instanceof Error ? error.message : String(error);
|
|
203
|
+
}
|
|
@@ -277,10 +277,11 @@ foxclaw uninstall-systemd
|
|
|
277
277
|
Update FoxClaw later:
|
|
278
278
|
|
|
279
279
|
```bash
|
|
280
|
-
|
|
281
|
-
foxclaw start
|
|
280
|
+
foxclaw update
|
|
282
281
|
```
|
|
283
282
|
|
|
283
|
+
You can also send `/update` in an authorized Telegram chat. When no turn, approval, or question is active, it upgrades, checks, restarts the service, and reports the result after restart.
|
|
284
|
+
|
|
284
285
|
## Next Step
|
|
285
286
|
|
|
286
287
|
After the first install works, read the [User Manual](./user-manual.md) for `/help`, `/setup`, `/threads`, `/watch`, `/auth`, Codex login, and multi-account auth rotation.
|
package/docs/troubleshooting.md
CHANGED
|
@@ -223,7 +223,7 @@ Then run:
|
|
|
223
223
|
foxclaw restart
|
|
224
224
|
```
|
|
225
225
|
|
|
226
|
-
FoxClaw writes proxychains into the main service and removes stale FoxClaw `ExecStart` overrides, so later upgrades
|
|
226
|
+
FoxClaw writes proxychains into the main service and removes stale FoxClaw `ExecStart` overrides, so later upgrades only need `foxclaw update`.
|
|
227
227
|
|
|
228
228
|
## Service Starts With The Wrong Node Version
|
|
229
229
|
|
package/docs/user-manual.md
CHANGED
|
@@ -136,6 +136,8 @@ foxclaw start
|
|
|
136
136
|
foxclaw status
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
+
For later upgrades, run `foxclaw update`. It uses the npm or pnpm installation method currently managing FoxClaw, upgrades globally, runs checks, and restarts the background service.
|
|
140
|
+
|
|
139
141
|
Linux service logs:
|
|
140
142
|
|
|
141
143
|
```bash
|
|
@@ -226,11 +228,12 @@ For group messages:
|
|
|
226
228
|
|
|
227
229
|
Later commands are sorted by recent usage. Plain text, photos, and files continue the currently bound thread; if no thread is bound, FoxClaw creates one.
|
|
228
230
|
|
|
229
|
-
### 3.2 `/status`, `/account`, `/quota`
|
|
231
|
+
### 3.2 `/status`, `/account`, `/quota`, `/update`
|
|
230
232
|
|
|
231
233
|
- `/status`: FoxClaw, app-server, current thread binding, model, access, and Codex usage summary.
|
|
232
234
|
- `/account`: current Codex account.
|
|
233
235
|
- `/quota`: Codex usage and quota window.
|
|
236
|
+
- `/update`: upgrade FoxClaw, run checks, and restart the service; it refuses while a turn, approval, or question is active, then reports the result after restart.
|
|
234
237
|
|
|
235
238
|
### 3.3 `/config`, `/requirements`, `/provider`
|
|
236
239
|
|
|
@@ -275,10 +275,11 @@ foxclaw uninstall-systemd
|
|
|
275
275
|
以后升级 FoxClaw:
|
|
276
276
|
|
|
277
277
|
```bash
|
|
278
|
-
|
|
279
|
-
foxclaw start
|
|
278
|
+
foxclaw update
|
|
280
279
|
```
|
|
281
280
|
|
|
281
|
+
也可以在已授权的 Telegram 私聊里发送 `/update`。它会在当前没有运行中回复、审批或待确认问题时,完成升级、自检和服务重启,并在重启后回报结果。
|
|
282
|
+
|
|
282
283
|
如果 `~/.foxclaw/.env` 已经存在,`foxclaw init` 会先询问是否更新 Telegram 和工作目录相关字段,其它配置保持不变。
|
|
283
284
|
|
|
284
285
|
## 鸣谢
|
|
@@ -224,7 +224,7 @@ FOXCLAW_PROXYCHAINS_CONF=/home/wuya/.proxychains-rt.conf
|
|
|
224
224
|
foxclaw restart
|
|
225
225
|
```
|
|
226
226
|
|
|
227
|
-
FoxClaw 会把 proxychains 写进主 service,并清理旧的 FoxClaw `ExecStart`
|
|
227
|
+
FoxClaw 会把 proxychains 写进主 service,并清理旧的 FoxClaw `ExecStart` 覆盖,后续升级直接运行 `foxclaw update` 即可。
|
|
228
228
|
|
|
229
229
|
## 服务用了错误的 Node 版本
|
|
230
230
|
|
package/docs/zh/user-manual.md
CHANGED
|
@@ -136,6 +136,8 @@ foxclaw start
|
|
|
136
136
|
foxclaw status
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
+
后续升级只需运行 `foxclaw update`;它会使用当前安装 FoxClaw 的 npm 或 pnpm,全局升级后执行自检并重启后台服务。
|
|
140
|
+
|
|
139
141
|
Linux 查看服务日志:
|
|
140
142
|
|
|
141
143
|
```bash
|
|
@@ -226,11 +228,12 @@ TG_ALLOWED_TOPIC_ID=42
|
|
|
226
228
|
|
|
227
229
|
后面的命令会按你最近使用情况排序。直接发送普通文本、图片或文件时,FoxClaw 会继续当前绑定线程;如果没有绑定线程,会自动新建线程。
|
|
228
230
|
|
|
229
|
-
### 3.2 `/status`、`/account`、`/quota`
|
|
231
|
+
### 3.2 `/status`、`/account`、`/quota`、`/update`
|
|
230
232
|
|
|
231
233
|
- `/status`:查看 FoxClaw、app-server、当前绑定线程、模型、权限和 Codex 用量摘要。
|
|
232
234
|
- `/account`:查看当前 Codex 登录账号。
|
|
233
235
|
- `/quota`:查看 Codex 用量和额度窗口。
|
|
236
|
+
- `/update`:升级 FoxClaw、自检并重启服务;当前有运行中回复、审批或待确认问题时会拒绝执行,重启后会回报结果。
|
|
234
237
|
|
|
235
238
|
### 3.3 `/config`、`/requirements`、`/provider`
|
|
236
239
|
|