wtt-connect 0.1.0 → 0.1.3

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.1.0",
3
+ "version": "0.1.3",
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",
@@ -11,18 +11,18 @@ export class ClaudeCodeAdapter {
11
11
  async run(prompt, context = {}) {
12
12
  if (context.images?.length && this.config.codexBin) {
13
13
  log('info', 'claude-code image fallback via codex', { sessionKey: context.sessionKey || 'default', images: context.images.length });
14
- return runCodexVision(this.config.codexBin, prompt, context.images, this.config.workDir, this.config.taskTimeoutSeconds * 1000, this.config);
14
+ return runCodexVision(this.config.codexBin, prompt, context.images, this.config.workDir, this.config.taskTimeoutSeconds * 1000, this.config, context.onProgress);
15
15
  }
16
16
  // Claude Code 2.x requires --verbose when stream-json is used with --print/-p.
17
17
  const args = ['-p', prompt, '--output-format', 'stream-json', '--verbose'];
18
18
  if (this.config.mode === 'yolo') args.push('--dangerously-skip-permissions', '--permission-mode', 'bypassPermissions');
19
19
  if (this.config.model) args.push('--model', this.config.model);
20
20
  log('info', 'claude-code launch', { sessionKey: context.sessionKey || 'default' });
21
- return runClaude(this.config.claudeBin, args, this.config.workDir, this.config.taskTimeoutSeconds * 1000);
21
+ return runClaude(this.config.claudeBin, args, this.config.workDir, this.config.taskTimeoutSeconds * 1000, context.onProgress);
22
22
  }
23
23
  }
24
24
 
25
- function runClaude(bin, args, cwd, timeoutMs) {
25
+ function runClaude(bin, args, cwd, timeoutMs, onEvent) {
26
26
  return new Promise((resolve, reject) => {
27
27
  const child = spawn(bin, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env } });
28
28
  let stderr = '';
@@ -39,6 +39,7 @@ function runClaude(bin, args, cwd, timeoutMs) {
39
39
  rawLines.push(line);
40
40
  if (rawLines.length > 20) rawLines.shift();
41
41
  const ev = JSON.parse(line);
42
+ if (onEvent) Promise.resolve(onEvent(ev)).catch(() => {});
42
43
  const evError = extractEventError(ev);
43
44
  if (evError) streamError = evError;
44
45
  if (typeof ev.result === 'string' && ev.result.trim()) {
@@ -57,7 +58,7 @@ function runClaude(bin, args, cwd, timeoutMs) {
57
58
  });
58
59
  }
59
60
 
60
- async function runCodexVision(bin, prompt, images, cwd, timeoutMs, config = {}) {
61
+ async function runCodexVision(bin, prompt, images, cwd, timeoutMs, config = {}, onEvent) {
61
62
  const args = ['exec', '--skip-git-repo-check', '--json', '--cd', cwd];
62
63
  if (config.mode === 'yolo') args.push('--dangerously-bypass-approvals-and-sandbox');
63
64
  for (const img of images || []) args.push('--image', img);
@@ -77,6 +78,7 @@ async function runCodexVision(bin, prompt, images, cwd, timeoutMs, config = {})
77
78
  if (!line.trim()) return;
78
79
  try {
79
80
  const event = JSON.parse(line);
81
+ if (onEvent) Promise.resolve(onEvent(event)).catch(() => {});
80
82
  const text = extractCodexText(event);
81
83
  if (text) messages.push(text);
82
84
  } catch {}
@@ -17,19 +17,32 @@ export class CodexAdapter {
17
17
  const threadId = this.threadBySession.get(sessionKey) || stored.codexThreadId;
18
18
  const args = this.buildArgs(threadId, context);
19
19
  log('info', 'codex launch', { sessionKey, resume: Boolean(threadId), mode: this.config.mode });
20
- const result = await runJsonCli(this.config.codexBin, args, prompt, this.config.workDir, (event) => {
20
+ let result;
21
+ try {
22
+ result = await this.runWithArgs(args, prompt, context, sessionKey);
23
+ } catch (err) {
24
+ if (!threadId || !shouldRetryFreshThread(err)) throw err;
25
+ log('warn', 'codex resume failed; retrying with fresh thread', { sessionKey, threadId, error: err.message });
26
+ this.threadBySession.delete(sessionKey);
27
+ this.store?.patchSession(sessionKey, { codexThreadId: '', adapter: this.name });
28
+ result = await this.runWithArgs(this.buildArgs('', context), prompt, context, sessionKey);
29
+ }
30
+ if (result.threadId) {
31
+ this.threadBySession.set(sessionKey, result.threadId);
32
+ this.store?.patchSession(sessionKey, { codexThreadId: result.threadId, adapter: this.name });
33
+ }
34
+ return result.text.trim();
35
+ }
36
+
37
+ async runWithArgs(args, prompt, context, sessionKey) {
38
+ return runJsonCli(this.config.codexBin, args, prompt, this.config.workDir, (event) => {
21
39
  const eventThreadId = event.thread_id || event.session_id || event.conversation_id;
22
40
  if ((event.type === 'thread.started' || event.type === 'session.started') && eventThreadId) {
23
41
  this.threadBySession.set(sessionKey, eventThreadId);
24
42
  this.store?.patchSession(sessionKey, { codexThreadId: eventThreadId, adapter: this.name });
25
43
  }
26
- if (this.config.publishProgress && context.onProgress) context.onProgress(event).catch(() => {});
44
+ if (context.onProgress) context.onProgress(event).catch(() => {});
27
45
  }, this.config.taskTimeoutSeconds * 1000);
28
- if (result.threadId) {
29
- this.threadBySession.set(sessionKey, result.threadId);
30
- this.store?.patchSession(sessionKey, { codexThreadId: result.threadId, adapter: this.name });
31
- }
32
- return result.text.trim();
33
46
  }
34
47
 
35
48
  buildArgs(threadId, context = {}) {
@@ -43,6 +56,18 @@ export class CodexAdapter {
43
56
  }
44
57
  }
45
58
 
59
+ function shouldRetryFreshThread(err) {
60
+ const message = String(err?.message || err || '').toLowerCase();
61
+ return [
62
+ 'exec resume',
63
+ 'transport channel closed',
64
+ 'http/request failed',
65
+ 'failed to refresh available models',
66
+ 'timed out',
67
+ 'timeout waiting for child process',
68
+ ].some((needle) => message.includes(needle));
69
+ }
70
+
46
71
  async function runJsonCli(bin, args, stdinText, cwd, onEvent, timeoutMs) {
47
72
  return new Promise((resolve, reject) => {
48
73
  const child = spawn(bin, args, { cwd, stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env } });
@@ -141,7 +141,7 @@ export class GenericCliAdapter {
141
141
  const result = await runCli(bin, built.args, built.stdin, this.config.workDir, (event) => {
142
142
  const eventSessionId = extractSessionId(event);
143
143
  if (eventSessionId) this.store?.patchSession(sessionKey, { [storedKey]: eventSessionId, adapter: this.name });
144
- if (this.config.publishProgress && context.onProgress) context.onProgress(event).catch(() => {});
144
+ if (context.onProgress) context.onProgress(event).catch(() => {});
145
145
  }, this.config.taskTimeoutSeconds * 1000);
146
146
  if (result.sessionId) this.store?.patchSession(sessionKey, { [storedKey]: result.sessionId, adapter: this.name });
147
147
  return result.text.trim();
package/src/events.js CHANGED
@@ -2,6 +2,24 @@ export function normalizeAgentEvent(adapterName, event) {
2
2
  const item = event?.item || {};
3
3
  const type = event?.type || '';
4
4
  const now = new Date().toISOString();
5
+
6
+ if (adapterName === 'claude-code') {
7
+ if (type === 'system') return { kind: 'session', status: 'started', text: event.subtype || 'session started', time: now, raw: event };
8
+ if (type === 'result') return { kind: 'turn', status: 'completed', text: event.result ? 'response completed' : 'turn completed', time: now, raw: event };
9
+ if (type === 'assistant') {
10
+ const parts = Array.isArray(event.message?.content) ? event.message.content : [];
11
+ const tool = parts.find((part) => part?.type === 'tool_use');
12
+ if (tool) {
13
+ const name = tool.name || 'tool';
14
+ const summary = summarizeToolInput(tool.input);
15
+ return { kind: 'tool', status: 'started', text: summary ? `${name} ${summary}` : name, time: now, raw: event };
16
+ }
17
+ if (parts.some((part) => part?.type === 'text' && String(part.text || '').trim())) {
18
+ return { kind: 'response', status: 'started', text: 'writing response', time: now, raw: event };
19
+ }
20
+ }
21
+ }
22
+
5
23
  if (type === 'item.started') {
6
24
  const kind = item.type || 'item';
7
25
  if (kind === 'command_execution') return { kind: 'command', status: 'started', text: item.command || 'command', time: now, raw: event };
@@ -21,3 +39,33 @@ export function renderStatusLine(evt) {
21
39
  if (!evt) return '';
22
40
  return `[TASK_STATUS] time=${evt.time} status=${evt.status} action=${evt.kind}:${String(evt.text || '').slice(0, 180)}`;
23
41
  }
42
+
43
+ export function renderActivityText(evt, adapterName = 'agent') {
44
+ if (!evt) return '';
45
+ const adapter = adapterName === 'claude-code' ? 'Claude Code' : adapterName === 'codex' ? 'Codex' : adapterName;
46
+ const text = String(evt.text || '').trim();
47
+ if (evt.status === 'failed') return `${adapter} 执行失败:${text || evt.kind || 'error'}`;
48
+ if (evt.status === 'completed') return `${adapter} 已完成 ${labelKind(evt.kind)}`;
49
+ if (evt.kind === 'command') return `${adapter} 正在执行命令:${text || 'command'}`;
50
+ if (evt.kind === 'tool') return `${adapter} 正在调用工具:${text || 'tool'}`;
51
+ if (evt.kind === 'web_search') return `${adapter} 正在搜索:${text || 'web search'}`;
52
+ if (evt.kind === 'response') return `${adapter} 正在组织回复`;
53
+ if (evt.kind === 'session') return `${adapter} 会话已启动`;
54
+ return `${adapter} 正在执行:${text || labelKind(evt.kind)}`;
55
+ }
56
+
57
+ function labelKind(kind) {
58
+ const value = String(kind || '').replace(/_/g, ' ').trim();
59
+ return value || 'step';
60
+ }
61
+
62
+ function summarizeToolInput(input) {
63
+ if (!input || typeof input !== 'object') return '';
64
+ const command = input.command || input.cmd;
65
+ if (command) return String(command).slice(0, 120);
66
+ const path = input.path || input.file_path || input.file;
67
+ if (path) return String(path).slice(0, 120);
68
+ const query = input.query || input.pattern;
69
+ if (query) return String(query).slice(0, 120);
70
+ return '';
71
+ }
package/src/runner.js CHANGED
@@ -8,7 +8,7 @@ import { AttachmentManager } from './attachments.js';
8
8
  import { DurableStore } from './store.js';
9
9
  import { PermissionBroker } from './permissions.js';
10
10
  import { STTManager } from './stt.js';
11
- import { normalizeAgentEvent, renderStatusLine } from './events.js';
11
+ import { normalizeAgentEvent, renderActivityText, renderStatusLine } from './events.js';
12
12
  import { log } from './logger.js';
13
13
  import { buildRuntimeInfo } from './runtime-info.js';
14
14
  import { runShellCommand } from './shell-runner.js';
@@ -151,16 +151,22 @@ export class Runner {
151
151
  if (!topicId) return;
152
152
  const staged = await this.attachments.stageMessage(m);
153
153
  if (!content && !staged.files.length) return;
154
- await this.wtt.typing(topicId, 'start');
154
+ await this.wtt.typing(topicId, 'start', { statusText: 'Agent 已接收消息,正在准备执行', statusKind: 'queued', ttlMs: 30000 });
155
155
  try {
156
156
  const transcripts = await this.transcribeAttachments(staged.files);
157
157
  const adapter = this.registry.select({ ...m, content });
158
+ await this.wtt.typing(topicId, 'start', { statusText: `${adapterDisplayName(adapter.name)} 正在执行`, statusKind: 'running', adapter: adapter.name, ttlMs: 30000 });
158
159
  const agentSoul = renderAgentSoulContext(m.metadata);
160
+ const discussionRouting = renderDiscussionRoutingInstruction(m);
159
161
  const prompt = [
160
162
  'You are replying to a WTT Web conversation. Do not mention implementation internals unless asked.',
161
163
  `WTT topic_id: ${topicId}`,
164
+ `WTT topic_type: ${messageTopicType(m) || 'unknown'}`,
162
165
  `sender_id: ${m.sender_id || 'human'}`,
166
+ m.sender_display_name || m.senderDisplayName ? `sender_display_name: ${m.sender_display_name || m.senderDisplayName}` : null,
163
167
  '',
168
+ discussionRouting,
169
+ discussionRouting ? '' : null,
164
170
  agentSoul,
165
171
  agentSoul ? '' : null,
166
172
  'User message:',
@@ -198,6 +204,7 @@ export class Runner {
198
204
  const staged = await this.attachments.stageMessage(task);
199
205
  const transcripts = await this.transcribeAttachments(staged.files);
200
206
  const adapter = this.registry.select(task);
207
+ if (topicId) await this.wtt.typing(topicId, 'start', { statusText: `${adapterDisplayName(adapter.name)} 正在执行任务`, statusKind: 'running', adapter: adapter.name, ttlMs: 30000 });
201
208
  const prompt = buildTaskPrompt(task, this.config, staged, transcripts);
202
209
  try {
203
210
  const output = await adapter.run(prompt, {
@@ -224,6 +231,8 @@ export class Runner {
224
231
  await this.api.patchTask(taskId, { status: 'blocked', output: String(err.message || err) });
225
232
  if (topicId) await this.wtt.publish(topicId, `任务失败:${err.message}`, 'TASK_BLOCKED');
226
233
  log('error', 'task failed', { taskId, error: err.message });
234
+ } finally {
235
+ if (topicId) await this.wtt.typing(topicId, 'stop');
227
236
  }
228
237
  }
229
238
 
@@ -258,8 +267,18 @@ export class Runner {
258
267
  }
259
268
 
260
269
  async maybePublishProgress(topicId, event, adapterName = 'agent') {
261
- if (!this.config.publishProgress || !topicId) return;
270
+ if (!topicId) return;
262
271
  const normalized = normalizeAgentEvent(adapterName, event);
272
+ const activityText = renderActivityText(normalized, adapterName);
273
+ if (activityText) {
274
+ await this.wtt.typing(topicId, 'start', {
275
+ statusText: activityText,
276
+ statusKind: normalized?.kind || 'running',
277
+ adapter: adapterName,
278
+ ttlMs: 30000,
279
+ });
280
+ }
281
+ if (!this.config.publishProgress) return;
263
282
  const line = renderStatusLine(normalized);
264
283
  if (line) await this.wtt.publish(topicId, line, 'TASK_STATUS');
265
284
  }
@@ -282,6 +301,35 @@ export class Runner {
282
301
  }
283
302
  }
284
303
 
304
+ function adapterDisplayName(name) {
305
+ if (name === 'claude-code') return 'Claude Code';
306
+ if (name === 'codex') return 'Codex';
307
+ return name || 'Agent';
308
+ }
309
+
310
+ function messageTopicType(m) {
311
+ return String(m.topic_type || metadataValue(m.metadata, 'topic_type') || metadataValue(m.metadata, 'topicType') || '').toLowerCase();
312
+ }
313
+
314
+ function renderDiscussionRoutingInstruction(m) {
315
+ const topicType = messageTopicType(m);
316
+ if (topicType !== 'discussion' && topicType !== 'collaborative') return '';
317
+
318
+ const senderId = String(m.sender_id || '').trim();
319
+ const senderName = String(m.sender_display_name || m.senderDisplayName || '').trim();
320
+ const directTarget = senderName || senderId;
321
+ const directMention = directTarget ? `@${directTarget.replace(/^@+/, '')}` : '@<agent-name>';
322
+
323
+ return [
324
+ 'Internal WTT group-discussion routing rule. Follow it silently and do not explain it:',
325
+ '- In discussion/collaborative topics, another agent will only run if your visible reply explicitly @mentions that agent.',
326
+ `- If you want the sender agent to continue, challenge, verify, or answer, include ${directMention} in the visible reply.`,
327
+ '- If you want a different agent to continue, @mention that agent by its exact visible name or agent id from the conversation.',
328
+ '- If your reply is a final answer, summary, or no further agent action is needed, do not @mention anyone.',
329
+ '- Do not use @all or vague mentions; mention only the specific next agent that should act.',
330
+ ].join('\n');
331
+ }
332
+
285
333
  function isChatMessage(m, config) {
286
334
  const content = String(m.content || '').trim();
287
335
  if (!content || content.startsWith('[TASK_')) return false;
@@ -385,6 +385,10 @@ function shouldPreserveEnvKey(key) {
385
385
  key === 'HTTPS_PROXY' ||
386
386
  key === 'ALL_PROXY' ||
387
387
  key === 'NO_PROXY' ||
388
+ key === 'http_proxy' ||
389
+ key === 'https_proxy' ||
390
+ key === 'all_proxy' ||
391
+ key === 'no_proxy' ||
388
392
  key === 'DISABLE_TELEMETRY' ||
389
393
  key === 'DISABLE_COST_WARNINGS' ||
390
394
  key === 'API_TIMEOUT_MS' ||
package/src/wtt-client.js CHANGED
@@ -122,8 +122,17 @@ export class WTTClient {
122
122
  return this.action('publish', { topic_id: topicId, content, content_type: 'text', semantic_type: semanticType });
123
123
  }
124
124
 
125
- async typing(topicId, state) {
126
- try { await this.action('typing', { topic_id: topicId, state, ttl_ms: 6000 }, 5000); } catch {}
125
+ async typing(topicId, state, options = {}) {
126
+ try {
127
+ await this.action('typing', {
128
+ topic_id: topicId,
129
+ state,
130
+ ttl_ms: options.ttlMs || options.ttl_ms || 6000,
131
+ ...(options.statusText ? { status_text: options.statusText } : {}),
132
+ ...(options.statusKind ? { status_kind: options.statusKind } : {}),
133
+ ...(options.adapter ? { adapter: options.adapter } : {}),
134
+ }, 5000);
135
+ } catch {}
127
136
  }
128
137
  }
129
138
 
@@ -0,0 +1,14 @@
1
+ [Unit]
2
+ Description=WTT Connect Claude Code Agent
3
+ After=network-online.target
4
+ Wants=network-online.target
5
+
6
+ [Service]
7
+ Type=simple
8
+ WorkingDirectory=/mnt/wd10t/saiph/wtt/wtt/tools/wtt-connect
9
+ ExecStart=/usr/bin/node --experimental-websocket /mnt/wd10t/saiph/wtt/wtt/tools/wtt-connect/bin/wtt-connect.js start --env-file /mnt/wd10t/saiph/wtt/wtt/tools/wtt-connect/.env.claude
10
+ Restart=always
11
+ RestartSec=5
12
+
13
+ [Install]
14
+ WantedBy=default.target
@@ -0,0 +1,14 @@
1
+ [Unit]
2
+ Description=WTT Connect Codex Agent
3
+ After=network-online.target
4
+ Wants=network-online.target
5
+
6
+ [Service]
7
+ Type=simple
8
+ WorkingDirectory=/mnt/wd10t/saiph/wtt/wtt/tools/wtt-connect
9
+ ExecStart=/usr/bin/node --experimental-websocket /mnt/wd10t/saiph/wtt/wtt/tools/wtt-connect/bin/wtt-connect.js start --env-file /mnt/wd10t/saiph/wtt/wtt/tools/wtt-connect/.env
10
+ Restart=always
11
+ RestartSec=5
12
+
13
+ [Install]
14
+ WantedBy=default.target