@foxden-app/foxclaw 0.11.1 → 0.13.0
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 +18 -1
- package/CHANGELOG.md +36 -0
- package/README.md +4 -3
- package/README_EN.md +4 -3
- package/dist/antigravity/account_ui.d.ts +34 -0
- package/dist/antigravity/account_ui.js +354 -0
- package/dist/antigravity/adapter.js +1 -1
- package/dist/antigravity/auth.d.ts +4 -1
- package/dist/antigravity/auth.js +17 -5
- package/dist/antigravity/controller.d.ts +3 -106
- package/dist/antigravity/controller.js +2 -2083
- package/dist/antigravity/conversation_ui.d.ts +56 -0
- package/dist/antigravity/conversation_ui.js +509 -0
- package/dist/antigravity/ui_callbacks.d.ts +6 -0
- package/dist/antigravity/ui_callbacks.js +6 -0
- package/dist/bridge/unified_bridge.d.ts +114 -0
- package/dist/bridge/unified_bridge.js +1343 -0
- package/dist/channels/bridge_messaging_router.d.ts +8 -4
- package/dist/channels/bridge_messaging_router.js +15 -4
- package/dist/channels/telegram/telegram_messaging_port.d.ts +16 -0
- package/dist/channels/telegram/telegram_messaging_port.js +22 -2
- package/dist/channels/text_actions.d.ts +14 -0
- package/dist/channels/text_actions.js +48 -0
- package/dist/channels/weixin/weixin_channel_adapter.d.ts +3 -4
- package/dist/channels/weixin/weixin_channel_adapter.js +2 -2
- package/dist/channels/weixin/weixin_messaging_port.d.ts +16 -1
- package/dist/channels/weixin/weixin_messaging_port.js +34 -7
- package/dist/codex_app/activity.d.ts +62 -0
- package/dist/codex_app/activity.js +418 -0
- package/dist/codex_app/adapter.d.ts +2 -2
- package/dist/codex_app/adapter.js +55 -25
- package/dist/codex_app/backend_ui.d.ts +4 -0
- package/dist/codex_app/backend_ui.js +48 -0
- package/dist/codex_app/client.js +0 -12
- package/dist/config.d.ts +4 -0
- package/dist/config.js +41 -5
- package/dist/controller/access.d.ts +1 -11
- package/dist/controller/access.js +1 -33
- package/dist/controller/activity.d.ts +1 -62
- package/dist/controller/activity.js +2 -418
- package/dist/controller/auth_errors.d.ts +11 -0
- package/dist/controller/auth_errors.js +124 -0
- package/dist/controller/auth_files.d.ts +13 -0
- package/dist/controller/auth_files.js +156 -0
- package/dist/controller/auth_presentation.d.ts +77 -0
- package/dist/controller/auth_presentation.js +697 -0
- package/dist/controller/auth_quota_service.d.ts +27 -0
- package/dist/controller/auth_quota_service.js +151 -0
- package/dist/controller/controller.d.ts +30 -56
- package/dist/controller/controller.js +181 -3497
- package/dist/controller/interactions.d.ts +74 -0
- package/dist/controller/interactions.js +560 -0
- package/dist/controller/local_usage_service.d.ts +19 -0
- package/dist/controller/local_usage_service.js +62 -0
- package/dist/controller/native_panel_service.d.ts +55 -0
- package/dist/controller/native_panel_service.js +214 -0
- package/dist/controller/native_panels.d.ts +61 -0
- package/dist/controller/native_panels.js +784 -0
- package/dist/controller/shared_helpers.d.ts +53 -0
- package/dist/controller/shared_helpers.js +399 -0
- package/dist/controller/state_types.d.ts +390 -0
- package/dist/controller/state_types.js +82 -0
- package/dist/controller/turn_rendering.d.ts +22 -0
- package/dist/controller/turn_rendering.js +288 -0
- package/dist/core/access.d.ts +11 -0
- package/dist/core/access.js +33 -0
- package/dist/core/attachment_files.d.ts +13 -0
- package/dist/core/attachment_files.js +180 -0
- package/dist/core/attachment_types.d.ts +22 -0
- package/dist/core/attachment_types.js +1 -0
- package/dist/core/attachments.d.ts +5 -3
- package/dist/core/attachments.js +2 -2
- package/dist/core/backend_registry.d.ts +22 -0
- package/dist/core/backend_registry.js +50 -0
- package/dist/core/backend_ui.d.ts +28 -0
- package/dist/core/backend_ui.js +1 -0
- package/dist/core/channel_events.d.ts +48 -0
- package/dist/core/channel_events.js +1 -0
- package/dist/core/channel_port.d.ts +52 -8
- package/dist/core/engine_spi.d.ts +20 -5
- package/dist/core/listener_scope.d.ts +11 -0
- package/dist/core/listener_scope.js +10 -0
- package/dist/core/message_text.d.ts +2 -0
- package/dist/core/message_text.js +28 -0
- package/dist/core/network_errors.d.ts +1 -0
- package/dist/core/network_errors.js +23 -0
- package/dist/core/orchestrator.d.ts +57 -22
- package/dist/core/orchestrator.js +877 -477
- package/dist/core/queued_engine_input.d.ts +12 -0
- package/dist/core/queued_engine_input.js +16 -0
- package/dist/core/runtime_supervisor.d.ts +19 -0
- package/dist/core/runtime_supervisor.js +45 -0
- package/dist/core/scope_operations.d.ts +7 -0
- package/dist/core/scope_operations.js +19 -0
- package/dist/core/stream_preview.js +21 -9
- package/dist/dsh/adapter.d.ts +48 -0
- package/dist/dsh/adapter.js +205 -0
- package/dist/dsh/backend.d.ts +7 -0
- package/dist/dsh/backend.js +21 -0
- package/dist/dsh/client.d.ts +46 -0
- package/dist/dsh/client.js +184 -0
- package/dist/dsh/permissions_plugin.d.ts +30 -0
- package/dist/dsh/permissions_plugin.js +21 -0
- package/dist/dsh/runtime.d.ts +20 -0
- package/dist/dsh/runtime.js +36 -0
- package/dist/dsh/ui.d.ts +38 -0
- package/dist/dsh/ui.js +342 -0
- package/dist/i18n.d.ts +4 -0
- package/dist/i18n.js +12 -0
- package/dist/main.js +144 -72
- package/dist/opencode/adapter.js +28 -9
- package/dist/opencode/controller.js +1 -1
- package/dist/store/channel_inbox.d.ts +14 -0
- package/dist/store/channel_inbox.js +21 -0
- package/dist/store/database.d.ts +10 -4
- package/dist/store/database.js +13 -3
- package/dist/store/task_journal.d.ts +32 -0
- package/dist/store/task_journal.js +70 -0
- package/dist/telegram/api.d.ts +2 -2
- package/dist/telegram/api.js +10 -28
- package/dist/telegram/gateway.d.ts +13 -27
- package/dist/telegram/gateway.js +86 -43
- package/dist/telegram/media.d.ts +2 -34
- package/dist/telegram/media.js +1 -180
- package/dist/telegram/task_preview.d.ts +13 -0
- package/dist/telegram/task_preview.js +92 -0
- package/dist/telegram/task_result.d.ts +2 -0
- package/dist/telegram/task_result.js +12 -0
- package/dist/telegram/text.d.ts +1 -1
- package/dist/telegram/text.js +2 -27
- package/dist/types.d.ts +2 -2
- package/docs/dsh.md +83 -0
- package/docs/release.md +13 -11
- package/docs/zh/architecture-review.md +106 -0
- package/docs/zh/dsh.md +83 -0
- package/docs/zh/release.md +13 -11
- package/docs/zh/telegram-experience.md +40 -0
- package/package.json +7 -5
package/dist/telegram/media.js
CHANGED
|
@@ -1,180 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export const TELEGRAM_INBOX_DIR = '.telegram-inbox';
|
|
3
|
-
export const TELEGRAM_BOT_API_DOWNLOAD_LIMIT_BYTES = 20 * 1024 * 1024;
|
|
4
|
-
const FALLBACK_EXTENSION_BY_KIND = {
|
|
5
|
-
photo: '.jpg',
|
|
6
|
-
document: '',
|
|
7
|
-
audio: '.mp3',
|
|
8
|
-
voice: '.ogg',
|
|
9
|
-
video: '.mp4',
|
|
10
|
-
animation: '.mp4',
|
|
11
|
-
sticker: '.webp',
|
|
12
|
-
videoNote: '.mp4',
|
|
13
|
-
};
|
|
14
|
-
const MIME_EXTENSION_MAP = {
|
|
15
|
-
'application/gzip': '.gz',
|
|
16
|
-
'application/json': '.json',
|
|
17
|
-
'application/pdf': '.pdf',
|
|
18
|
-
'application/zip': '.zip',
|
|
19
|
-
'audio/mpeg': '.mp3',
|
|
20
|
-
'audio/mp4': '.m4a',
|
|
21
|
-
'audio/ogg': '.ogg',
|
|
22
|
-
'audio/wav': '.wav',
|
|
23
|
-
'image/gif': '.gif',
|
|
24
|
-
'image/jpeg': '.jpg',
|
|
25
|
-
'image/png': '.png',
|
|
26
|
-
'image/webp': '.webp',
|
|
27
|
-
'text/csv': '.csv',
|
|
28
|
-
'text/html': '.html',
|
|
29
|
-
'text/markdown': '.md',
|
|
30
|
-
'text/plain': '.txt',
|
|
31
|
-
'video/mp4': '.mp4',
|
|
32
|
-
'video/quicktime': '.mov',
|
|
33
|
-
};
|
|
34
|
-
export function isNativeImageAttachment(attachment) {
|
|
35
|
-
if (attachment.kind === 'photo') {
|
|
36
|
-
return true;
|
|
37
|
-
}
|
|
38
|
-
if (attachment.kind === 'sticker') {
|
|
39
|
-
return !attachment.isAnimated && !attachment.isVideo;
|
|
40
|
-
}
|
|
41
|
-
if (attachment.kind !== 'document') {
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
const lowerMime = attachment.mimeType?.toLowerCase() ?? '';
|
|
45
|
-
if (lowerMime === 'image/jpeg' || lowerMime === 'image/png' || lowerMime === 'image/webp' || lowerMime === 'image/gif') {
|
|
46
|
-
return true;
|
|
47
|
-
}
|
|
48
|
-
const extension = extensionFromName(attachment.fileName);
|
|
49
|
-
return extension === '.jpg' || extension === '.jpeg' || extension === '.png' || extension === '.webp' || extension === '.gif';
|
|
50
|
-
}
|
|
51
|
-
export function planAttachmentStoragePath(cwd, threadId, attachment, remoteFilePath, now = new Date()) {
|
|
52
|
-
const dateSegment = now.toISOString().slice(0, 10);
|
|
53
|
-
const threadSegment = sanitizePathSegment(threadId);
|
|
54
|
-
const timestamp = now.toISOString().slice(0, 19).replace(/[-:]/g, '').replace('T', '-');
|
|
55
|
-
const resolvedFileName = resolveAttachmentFileName(attachment, remoteFilePath);
|
|
56
|
-
const uniquePrefix = `${timestamp}-${sanitizePathSegment(attachment.fileUniqueId).slice(0, 16)}`;
|
|
57
|
-
const joinedName = truncateFileName(`${uniquePrefix}-${resolvedFileName}`, 160);
|
|
58
|
-
const relativePath = path.join(TELEGRAM_INBOX_DIR, dateSegment, threadSegment, joinedName);
|
|
59
|
-
return {
|
|
60
|
-
fileName: resolvedFileName,
|
|
61
|
-
localPath: path.resolve(cwd, relativePath),
|
|
62
|
-
relativePath,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
export function buildAttachmentPrompt(userText, attachments) {
|
|
66
|
-
const normalizedText = userText.trim();
|
|
67
|
-
const lines = [];
|
|
68
|
-
if (normalizedText) {
|
|
69
|
-
lines.push(normalizedText, '');
|
|
70
|
-
}
|
|
71
|
-
else {
|
|
72
|
-
lines.push('User sent Telegram attachments without a caption.', '');
|
|
73
|
-
}
|
|
74
|
-
lines.push('Telegram attachments:');
|
|
75
|
-
attachments.forEach((attachment, index) => {
|
|
76
|
-
lines.push(`${index + 1}. ${describeAttachment(attachment)}`);
|
|
77
|
-
lines.push(` filename: ${attachment.fileName}`);
|
|
78
|
-
lines.push(` path: ${attachment.localPath}`);
|
|
79
|
-
if (attachment.mimeType)
|
|
80
|
-
lines.push(` mime: ${attachment.mimeType}`);
|
|
81
|
-
if (attachment.fileSize !== null)
|
|
82
|
-
lines.push(` size_bytes: ${attachment.fileSize}`);
|
|
83
|
-
if (attachment.width !== null && attachment.height !== null) {
|
|
84
|
-
lines.push(` dimensions: ${attachment.width}x${attachment.height}`);
|
|
85
|
-
}
|
|
86
|
-
if (attachment.durationSeconds !== null) {
|
|
87
|
-
lines.push(` duration_seconds: ${attachment.durationSeconds}`);
|
|
88
|
-
}
|
|
89
|
-
if (attachment.nativeImage) {
|
|
90
|
-
lines.push(' attached_as: localImage');
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
lines.push('', 'Use the local file paths above when you inspect these attachments.');
|
|
94
|
-
return lines.join('\n');
|
|
95
|
-
}
|
|
96
|
-
export function summarizeTelegramInput(text, attachments) {
|
|
97
|
-
const lines = [];
|
|
98
|
-
const normalizedText = text.trim();
|
|
99
|
-
if (normalizedText) {
|
|
100
|
-
lines.push(normalizedText);
|
|
101
|
-
}
|
|
102
|
-
if (attachments.length > 0) {
|
|
103
|
-
lines.push(`[attachments: ${attachments.map((attachment) => `${attachment.kind}:${attachment.fileName ?? attachment.fileUniqueId}`).join(', ')}]`);
|
|
104
|
-
}
|
|
105
|
-
return truncateSummary(lines.join(' '), 500);
|
|
106
|
-
}
|
|
107
|
-
function resolveAttachmentFileName(attachment, remoteFilePath) {
|
|
108
|
-
const providedName = attachment.fileName ? path.basename(attachment.fileName) : '';
|
|
109
|
-
const remoteName = remoteFilePath ? path.basename(remoteFilePath) : '';
|
|
110
|
-
const baseCandidate = providedName || remoteName || `${attachment.kind}-${attachment.fileUniqueId}`;
|
|
111
|
-
const sanitizedBase = sanitizeFileName(baseCandidate);
|
|
112
|
-
const extension = extensionFromName(sanitizedBase)
|
|
113
|
-
|| extensionFromName(remoteName)
|
|
114
|
-
|| extensionFromMimeType(attachment.mimeType)
|
|
115
|
-
|| fallbackExtensionByKind(attachment.kind);
|
|
116
|
-
if (!extension) {
|
|
117
|
-
return sanitizedBase;
|
|
118
|
-
}
|
|
119
|
-
const withoutExtension = sanitizedBase.slice(0, sanitizedBase.length - extensionFromName(sanitizedBase).length) || `${attachment.kind}-${attachment.fileUniqueId}`;
|
|
120
|
-
return truncateFileName(`${withoutExtension}${extension}`, 120);
|
|
121
|
-
}
|
|
122
|
-
function describeAttachment(attachment) {
|
|
123
|
-
switch (attachment.kind) {
|
|
124
|
-
case 'photo':
|
|
125
|
-
return 'photo';
|
|
126
|
-
case 'document':
|
|
127
|
-
return 'document';
|
|
128
|
-
case 'audio':
|
|
129
|
-
return 'audio file';
|
|
130
|
-
case 'voice':
|
|
131
|
-
return 'voice message';
|
|
132
|
-
case 'video':
|
|
133
|
-
return 'video';
|
|
134
|
-
case 'animation':
|
|
135
|
-
return 'animation';
|
|
136
|
-
case 'sticker':
|
|
137
|
-
return attachment.isAnimated || attachment.isVideo ? 'animated sticker' : 'sticker';
|
|
138
|
-
case 'videoNote':
|
|
139
|
-
return 'video note';
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
function sanitizeFileName(fileName) {
|
|
143
|
-
const trimmed = fileName.trim().replace(/[\\/]+/g, '-');
|
|
144
|
-
const sanitized = trimmed
|
|
145
|
-
.replace(/[^\w.\-()+@]+/g, '-')
|
|
146
|
-
.replace(/-+/g, '-')
|
|
147
|
-
.replace(/^\.+/, '')
|
|
148
|
-
.replace(/^-+/, '')
|
|
149
|
-
.replace(/[.-]+$/, '');
|
|
150
|
-
return sanitized || 'attachment';
|
|
151
|
-
}
|
|
152
|
-
function sanitizePathSegment(value) {
|
|
153
|
-
return value.replace(/[^\w.-]+/g, '-').replace(/-+/g, '-').replace(/^-+|-+$/g, '') || 'thread';
|
|
154
|
-
}
|
|
155
|
-
function extensionFromMimeType(mimeType) {
|
|
156
|
-
if (!mimeType)
|
|
157
|
-
return '';
|
|
158
|
-
return MIME_EXTENSION_MAP[mimeType.toLowerCase()] ?? '';
|
|
159
|
-
}
|
|
160
|
-
function extensionFromName(fileName) {
|
|
161
|
-
if (!fileName)
|
|
162
|
-
return '';
|
|
163
|
-
const extension = path.extname(fileName).toLowerCase();
|
|
164
|
-
return extension === '.' ? '' : extension;
|
|
165
|
-
}
|
|
166
|
-
function truncateFileName(fileName, maxLength) {
|
|
167
|
-
if (fileName.length <= maxLength)
|
|
168
|
-
return fileName;
|
|
169
|
-
const extension = extensionFromName(fileName);
|
|
170
|
-
const base = extension ? fileName.slice(0, -extension.length) : fileName;
|
|
171
|
-
return `${base.slice(0, Math.max(1, maxLength - extension.length))}${extension}`;
|
|
172
|
-
}
|
|
173
|
-
function truncateSummary(text, maxLength) {
|
|
174
|
-
if (text.length <= maxLength)
|
|
175
|
-
return text;
|
|
176
|
-
return `${text.slice(0, maxLength - 3)}...`;
|
|
177
|
-
}
|
|
178
|
-
function fallbackExtensionByKind(kind) {
|
|
179
|
-
return FALLBACK_EXTENSION_BY_KIND[kind] ?? '';
|
|
180
|
-
}
|
|
1
|
+
export { ATTACHMENT_INBOX_DIR as TELEGRAM_INBOX_DIR, DEFAULT_REMOTE_DOWNLOAD_LIMIT_BYTES as TELEGRAM_BOT_API_DOWNLOAD_LIMIT_BYTES, isNativeImageAttachment, planAttachmentStoragePath, buildAttachmentPrompt, summarizeInput as summarizeTelegramInput } from '../core/attachment_files.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { TelegramGateway } from './gateway.js';
|
|
2
|
+
/** Owns live drafts and fallback cards; draft ids are never persistent message ids. */
|
|
3
|
+
export declare class TelegramTaskPreviews {
|
|
4
|
+
private readonly gateway;
|
|
5
|
+
private readonly previews;
|
|
6
|
+
private readonly draftIds;
|
|
7
|
+
constructor(gateway: TelegramGateway);
|
|
8
|
+
private keyboard;
|
|
9
|
+
private sendCard;
|
|
10
|
+
begin(scopeId: string, taskId: string, text: string, reuseMessageId?: number): Promise<number>;
|
|
11
|
+
update(scopeId: string, taskId: string, messageId: number, html: string): Promise<number>;
|
|
12
|
+
end(scopeId: string, taskId: string): Promise<void>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { randomInt } from 'node:crypto';
|
|
2
|
+
import { parseTelegramTargetFromBridgeScope } from '../core/bridge_scope.js';
|
|
3
|
+
import { telegramRichHtml } from './rich.js';
|
|
4
|
+
import { renderTelegramMarkdownRichHtml } from './rich_markdown.js';
|
|
5
|
+
/** Owns live drafts and fallback cards; draft ids are never persistent message ids. */
|
|
6
|
+
export class TelegramTaskPreviews {
|
|
7
|
+
gateway;
|
|
8
|
+
previews = new Map();
|
|
9
|
+
draftIds = new Set();
|
|
10
|
+
constructor(gateway) {
|
|
11
|
+
this.gateway = gateway;
|
|
12
|
+
}
|
|
13
|
+
keyboard(taskId) { return [[{ text: '🛑 停止 / Stop', callback_data: `engine:stop:${taskId}` }]]; }
|
|
14
|
+
async sendCard(scopeId, taskId, html) {
|
|
15
|
+
const target = parseTelegramTargetFromBridgeScope(scopeId);
|
|
16
|
+
try {
|
|
17
|
+
return await this.gateway.sendRichMessage(target.chatId, telegramRichHtml(html), this.keyboard(taskId), target.topicId);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return this.gateway.sendHtmlMessage(target.chatId, html.replace(/<\/?p>/g, ''), this.keyboard(taskId), target.topicId);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async begin(scopeId, taskId, text, reuseMessageId = 0) {
|
|
24
|
+
const target = parseTelegramTargetFromBridgeScope(scopeId);
|
|
25
|
+
const html = renderTelegramMarkdownRichHtml(text);
|
|
26
|
+
const preview = { scopeId, draftId: null, messageId: reuseMessageId };
|
|
27
|
+
this.previews.set(taskId, preview);
|
|
28
|
+
if (reuseMessageId > 0) {
|
|
29
|
+
await this.gateway.editRichMessage(target.chatId, reuseMessageId, telegramRichHtml(html), this.keyboard(taskId));
|
|
30
|
+
return reuseMessageId;
|
|
31
|
+
}
|
|
32
|
+
if (/^[1-9]\d*$/.test(target.chatId)) {
|
|
33
|
+
let draftId = randomInt(1, 2 ** 31);
|
|
34
|
+
while (this.draftIds.has(draftId))
|
|
35
|
+
draftId = randomInt(1, 2 ** 31);
|
|
36
|
+
this.draftIds.add(draftId);
|
|
37
|
+
preview.draftId = draftId;
|
|
38
|
+
this.gateway.registerGeneration(draftId, scopeId, taskId, target.chatId, target.topicId);
|
|
39
|
+
try {
|
|
40
|
+
await this.gateway.sendRichMessageDraft(target.chatId, draftId, telegramRichHtml(html), target.topicId, true);
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
this.gateway.releaseGeneration(draftId);
|
|
45
|
+
this.draftIds.delete(draftId);
|
|
46
|
+
preview.draftId = null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
preview.messageId = await this.sendCard(scopeId, taskId, html);
|
|
50
|
+
return preview.messageId;
|
|
51
|
+
}
|
|
52
|
+
async update(scopeId, taskId, messageId, html) {
|
|
53
|
+
const preview = this.previews.get(taskId);
|
|
54
|
+
if (!preview || preview.scopeId !== scopeId)
|
|
55
|
+
return messageId;
|
|
56
|
+
const target = parseTelegramTargetFromBridgeScope(scopeId);
|
|
57
|
+
if (preview.draftId !== null) {
|
|
58
|
+
try {
|
|
59
|
+
await this.gateway.sendRichMessageDraft(target.chatId, preview.draftId, telegramRichHtml(html), target.topicId, true);
|
|
60
|
+
return 0;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
const draftId = preview.draftId;
|
|
64
|
+
this.gateway.releaseGeneration(draftId);
|
|
65
|
+
this.draftIds.delete(draftId);
|
|
66
|
+
preview.draftId = null;
|
|
67
|
+
await this.gateway.sendMessageDraft(target.chatId, draftId, '', target.topicId).catch(() => { });
|
|
68
|
+
preview.messageId = await this.sendCard(scopeId, taskId, html);
|
|
69
|
+
return preview.messageId;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
await this.gateway.editRichMessage(target.chatId, preview.messageId, telegramRichHtml(html), this.keyboard(taskId));
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
await this.gateway.editHtmlMessage(target.chatId, preview.messageId, html, this.keyboard(taskId));
|
|
77
|
+
}
|
|
78
|
+
return preview.messageId;
|
|
79
|
+
}
|
|
80
|
+
async end(scopeId, taskId) {
|
|
81
|
+
const preview = this.previews.get(taskId);
|
|
82
|
+
if (!preview || preview.scopeId !== scopeId)
|
|
83
|
+
return;
|
|
84
|
+
this.previews.delete(taskId);
|
|
85
|
+
if (preview.draftId !== null) {
|
|
86
|
+
this.gateway.releaseGeneration(preview.draftId);
|
|
87
|
+
this.draftIds.delete(preview.draftId);
|
|
88
|
+
const target = parseTelegramTargetFromBridgeScope(scopeId);
|
|
89
|
+
await this.gateway.sendMessageDraft(target.chatId, preview.draftId, '', target.topicId).catch(() => { });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { telegramRichHtml, telegramRichMarkdown } from './rich.js';
|
|
2
|
+
import { renderTelegramMarkdownRichHtml } from './rich_markdown.js';
|
|
3
|
+
/** Only the generated fold formatting is accepted; arbitrary model HTML remains escaped. */
|
|
4
|
+
export function telegramTaskResult(content) {
|
|
5
|
+
const folded = /^(<blockquote expandable>[\s\S]*?<\/blockquote>)(?:\s*|$)/.exec(content);
|
|
6
|
+
if (!folded)
|
|
7
|
+
return telegramRichMarkdown(content, { skipEntityDetection: true });
|
|
8
|
+
const response = content.slice(folded[0].length);
|
|
9
|
+
const summary = folded[1].replace(/<[^>]*>/g, tag => /^(?:<\/?(?:b|i|code)>|<blockquote expandable>|<\/blockquote>)$/.test(tag)
|
|
10
|
+
? tag : tag.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'));
|
|
11
|
+
return telegramRichHtml(`${response.trim() ? renderTelegramMarkdownRichHtml(response) : ''}\n${summary}`, { skipEntityDetection: true });
|
|
12
|
+
}
|
package/dist/telegram/text.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ export declare const TELEGRAM_MESSAGE_LIMIT = 4000;
|
|
|
2
2
|
export declare const TELEGRAM_STREAM_MESSAGE_LIMIT = 1200;
|
|
3
3
|
export declare const TELEGRAM_DRAFT_LIMIT = 4000;
|
|
4
4
|
export declare function sanitizeTelegramPreview(text: string): string;
|
|
5
|
-
export
|
|
5
|
+
export { chunkMessage as chunkTelegramMessage } from '../core/message_text.js';
|
|
6
6
|
export declare function chunkTelegramStreamMessage(text: string, limit?: number): string[];
|
|
7
7
|
export declare function clipTelegramDraftMessage(text: string, fallbackText?: string): string;
|
package/dist/telegram/text.js
CHANGED
|
@@ -8,33 +8,8 @@ export function sanitizeTelegramPreview(text) {
|
|
|
8
8
|
? `${text.slice(0, TELEGRAM_MESSAGE_LIMIT - 3)}...`
|
|
9
9
|
: text;
|
|
10
10
|
}
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
if (!source) {
|
|
14
|
-
return [];
|
|
15
|
-
}
|
|
16
|
-
if (source.length <= limit) {
|
|
17
|
-
return [source];
|
|
18
|
-
}
|
|
19
|
-
const chunks = [];
|
|
20
|
-
let start = 0;
|
|
21
|
-
while (start < source.length) {
|
|
22
|
-
const remaining = source.length - start;
|
|
23
|
-
if (remaining <= limit) {
|
|
24
|
-
chunks.push(source.slice(start));
|
|
25
|
-
break;
|
|
26
|
-
}
|
|
27
|
-
const tentativeEnd = start + limit;
|
|
28
|
-
const window = source.slice(start, tentativeEnd);
|
|
29
|
-
const splitAt = Math.max(window.lastIndexOf('\n\n'), window.lastIndexOf('\n'));
|
|
30
|
-
const end = splitAt >= Math.floor(limit / 2)
|
|
31
|
-
? start + splitAt + 1
|
|
32
|
-
: tentativeEnd;
|
|
33
|
-
chunks.push(source.slice(start, end));
|
|
34
|
-
start = end;
|
|
35
|
-
}
|
|
36
|
-
return chunks.filter(chunk => chunk.length > 0);
|
|
37
|
-
}
|
|
11
|
+
export { chunkMessage as chunkTelegramMessage } from '../core/message_text.js';
|
|
12
|
+
import { chunkMessage as chunkTelegramMessage } from '../core/message_text.js';
|
|
38
13
|
export function chunkTelegramStreamMessage(text, limit = TELEGRAM_STREAM_MESSAGE_LIMIT) {
|
|
39
14
|
return chunkTelegramMessage(text, limit, '');
|
|
40
15
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export interface ChatSessionSettings {
|
|
|
17
17
|
/** Bridge scope id (e.g. `telegram:…`). */
|
|
18
18
|
chatId: string;
|
|
19
19
|
model: string | null;
|
|
20
|
-
reasoningEffort:
|
|
20
|
+
reasoningEffort: string | null;
|
|
21
21
|
locale: AppLocale | null;
|
|
22
22
|
accessPreset: AccessPresetValue | null;
|
|
23
23
|
collaborationMode: CollaborationModeValue | null;
|
|
@@ -403,7 +403,7 @@ export interface RuntimeStatus {
|
|
|
403
403
|
activeTurns: number;
|
|
404
404
|
runtimeKind?: 'default' | 'isolated';
|
|
405
405
|
currentAuth?: string | null;
|
|
406
|
-
defaultBackend?: 'codex' | 'antigravity';
|
|
406
|
+
defaultBackend?: 'codex' | 'antigravity' | 'dsh';
|
|
407
407
|
codexHome?: string | null;
|
|
408
408
|
antigravityHome?: string | null;
|
|
409
409
|
codexAppServer?: RuntimeStatus['codexAppServer'];
|
package/docs/dsh.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# DeepSeek Harness backend
|
|
2
|
+
|
|
3
|
+
FoxClaw launches DSH's shipped `acp` profile and reuses Telegram progress, tool status, final answers, queueing, and interruption. Models and reasoning levels come from the current DSH session. Bindings and settings are stored separately for each bot, chat, and topic.
|
|
4
|
+
|
|
5
|
+
## Existing bot
|
|
6
|
+
|
|
7
|
+
Add to FoxClaw's `.env`:
|
|
8
|
+
|
|
9
|
+
```dotenv
|
|
10
|
+
DSH_ENABLED=true
|
|
11
|
+
DSH_SOURCE_DIR=/absolute/path/to/deepseek-harness
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Prepare DSH dependencies and its platform native addon first. Source mode runs `apps/cli/src/bin.ts` with FoxClaw's Node and resolves `tsx` from the DSH repository. For Linux development, run `pnpm install` and `pnpm run build:native-system` inside DSH; follow DSH's installation instructions for other platforms.
|
|
15
|
+
|
|
16
|
+
For an installed CLI, replace `DSH_SOURCE_DIR` with `DSH_CLI_BIN=/absolute/path/to/dsh`. A `.js`, `.mjs`, or `.cjs` entry point also works. Source mode takes precedence when both are set.
|
|
17
|
+
|
|
18
|
+
Restart FoxClaw, then send `/backend dsh` and `/setup` to your Codex or Antigravity bot. Switching back restores the previous backend's session and settings. Interrupt an active turn before switching backends.
|
|
19
|
+
|
|
20
|
+
## Dedicated bot
|
|
21
|
+
|
|
22
|
+
```dotenv
|
|
23
|
+
CODEX_BOT_TOKENS=
|
|
24
|
+
DSH_BOT_TOKEN=123456:your-independent-bot-token
|
|
25
|
+
DSH_SOURCE_DIR=/absolute/path/to/deepseek-harness
|
|
26
|
+
TG_ALLOWED_USER_ID=123456789
|
|
27
|
+
DEFAULT_CWD=/absolute/path/to/workspace
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`DSH_BOT_TOKEN` enables DSH automatically and must differ from other backend tokens. Standalone DSH requires neither Codex nor Antigravity. Run `foxclaw doctor` and start the service normally. DSH currently connects through Telegram; Weixin uses the existing backends.
|
|
31
|
+
|
|
32
|
+
## Configuration and controls
|
|
33
|
+
|
|
34
|
+
| Setting | Default / purpose |
|
|
35
|
+
| --- | --- |
|
|
36
|
+
| `DSH_HOME` | Existing DSH home, normally `~/.dsh`; credentials, profiles, and persisted sessions |
|
|
37
|
+
| `DSH_PROFILE` | `acp`; custom profiles must expose ACP, model controls, and `permissionPresets` |
|
|
38
|
+
| `DSH_PATCHES` | JSON array, e.g. `["/absolute/path/to/patch.yml"]`, applied in order |
|
|
39
|
+
| `DSH_STARTUP_TIMEOUT_MS` | `60000`; startup timeout, including first profile initialization |
|
|
40
|
+
| `DEFAULT_SANDBOX_MODE` | Initial access: `read-only`, `workspace-write`, or `danger-full-access` |
|
|
41
|
+
|
|
42
|
+
Provider credentials remain managed by DSH. Provider environment variables such as `DEEPSEEK_API_KEY` can be placed in FoxClaw's `.env` and are inherited by DSH. Codex/AGY account rotation and auth synchronization are not used by this backend.
|
|
43
|
+
|
|
44
|
+
| Control | Purpose |
|
|
45
|
+
| --- | --- |
|
|
46
|
+
| `/setup` | Models, reasoning, permissions, queue/interrupt mode, sessions, and plugin configuration |
|
|
47
|
+
| `/models`, `/model <provider/model>` | Native model selection; `/model default` restores the session's initial model |
|
|
48
|
+
| `/effort`, `/effort <level>` | Reasoning choices advertised by the current model; changing models resets reasoning to the new model's default |
|
|
49
|
+
| `/permissions read-only\|default\|full-access` | Read-only, workspace-write, or full-access, also available as buttons |
|
|
50
|
+
| `/threads`, `/open <session ID>` | List and resume persisted DSH sessions |
|
|
51
|
+
| `/new [directory]` | New session with an optional working directory |
|
|
52
|
+
| `/active queue\|steer`, `/queue`, `/steer <message>`, `/interrupt` | Queueing, interruption, and takeover |
|
|
53
|
+
| `/plugins` | Profile dependencies and configured patch paths |
|
|
54
|
+
|
|
55
|
+
The bundled `foxclaw-permissions` plugin applies native DSH permission presets to root agents; children inherit according to DSH's rules. Native approval requests become Allow once / Reject buttons. Interrupting or stopping cancels pending approvals. Access changes require an idle turn.
|
|
56
|
+
|
|
57
|
+
## Plugins and limitations
|
|
58
|
+
|
|
59
|
+
`/plugins` shows explicit profile dependencies and configured patches, rather than a complete inventory of active built-in plugins. Install/remove packages with DSH's own command, using the same `DSH_HOME` and profile, then restart FoxClaw:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
dsh plugin --profile acp add <package>
|
|
63
|
+
dsh plugin --profile acp remove <package>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
In source mode, run `node --import tsx/esm apps/cli/src/bin.ts plugin --profile acp ...` inside the DSH repository. There are currently no chat buttons for installing/removing packages.
|
|
67
|
+
|
|
68
|
+
Progress follows committed ACP messages and tool events, rather than raw model token streaming. Images are sent natively when advertised by ACP; other attachments are passed as local paths. The current DSH ACP surface does not expose transcript replay, forks, renaming, or Agent/Plan switching, so those Codex-specific controls are omitted.
|
|
69
|
+
|
|
70
|
+
ACP reports context occupancy rather than per-turn input/output/cache token consumption. The DSH status page explicitly marks those counts as unavailable.
|
|
71
|
+
|
|
72
|
+
## Development verification
|
|
73
|
+
|
|
74
|
+
Ordinary tests use a keyless ACP subprocess fixture. To exercise the actual source CLI with DSH's bundled control-surface fixture:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
FOXCLAW_DSH_TEST_SOURCE_DIR=/absolute/path/to/deepseek-harness \
|
|
78
|
+
DSH_TELEMETRY_DISABLED=1 node --test --import tsx src/dsh/source.integration.test.ts
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
This uses temporary DSH home/session directories and verifies model/reasoning selection, native permission logs, resume, tools, and cancellation without paid model calls. It is skipped when the source path is unset.
|
|
82
|
+
|
|
83
|
+
Run `npm run build` first and add `FOXCLAW_DSH_TEST_BUILT=1` to verify the compiled backend and packaged permission plugin.
|
package/docs/release.md
CHANGED
|
@@ -16,38 +16,40 @@ Check the worktree and remote state:
|
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
git status --short --branch
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
node -p "require('./package.json').name + '@' + require('./package.json').version"
|
|
20
|
+
pnpm view @foxden-app/foxclaw version
|
|
21
21
|
git tag --list 'v*' --sort=-v:refname | head
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
Confirm the target version and remote tag do not already exist:
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
|
|
27
|
+
pnpm view @foxden-app/foxclaw@0.5.10 version 2>/dev/null || true
|
|
28
28
|
git ls-remote --tags origin refs/tags/v0.5.10
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
Use the version pinned in `packageManager` and install with `pnpm install --frozen-lockfile`. npm is used only by the publishing workflow for registry authentication and publication.
|
|
32
|
+
|
|
31
33
|
Run the same local checks as the publish workflow:
|
|
32
34
|
|
|
33
35
|
```bash
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
pnpm lint
|
|
37
|
+
pnpm typecheck
|
|
38
|
+
pnpm test
|
|
39
|
+
pnpm pack --pack-destination /tmp/foxclaw-release
|
|
38
40
|
git diff --check
|
|
39
41
|
```
|
|
40
42
|
|
|
41
|
-
`
|
|
43
|
+
`pnpm pack` runs `prepack` and creates the release archive. Inspect it with `tar -tzf /tmp/foxclaw-release/*.tgz` and confirm `CHANGELOG.md` is included.
|
|
42
44
|
|
|
43
45
|
## Prepare The Version
|
|
44
46
|
|
|
45
|
-
1. Update `package.json` and `
|
|
47
|
+
1. Update `package.json` and `pnpm-lock.yaml`.
|
|
46
48
|
2. Add a new top entry to `CHANGELOG.md` with both `### 中文` and `### English` sections.
|
|
47
49
|
3. Commit the release metadata:
|
|
48
50
|
|
|
49
51
|
```bash
|
|
50
|
-
git add package.json
|
|
52
|
+
git add package.json pnpm-lock.yaml CHANGELOG.md
|
|
51
53
|
git commit -m "发布 0.5.10:short release summary"
|
|
52
54
|
```
|
|
53
55
|
|
|
@@ -71,7 +73,7 @@ gh run watch <run-id> --repo foxden-app/foxclaw --exit-status
|
|
|
71
73
|
After success, verify npm and GitHub Releases:
|
|
72
74
|
|
|
73
75
|
```bash
|
|
74
|
-
|
|
76
|
+
pnpm view @foxden-app/foxclaw version
|
|
75
77
|
gh release view v0.5.10 --repo foxden-app/foxclaw --json tagName,name,url,publishedAt,isDraft,isPrerelease
|
|
76
78
|
```
|
|
77
79
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# FoxClaw 架构复审与重构
|
|
2
|
+
|
|
3
|
+
日期:2026-10-06。基线:v0.12.0。
|
|
4
|
+
|
|
5
|
+
## 判断
|
|
6
|
+
|
|
7
|
+
保留后端适配器 + 共享调度的方向,但 v0.12.0 还没有形成完整的后端边界。新增 DSH 时必须同时修改 Antigravity 控制器、调度器、启动逻辑及多处面板分支。问题不是目录名称,而是执行身份、会话状态、界面入口和资源清理由不同地方分别决定。
|
|
8
|
+
|
|
9
|
+
本轮改动任务调度、通道契约、持久任务日志、资源归属,并提取存量控制器的独立功能。Telegram 为主通道;微信保留文字入口,完整迁移等待观察消息方案落定。存量数据库和导入路径继续兼容。
|
|
10
|
+
|
|
11
|
+
## 当前边界
|
|
12
|
+
|
|
13
|
+
```mermaid
|
|
14
|
+
flowchart TD
|
|
15
|
+
Main[main.ts:运行时组装] --> Bridge[bridge/unified_bridge:存量 Codex / AGY 功能接入]
|
|
16
|
+
Main --> Definition[后端定义:adapter / defaults / commands / createUi]
|
|
17
|
+
Bridge --> Registry[BackendRegistry:注册与面板生命周期]
|
|
18
|
+
Definition --> Registry
|
|
19
|
+
Registry --> UI[BackendUi:模型、权限、历史与审批面板]
|
|
20
|
+
UI --> Host[BackendUiHost:有限的界面操作]
|
|
21
|
+
Host --> Orchestrator[Orchestrator:任务调度与交付]
|
|
22
|
+
Orchestrator --> ScopeOps[ScopeOperations:每会话串行操作]
|
|
23
|
+
Orchestrator --> SPI[Engine SPI:后端执行、事件、取消]
|
|
24
|
+
Orchestrator --> Store[BridgeStore:会话、任务日志、持久队列]
|
|
25
|
+
Orchestrator --> Channel[ChannelPort:消息与交互能力]
|
|
26
|
+
Main --> Supervisor[RuntimeSupervisor:依赖逆序清理]
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
- `core/engine_spi.ts` 定义执行契约和后端声明;原生推理档位使用字符串,不受 Codex 枚举限制。
|
|
30
|
+
- `core/backend_registry.ts` 保持后端执行身份稳定,允许发现逻辑刷新展示元数据;注册失败不会残留半个后端,清理统一执行且只执行一次。
|
|
31
|
+
- `core/backend_ui.ts` 定义面板接口与操作端口。后端面板不再依赖整个调度器实现。全局服务命令优先于后端命令;后端回调归属由注册的界面声明。
|
|
32
|
+
- `dsh/backend.ts` 提供一份 DSH 定义,共享 Bot 与独立 Bot 均复用它。统一控制器无需导入或构造 DSH。
|
|
33
|
+
- `bridge/unified_bridge.ts` 是存量后端的组合层,不再挂在 Antigravity 引擎目录下。旧导入路径保留为兼容转发。
|
|
34
|
+
- `core/access.ts` 是权限策略的领域逻辑,Codex 适配器将当前设置传入原生线程和任务请求。
|
|
35
|
+
|
|
36
|
+
新增后端应在组装处注入定义。调度器不应再增加 `adapter.id === 某后端` 来处理附件、取消、默认推理档位或菜单。
|
|
37
|
+
|
|
38
|
+
## 状态约束与修复
|
|
39
|
+
|
|
40
|
+
### 会话操作
|
|
41
|
+
|
|
42
|
+
启动、插话、中断、新会话、切换后端和队列领取按 scope 串行执行;不同 scope 独立。任务从启动准备到最终回答交付都保留占用状态,避免两个消息在准备阶段同时启动任务。
|
|
43
|
+
|
|
44
|
+
所有事件回调验证任务对象的所有权,忽略已取消或被替换任务的会话 ID、增量、错误和终态。取消前先屏蔽旧任务回调,并等待适配器返回的取消承诺完成后释放槽位。取消失败保留占用,不能自动启动重叠任务。Codex / OpenCode 的启动阶段取消也会结束等待者并释放监听器;DSH 无法及时取消时关闭其自有进程。
|
|
45
|
+
|
|
46
|
+
执行中或仍有排队任务时不能切换后端。未启用的已选后端不会静默退回其他执行器,原会话与设置保留,用户可通过 `/backend` 重新选择。新后端的首次使用清除旧后端权限及模型覆盖;返回时恢复该后端保存的配置。
|
|
47
|
+
|
|
48
|
+
### 通道收件箱与持久队列
|
|
49
|
+
|
|
50
|
+
Telegram 更新在分类、准备附件和等待会话锁之前写入持久收件箱。通道只在处理完成后推进收取位置;重启先恢复未完成收件,再接收新更新。处理成功后清除原始载荷,保留收件标识用于去重;原生任务日志另行去重,避免收件重放导致重复执行工具。
|
|
51
|
+
|
|
52
|
+
新队列载荷记录版本、原始 prompt、后端、工作目录和已落盘附件。已有文本数组格式继续可读。
|
|
53
|
+
|
|
54
|
+
队列状态为 `queued → processing → completed / failed / cancelled`,只有实际执行和交付结束才进入终态。任务日志为 `accepted → queued / running → delivery_pending → completed / failed / cancelled`,另外记录 `awaiting_confirmation`。普通输入、显式排队和插话在等待会话锁之前保存原始输入、附件引用、模型、权限和后端配置;队列写入与任务关联使用同一数据库事务。队列已领取、任务日志仍为 queued 时可确认尚未调用执行器,重启重新排队。上下文不匹配会失败并提示。
|
|
55
|
+
|
|
56
|
+
附件只在通道入口落盘,原始文本与附件清单交给适配器;适配器决定使用原生图片还是文件提示。JSON 文件不再被调度器统一过滤。OAuth 文件导入仅由 Antigravity 的入站界面处理。
|
|
57
|
+
|
|
58
|
+
### 重启与交付行为(本地实现,尚未部署)
|
|
59
|
+
|
|
60
|
+
- 已接受但尚未启动:恢复原始输入与配置,再准备附件。
|
|
61
|
+
- 已保存原生结果:只重试剩余结果交付,不再次执行工具。
|
|
62
|
+
- 已开始但结果未知:保留输入与附件,暂停该会话队列,提供 `/recover continue|retry|cancel` 和按钮。继续会要求模型检查已有工作;重试可能重复操作。该保守恢复策略随 v0.13.0 启用。
|
|
63
|
+
- 最终消息逐块保存交付进度。消息平台成功发送却丢失确认时,恢复可能重复一条回答;不能保证平台交付恰好一次,但不会为了补发结果重跑工具。
|
|
64
|
+
- 启动时按 scope 归属导入旧预览和 processing 队列,不触碰其他 Bot 的记录。
|
|
65
|
+
|
|
66
|
+
### 通道与资源
|
|
67
|
+
|
|
68
|
+
`ChannelPort`、入站事件和附件类型独立于 Telegram。Telegram 自行处理寻址、命令菜单与原生消息编号;无按钮通道可把操作转换成有 scope 归属、有效期、单次使用约束的文字选择。不可编辑通道不推送每段增量。
|
|
69
|
+
|
|
70
|
+
`RuntimeSupervisor` 从分配起登记资源,覆盖部分启动失败,按消费者到依赖逆序关闭,继续清理其他资源并收集错误。数据库最后关闭、进程锁最后释放。关闭信号只执行一次;Telegram 长轮询可取消并等待退出。原生 RPC 监听器由控制面拥有,停止时移除并等待已开始的审批、通知、观察轮询及定时后台操作完成;共享原生客户端由运行时组装层关闭。客户端分配后立即登记,即使后续初始化失败也会清理。
|
|
71
|
+
|
|
72
|
+
Codex 控制面挂在后端注册界面下,负责账号、原生面板和审批,不再启动另一套任务恢复逻辑,也不会为统一调度器拥有的任务启动第二个渲染器。多 Bot 共享 RPC 时只处理自己拥有的会话。Antigravity 账号面板与会话/观察服务已独立提取,观察轮询在停止时等待结束。
|
|
73
|
+
|
|
74
|
+
### 展示依据
|
|
75
|
+
|
|
76
|
+
默认档位、Boost 和逐轮 token 统计由后端定义声明,模型可携带自己的推理档位。Codex 模型列表完全使用服务端返回结果,移除硬编码补入的模型。状态与模型入口使用共享回调,具体面板由已选后端渲染。
|
|
77
|
+
|
|
78
|
+
## 已拆分与明确保留的边界
|
|
79
|
+
|
|
80
|
+
- Codex 提取用量缓存服务、配额服务、原生面板服务,以及账号文件、账号展示、交互解析、工具展示等模块。控制器约从 14,000 行降至 10,000 行,仍保留存量会话、观察与鉴权协调,不能称为全部技术债归零。
|
|
81
|
+
- AGY 提取账号面板和会话/观察服务,统一组合层约从 2,500 行降至 1,600 行。DSH 继续通过独立后端定义接入。
|
|
82
|
+
- 微信仍使用存量 Codex 会话核心,已增加文字选择;没有自动获得完整多后端能力。按用户优先级暂不迁移其观察执行路径。
|
|
83
|
+
- 调度器本身仍承担较多任务恢复和交付工作,后续可沿任务日志与交付服务边界提取。通道收件箱已补齐收取到分类之间的恢复缺口;平台消息交付仍不承诺恰好一次。
|
|
84
|
+
- 独立 OpenCode 控制器仍使用存量任务流程,未完整迁移持久任务日志;已有 OpenCode 执行适配器与统一执行契约兼容,但不能据此声称独立 Bot 已达标。
|
|
85
|
+
- `main.ts` 仍组装鉴权同步与各 Bot 状态;资源退出责任已集中,组装代码可继续按运行时工厂提取。
|
|
86
|
+
|
|
87
|
+
## 四项完成标准的验收范围
|
|
88
|
+
|
|
89
|
+
| 标准 | Telegram 统一任务路径 | 证据与边界 |
|
|
90
|
+
| --- | --- | --- |
|
|
91
|
+
| 新增后端不改其他后端 | 达到 | 注入后端定义与界面工厂;未知后端的默认档位、面板、回调和执行通过契约测试 |
|
|
92
|
+
| 新增通道不改执行器 | 达到 | 入站事件、附件与消息能力为通用契约;未知通道可使用同一个后端,执行器不导入 Telegram 文件工具 |
|
|
93
|
+
| 任务全程可恢复 | 达到有边界的恢复要求 | 持久收件箱 → 接受与排队 → 原生配置 → 结果与交付游标;原生结果未知时暂停并要求确认,不能承诺任意外部工具恰好执行一次 |
|
|
94
|
+
| 资源有唯一负责人 | 达到已迁移路径要求 | 运行时拥有共享客户端和数据库;控制面拥有监听与后台操作;调度器拥有任务槽位;Telegram 通道拥有草稿和停止关联;部分启动失败和停止竞态已有测试 |
|
|
95
|
+
|
|
96
|
+
验收范围为 Telegram 的 Codex / Antigravity / DSH 统一任务路径。微信存量执行路径、独立 OpenCode 控制器和外部会话观察展示仍有遗留边界,因此全仓库四项标准尚未全部达成。
|
|
97
|
+
|
|
98
|
+
Telegram 已实现单条进度卡、停止操作与完成后的折叠执行摘要。私聊优先使用原生临时草稿,不支持时回退持久消息卡;群聊编辑同一持久消息。停止保留排队任务,并保留已生成内容;最终移除旧停止操作。草稿编号不写成持久消息编号,最终交付前等待正在进行的预览编辑。大工具摘要按完整格式行限长,避免拆坏折叠标签。个人临时设置面板和微信观察摘要尚未启用。
|
|
99
|
+
|
|
100
|
+
官方功能依据和接入状态见 [Telegram 主通道](telegram-experience.md)。
|
|
101
|
+
|
|
102
|
+
## 验证
|
|
103
|
+
|
|
104
|
+
回归覆盖每会话并发启动、跨会话独立执行、取消完成屏障、取消失败、旧回调隔离、新会话与启动竞态、终态重复事件、最终回答交付期间排队、附件队列重启恢复、所有后端切换限制、原生权限 RPC、注册清理与禁用后端保护。另保留全量测试,并运行真实 DSH ACP 控制面集成测试。
|
|
105
|
+
|
|
106
|
+
v0.13.0 发布前全量回归 547 项:545 通过、2 跳过。任务生命周期、长摘要、控制面审批与后台清理专项 40 项全部通过;准备阶段预览释放测试也已纳入全量回归。pnpm lint、typecheck、build 通过;使用编译产物的真实 DSH ACP 控制面集成测试通过(模型切换、权限保存、恢复、取消、桥接插件加载)。Telegram 新停止字段、收取确认顺序、草稿与卡片降级、过期按钮隔离均有模拟接口验证;未向真实 Bot 发送测试消息,手机交互尚需上线验收。上述改动纳入 v0.13.0 正式版。
|