@foxden-app/foxclaw 0.13.7 → 0.13.8
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/CHANGELOG.md +14 -0
- package/dist/channels/telegram/telegram_messaging_port.d.ts +2 -0
- package/dist/channels/telegram/telegram_messaging_port.js +11 -0
- package/dist/codex_app/adapter.js +26 -2
- package/dist/core/channel_port.d.ts +2 -0
- package/dist/core/commentary_archive.d.ts +13 -0
- package/dist/core/commentary_archive.js +1 -0
- package/dist/core/orchestrator.d.ts +1 -0
- package/dist/core/orchestrator.js +59 -19
- package/dist/store/task_journal.d.ts +7 -0
- package/dist/telegram/commentary_archive.d.ts +11 -0
- package/dist/telegram/commentary_archive.js +36 -0
- package/dist/telegram/gateway.d.ts +5 -0
- package/dist/telegram/gateway.js +12 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
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.13.8 - 2026-10-06
|
|
6
|
+
|
|
7
|
+
### 中文
|
|
8
|
+
|
|
9
|
+
- 任务结束后将所有过程小结合并为一条默认收起的 Telegram 归档消息:收起显示总耗时与 Token 总/入/出/缓存,展开按时间段展示全部小结;最终结论独立保留,移除其他小结和工作状态卡。
|
|
10
|
+
- Codex 按本轮模型请求累计原生 Token 用量,忽略重复及其他会话/轮次通知;未提供的计数显示“—”。记录任务与小结时间,交付重试不增加耗时。
|
|
11
|
+
- 超过富消息文字或内容块上限时,在同一归档内显示几条预览及“查看全部小结”HTML 文件,保留完整内容,不额外发布消息或公开网页。合并成功后才删除原消息,持久化清理进度;失败可通过重启或 `/recover` 继续,避免丢失小结或重发最终结论。
|
|
12
|
+
|
|
13
|
+
### English
|
|
14
|
+
|
|
15
|
+
- Consolidate all progress summaries into one closed Telegram archive at completion. Its header shows duration and total/input/output/cached tokens; expand to see timed summaries. Keep the final answer separate and remove other summaries and the working card.
|
|
16
|
+
- Accumulate native Codex token usage for this turn's model requests, excluding duplicate and foreign notifications. Show unavailable counters as “—”. Persist task and summary timing so delivery retries do not inflate duration.
|
|
17
|
+
- When rich message text or block limits are exceeded, embed previews and a complete HTML archive in the same message. No extra messages or public hosting. Delete originals only after successful consolidation and persist cleanup checkpoints; resume failures after restart or through `/recover` without losing summaries or resending the final answer.
|
|
18
|
+
|
|
5
19
|
## 0.13.7 - 2026-10-06
|
|
6
20
|
|
|
7
21
|
### 中文
|
|
@@ -3,6 +3,7 @@ import type { ChannelTextEvent } from '../../core/channel_events.js';
|
|
|
3
3
|
import type { ChannelPort } from '../../core/channel_port.js';
|
|
4
4
|
import type { TelegramGateway } from '../../telegram/gateway.js';
|
|
5
5
|
import type { TelegramRemoteFile } from '../../telegram/api.js';
|
|
6
|
+
import type { TaskCommentaryArchive } from '../../core/commentary_archive.js';
|
|
6
7
|
export type InlineKeyboard = Array<Array<{
|
|
7
8
|
text: string;
|
|
8
9
|
callback_data: string;
|
|
@@ -18,6 +19,7 @@ export declare class TelegramMessagingPort implements ChannelPort {
|
|
|
18
19
|
updateTaskPreview(scopeId: string, taskId: string, messageId: number, html: string): Promise<number>;
|
|
19
20
|
endTaskPreview(scopeId: string, taskId: string): Promise<void>;
|
|
20
21
|
sendTaskCommentary(scopeId: string, text: string): Promise<number>;
|
|
22
|
+
archiveTaskCommentary(scopeId: string, messageId: number, archive: TaskCommentaryArchive): Promise<void>;
|
|
21
23
|
foldTaskCommentary(scopeId: string, messageId: number, text: string): Promise<void>;
|
|
22
24
|
readonly capabilities: {
|
|
23
25
|
editableMessages: boolean;
|
|
@@ -6,6 +6,7 @@ import { parseTelegramTargetFromBridgeScope } from '../../core/bridge_scope.js';
|
|
|
6
6
|
import { renderTelegramMarkdownRichHtml } from '../../telegram/rich_markdown.js';
|
|
7
7
|
import { escapeTelegramHtml } from '../../telegram/html.js';
|
|
8
8
|
import { telegramRichHtml, telegramRichMarkdown } from '../../telegram/rich.js';
|
|
9
|
+
import { buildTelegramCommentaryArchive } from '../../telegram/commentary_archive.js';
|
|
9
10
|
/**
|
|
10
11
|
* Telegram outbound operations addressed by bridge scope id (`telegram:…`).
|
|
11
12
|
*/
|
|
@@ -22,6 +23,16 @@ export class TelegramMessagingPort {
|
|
|
22
23
|
async sendTaskCommentary(scopeId, text) {
|
|
23
24
|
return this.sendRichMarkdown(scopeId, text);
|
|
24
25
|
}
|
|
26
|
+
async archiveTaskCommentary(scopeId, messageId, archive) {
|
|
27
|
+
const target = parseTelegramTargetFromBridgeScope(scopeId);
|
|
28
|
+
const rendered = buildTelegramCommentaryArchive(archive);
|
|
29
|
+
if (rendered.document) {
|
|
30
|
+
await this.gateway.editRichMessageWithDocument(target.chatId, messageId, rendered.html, rendered.document);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
await this.gateway.editRichMessage(target.chatId, messageId, telegramRichHtml(rendered.html, { skipEntityDetection: true }), []);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
25
36
|
async foldTaskCommentary(scopeId, messageId, text) {
|
|
26
37
|
const target = parseTelegramTargetFromBridgeScope(scopeId);
|
|
27
38
|
try {
|
|
@@ -41,6 +41,8 @@ export class CodexEngineAdapter {
|
|
|
41
41
|
let turnId = null;
|
|
42
42
|
let cancelled = false;
|
|
43
43
|
const earlyNotifications = [];
|
|
44
|
+
let usage;
|
|
45
|
+
let lastUsageTotal;
|
|
44
46
|
let promptText = request.prompt;
|
|
45
47
|
if (request.stagedAttachments && request.stagedAttachments.length > 0 && !request.prompt.includes('Telegram attachments:')) {
|
|
46
48
|
promptText = buildAttachmentPrompt(request.prompt, request.stagedAttachments);
|
|
@@ -68,6 +70,27 @@ export class CodexEngineAdapter {
|
|
|
68
70
|
return;
|
|
69
71
|
}
|
|
70
72
|
try {
|
|
73
|
+
if (msg.method === 'thread/tokenUsage/updated') {
|
|
74
|
+
if (msg.params?.turnId && msg.params.turnId !== turnId)
|
|
75
|
+
return;
|
|
76
|
+
const native = msg.params?.tokenUsage;
|
|
77
|
+
const current = native?.last;
|
|
78
|
+
if (!current)
|
|
79
|
+
return;
|
|
80
|
+
// `last` is one model request; `total` is cumulative for the thread.
|
|
81
|
+
// Repeated notifications with the same cumulative counters are not new usage.
|
|
82
|
+
const signature = native.total ? JSON.stringify(native.total) : undefined;
|
|
83
|
+
if (signature && signature === lastUsageTotal)
|
|
84
|
+
return;
|
|
85
|
+
lastUsageTotal = signature;
|
|
86
|
+
usage ??= {};
|
|
87
|
+
for (const [key, nativeKey] of [['inputTokens', 'inputTokens'], ['outputTokens', 'outputTokens'], ['cachedTokens', 'cachedInputTokens'], ['totalTokens', 'totalTokens']]) {
|
|
88
|
+
const value = current[nativeKey];
|
|
89
|
+
if (typeof value === 'number' && Number.isFinite(value) && value >= 0)
|
|
90
|
+
usage[key] = (usage[key] ?? 0) + value;
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
71
94
|
const ev = normalizeTurnActivityEvent(msg);
|
|
72
95
|
if (!ev)
|
|
73
96
|
return;
|
|
@@ -123,6 +146,7 @@ export class CodexEngineAdapter {
|
|
|
123
146
|
...(failure ? { error: String(failure) } : {}),
|
|
124
147
|
response: accumulatedResponse,
|
|
125
148
|
conversationId: threadId,
|
|
149
|
+
usage,
|
|
126
150
|
};
|
|
127
151
|
emitter.emit('result', finalResult);
|
|
128
152
|
resolveWaiters(finalResult);
|
|
@@ -142,7 +166,7 @@ export class CodexEngineAdapter {
|
|
|
142
166
|
return;
|
|
143
167
|
cleanup();
|
|
144
168
|
finalResult = { kind: 'result', status: 'ERROR', response: responseText(),
|
|
145
|
-
error: 'Codex connection closed before the task outcome was confirmed.', outcomeUnknown: true, conversationId: threadId };
|
|
169
|
+
error: 'Codex connection closed before the task outcome was confirmed.', outcomeUnknown: true, conversationId: threadId, usage };
|
|
146
170
|
emitter.emit('result', finalResult);
|
|
147
171
|
resolveWaiters(finalResult);
|
|
148
172
|
};
|
|
@@ -271,7 +295,7 @@ export class CodexEngineAdapter {
|
|
|
271
295
|
if (threadId && turnId)
|
|
272
296
|
await this.client.interruptTurn(threadId, turnId);
|
|
273
297
|
cleanup();
|
|
274
|
-
finalResult = { kind: 'result', status: 'INTERRUPTED', response: accumulatedResponse, conversationId: threadId };
|
|
298
|
+
finalResult = { kind: 'result', status: 'INTERRUPTED', response: accumulatedResponse, conversationId: threadId, usage };
|
|
275
299
|
emitter.emit('result', finalResult);
|
|
276
300
|
resolveWaiters(finalResult);
|
|
277
301
|
})();
|
|
@@ -3,6 +3,7 @@ import type { AppLocale } from '../types.js';
|
|
|
3
3
|
import type { ChannelTextEvent, ChannelCallbackEvent, ChannelInbound } from './channel_events.js';
|
|
4
4
|
import type { ParsedCommand } from '../controller/commands.js';
|
|
5
5
|
import type { AttachmentDownloader } from './attachments.js';
|
|
6
|
+
import type { TaskCommentaryArchive } from './commentary_archive.js';
|
|
6
7
|
/** Bridge-local handles; a transport maps these to its native message identifiers. */
|
|
7
8
|
export type ChannelMessageRef = number;
|
|
8
9
|
export type ChannelInlineKeyboard = Array<Array<{
|
|
@@ -34,6 +35,7 @@ export interface ChannelPort extends AttachmentDownloader {
|
|
|
34
35
|
updateTaskPreview?(scopeId: string, taskId: string, messageId: number, html: string): Promise<number>;
|
|
35
36
|
sendTaskCommentary?(scopeId: string, text: string): Promise<number>;
|
|
36
37
|
foldTaskCommentary?(scopeId: string, messageId: number, text: string): Promise<void>;
|
|
38
|
+
archiveTaskCommentary?(scopeId: string, messageId: number, archive: TaskCommentaryArchive): Promise<void>;
|
|
37
39
|
endTaskPreview?(scopeId: string, taskId: string): Promise<void>;
|
|
38
40
|
resolveAction?(event: ChannelTextEvent): ChannelCallbackEvent | null;
|
|
39
41
|
editPlain(scopeId: string, messageId: ChannelMessageRef, text: string, keyboard?: ChannelInlineKeyboard): Promise<void>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AppLocale } from '../types.js';
|
|
2
|
+
import type { EngineTurnResult } from './engine_spi.js';
|
|
3
|
+
export interface TaskCommentaryArchive {
|
|
4
|
+
startedAt: number;
|
|
5
|
+
endedAt: number;
|
|
6
|
+
locale: AppLocale;
|
|
7
|
+
usage?: EngineTurnResult['usage'];
|
|
8
|
+
entries: Array<{
|
|
9
|
+
text: string;
|
|
10
|
+
startedAt?: number | undefined;
|
|
11
|
+
endedAt?: number | undefined;
|
|
12
|
+
}>;
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -539,7 +539,7 @@ export class UnifiedChannelOrchestrator {
|
|
|
539
539
|
const task = this.store.taskJournal.get(turn.taskId);
|
|
540
540
|
const folded = buildFoldedToolsSummary({ toolLines: turn.toolLines, toolCount: turn.toolCount, stepIndex: turn.stepIndex, startTime: turn.startTime, locale: task.request.locale });
|
|
541
541
|
const response = `${task.request.locale === 'zh' ? '🛑 已停止任务;排队任务保留。' : '🛑 Task stopped; queued tasks kept.'}${turn.accumulatedText.trim() ? `\n\n${turn.accumulatedText.trim()}` : ''}`;
|
|
542
|
-
this.store.taskJournal.update(turn.taskId, 'delivery_pending', { result: { kind: 'result', status: 'INTERRUPTED', response, conversationId: turn.threadId }, delivery: combineSummaryAndResponse(folded, response), deliveredChunks: 0, previewMessageId: turn.messageId, separateFinal: turn.separateFinal === true });
|
|
542
|
+
this.store.taskJournal.update(turn.taskId, 'delivery_pending', { result: { kind: 'result', status: 'INTERRUPTED', response, conversationId: turn.threadId }, taskEndedAt: Date.now(), delivery: turn.separateFinal ? chunkMessage(response) : combineSummaryAndResponse(folded, response), deliveredChunks: 0, previewMessageId: turn.messageId, separateFinal: turn.separateFinal === true });
|
|
543
543
|
try {
|
|
544
544
|
await this.deliverTaskChunks(turn.taskId);
|
|
545
545
|
this.resolveDeliveredTask(turn.taskId);
|
|
@@ -635,27 +635,60 @@ export class UnifiedChannelOrchestrator {
|
|
|
635
635
|
task = this.store.taskJournal.update(task.id, 'delivery_pending', { deliveredChunks: index + 1 });
|
|
636
636
|
}
|
|
637
637
|
if (task.separateFinal) {
|
|
638
|
-
|
|
639
|
-
const
|
|
640
|
-
if (
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
638
|
+
if (task.commentary?.length && this.messaging.archiveTaskCommentary && this.messaging.capabilities?.editableMessages !== false) {
|
|
639
|
+
const messageId = task.commentaryArchiveMessageId ?? task.commentary[0].messageId;
|
|
640
|
+
if (!task.commentaryArchiveReady) {
|
|
641
|
+
const entries = [];
|
|
642
|
+
let previousSource;
|
|
643
|
+
for (const item of task.commentary) {
|
|
644
|
+
if (item.sourceMessageId && item.sourceMessageId === previousSource)
|
|
645
|
+
entries.at(-1).text += `\n${item.text}`;
|
|
646
|
+
else
|
|
647
|
+
entries.push({ text: item.text, startedAt: item.startedAt, endedAt: item.endedAt });
|
|
648
|
+
previousSource = item.sourceMessageId;
|
|
649
|
+
}
|
|
650
|
+
await this.messaging.archiveTaskCommentary(task.event.scopeId, messageId, {
|
|
651
|
+
startedAt: task.taskStartedAt ?? task.createdAt, endedAt: task.taskEndedAt ?? task.updatedAt,
|
|
652
|
+
locale: task.request.locale, usage: task.result?.usage, entries,
|
|
653
|
+
});
|
|
654
|
+
task = this.store.taskJournal.update(task.id, 'delivery_pending', { commentaryArchiveMessageId: messageId, commentaryArchiveReady: true, commentary: task.commentary.map(item => ({ ...item, folded: false })) });
|
|
655
|
+
}
|
|
656
|
+
for (let index = 0; index < task.commentary.length; index++) {
|
|
657
|
+
const item = task.commentary[index];
|
|
658
|
+
if (item.folded)
|
|
659
|
+
continue;
|
|
660
|
+
if (item.messageId !== messageId)
|
|
661
|
+
await this.messaging.deleteMessage(task.event.scopeId, item.messageId);
|
|
647
662
|
const commentary = [...task.commentary];
|
|
648
663
|
commentary[index] = { ...item, folded: true };
|
|
649
664
|
task = this.store.taskJournal.update(task.id, 'delivery_pending', { commentary });
|
|
650
665
|
}
|
|
651
|
-
|
|
652
|
-
|
|
666
|
+
}
|
|
667
|
+
else {
|
|
668
|
+
for (let index = 0; index < (task.commentary?.length ?? 0); index++) {
|
|
669
|
+
const item = task.commentary[index];
|
|
670
|
+
if (item.folded || this.messaging.capabilities?.editableMessages === false)
|
|
671
|
+
continue;
|
|
672
|
+
try {
|
|
673
|
+
if (this.messaging.foldTaskCommentary)
|
|
674
|
+
await this.messaging.foldTaskCommentary(task.event.scopeId, item.messageId, item.text);
|
|
675
|
+
else
|
|
676
|
+
await this.editMessage(task.event.scopeId, item.messageId, `<blockquote expandable>${escapeHtml(item.text)}</blockquote>`, []);
|
|
677
|
+
const commentary = [...task.commentary];
|
|
678
|
+
commentary[index] = { ...item, folded: true };
|
|
679
|
+
task = this.store.taskJournal.update(task.id, 'delivery_pending', { commentary });
|
|
680
|
+
}
|
|
681
|
+
catch (error) {
|
|
682
|
+
this.logger.warn('orchestrator.commentary_fold_failed', { taskId, messageId: item.messageId, error: String(error) });
|
|
683
|
+
}
|
|
653
684
|
}
|
|
654
685
|
}
|
|
655
686
|
if (!task.previewSettled) {
|
|
656
687
|
if (task.previewMessageId > 0 && this.messaging.capabilities?.editableMessages !== false) {
|
|
657
|
-
if (task.finalPreviewText)
|
|
688
|
+
if (task.finalPreviewText && !task.commentaryArchiveReady)
|
|
658
689
|
await this.editMessage(task.event.scopeId, task.previewMessageId, task.finalPreviewText, []);
|
|
690
|
+
else if (task.commentaryArchiveReady)
|
|
691
|
+
await this.messaging.deleteMessage(task.event.scopeId, task.previewMessageId);
|
|
659
692
|
else
|
|
660
693
|
await this.messaging.deleteMessage(task.event.scopeId, task.previewMessageId).catch(() => { });
|
|
661
694
|
}
|
|
@@ -1151,7 +1184,8 @@ export class UnifiedChannelOrchestrator {
|
|
|
1151
1184
|
throw new Error('Orchestrator is stopped');
|
|
1152
1185
|
}
|
|
1153
1186
|
let execution;
|
|
1154
|
-
|
|
1187
|
+
const taskStartedAt = task.taskStartedAt ?? Date.now();
|
|
1188
|
+
task = this.store.taskJournal.update(task.id, 'running', { result: null, delivery: [], deliveredChunks: 0, previewSettled: false, separateFinal: adapter.supportsCommentary === true, taskStartedAt });
|
|
1155
1189
|
try {
|
|
1156
1190
|
execution = adapter.executeTurn(req);
|
|
1157
1191
|
}
|
|
@@ -1174,12 +1208,13 @@ export class UnifiedChannelOrchestrator {
|
|
|
1174
1208
|
stepIndex: 1,
|
|
1175
1209
|
toolCount: 0,
|
|
1176
1210
|
currentTool: null,
|
|
1177
|
-
startTime:
|
|
1211
|
+
startTime: taskStartedAt,
|
|
1178
1212
|
previewKey: turnKey,
|
|
1179
1213
|
queueId: options?.queueId,
|
|
1180
1214
|
taskId: task.id,
|
|
1181
1215
|
separateFinal: adapter.supportsCommentary === true,
|
|
1182
1216
|
commentaryIds: new Set(),
|
|
1217
|
+
lastCommentaryTime: task.commentary?.at(-1)?.endedAt ?? taskStartedAt,
|
|
1183
1218
|
};
|
|
1184
1219
|
this.activeTurns.set(scopeId, activeTurn);
|
|
1185
1220
|
this.messaging.sendTypingInScope(scopeId).catch(() => { });
|
|
@@ -1242,6 +1277,9 @@ export class UnifiedChannelOrchestrator {
|
|
|
1242
1277
|
activeTurn.commentaryIds?.add(message.messageId);
|
|
1243
1278
|
activeTurn.separateFinal = true;
|
|
1244
1279
|
activeTurn.accumulatedText = '';
|
|
1280
|
+
const endedAt = Date.now();
|
|
1281
|
+
const startedAt = activeTurn.lastCommentaryTime ?? activeTurn.startTime;
|
|
1282
|
+
activeTurn.lastCommentaryTime = endedAt;
|
|
1245
1283
|
void this.messageOperations.run(`${scopeId}:preview:${activeTurn.taskId}`, async () => {
|
|
1246
1284
|
if (!ownsTurn())
|
|
1247
1285
|
return;
|
|
@@ -1252,7 +1290,7 @@ export class UnifiedChannelOrchestrator {
|
|
|
1252
1290
|
: await this.messaging.sendRichMarkdown(scopeId, text);
|
|
1253
1291
|
const current = this.store.taskJournal.get(activeTurn.taskId);
|
|
1254
1292
|
this.store.taskJournal.update(current.id, current.state, {
|
|
1255
|
-
commentary: [...(current.commentary ?? []), { messageId, text, folded: false }], separateFinal: true,
|
|
1293
|
+
commentary: [...(current.commentary ?? []), { messageId, text, folded: false, sourceMessageId: message.messageId, startedAt, endedAt }], separateFinal: true,
|
|
1256
1294
|
});
|
|
1257
1295
|
}
|
|
1258
1296
|
}).catch(error => this.logger.warn('orchestrator.commentary_send_failed', { taskId: activeTurn.taskId, error: String(error) }));
|
|
@@ -1325,7 +1363,9 @@ export class UnifiedChannelOrchestrator {
|
|
|
1325
1363
|
const task = this.store.taskJournal.get(activeTurn.taskId);
|
|
1326
1364
|
if (task.state === 'awaiting_confirmation')
|
|
1327
1365
|
return;
|
|
1328
|
-
if (task.state === 'delivery_pending' && task.deliveredChunks < task.delivery.length
|
|
1366
|
+
if (task.state === 'delivery_pending' && (task.deliveredChunks < task.delivery.length
|
|
1367
|
+
|| (task.separateFinal && this.messaging.archiveTaskCommentary && this.messaging.capabilities?.editableMessages !== false && task.commentary?.length && (!task.commentaryArchiveReady || task.commentary.some(item => !item.folded)))
|
|
1368
|
+
|| (task.separateFinal && !task.previewSettled))) {
|
|
1329
1369
|
this.logger.warn('orchestrator.delivery_pending', { taskId: task.id, scopeId });
|
|
1330
1370
|
return;
|
|
1331
1371
|
}
|
|
@@ -1340,7 +1380,7 @@ export class UnifiedChannelOrchestrator {
|
|
|
1340
1380
|
if (!ownsTurn() || activeTurn.settling)
|
|
1341
1381
|
return;
|
|
1342
1382
|
activeTurn.settling = true;
|
|
1343
|
-
this.store.taskJournal.update(activeTurn.taskId, 'running', { result: res, request: { ...req, threadId: res.conversationId || activeTurn.threadId } });
|
|
1383
|
+
this.store.taskJournal.update(activeTurn.taskId, 'running', { result: res, taskEndedAt: Date.now(), request: { ...req, threadId: res.conversationId || activeTurn.threadId } });
|
|
1344
1384
|
try {
|
|
1345
1385
|
if (activeTurn.flushTimer)
|
|
1346
1386
|
clearTimeout(activeTurn.flushTimer);
|
|
@@ -1471,7 +1511,7 @@ export class UnifiedChannelOrchestrator {
|
|
|
1471
1511
|
if (!ownsTurn() || activeTurn.settling)
|
|
1472
1512
|
return;
|
|
1473
1513
|
activeTurn.settling = true;
|
|
1474
|
-
this.store.taskJournal.update(activeTurn.taskId, 'running', { result: { kind: 'result', status: 'ERROR', response: activeTurn.accumulatedText, error: err.message, conversationId: activeTurn.threadId } });
|
|
1514
|
+
this.store.taskJournal.update(activeTurn.taskId, 'running', { taskEndedAt: Date.now(), result: { kind: 'result', status: 'ERROR', response: activeTurn.accumulatedText, error: err.message, conversationId: activeTurn.threadId } });
|
|
1475
1515
|
try {
|
|
1476
1516
|
if (activeTurn.flushTimer)
|
|
1477
1517
|
clearTimeout(activeTurn.flushTimer);
|
|
@@ -16,7 +16,14 @@ export interface JournalTask {
|
|
|
16
16
|
messageId: number;
|
|
17
17
|
text: string;
|
|
18
18
|
folded: boolean;
|
|
19
|
+
sourceMessageId?: string;
|
|
20
|
+
startedAt?: number;
|
|
21
|
+
endedAt?: number;
|
|
19
22
|
}>;
|
|
23
|
+
taskStartedAt?: number;
|
|
24
|
+
taskEndedAt?: number;
|
|
25
|
+
commentaryArchiveMessageId?: number;
|
|
26
|
+
commentaryArchiveReady?: boolean;
|
|
20
27
|
separateFinal?: boolean;
|
|
21
28
|
finalPreviewText?: string;
|
|
22
29
|
previewSettled?: boolean;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { TaskCommentaryArchive } from '../core/commentary_archive.js';
|
|
2
|
+
export interface TelegramCommentaryArchive {
|
|
3
|
+
html: string;
|
|
4
|
+
document?: {
|
|
5
|
+
filename: string;
|
|
6
|
+
contents: Buffer;
|
|
7
|
+
contentType: string;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export declare function commentaryArchiveHeader(archive: TaskCommentaryArchive): string;
|
|
11
|
+
export declare function buildTelegramCommentaryArchive(archive: TaskCommentaryArchive): TelegramCommentaryArchive;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { escapeTelegramHtml } from './html.js';
|
|
2
|
+
import { renderTelegramMarkdownRichHtml } from './rich_markdown.js';
|
|
3
|
+
export function commentaryArchiveHeader(archive) {
|
|
4
|
+
const zh = archive.locale === 'zh';
|
|
5
|
+
const seconds = Math.max(0, Math.round((archive.endedAt - archive.startedAt) / 1000));
|
|
6
|
+
const duration = seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m${seconds % 60}s`;
|
|
7
|
+
const count = (n) => typeof n === 'number' && Number.isFinite(n) ? Math.round(Math.max(0, n)).toLocaleString('en-US') : '—';
|
|
8
|
+
const usage = archive.usage;
|
|
9
|
+
const total = usage?.totalTokens ?? (usage?.inputTokens !== undefined && usage.outputTokens !== undefined ? usage.inputTokens + usage.outputTokens : undefined);
|
|
10
|
+
return `${zh ? '总时间' : 'Total time'}: ${duration} ${zh ? 'Token(总/入/出/缓存)' : 'Tokens (total/in/out/cache)'}: ${[total, usage?.inputTokens, usage?.outputTokens, usage?.cachedTokens].map(count).join('/')}`;
|
|
11
|
+
}
|
|
12
|
+
function interval(entry) {
|
|
13
|
+
const clock = (time) => time === undefined ? '—' : new Date(time).toLocaleTimeString('en-GB', { hour12: false });
|
|
14
|
+
return `${clock(entry.startedAt)}–${clock(entry.endedAt)}`;
|
|
15
|
+
}
|
|
16
|
+
function entryHtml(entry) {
|
|
17
|
+
return `<p><b>${escapeTelegramHtml(interval(entry))}</b></p>${renderTelegramMarkdownRichHtml(entry.text)}`;
|
|
18
|
+
}
|
|
19
|
+
export function buildTelegramCommentaryArchive(archive) {
|
|
20
|
+
const header = commentaryArchiveHeader(archive);
|
|
21
|
+
const body = archive.entries.map(entryHtml).join('\n');
|
|
22
|
+
const wrap = (content) => `<details><summary>${escapeTelegramHtml(header)}</summary>${content}</details>`;
|
|
23
|
+
const html = wrap(body);
|
|
24
|
+
// Conservative budgets also cover expanded entities and nested list blocks.
|
|
25
|
+
const blocks = (html.match(/<(?:p|h[1-6]|pre|ul|ol|li|blockquote|details)\b/g) ?? []).length;
|
|
26
|
+
if (html.length <= 30000 && blocks <= 450)
|
|
27
|
+
return { html };
|
|
28
|
+
const zh = archive.locale === 'zh';
|
|
29
|
+
const preview = archive.entries.slice(0, 3).map(entry => entryHtml({ ...entry, text: entry.text.length > 600 ? `${entry.text.slice(0, 600)}…` : entry.text })).join('\n');
|
|
30
|
+
const label = zh ? '查看全部小结' : 'View all summaries';
|
|
31
|
+
const fullDocument = `<!doctype html><html lang="${zh ? 'zh' : 'en'}"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'"><title>${label}</title><style>body{font:16px/1.7 system-ui,sans-serif;max-width:900px;margin:24px auto;padding:0 20px;overflow-wrap:anywhere}pre{white-space:pre-wrap;background:#f3f4f6;padding:12px}code{font-family:monospace}</style><body><h1>${escapeTelegramHtml(header)}</h1>${body}</body></html>`;
|
|
32
|
+
return {
|
|
33
|
+
html: wrap(`${preview}<p>${zh ? `共 ${archive.entries.length} 条小结,完整内容见下方文件。` : `${archive.entries.length} summaries; complete content in the file below.`}</p><figure><tg-document src="tg://document?id=commentary_archive"></tg-document><figcaption>${label}</figcaption></figure>`),
|
|
34
|
+
document: { filename: zh ? '查看全部小结.html' : 'all-summaries.html', contents: Buffer.from(fullDocument), contentType: 'text/html' },
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -75,6 +75,11 @@ export declare class TelegramGateway extends EventEmitter {
|
|
|
75
75
|
text: string;
|
|
76
76
|
callback_data: string;
|
|
77
77
|
}>>): Promise<void>;
|
|
78
|
+
editRichMessageWithDocument(chatId: string, messageId: number, html: string, document: {
|
|
79
|
+
filename: string;
|
|
80
|
+
contents: Buffer;
|
|
81
|
+
contentType: string;
|
|
82
|
+
}): Promise<void>;
|
|
78
83
|
clearMessageInlineKeyboard(chatId: string, messageId: number): Promise<void>;
|
|
79
84
|
private sendMessageWithOptions;
|
|
80
85
|
private editMessageWithOptions;
|
package/dist/telegram/gateway.js
CHANGED
|
@@ -180,6 +180,17 @@ export class TelegramGateway extends EventEmitter {
|
|
|
180
180
|
throw new Error(result.description || 'Failed to edit Telegram rich message');
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
|
+
async editRichMessageWithDocument(chatId, messageId, html, document) {
|
|
184
|
+
const result = await callTelegramMultipartApi(this.botToken, 'editMessageText', {
|
|
185
|
+
chat_id: chatId, message_id: String(messageId),
|
|
186
|
+
rich_message: JSON.stringify({ html, skip_entity_detection: true,
|
|
187
|
+
media: [{ id: 'commentary_archive', media: { type: 'document', media: 'attach://commentary_archive' } }] }),
|
|
188
|
+
reply_markup: JSON.stringify({ inline_keyboard: [] }),
|
|
189
|
+
}, [{ fieldName: 'commentary_archive', ...document }]);
|
|
190
|
+
if (!result.ok && !String(result.description || '').includes('message is not modified')) {
|
|
191
|
+
throw new Error(result.description || 'Failed to attach commentary archive');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
183
194
|
async clearMessageInlineKeyboard(chatId, messageId) {
|
|
184
195
|
const result = await callTelegramApi(this.botToken, 'editMessageReplyMarkup', {
|
|
185
196
|
chat_id: chatId,
|
|
@@ -222,7 +233,7 @@ export class TelegramGateway extends EventEmitter {
|
|
|
222
233
|
chat_id: chatId,
|
|
223
234
|
message_id: messageId,
|
|
224
235
|
});
|
|
225
|
-
if (!result.ok) {
|
|
236
|
+
if (!result.ok && !String(result.description || '').includes('message to delete not found')) {
|
|
226
237
|
throw new Error(result.description || 'Failed to delete Telegram message');
|
|
227
238
|
}
|
|
228
239
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@foxden-app/foxclaw",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.8",
|
|
4
4
|
"description": "Foxden local execution claw for controlling Codex, OpenCode, Antigravity, and DeepSeek Harness from trusted chat interfaces.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/main.js",
|