@foxden-app/foxclaw 0.5.54 → 0.5.56
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 +11 -4
- package/CHANGELOG.md +22 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +6 -1
- package/dist/controller/controller.d.ts +2 -1
- package/dist/controller/controller.js +33 -23
- package/dist/main.js +5 -2
- package/dist/update_status.d.ts +11 -0
- package/dist/update_status.js +18 -0
- package/dist/voice/tts.js +63 -0
- package/package.json +1 -1
package/.env.example
CHANGED
|
@@ -32,9 +32,13 @@ LOG_LEVEL=info
|
|
|
32
32
|
DEFAULT_CWD=/absolute/path/to/workspace
|
|
33
33
|
DEFAULT_APPROVAL_POLICY=on-request
|
|
34
34
|
DEFAULT_SANDBOX_MODE=workspace-write
|
|
35
|
-
TELEGRAM_POLL_INTERVAL_MS=1200
|
|
36
|
-
TELEGRAM_PREVIEW_THROTTLE_MS=800
|
|
37
|
-
|
|
35
|
+
TELEGRAM_POLL_INTERVAL_MS=1200
|
|
36
|
+
TELEGRAM_PREVIEW_THROTTLE_MS=800
|
|
37
|
+
# Delete folded operation-detail messages after the final answer is sent.
|
|
38
|
+
# Set to false if you prefer to keep "read files / edited files / ran command"
|
|
39
|
+
# detail cards in the chat history.
|
|
40
|
+
# TELEGRAM_DELETE_TOOL_DETAILS_AFTER_FINAL=true
|
|
41
|
+
THREAD_LIST_LIMIT=10
|
|
38
42
|
CODEX_CLI_BIN=/absolute/path/to/codex
|
|
39
43
|
|
|
40
44
|
# Optional cross-node Codex auth sync.
|
|
@@ -59,6 +63,7 @@ CODEX_CLI_BIN=/absolute/path/to/codex
|
|
|
59
63
|
# your own speech host; HTTP mode calls your own VOICE_TTS_URL and converts
|
|
60
64
|
# locally. FoxClaw does not ship with a public TTS backend.
|
|
61
65
|
# VOICE_TTS_ENABLED=false
|
|
66
|
+
# VOICE_TTS_ENGINE=qwen
|
|
62
67
|
# HTTP backend example: compatible service exposing /v1/tts/custom or
|
|
63
68
|
# /v1/tts/design and returning WAV audio. Local ffmpeg is required.
|
|
64
69
|
# VOICE_TTS_MODE=http
|
|
@@ -70,11 +75,13 @@ CODEX_CLI_BIN=/absolute/path/to/codex
|
|
|
70
75
|
# VOICE_TTS_MODE=ssh
|
|
71
76
|
# VOICE_TTS_SSH_HOST=<ssh-host>
|
|
72
77
|
# VOICE_TTS_SSH_DIR=/path/to/qwen-speech-server
|
|
78
|
+
# For SoulX-compatible backends, set VOICE_TTS_ENGINE=soulx and point
|
|
79
|
+
# VOICE_TTS_URL at the SoulX service, for example http://127.0.0.1:18082.
|
|
73
80
|
# VOICE_TTS_DESIGN_INSTRUCT=用自然清晰的中文女声朗读,语速适中。
|
|
74
81
|
# VOICE_SUMMARY_BUTTON_ENABLED=true
|
|
75
82
|
# VOICE_SUMMARY_TEXT_LIMIT=180
|
|
76
83
|
# VOICE_TEXT_LIMIT=2800
|
|
77
|
-
# VOICE_TTS_TIMEOUT_MS=
|
|
84
|
+
# VOICE_TTS_TIMEOUT_MS=60000
|
|
78
85
|
|
|
79
86
|
# Optional: standard HTTP(S) proxy for Telegram and ChatGPT/Codex backend requests.
|
|
80
87
|
# Put these in the same env file that `foxclaw start` installs into systemd/launchd.
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
All notable FoxClaw changes are listed here. Each release note is bilingual so GitHub Releases and the npm package are useful to both Chinese and English readers.
|
|
4
4
|
|
|
5
|
+
## 0.5.56 - 2026-06-21
|
|
6
|
+
|
|
7
|
+
### 中文
|
|
8
|
+
- 修正 `foxclaw status` 和 Telegram `/status` 的升级状态摘要:旧的成功 self-update 记录如果不是当前 FoxClaw 版本,不再显示为 “Last update/Last service update”,避免误导当前运行版本判断。
|
|
9
|
+
- 多 runtime 状态聚合现在携带当前 FoxClaw 版本,用于判断升级记录是否仍然相关;失败、运行中等需要处理的升级状态仍会展示。
|
|
10
|
+
|
|
11
|
+
### English
|
|
12
|
+
- Fixed `foxclaw status` and Telegram `/status` update summaries so stale successful self-update records are no longer shown as the current "Last update/Last service update" when they do not match the running FoxClaw version.
|
|
13
|
+
- Multi-runtime status aggregation now carries the current FoxClaw version for this relevance check; failed or in-progress update states are still shown.
|
|
14
|
+
|
|
15
|
+
## 0.5.55 - 2026-06-21
|
|
16
|
+
|
|
17
|
+
### 中文
|
|
18
|
+
- 最终答复发出后,默认删除已折叠的工具操作明细消息,让 Telegram 回顾时更接近“一问一答”;新增 `TELEGRAM_DELETE_TOOL_DETAILS_AFTER_FINAL=false` 可保留这些明细。
|
|
19
|
+
- “听总结”新增 `VOICE_TTS_ENGINE=soulx`,SSH 模式会先检查 SoulX `/health`,再调用 `/v1/tts` 并转成 Telegram voice;语音 TTS 默认超时缩短到 60 秒。
|
|
20
|
+
- 降噪内部状态通知:不再向 Telegram 展示“目标已清除”“线程 active/running”“MCP ready/starting”等低价值生命周期消息,错误和需要处理的状态仍会提示。
|
|
21
|
+
|
|
22
|
+
### English
|
|
23
|
+
- After a final answer is sent, FoxClaw now deletes archived tool-detail messages by default so Telegram history reads closer to one user turn and one final reply. Set `TELEGRAM_DELETE_TOOL_DETAILS_AFTER_FINAL=false` to keep them.
|
|
24
|
+
- Added `VOICE_TTS_ENGINE=soulx` for voice summaries. SSH mode checks the SoulX `/health` endpoint, calls `/v1/tts`, and converts the result to Telegram voice; the default TTS timeout is now 60 seconds.
|
|
25
|
+
- Reduced noisy internal lifecycle notifications: Telegram no longer shows low-value "goal cleared", active/running thread status, or MCP ready/starting messages, while errors and actionable states are still reported.
|
|
26
|
+
|
|
5
27
|
## 0.5.54 - 2026-06-20
|
|
6
28
|
|
|
7
29
|
### 中文
|
package/dist/config.d.ts
CHANGED
|
@@ -40,6 +40,7 @@ export interface AppConfig {
|
|
|
40
40
|
defaultSandboxMode: SandboxModeValue;
|
|
41
41
|
telegramPollIntervalMs: number;
|
|
42
42
|
telegramPreviewThrottleMs: number;
|
|
43
|
+
telegramDeleteToolDetailsAfterFinal: boolean;
|
|
43
44
|
threadListLimit: number;
|
|
44
45
|
statusPath: string;
|
|
45
46
|
logPath: string;
|
|
@@ -64,6 +65,7 @@ export interface AppConfig {
|
|
|
64
65
|
authSyncTempDir: string;
|
|
65
66
|
authAutoDeleteNeedsRepair: boolean;
|
|
66
67
|
voiceTtsEnabled: boolean;
|
|
68
|
+
voiceTtsEngine: 'qwen' | 'soulx';
|
|
67
69
|
voiceTtsMode: 'http' | 'ssh';
|
|
68
70
|
voiceTtsUrl: string | null;
|
|
69
71
|
voiceTtsToken: string | null;
|
package/dist/config.js
CHANGED
|
@@ -77,6 +77,7 @@ export function loadConfig() {
|
|
|
77
77
|
defaultSandboxMode: parseSandboxMode(process.env.DEFAULT_SANDBOX_MODE || 'workspace-write'),
|
|
78
78
|
telegramPollIntervalMs: intEnv('TELEGRAM_POLL_INTERVAL_MS', 1200),
|
|
79
79
|
telegramPreviewThrottleMs: intEnv('TELEGRAM_PREVIEW_THROTTLE_MS', 800),
|
|
80
|
+
telegramDeleteToolDetailsAfterFinal: boolEnv('TELEGRAM_DELETE_TOOL_DETAILS_AFTER_FINAL', true),
|
|
80
81
|
threadListLimit: intEnv('THREAD_LIST_LIMIT', 10),
|
|
81
82
|
statusPath: DEFAULT_STATUS_PATH,
|
|
82
83
|
logPath: DEFAULT_LOG_PATH,
|
|
@@ -98,6 +99,7 @@ export function loadConfig() {
|
|
|
98
99
|
authSyncTempDir: process.env.AUTH_SYNC_TEMP_DIR || DEFAULT_AUTH_SYNC_TEMP_DIR,
|
|
99
100
|
authAutoDeleteNeedsRepair: boolEnv('AUTH_AUTO_DELETE_NEEDS_REPAIR', false),
|
|
100
101
|
voiceTtsEnabled: boolEnv('VOICE_TTS_ENABLED', false),
|
|
102
|
+
voiceTtsEngine: parseVoiceTtsEngine(process.env.VOICE_TTS_ENGINE || 'qwen'),
|
|
101
103
|
voiceTtsMode: parseVoiceTtsMode(process.env.VOICE_TTS_MODE || (process.env.VOICE_TTS_SSH_HOST?.trim() ? 'ssh' : 'http')),
|
|
102
104
|
voiceTtsUrl: optional('VOICE_TTS_URL'),
|
|
103
105
|
voiceTtsToken: optional('VOICE_TTS_TOKEN'),
|
|
@@ -108,7 +110,7 @@ export function loadConfig() {
|
|
|
108
110
|
voiceSummaryButtonEnabled: boolEnv('VOICE_SUMMARY_BUTTON_ENABLED', true),
|
|
109
111
|
voiceSummaryTextLimit: intEnv('VOICE_SUMMARY_TEXT_LIMIT', 180),
|
|
110
112
|
voiceTextLimit: intEnv('VOICE_TEXT_LIMIT', 2800),
|
|
111
|
-
voiceTtsTimeoutMs: intEnv('VOICE_TTS_TIMEOUT_MS',
|
|
113
|
+
voiceTtsTimeoutMs: intEnv('VOICE_TTS_TIMEOUT_MS', 60_000),
|
|
112
114
|
};
|
|
113
115
|
ensureAppDirs(config);
|
|
114
116
|
return config;
|
|
@@ -197,6 +199,9 @@ function parseSandboxMode(value) {
|
|
|
197
199
|
function parseVoiceTtsMode(value) {
|
|
198
200
|
return value.trim().toLowerCase() === 'http' ? 'http' : 'ssh';
|
|
199
201
|
}
|
|
202
|
+
function parseVoiceTtsEngine(value) {
|
|
203
|
+
return value.trim().toLowerCase() === 'soulx' ? 'soulx' : 'qwen';
|
|
204
|
+
}
|
|
200
205
|
function resolveCommand(commandName) {
|
|
201
206
|
try {
|
|
202
207
|
const which = process.platform === 'win32' ? 'where' : 'which';
|
|
@@ -38,6 +38,7 @@ export interface CoreCoordinator {
|
|
|
38
38
|
}>;
|
|
39
39
|
statusUpdated?: (status: RuntimeStatus) => void;
|
|
40
40
|
getServiceStatus?: () => Promise<{
|
|
41
|
+
currentVersion?: string;
|
|
41
42
|
bots: NonNullable<RuntimeStatus['bots']>;
|
|
42
43
|
weixinRuntime?: RuntimeStatus['weixinRuntime'];
|
|
43
44
|
authMirror?: RuntimeStatus['authMirror'];
|
|
@@ -239,7 +240,7 @@ export declare class BridgeSessionCore {
|
|
|
239
240
|
private sendTyping;
|
|
240
241
|
private sendObservedCliUserMessage;
|
|
241
242
|
private collapseTurnCommentary;
|
|
242
|
-
private
|
|
243
|
+
private cleanupTransientProgressMessages;
|
|
243
244
|
private hasObservedPersistentReply;
|
|
244
245
|
private ensureThreadReady;
|
|
245
246
|
private handleAsyncError;
|
|
@@ -7,6 +7,7 @@ import { normalizeLocale, t } from '../i18n.js';
|
|
|
7
7
|
import { chatGptAuthMetadataMatchesCandidateName, parseChatGptAuthMetadata, readChatGptAuthRecord, readChatGptAuthMetadata, } from '../auth/mirror.js';
|
|
8
8
|
import { readAccessTokenExpiresAtMs } from '../auth/cross_node_sync.js';
|
|
9
9
|
import { parseCommand } from './commands.js';
|
|
10
|
+
import { shouldShowRuntimeLastUpdate } from '../update_status.js';
|
|
10
11
|
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';
|
|
11
12
|
import { clampServiceTierToModel, resolveFastTierForModel } from './service_tier.js';
|
|
12
13
|
import { TELEGRAM_BOT_API_DOWNLOAD_LIMIT_BYTES, buildAttachmentPrompt, isNativeImageAttachment, planAttachmentStoragePath, summarizeTelegramInput, } from '../telegram/media.js';
|
|
@@ -496,13 +497,14 @@ export class BridgeSessionCore {
|
|
|
496
497
|
value: formatAuthProactiveRefreshStatus(locale, serviceStatus.authProactiveRefresh),
|
|
497
498
|
}));
|
|
498
499
|
}
|
|
499
|
-
if (serviceStatus
|
|
500
|
+
if (shouldShowRuntimeLastUpdate(serviceStatus)) {
|
|
501
|
+
const lastUpdate = serviceStatus.lastUpdate;
|
|
500
502
|
lines.push(t(locale, 'status_last_update', {
|
|
501
|
-
from:
|
|
502
|
-
to:
|
|
503
|
-
time:
|
|
503
|
+
from: lastUpdate.fromVersion,
|
|
504
|
+
to: lastUpdate.toVersion ?? t(locale, 'unknown'),
|
|
505
|
+
time: lastUpdate.updatedAt,
|
|
504
506
|
}));
|
|
505
|
-
const codexUpdateLine = this.formatCodexUpdateResult(
|
|
507
|
+
const codexUpdateLine = this.formatCodexUpdateResult(lastUpdate);
|
|
506
508
|
if (codexUpdateLine) {
|
|
507
509
|
lines.push(t(locale, 'status_last_codex_update', { value: codexUpdateLine }));
|
|
508
510
|
}
|
|
@@ -1495,6 +1497,9 @@ export class BridgeSessionCore {
|
|
|
1495
1497
|
if (status === 'idle') {
|
|
1496
1498
|
return;
|
|
1497
1499
|
}
|
|
1500
|
+
if (status === 'active' || status === 'running') {
|
|
1501
|
+
return;
|
|
1502
|
+
}
|
|
1498
1503
|
const locale = this.localeForChat(scopeId);
|
|
1499
1504
|
await this.sendMessage(scopeId, t(locale, 'thread_status_changed', { threadId, status }));
|
|
1500
1505
|
}
|
|
@@ -1534,7 +1539,6 @@ export class BridgeSessionCore {
|
|
|
1534
1539
|
}
|
|
1535
1540
|
const locale = this.localeForChat(scopeId);
|
|
1536
1541
|
if (method === 'thread/goal/cleared') {
|
|
1537
|
-
await this.sendMessage(scopeId, t(locale, 'goal_cleared_notification', { threadId }));
|
|
1538
1542
|
return;
|
|
1539
1543
|
}
|
|
1540
1544
|
const goal = mapGoalNotification(params?.goal);
|
|
@@ -1828,6 +1832,10 @@ export class BridgeSessionCore {
|
|
|
1828
1832
|
const message = params?.error
|
|
1829
1833
|
? `MCP ${name}: ${status} (${String(params.error)})`
|
|
1830
1834
|
: `MCP ${name}: ${status}`;
|
|
1835
|
+
const normalized = status.toLowerCase();
|
|
1836
|
+
if (!params?.error && ['ready', 'running', 'starting', 'connected'].includes(normalized)) {
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1831
1839
|
await this.notifyBoundScopes(message);
|
|
1832
1840
|
}
|
|
1833
1841
|
async handleMcpOauthLoginCompleted(params) {
|
|
@@ -1877,7 +1885,7 @@ export class BridgeSessionCore {
|
|
|
1877
1885
|
}
|
|
1878
1886
|
try {
|
|
1879
1887
|
await this.completeTurn(active);
|
|
1880
|
-
await this.
|
|
1888
|
+
await this.cleanupTransientProgressMessages(active);
|
|
1881
1889
|
await this.finalizeUserInputsForTurn(active, 'resolved');
|
|
1882
1890
|
this.markQueuedTurnCompleted(active);
|
|
1883
1891
|
}
|
|
@@ -3234,7 +3242,7 @@ export class BridgeSessionCore {
|
|
|
3234
3242
|
try {
|
|
3235
3243
|
this.promoteReadyToolBatch(active);
|
|
3236
3244
|
await this.completeTurn(active);
|
|
3237
|
-
await this.
|
|
3245
|
+
await this.cleanupTransientProgressMessages(active);
|
|
3238
3246
|
await this.finalizeUserInputsForTurn(active, active.interruptRequested ? 'interrupted' : 'resolved');
|
|
3239
3247
|
await this.maybeSendPlanImplementationPrompt(active);
|
|
3240
3248
|
this.markQueuedTurnCompleted(active);
|
|
@@ -3481,21 +3489,25 @@ export class BridgeSessionCore {
|
|
|
3481
3489
|
segment.messages = [];
|
|
3482
3490
|
}
|
|
3483
3491
|
}
|
|
3484
|
-
async
|
|
3485
|
-
if (!
|
|
3492
|
+
async cleanupTransientProgressMessages(active) {
|
|
3493
|
+
if (!this.hasObservedPersistentReply(active)) {
|
|
3486
3494
|
return;
|
|
3487
3495
|
}
|
|
3488
3496
|
const messageIds = new Set();
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3497
|
+
if (active.isObserved) {
|
|
3498
|
+
for (const segment of active.segments) {
|
|
3499
|
+
if (segment.outputKind === 'final_answer') {
|
|
3500
|
+
continue;
|
|
3501
|
+
}
|
|
3502
|
+
for (const message of segment.messages) {
|
|
3503
|
+
messageIds.add(message.messageId);
|
|
3504
|
+
}
|
|
3495
3505
|
}
|
|
3496
3506
|
}
|
|
3497
|
-
|
|
3498
|
-
|
|
3507
|
+
if (this.config.telegramDeleteToolDetailsAfterFinal) {
|
|
3508
|
+
for (const messageId of active.archivedMessageIds) {
|
|
3509
|
+
messageIds.add(messageId);
|
|
3510
|
+
}
|
|
3499
3511
|
}
|
|
3500
3512
|
for (const messageId of messageIds) {
|
|
3501
3513
|
try {
|
|
@@ -3503,7 +3515,7 @@ export class BridgeSessionCore {
|
|
|
3503
3515
|
}
|
|
3504
3516
|
catch (error) {
|
|
3505
3517
|
if (!isTelegramMessageGone(error)) {
|
|
3506
|
-
this.logger.warn('telegram.
|
|
3518
|
+
this.logger.warn('telegram.transient_progress_cleanup_delete_failed', {
|
|
3507
3519
|
error: String(error),
|
|
3508
3520
|
turnId: active.turnId,
|
|
3509
3521
|
messageId,
|
|
@@ -8080,7 +8092,7 @@ export class BridgeSessionCore {
|
|
|
8080
8092
|
else {
|
|
8081
8093
|
messageId = await this.sendMessage(active.scopeId, content.text);
|
|
8082
8094
|
}
|
|
8083
|
-
if (
|
|
8095
|
+
if (messageId !== null) {
|
|
8084
8096
|
active.archivedMessageIds.push(messageId);
|
|
8085
8097
|
}
|
|
8086
8098
|
}
|
|
@@ -8098,9 +8110,7 @@ export class BridgeSessionCore {
|
|
|
8098
8110
|
else {
|
|
8099
8111
|
await this.editMessage(active.scopeId, active.previewMessageId, content.text, []);
|
|
8100
8112
|
}
|
|
8101
|
-
|
|
8102
|
-
active.archivedMessageIds.push(active.previewMessageId);
|
|
8103
|
-
}
|
|
8113
|
+
active.archivedMessageIds.push(active.previewMessageId);
|
|
8104
8114
|
}
|
|
8105
8115
|
catch (error) {
|
|
8106
8116
|
if (isTelegramMessageGone(error)) {
|
package/dist/main.js
CHANGED
|
@@ -17,6 +17,7 @@ import { buildFoxclawSystemdUnitText, buildSystemdRestartHelperArgs, cgroupConta
|
|
|
17
17
|
import { clearPendingClusterUpdateBroadcast, createSelfUpdateRuntime, inferPnpmHomeFromEntryPoint, performSelfUpdate, readPendingClusterUpdateBroadcast, readSelfUpdateStatus, writeSelfUpdateStatus, } from './update.js';
|
|
18
18
|
import { TELEGRAM_VOICE_MAX_BYTES, TELEGRAM_VOICE_SUPPORTED_EXTENSIONS, telegramVoiceContentType, } from './voice/files.js';
|
|
19
19
|
import { inferTelegramBotId, resolveTelegramVoiceTarget } from './voice/target.js';
|
|
20
|
+
import { shouldShowRuntimeLastUpdate } from './update_status.js';
|
|
20
21
|
const rawCommand = process.argv[2];
|
|
21
22
|
const command = rawCommand || 'serve';
|
|
22
23
|
loadEnv();
|
|
@@ -396,8 +397,9 @@ function formatRuntimeStatusSummary(status) {
|
|
|
396
397
|
if (status.authProactiveRefresh) {
|
|
397
398
|
lines.push(`Auth refresh: ${status.authProactiveRefresh.state}${status.authProactiveRefresh.finishedAt ? ` ${formatAge(status.authProactiveRefresh.finishedAt)}` : ''}`);
|
|
398
399
|
}
|
|
399
|
-
if (status
|
|
400
|
-
|
|
400
|
+
if (shouldShowRuntimeLastUpdate(status, readPackageVersion())) {
|
|
401
|
+
const lastUpdate = status.lastUpdate;
|
|
402
|
+
lines.push(`Last update: ${lastUpdate.fromVersion} -> ${lastUpdate.toVersion ?? 'unknown'} ${formatAge(lastUpdate.updatedAt)}`);
|
|
401
403
|
}
|
|
402
404
|
if (status.lastError) {
|
|
403
405
|
lines.push(`Last error: ${truncateStatusLine(status.lastError, 160)}`);
|
|
@@ -627,6 +629,7 @@ async function runServeCli() {
|
|
|
627
629
|
authSyncTest: () => authSync?.testPeers() ?? Promise.resolve({ sent: 0, replied: 0, missing: [] }),
|
|
628
630
|
statusUpdated: () => writeAggregateStatus(),
|
|
629
631
|
getServiceStatus: async () => ({
|
|
632
|
+
currentVersion: readPackageVersion(),
|
|
630
633
|
bots: await Promise.all(runtimes.map(async (runtime) => {
|
|
631
634
|
const status = runtime.core.getRuntimeStatus();
|
|
632
635
|
return {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
type RuntimeLastUpdateStatus = {
|
|
2
|
+
userAgent?: string | null;
|
|
3
|
+
currentVersion?: string | null;
|
|
4
|
+
lastUpdate?: {
|
|
5
|
+
state: string;
|
|
6
|
+
toVersion: string | null;
|
|
7
|
+
} | null;
|
|
8
|
+
};
|
|
9
|
+
export declare function shouldShowRuntimeLastUpdate(status: RuntimeLastUpdateStatus, currentVersion?: string | null): boolean;
|
|
10
|
+
export declare function extractFoxClawVersionFromUserAgent(userAgent: string | null | undefined): string | null;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function shouldShowRuntimeLastUpdate(status, currentVersion = null) {
|
|
2
|
+
const update = status.lastUpdate;
|
|
3
|
+
if (!update) {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
if (update.state !== 'succeeded') {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
const version = currentVersion?.trim() || status.currentVersion?.trim() || extractFoxClawVersionFromUserAgent(status.userAgent);
|
|
10
|
+
return Boolean(version && update.toVersion === version);
|
|
11
|
+
}
|
|
12
|
+
export function extractFoxClawVersionFromUserAgent(userAgent) {
|
|
13
|
+
if (!userAgent) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const match = userAgent.match(/\(foxclaw;\s*([^)]+)\)/i);
|
|
17
|
+
return match?.[1]?.trim() || null;
|
|
18
|
+
}
|
package/dist/voice/tts.js
CHANGED
|
@@ -41,6 +41,9 @@ async function synthesizeViaHttp(text, config) {
|
|
|
41
41
|
if (!config.voiceTtsUrl) {
|
|
42
42
|
throw new Error('VOICE_TTS_URL is not configured');
|
|
43
43
|
}
|
|
44
|
+
if (config.voiceTtsEngine === 'soulx') {
|
|
45
|
+
return convertToOggOpus(await requestTtsWav(config, '/v1/tts', { text, seed: '7' }), config.voiceFfmpegBin);
|
|
46
|
+
}
|
|
44
47
|
try {
|
|
45
48
|
return convertToOggOpus(await requestTtsWav(config, '/v1/tts/custom', { text, language: 'zh' }), config.voiceFfmpegBin);
|
|
46
49
|
}
|
|
@@ -114,6 +117,9 @@ async function synthesizeViaSsh(text, config) {
|
|
|
114
117
|
if (!config.voiceTtsSshDir) {
|
|
115
118
|
throw new Error('VOICE_TTS_SSH_DIR is not configured');
|
|
116
119
|
}
|
|
120
|
+
if (config.voiceTtsEngine === 'soulx') {
|
|
121
|
+
return synthesizeSoulxViaSsh(text, config);
|
|
122
|
+
}
|
|
117
123
|
const encodedText = Buffer.from(text, 'utf8').toString('base64');
|
|
118
124
|
const script = String.raw `set -euo pipefail
|
|
119
125
|
TEXT="$(printf '%s' "$1" | base64 -d)"
|
|
@@ -177,6 +183,63 @@ PY
|
|
|
177
183
|
config.voiceTtsDesignInstruct,
|
|
178
184
|
], script, config.voiceTtsTimeoutMs);
|
|
179
185
|
}
|
|
186
|
+
async function synthesizeSoulxViaSsh(text, config) {
|
|
187
|
+
const sshHost = config.voiceTtsSshHost;
|
|
188
|
+
const envDir = config.voiceTtsSshDir;
|
|
189
|
+
if (!sshHost) {
|
|
190
|
+
throw new Error('VOICE_TTS_SSH_HOST is not configured');
|
|
191
|
+
}
|
|
192
|
+
if (!envDir) {
|
|
193
|
+
throw new Error('VOICE_TTS_SSH_DIR is not configured');
|
|
194
|
+
}
|
|
195
|
+
const encodedText = Buffer.from(text, 'utf8').toString('base64');
|
|
196
|
+
const baseUrl = config.voiceTtsUrl || 'http://127.0.0.1:18082';
|
|
197
|
+
const script = String.raw `set -euo pipefail
|
|
198
|
+
TEXT="$(printf '%s' "$1" | base64 -d)"
|
|
199
|
+
ENV_DIR="$2"
|
|
200
|
+
BASE_URL="$3"
|
|
201
|
+
cd "$ENV_DIR"
|
|
202
|
+
set -a
|
|
203
|
+
. ./.env
|
|
204
|
+
set +a
|
|
205
|
+
tmp="$(mktemp -d)"
|
|
206
|
+
trap 'rm -rf "$tmp"' EXIT
|
|
207
|
+
curl -sS "\${BASE_URL}/health" >/dev/null
|
|
208
|
+
TEXT="$TEXT" python3 - <<'PY' > "$tmp/body.json"
|
|
209
|
+
import json
|
|
210
|
+
import os
|
|
211
|
+
print(json.dumps({"text": os.environ["TEXT"], "seed": 7, "dialect_prompt": ""}, ensure_ascii=False))
|
|
212
|
+
PY
|
|
213
|
+
status="$(curl -sS -w '%{http_code}' -X POST "\${BASE_URL}/v1/tts" \
|
|
214
|
+
-H "Authorization: Bearer $QWEN_SPEECH_API_TOKEN" \
|
|
215
|
+
-H "Content-Type: application/json" \
|
|
216
|
+
--data-binary "@$tmp/body.json" \
|
|
217
|
+
--output "$tmp/tts.wav")"
|
|
218
|
+
if [[ "$status" != "200" ]]; then
|
|
219
|
+
python3 - "$tmp/tts.wav" <<'PY' >&2
|
|
220
|
+
import pathlib
|
|
221
|
+
import sys
|
|
222
|
+
sys.stdout.buffer.write(pathlib.Path(sys.argv[1]).read_bytes())
|
|
223
|
+
PY
|
|
224
|
+
exit 22
|
|
225
|
+
fi
|
|
226
|
+
ffmpeg -nostdin -hide_banner -loglevel error -y -i "$tmp/tts.wav" -ac 1 -c:a libopus -b:a 32k "$tmp/voice.ogg"
|
|
227
|
+
python3 - "$tmp/voice.ogg" <<'PY'
|
|
228
|
+
import pathlib
|
|
229
|
+
import sys
|
|
230
|
+
sys.stdout.buffer.write(pathlib.Path(sys.argv[1]).read_bytes())
|
|
231
|
+
PY
|
|
232
|
+
`;
|
|
233
|
+
return runSshBinary([
|
|
234
|
+
sshHost,
|
|
235
|
+
'bash',
|
|
236
|
+
'-s',
|
|
237
|
+
'--',
|
|
238
|
+
encodedText,
|
|
239
|
+
envDir,
|
|
240
|
+
baseUrl,
|
|
241
|
+
], script, config.voiceTtsTimeoutMs);
|
|
242
|
+
}
|
|
180
243
|
async function runSshBinary(args, stdin, timeoutMs) {
|
|
181
244
|
return new Promise((resolve, reject) => {
|
|
182
245
|
const child = spawn('ssh', args, { stdio: ['pipe', 'pipe', 'pipe'] });
|