wtt-connect 0.2.57 → 0.2.59
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/package.json +1 -1
- package/src/config.js +2 -0
- package/src/runner.js +120 -5
- package/src/wtt-client.js +73 -2
package/package.json
CHANGED
package/src/config.js
CHANGED
|
@@ -79,6 +79,8 @@ export function loadConfig(argv = {}) {
|
|
|
79
79
|
devinArgs: parseListEnv(process.env.WTT_DEVIN_ARGS || fileConfig.devinArgs || 'acp'),
|
|
80
80
|
openclawBin: process.env.WTT_OPENCLAW_BIN || fileConfig.openclawBin || 'openclaw',
|
|
81
81
|
enableChat: parseBool(process.env.WTT_CONNECT_ENABLE_CHAT, fileConfig.enableChat ?? true),
|
|
82
|
+
messageStream: parseBool(process.env.WTT_CONNECT_MESSAGE_STREAM, fileConfig.messageStream ?? true),
|
|
83
|
+
messageStreamMinIntervalMs: parseIntEnv(process.env.WTT_CONNECT_MESSAGE_STREAM_MIN_INTERVAL_MS, fileConfig.messageStreamMinIntervalMs || 250),
|
|
82
84
|
publishProgress: parseBool(process.env.WTT_CONNECT_PUBLISH_PROGRESS, fileConfig.publishProgress ?? false),
|
|
83
85
|
requireCommitPush: parseBool(process.env.WTT_REQUIRE_COMMIT_PUSH, fileConfig.requireCommitPush ?? true),
|
|
84
86
|
heartbeatSeconds: parseIntEnv(process.env.WTT_CONNECT_HEARTBEAT_SECONDS, fileConfig.heartbeatSeconds || 25),
|
package/src/runner.js
CHANGED
|
@@ -170,11 +170,14 @@ export class Runner {
|
|
|
170
170
|
if (!content && !staged.files.length) return;
|
|
171
171
|
await this.wtt.typing(topicId, 'start', { statusText: 'Agent 已接收消息,正在准备执行', statusKind: 'queued', ttlMs: 30000 });
|
|
172
172
|
let runtimeSelection = null;
|
|
173
|
+
let chatStream = null;
|
|
173
174
|
try {
|
|
174
175
|
const transcripts = await this.transcribeAttachments(staged.files);
|
|
175
176
|
const modelConfig = modelConfigFromMessage(m);
|
|
176
177
|
const adapter = this.registry.select({ ...m, content, model_config: modelConfig });
|
|
177
178
|
runtimeSelection = { adapter: adapter.name, modelConfig };
|
|
179
|
+
chatStream = this.createChatStream(topicId, adapter.name, modelConfig);
|
|
180
|
+
await chatStream.start();
|
|
178
181
|
this.recordRuntimeSelection(adapter.name, modelConfig, 'running');
|
|
179
182
|
await this.wtt.typing(topicId, 'start', { statusText: `${adapterDisplayName(adapter.name)} 正在执行${modelConfig.model ? ` · ${modelConfig.model}` : ''}`, statusKind: 'running', adapter: adapter.name, model: modelConfig.model || undefined, ttlMs: 30000 });
|
|
180
183
|
const agentProfile = await this.getAgentProfile();
|
|
@@ -190,11 +193,12 @@ export class Runner {
|
|
|
190
193
|
modelConfig,
|
|
191
194
|
files: staged.files,
|
|
192
195
|
images: staged.images,
|
|
193
|
-
onProgress: (event) => this.
|
|
196
|
+
onProgress: (event) => this.handleChatProgress(topicId, event, adapter.name, chatStream),
|
|
194
197
|
});
|
|
195
198
|
if (localSlash !== null) {
|
|
196
199
|
const reply = stripHiddenContextLeak(localSlash || '(empty response)') || '(empty response)';
|
|
197
|
-
await
|
|
200
|
+
await chatStream.finish(reply);
|
|
201
|
+
await this.wtt.publish(topicId, reply, 'CHAT_REPLY', { stream_id: chatStream.id });
|
|
198
202
|
log('info', 'slash command replied', { topicId, adapter: adapter.name, chars: reply.length });
|
|
199
203
|
return;
|
|
200
204
|
}
|
|
@@ -231,16 +235,18 @@ export class Runner {
|
|
|
231
235
|
modelConfig,
|
|
232
236
|
files: staged.files,
|
|
233
237
|
images: staged.images,
|
|
234
|
-
onProgress: (event) => this.
|
|
238
|
+
onProgress: (event) => this.handleChatProgress(topicId, event, adapter.name, chatStream),
|
|
235
239
|
});
|
|
236
240
|
const prepared = prepareGeneratedFileArtifacts(stripHiddenContextLeak(output || '(empty response)') || '(empty response)');
|
|
237
241
|
const reply = prepared.text || '(empty response)';
|
|
242
|
+
await chatStream.finish(reply);
|
|
238
243
|
await this.maybeAttachSpeech(topicId, reply, `chat-${topicId}`);
|
|
239
|
-
await this.wtt.publish(topicId, reply, 'CHAT_REPLY');
|
|
244
|
+
await this.wtt.publish(topicId, reply, 'CHAT_REPLY', { stream_id: chatStream.id });
|
|
240
245
|
await this.publishGeneratedFileArtifacts(topicId, prepared.refs, { source: 'chat', sourceId: topicId, adapter: adapter.name });
|
|
241
246
|
log('info', 'chat replied', { topicId, chars: reply.length });
|
|
242
247
|
} catch (err) {
|
|
243
|
-
await
|
|
248
|
+
await chatStream?.fail(err?.message || String(err));
|
|
249
|
+
await this.wtt.publish(topicId, `执行失败:${err.message}`, 'CHAT_REPLY', chatStream?.id ? { stream_id: chatStream.id } : null);
|
|
244
250
|
log('error', 'chat failed', { topicId, error: err.message });
|
|
245
251
|
} finally {
|
|
246
252
|
if (runtimeSelection) this.recordRuntimeSelection(runtimeSelection.adapter, runtimeSelection.modelConfig, 'idle');
|
|
@@ -582,6 +588,71 @@ export class Runner {
|
|
|
582
588
|
}
|
|
583
589
|
}
|
|
584
590
|
|
|
591
|
+
createChatStream(topicId, adapterName = 'agent', modelConfig = {}) {
|
|
592
|
+
const enabled = this.config.messageStream !== false && Boolean(topicId);
|
|
593
|
+
const streamId = `wtt-${Date.now().toString(36)}-${crypto.randomUUID().slice(0, 8)}`;
|
|
594
|
+
let seq = 0;
|
|
595
|
+
let lastText = '';
|
|
596
|
+
let lastSentAt = 0;
|
|
597
|
+
const minInterval = Math.max(100, Number(this.config.messageStreamMinIntervalMs || 250));
|
|
598
|
+
|
|
599
|
+
const send = async (state, payload = {}) => {
|
|
600
|
+
if (!enabled) return;
|
|
601
|
+
await this.wtt.streamMessage(topicId, streamId, state, {
|
|
602
|
+
...payload,
|
|
603
|
+
seq: ++seq,
|
|
604
|
+
metadata: {
|
|
605
|
+
adapter: adapterName,
|
|
606
|
+
model: modelConfig.model || modelConfig.model_id || modelConfig.modelId || '',
|
|
607
|
+
},
|
|
608
|
+
});
|
|
609
|
+
};
|
|
610
|
+
|
|
611
|
+
return {
|
|
612
|
+
id: streamId,
|
|
613
|
+
async start() {
|
|
614
|
+
await send('start');
|
|
615
|
+
},
|
|
616
|
+
async offer(event) {
|
|
617
|
+
if (!enabled) return;
|
|
618
|
+
const extracted = extractAssistantStreamUpdate(adapterName, event, lastText);
|
|
619
|
+
if (!extracted.text) return;
|
|
620
|
+
const cleanText = stripHiddenContextLeak(extracted.text).trimEnd();
|
|
621
|
+
if (!cleanText || cleanText === lastText) return;
|
|
622
|
+
const now = Date.now();
|
|
623
|
+
const grewEnough = cleanText.length - lastText.length >= 80;
|
|
624
|
+
if (!grewEnough && now - lastSentAt < minInterval) {
|
|
625
|
+
lastText = cleanText;
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
lastText = cleanText;
|
|
629
|
+
lastSentAt = now;
|
|
630
|
+
await send('snapshot', { fullText: cleanText });
|
|
631
|
+
},
|
|
632
|
+
async finish(finalText) {
|
|
633
|
+
if (!enabled) return;
|
|
634
|
+
const cleanText = stripHiddenContextLeak(finalText || '').trim();
|
|
635
|
+
if (cleanText && cleanText !== lastText) {
|
|
636
|
+
lastText = cleanText;
|
|
637
|
+
lastSentAt = Date.now();
|
|
638
|
+
await send('snapshot', { fullText: cleanText });
|
|
639
|
+
}
|
|
640
|
+
await send('done', { fullText: cleanText || lastText });
|
|
641
|
+
},
|
|
642
|
+
async fail(error) {
|
|
643
|
+
if (!enabled) return;
|
|
644
|
+
await send('error', { error: String(error || 'stream failed') });
|
|
645
|
+
},
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
async handleChatProgress(topicId, event, adapterName, chatStream) {
|
|
650
|
+
await Promise.all([
|
|
651
|
+
this.maybePublishProgress(topicId, event, adapterName),
|
|
652
|
+
chatStream?.offer(event) || Promise.resolve(),
|
|
653
|
+
]);
|
|
654
|
+
}
|
|
655
|
+
|
|
585
656
|
async maybePublishProgress(topicId, event, adapterName = 'agent') {
|
|
586
657
|
if (!topicId) return;
|
|
587
658
|
const normalized = normalizeAgentEvent(adapterName, event);
|
|
@@ -624,6 +695,50 @@ function adapterDisplayName(name) {
|
|
|
624
695
|
return name || 'Agent';
|
|
625
696
|
}
|
|
626
697
|
|
|
698
|
+
function extractAssistantStreamUpdate(adapterName, event, previousText = '') {
|
|
699
|
+
if (!event || typeof event !== 'object') return { text: '' };
|
|
700
|
+
const role = String(event.role || event.message?.role || '').toLowerCase();
|
|
701
|
+
if (role && role !== 'assistant') return { text: '' };
|
|
702
|
+
|
|
703
|
+
const type = String(event.type || event.event || event.method || '').toLowerCase();
|
|
704
|
+
if (type.includes('tool') || type.includes('permission') || type.includes('status')) return { text: '' };
|
|
705
|
+
|
|
706
|
+
let text = '';
|
|
707
|
+
if (adapterName === 'claude-code' || type === 'assistant') {
|
|
708
|
+
text = assistantTextFromValue(event.message?.content || event.content || event.message);
|
|
709
|
+
}
|
|
710
|
+
if (!text && (type === 'item.completed' || type === 'response.output_item.done') && event.item) {
|
|
711
|
+
text = assistantTextFromValue(event.item);
|
|
712
|
+
}
|
|
713
|
+
if (!text) {
|
|
714
|
+
for (const key of ['result', 'text', 'content', 'message', 'output_text', 'response', 'final']) {
|
|
715
|
+
text = assistantTextFromValue(event[key]);
|
|
716
|
+
if (text) break;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
if (!text) return { text: '' };
|
|
720
|
+
|
|
721
|
+
if (event.delta === true) {
|
|
722
|
+
return { text: `${previousText || ''}${text}`, mode: 'delta' };
|
|
723
|
+
}
|
|
724
|
+
return { text, mode: 'snapshot' };
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
function assistantTextFromValue(value) {
|
|
728
|
+
if (!value) return '';
|
|
729
|
+
if (typeof value === 'string') return value;
|
|
730
|
+
if (Array.isArray(value)) return value.map(assistantTextFromValue).filter(Boolean).join('');
|
|
731
|
+
if (typeof value === 'object') {
|
|
732
|
+
const kind = String(value.type || '').toLowerCase();
|
|
733
|
+
if (kind && !['assistant', 'agent_message', 'message', 'text', 'output_text'].includes(kind)) return '';
|
|
734
|
+
for (const key of ['text', 'content', 'message', 'output_text', 'result']) {
|
|
735
|
+
const text = assistantTextFromValue(value[key]);
|
|
736
|
+
if (text) return text;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
return '';
|
|
740
|
+
}
|
|
741
|
+
|
|
627
742
|
function isKnownAdapterSlashCommand(adapterName, command) {
|
|
628
743
|
const normalized = String(command || '').trim().toLowerCase();
|
|
629
744
|
if (!normalized) return false;
|
package/src/wtt-client.js
CHANGED
|
@@ -176,13 +176,49 @@ export class WTTClient {
|
|
|
176
176
|
log('info', 'dry-run publish', { topicId, semanticType, chars: content.length });
|
|
177
177
|
return null;
|
|
178
178
|
}
|
|
179
|
-
|
|
179
|
+
const payload = {
|
|
180
180
|
topic_id: topicId,
|
|
181
181
|
content,
|
|
182
182
|
content_type: 'text',
|
|
183
183
|
semantic_type: semanticType,
|
|
184
184
|
...(metadata && typeof metadata === 'object' ? { metadata } : {}),
|
|
185
|
-
}
|
|
185
|
+
};
|
|
186
|
+
try {
|
|
187
|
+
return await this.action('publish', payload, 60000);
|
|
188
|
+
} catch (err) {
|
|
189
|
+
log('warn', 'websocket publish failed; trying HTTP fallback', {
|
|
190
|
+
topicId,
|
|
191
|
+
semanticType,
|
|
192
|
+
error: err?.message || err,
|
|
193
|
+
});
|
|
194
|
+
return await this.httpPublish(payload, 60000);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async streamMessage(topicId, streamId, state, payload = {}) {
|
|
199
|
+
if (this.config.dryRun) return null;
|
|
200
|
+
if (!topicId || !streamId) return null;
|
|
201
|
+
const data = {
|
|
202
|
+
topic_id: topicId,
|
|
203
|
+
stream_id: streamId,
|
|
204
|
+
state,
|
|
205
|
+
seq: payload.seq || 0,
|
|
206
|
+
...(payload.delta ? { delta: payload.delta } : {}),
|
|
207
|
+
...(payload.fullText ? { full_text: payload.fullText } : {}),
|
|
208
|
+
...(payload.error ? { error: payload.error } : {}),
|
|
209
|
+
...(payload.metadata && typeof payload.metadata === 'object' ? { metadata: payload.metadata } : {}),
|
|
210
|
+
};
|
|
211
|
+
try {
|
|
212
|
+
return await this.action('stream_message', data, payload.timeoutMs || 5000);
|
|
213
|
+
} catch (err) {
|
|
214
|
+
log('debug', 'message stream event dropped', {
|
|
215
|
+
topicId,
|
|
216
|
+
streamId,
|
|
217
|
+
state,
|
|
218
|
+
error: err?.message || err,
|
|
219
|
+
});
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
186
222
|
}
|
|
187
223
|
|
|
188
224
|
async typing(topicId, state, options = {}) {
|
|
@@ -251,6 +287,41 @@ export class WTTClient {
|
|
|
251
287
|
clearTimeout(timer);
|
|
252
288
|
}
|
|
253
289
|
}
|
|
290
|
+
|
|
291
|
+
async httpPublish(payload, timeoutMs = 60000) {
|
|
292
|
+
const base = String(this.config.wttBaseUrl || '').replace(/\/$/, '');
|
|
293
|
+
if (!base || !this.config.agentId || !this.config.token) throw new Error('missing HTTP publish configuration');
|
|
294
|
+
const controller = new AbortController();
|
|
295
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
296
|
+
try {
|
|
297
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
298
|
+
if (this.config.httpToken) headers.Authorization = `Bearer ${this.config.httpToken}`;
|
|
299
|
+
else headers['X-Agent-Token'] = this.config.token;
|
|
300
|
+
const response = await fetch(
|
|
301
|
+
`${base}/topics/${encodeURIComponent(payload.topic_id)}/messages?agent_id=${encodeURIComponent(this.config.agentId)}`,
|
|
302
|
+
{
|
|
303
|
+
method: 'POST',
|
|
304
|
+
headers,
|
|
305
|
+
body: JSON.stringify({
|
|
306
|
+
sender_id: this.config.agentId,
|
|
307
|
+
sender_type: 'AGENT',
|
|
308
|
+
content: payload.content,
|
|
309
|
+
content_type: payload.content_type || 'text',
|
|
310
|
+
semantic_type: payload.semantic_type || 'CHAT_REPLY',
|
|
311
|
+
...(payload.metadata ? { metadata: payload.metadata } : {}),
|
|
312
|
+
}),
|
|
313
|
+
signal: controller.signal,
|
|
314
|
+
},
|
|
315
|
+
);
|
|
316
|
+
if (!response.ok) {
|
|
317
|
+
const detail = await response.text().catch(() => '');
|
|
318
|
+
throw new Error(`HTTP publish failed: ${response.status}${detail ? ` ${detail.slice(0, 160)}` : ''}`);
|
|
319
|
+
}
|
|
320
|
+
return await response.json().catch(() => ({}));
|
|
321
|
+
} finally {
|
|
322
|
+
clearTimeout(timer);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
254
325
|
}
|
|
255
326
|
|
|
256
327
|
function rid(prefix) {
|