wtt-connect 0.2.58 → 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 +26 -0
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
|
@@ -195,6 +195,32 @@ export class WTTClient {
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
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
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
198
224
|
async typing(topicId, state, options = {}) {
|
|
199
225
|
const payload = {
|
|
200
226
|
topic_id: topicId,
|