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 +1 -1
- package/src/adapters/claude-code.js +6 -4
- package/src/adapters/codex.js +32 -7
- package/src/adapters/generic-cli.js +1 -1
- package/src/events.js +48 -0
- package/src/runner.js +51 -3
- package/src/service-manager.js +4 -0
- package/src/wtt-client.js +11 -2
- package/systemd/wtt-connect-claude.service +14 -0
- package/systemd/wtt-connect-codex.service +14 -0
package/package.json
CHANGED
|
@@ -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 {}
|
package/src/adapters/codex.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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 (
|
|
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 (!
|
|
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;
|
package/src/service-manager.js
CHANGED
|
@@ -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 {
|
|
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
|