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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wtt-connect",
3
- "version": "0.2.57",
3
+ "version": "0.2.59",
4
4
  "private": false,
5
5
  "description": "WTT-native connector daemon for Codex, Claude Code, Cursor, Gemini, ACP, and other coding agent surfaces.",
6
6
  "type": "module",
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.maybePublishProgress(topicId, event, adapter.name),
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 this.wtt.publish(topicId, reply, 'CHAT_REPLY');
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.maybePublishProgress(topicId, event, adapter.name),
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 this.wtt.publish(topicId, `执行失败:${err.message}`, 'CHAT_REPLY');
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
- return this.action('publish', {
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) {