@foxden-app/foxclaw 0.5.49 → 0.5.50

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 CHANGED
@@ -72,7 +72,9 @@ CODEX_CLI_BIN=/absolute/path/to/codex
72
72
  # VOICE_TTS_SSH_DIR=/path/to/qwen-speech-server
73
73
  # VOICE_TTS_DESIGN_INSTRUCT=用自然清晰的中文女声朗读,语速适中。
74
74
  # VOICE_SUMMARY_BUTTON_ENABLED=true
75
+ # VOICE_SUMMARY_TEXT_LIMIT=180
75
76
  # VOICE_TEXT_LIMIT=2800
77
+ # VOICE_TTS_TIMEOUT_MS=120000
76
78
 
77
79
  # Optional: standard HTTP(S) proxy for Telegram and ChatGPT/Codex backend requests.
78
80
  # Put these in the same env file that `foxclaw start` installs into systemd/launchd.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
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.50 - 2026-06-20
6
+
7
+ ### 中文
8
+ - 给 Telegram “听总结”按钮增加独立的 `VOICE_SUMMARY_TEXT_LIMIT`,默认只朗读短摘要,避免把完整最终回复交给慢速 TTS 后端导致长时间排队或超时。
9
+ - 新增 `VOICE_TTS_TIMEOUT_MS`,HTTP/SSH 语音后端共用该超时配置;SSH 超时会终止远端调用,HTTP 超时会中止请求。
10
+ - `/voice <文本>` 仍使用 `VOICE_TEXT_LIMIT`,方便手动朗读较长文本;最终总结按钮走更短的摘要上限,优先保证可听、及时返回。
11
+
12
+ ### English
13
+ - Added a separate `VOICE_SUMMARY_TEXT_LIMIT` for Telegram "Listen" buttons, so final-answer voice summaries read a short digest instead of sending the entire final response to slow TTS backends.
14
+ - Added `VOICE_TTS_TIMEOUT_MS` for both HTTP and SSH voice backends; SSH calls are terminated on timeout and HTTP requests are aborted.
15
+ - `/voice <text>` still uses `VOICE_TEXT_LIMIT` for manual longer reads, while final-answer buttons use the shorter summary limit for timely playback.
16
+
5
17
  ## 0.5.49 - 2026-06-20
6
18
 
7
19
  ### 中文
package/dist/config.d.ts CHANGED
@@ -72,7 +72,9 @@ export interface AppConfig {
72
72
  voiceTtsDesignInstruct: string;
73
73
  voiceFfmpegBin: string;
74
74
  voiceSummaryButtonEnabled: boolean;
75
+ voiceSummaryTextLimit: number;
75
76
  voiceTextLimit: number;
77
+ voiceTtsTimeoutMs: number;
76
78
  }
77
79
  export declare function loadConfig(): AppConfig;
78
80
  export declare function selectDefaultRuntimeBotToken(configuredTokens: string[], legacyToken: string | null): string | null;
package/dist/config.js CHANGED
@@ -106,7 +106,9 @@ export function loadConfig() {
106
106
  voiceTtsDesignInstruct: process.env.VOICE_TTS_DESIGN_INSTRUCT?.trim() || '用自然清晰的中文女声朗读,语速适中。',
107
107
  voiceFfmpegBin: process.env.VOICE_FFMPEG_BIN?.trim() || 'ffmpeg',
108
108
  voiceSummaryButtonEnabled: boolEnv('VOICE_SUMMARY_BUTTON_ENABLED', true),
109
+ voiceSummaryTextLimit: intEnv('VOICE_SUMMARY_TEXT_LIMIT', 180),
109
110
  voiceTextLimit: intEnv('VOICE_TEXT_LIMIT', 2800),
111
+ voiceTtsTimeoutMs: intEnv('VOICE_TTS_TIMEOUT_MS', 120_000),
110
112
  };
111
113
  ensureAppDirs(config);
112
114
  return config;
@@ -8394,7 +8394,7 @@ export class BridgeSessionCore {
8394
8394
  }
8395
8395
  registerVoiceSnippet(scopeId, text) {
8396
8396
  this.pruneVoiceSnippets();
8397
- const normalized = normalizeVoiceText(text, this.config.voiceTextLimit);
8397
+ const normalized = normalizeVoiceText(text, this.config.voiceSummaryTextLimit);
8398
8398
  const id = crypto.randomBytes(6).toString('hex');
8399
8399
  this.voiceSnippets.set(id, { scopeId, text: normalized, createdAt: Date.now() });
8400
8400
  this.latestVoiceSnippetByScope.set(scopeId, id);
package/dist/voice/tts.js CHANGED
@@ -61,14 +61,17 @@ async function requestTtsWav(config, pathname, body) {
61
61
  throw new Error('VOICE_TTS_URL is not configured');
62
62
  }
63
63
  const endpoint = new URL(pathname, config.voiceTtsUrl.endsWith('/') ? config.voiceTtsUrl : `${config.voiceTtsUrl}/`);
64
+ const abortController = new AbortController();
65
+ const timer = setTimeout(() => abortController.abort(), config.voiceTtsTimeoutMs);
64
66
  const response = await fetch(endpoint, {
65
67
  method: 'POST',
68
+ signal: abortController.signal,
66
69
  headers: {
67
70
  'content-type': 'application/json',
68
71
  ...(config.voiceTtsToken ? { authorization: `Bearer ${config.voiceTtsToken}` } : {}),
69
72
  },
70
73
  body: JSON.stringify(body),
71
- });
74
+ }).finally(() => clearTimeout(timer));
72
75
  if (!response.ok) {
73
76
  throw new Error(`TTS request failed: ${response.status} ${response.statusText}: ${await response.text().catch(() => '')}`);
74
77
  }
@@ -172,9 +175,9 @@ PY
172
175
  encodedText,
173
176
  config.voiceTtsSshDir,
174
177
  config.voiceTtsDesignInstruct,
175
- ], script);
178
+ ], script, config.voiceTtsTimeoutMs);
176
179
  }
177
- async function runSshBinary(args, stdin) {
180
+ async function runSshBinary(args, stdin, timeoutMs) {
178
181
  return new Promise((resolve, reject) => {
179
182
  const child = spawn('ssh', args, { stdio: ['pipe', 'pipe', 'pipe'] });
180
183
  const stdout = [];
@@ -182,7 +185,7 @@ async function runSshBinary(args, stdin) {
182
185
  const timer = setTimeout(() => {
183
186
  child.kill('SIGTERM');
184
187
  reject(new Error('remote TTS timed out'));
185
- }, 120_000);
188
+ }, timeoutMs);
186
189
  child.stdout.on('data', (chunk) => {
187
190
  stdout.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
188
191
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.5.49",
3
+ "version": "0.5.50",
4
4
  "description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",