wtt-connect 0.2.11 → 0.2.14
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/README.md +12 -4
- package/package.json +1 -1
- package/src/main.js +91 -0
- package/src/runner.js +63 -103
- package/src/wtt-client.js +25 -0
- package/systemd/wtt-connect-claude.service +14 -0
- package/systemd/wtt-connect-codex.service +14 -0
package/README.md
CHANGED
|
@@ -21,7 +21,6 @@ Implemented production-oriented surfaces:
|
|
|
21
21
|
- `acp` for any Agent Client Protocol compatible agent
|
|
22
22
|
- `devin` through ACP (`devin acp`)
|
|
23
23
|
- Normalized tool/progress event publishing
|
|
24
|
-
- OpenDesign visual artifact generation for formula/principle/process/UI explanations, with a quality gate that checks for dense structure, SVG/canvas diagrams, animation, examples, and formula/metric explanation before upload
|
|
25
24
|
- TTS extension point:
|
|
26
25
|
- `none`
|
|
27
26
|
- `macos-say` using the macOS `say` command, emitted as WAV when upload is enabled
|
|
@@ -122,10 +121,19 @@ Codex, Claude Code, and other adapters can generate user-facing files on the age
|
|
|
122
121
|
Supported generated artifact types:
|
|
123
122
|
|
|
124
123
|
```text
|
|
125
|
-
.doc .docx .ppt .pptx .xls .xlsx .pdf .csv .md .txt .zip
|
|
124
|
+
.doc .docx .ppt .pptx .xls .xlsx .pdf .csv .md .txt .zip .html .png .jpg .webp .mp4 .webm .mov
|
|
126
125
|
```
|
|
127
126
|
|
|
128
|
-
When an agent creates a deliverable file, it
|
|
127
|
+
When an agent creates a deliverable file, it can save the file anywhere under its own WTT workspace (`~/.wtt-connect/<agentID>` by default, or `WTT_CONNECT_WORKDIR` when set). The agent should then explicitly publish it with:
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
wtt-connect upload-file --topic-id <topic_id> --title "Project Report" ./report.pdf
|
|
131
|
+
wtt-connect upload-file --topic-id <topic_id> --title "Design Pack" ./a.pdf ./slides.pptx ./notes.md
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`upload-file` uploads the file through WTT media and publishes a clickable file card into the topic. If several files are passed, it packages them into one zip before publishing. `wtt-connect` does not scan the workspace automatically.
|
|
135
|
+
|
|
136
|
+
Agents can also include one final-response marker per file to provide a better display title:
|
|
129
137
|
|
|
130
138
|
```text
|
|
131
139
|
[WTT_ARTIFACT_FILE path="report.docx" title="Project Report"]
|
|
@@ -140,7 +148,7 @@ When an agent creates a deliverable file, it should save the file under its work
|
|
|
140
148
|
|
|
141
149
|
Notes:
|
|
142
150
|
|
|
143
|
-
- Generated file markers are
|
|
151
|
+
- Generated files published through `upload-file` and explicit markers are user-facing uploads; they are uploaded even when `WTT_CONNECT_UPLOAD_ARTIFACTS=0`.
|
|
144
152
|
- Files outside the configured workspace, state directory, inbox directory, or artifact directory are ignored to avoid accidental leakage.
|
|
145
153
|
- `WTT_CONNECT_UPLOAD_ARTIFACTS=1` still controls automatic summary/TTS uploads; it is not required for explicit generated file markers.
|
|
146
154
|
|
package/package.json
CHANGED
package/src/main.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
2
4
|
import { loadDefaultEnvFiles } from './env.js';
|
|
3
5
|
import { loadConfig } from './config.js';
|
|
4
6
|
import { Runner } from './runner.js';
|
|
@@ -7,6 +9,7 @@ import { setup } from './setup.js';
|
|
|
7
9
|
import { smokeChat, smokeTask } from './smoke.js';
|
|
8
10
|
import { ArtifactManager } from './artifacts.js';
|
|
9
11
|
import { DurableStore } from './store.js';
|
|
12
|
+
import { WTTClient } from './wtt-client.js';
|
|
10
13
|
import { log, redact } from './logger.js';
|
|
11
14
|
import { adapterBin, normalizeProfileName } from './adapters/generic-cli.js';
|
|
12
15
|
import { normalizeAdapterName } from './adapters/index.js';
|
|
@@ -30,6 +33,7 @@ export async function main(args) {
|
|
|
30
33
|
if (cmd === 'claim-code') return setup(loadConfig(argv), { ...argv, noRegister: true, claimCode: true });
|
|
31
34
|
if (cmd === 'smoke-task') return smokeTask(loadConfig(argv), argv);
|
|
32
35
|
if (cmd === 'smoke-chat') return smokeChat(loadConfig(argv), argv);
|
|
36
|
+
if (cmd === 'upload-file' || cmd === 'send-file') return uploadFile(loadConfig(argv), argv);
|
|
33
37
|
if (cmd === 'upload-artifact' || cmd === 'opendesign-upload') return uploadArtifact(loadConfig(argv), argv);
|
|
34
38
|
if (cmd === 'start') {
|
|
35
39
|
const config = loadConfig(argv);
|
|
@@ -73,6 +77,14 @@ function parseArgs(args) {
|
|
|
73
77
|
else if (a === '--expected') out.expected = args[++i];
|
|
74
78
|
else if (a === '--prompt') out.prompt = args[++i];
|
|
75
79
|
else if (a === '--message') out.message = args[++i];
|
|
80
|
+
else if (a === '--topic-id' || a === '--topic') out.topicId = args[++i];
|
|
81
|
+
else if (a === '--file') {
|
|
82
|
+
if (!out.files) out.files = [];
|
|
83
|
+
out.files.push(args[++i]);
|
|
84
|
+
}
|
|
85
|
+
else if (a === '--zip') out.zip = true;
|
|
86
|
+
else if (a === '--no-zip') out.noZip = true;
|
|
87
|
+
else if (a === '--semantic-type') out.semanticType = args[++i];
|
|
76
88
|
else if (a === '--dir') out.dir = args[++i];
|
|
77
89
|
else if (a === '--source') out.source = args[++i];
|
|
78
90
|
else if (a === '--source-id') out.sourceId = args[++i];
|
|
@@ -111,6 +123,8 @@ Commands:
|
|
|
111
123
|
smoke-task Create/run a WTT task and wait for connector result
|
|
112
124
|
smoke-chat Send P2P chat from smoke sender and wait for reply
|
|
113
125
|
claim-code Print claim code for current WTT_AGENT_ID/TOKEN
|
|
126
|
+
upload-file --topic-id <id> <file...>
|
|
127
|
+
Upload generated file(s) and publish WTT file card
|
|
114
128
|
upload-artifact --dir <path> Upload an OpenDesign/artifact directory to WTT
|
|
115
129
|
opendesign-upload --dir <path>
|
|
116
130
|
Alias for upload-artifact
|
|
@@ -118,6 +132,83 @@ Commands:
|
|
|
118
132
|
`);
|
|
119
133
|
}
|
|
120
134
|
|
|
135
|
+
async function uploadFile(config, argv) {
|
|
136
|
+
const topicId = String(argv.topicId || argv.sourceId || '').trim();
|
|
137
|
+
if (!topicId) throw new Error('upload-file requires --topic-id <topic_id>');
|
|
138
|
+
const rawFiles = [...(argv.files || []), ...(argv._ || [])].map((file) => String(file || '').trim()).filter(Boolean);
|
|
139
|
+
if (!rawFiles.length) throw new Error('upload-file requires at least one file path');
|
|
140
|
+
const files = rawFiles.map((file) => resolveUploadFilePath(file, config));
|
|
141
|
+
for (const file of files) {
|
|
142
|
+
const stat = fs.statSync(file);
|
|
143
|
+
if (!stat.isFile()) throw new Error(`not a file: ${file}`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const store = new DurableStore(config.storeFile).load();
|
|
147
|
+
const artifacts = new ArtifactManager(config, store);
|
|
148
|
+
const wtt = new WTTClient(config, async () => {});
|
|
149
|
+
try {
|
|
150
|
+
await wtt.connectForActions();
|
|
151
|
+
const target = files.length > 1 || argv.zip ? packageUploadFiles(files, config, argv) : { path: files[0], originalFiles: files };
|
|
152
|
+
const stat = fs.statSync(target.path);
|
|
153
|
+
const asset = await artifacts.uploadFile(target.path, { force: true });
|
|
154
|
+
const url = asset?.url || asset?.public_url;
|
|
155
|
+
if (!url) throw new Error('WTT media upload returned no URL');
|
|
156
|
+
const title = String(argv.title || '').trim() || path.basename(target.path);
|
|
157
|
+
const content = `[file:${title}](${url})`;
|
|
158
|
+
const published = await wtt.publish(topicId, content, argv.semanticType || 'TASK_ARTIFACT', {
|
|
159
|
+
type: 'generated_file',
|
|
160
|
+
source: 'agent_upload',
|
|
161
|
+
source_id: topicId,
|
|
162
|
+
file_name: title,
|
|
163
|
+
local_path: target.path,
|
|
164
|
+
size: asset.size || stat.size,
|
|
165
|
+
packaged: files.length > 1 || Boolean(argv.zip),
|
|
166
|
+
original_files: target.originalFiles?.map((file) => path.basename(file)) || undefined,
|
|
167
|
+
});
|
|
168
|
+
console.log(JSON.stringify({ ok: true, topic_id: topicId, file: target.path, url, message: published }, null, 2));
|
|
169
|
+
} finally {
|
|
170
|
+
await wtt.close();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function packageUploadFiles(files, config, argv) {
|
|
175
|
+
if (argv.noZip && files.length === 1) return { path: files[0], originalFiles: files };
|
|
176
|
+
const dir = config.artifactDir || path.join(config.workDir || process.cwd(), '.wtt-connect', 'artifacts');
|
|
177
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
178
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
179
|
+
const title = String(argv.title || 'agent-files').replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64) || 'agent-files';
|
|
180
|
+
const zipPath = path.join(dir, `${title}-${stamp}.zip`);
|
|
181
|
+
const result = spawnSync('zip', ['-j', '-q', zipPath, ...files], { encoding: 'utf8' });
|
|
182
|
+
if (result.status !== 0) {
|
|
183
|
+
const detail = result.stderr || result.stdout || `exit ${result.status}`;
|
|
184
|
+
throw new Error(`failed to package files with zip: ${detail}`);
|
|
185
|
+
}
|
|
186
|
+
return { path: zipPath, originalFiles: files };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function resolveUploadFilePath(filePath, config) {
|
|
190
|
+
const raw = String(filePath || '').trim();
|
|
191
|
+
if (!raw) throw new Error('empty file path');
|
|
192
|
+
const expanded = raw.startsWith('~/') ? path.join(process.env.HOME || '', raw.slice(2)) : raw;
|
|
193
|
+
const base = path.resolve(config.workDir || process.cwd());
|
|
194
|
+
const resolved = path.resolve(path.isAbsolute(expanded) ? expanded : path.join(base, expanded));
|
|
195
|
+
const roots = [
|
|
196
|
+
config.workDir,
|
|
197
|
+
config.artifactDir,
|
|
198
|
+
config.storeFile ? path.dirname(config.storeFile) : '',
|
|
199
|
+
config.inboxDir,
|
|
200
|
+
].filter(Boolean).map((root) => path.resolve(root));
|
|
201
|
+
if (!roots.some((root) => isPathInside(resolved, root))) {
|
|
202
|
+
throw new Error(`upload-file refused path outside WTT workspace/state roots: ${resolved}`);
|
|
203
|
+
}
|
|
204
|
+
return resolved;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function isPathInside(candidate, root) {
|
|
208
|
+
const rel = path.relative(root, candidate);
|
|
209
|
+
return rel === '' || (rel && !rel.startsWith('..') && !path.isAbsolute(rel));
|
|
210
|
+
}
|
|
211
|
+
|
|
121
212
|
async function uploadArtifact(config, argv) {
|
|
122
213
|
const dir = argv.dir || argv._[0];
|
|
123
214
|
if (!dir) throw new Error('upload-artifact requires --dir <path>');
|
package/src/runner.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { WTTClient } from './wtt-client.js';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
2
3
|
import fs from 'node:fs';
|
|
3
4
|
import fsp from 'node:fs/promises';
|
|
4
5
|
import path from 'node:path';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
5
7
|
import { WTTApi } from './wtt-api.js';
|
|
6
8
|
import { SessionManager } from './session-manager.js';
|
|
7
9
|
import { TTSManager } from './tts.js';
|
|
@@ -16,12 +18,14 @@ import { log } from './logger.js';
|
|
|
16
18
|
import { buildRuntimeInfo } from './runtime-info.js';
|
|
17
19
|
import { runShellCommand } from './shell-runner.js';
|
|
18
20
|
import { TerminalSessionManager } from './terminal-session.js';
|
|
19
|
-
import { buildOpenDesignPrompt, buildOpenDesignRepairPrompt, chooseOpenDesignSkills, evaluateOpenDesignArtifact, prepareOpenDesignDir, shouldRenderOpenDesignArtifact } from './opendesign.js';
|
|
20
21
|
|
|
21
22
|
const TERMINAL_STATUSES = new Set(['review', 'done', 'approved', 'cancelled']);
|
|
22
|
-
const GENERATED_FILE_EXTENSIONS = new Set([
|
|
23
|
-
|
|
23
|
+
const GENERATED_FILE_EXTENSIONS = new Set([
|
|
24
|
+
'.doc', '.docx', '.ppt', '.pptx', '.xls', '.xlsx', '.pdf', '.csv', '.md', '.txt', '.zip',
|
|
25
|
+
'.html', '.htm', '.png', '.jpg', '.jpeg', '.webp', '.mp4', '.webm', '.mov', '.m4v',
|
|
26
|
+
]);
|
|
24
27
|
const BROADCAST_MENTIONS = new Set(['all', 'everyone', '全体', '所有人']);
|
|
28
|
+
const execFileAsync = promisify(execFile);
|
|
25
29
|
|
|
26
30
|
export class Runner {
|
|
27
31
|
constructor(config) {
|
|
@@ -188,7 +192,7 @@ export class Runner {
|
|
|
188
192
|
'',
|
|
189
193
|
staged.promptBlock,
|
|
190
194
|
renderTranscriptBlock(transcripts),
|
|
191
|
-
renderGeneratedFileArtifactInstruction(this.config),
|
|
195
|
+
renderGeneratedFileArtifactInstruction(this.config, topicId),
|
|
192
196
|
'Reply naturally and concisely unless the user asks for detail.',
|
|
193
197
|
].filter(Boolean).join('\n');
|
|
194
198
|
const output = await adapter.run(prompt, {
|
|
@@ -203,7 +207,6 @@ export class Runner {
|
|
|
203
207
|
await this.maybeAttachSpeech(topicId, reply, `chat-${topicId}`);
|
|
204
208
|
await this.wtt.publish(topicId, reply, 'CHAT_REPLY');
|
|
205
209
|
await this.publishGeneratedFileArtifacts(topicId, prepared.refs, { source: 'chat', sourceId: topicId, adapter: adapter.name });
|
|
206
|
-
await this.maybeRenderOpenDesignArtifact(m, reply, adapter);
|
|
207
210
|
log('info', 'chat replied', { topicId, chars: reply.length });
|
|
208
211
|
} catch (err) {
|
|
209
212
|
await this.wtt.publish(topicId, `执行失败:${err.message}`, 'CHAT_REPLY');
|
|
@@ -286,73 +289,11 @@ export class Runner {
|
|
|
286
289
|
}
|
|
287
290
|
}
|
|
288
291
|
|
|
289
|
-
async maybeRenderOpenDesignArtifact(message, reply, adapter) {
|
|
290
|
-
const topicId = String(message.topic_id || '');
|
|
291
|
-
if (!topicId || !shouldRenderOpenDesignArtifact(message, reply)) return;
|
|
292
|
-
try {
|
|
293
|
-
await this.wtt.typing(topicId, 'start', {
|
|
294
|
-
statusText: 'OpenDesign 正在生成可视化 artifact',
|
|
295
|
-
statusKind: 'opendesign',
|
|
296
|
-
adapter: adapter.name,
|
|
297
|
-
ttlMs: 90000,
|
|
298
|
-
});
|
|
299
|
-
const outputDir = await prepareOpenDesignDir(this.config, topicId);
|
|
300
|
-
const skills = chooseOpenDesignSkills(message, reply);
|
|
301
|
-
const locale = inferLocale(`${message.content || ''}\n${reply || ''}`);
|
|
302
|
-
const prompt = await buildOpenDesignPrompt({
|
|
303
|
-
config: this.config,
|
|
304
|
-
outputDir,
|
|
305
|
-
userMessage: message.content || '',
|
|
306
|
-
reply,
|
|
307
|
-
topicId,
|
|
308
|
-
locale,
|
|
309
|
-
skills,
|
|
310
|
-
});
|
|
311
|
-
await adapter.run(prompt, {
|
|
312
|
-
sessionKey: `wtt:opendesign:${topicId}`,
|
|
313
|
-
topicId,
|
|
314
|
-
onProgress: (event) => this.maybePublishProgress(topicId, event, adapter.name),
|
|
315
|
-
});
|
|
316
|
-
let quality = await evaluateOpenDesignArtifact(outputDir);
|
|
317
|
-
if (!quality.ok) {
|
|
318
|
-
log('warn', 'opendesign artifact quality check failed; requesting rewrite', { topicId, score: quality.score, issues: quality.issues.join('; ') });
|
|
319
|
-
await this.wtt.typing(topicId, 'start', {
|
|
320
|
-
statusText: 'OpenDesign 正在重写低质量 artifact',
|
|
321
|
-
statusKind: 'opendesign_repair',
|
|
322
|
-
adapter: adapter.name,
|
|
323
|
-
ttlMs: 120000,
|
|
324
|
-
});
|
|
325
|
-
await adapter.run(buildOpenDesignRepairPrompt({ outputDir, issues: quality.issues, locale }), {
|
|
326
|
-
sessionKey: `wtt:opendesign:${topicId}`,
|
|
327
|
-
topicId,
|
|
328
|
-
onProgress: (event) => this.maybePublishProgress(topicId, event, adapter.name),
|
|
329
|
-
});
|
|
330
|
-
quality = await evaluateOpenDesignArtifact(outputDir);
|
|
331
|
-
log(quality.ok ? 'info' : 'warn', 'opendesign artifact quality check after rewrite', { topicId, score: quality.score, issues: quality.issues.join('; ') });
|
|
332
|
-
}
|
|
333
|
-
const artifact = await this.artifacts.uploadDirectory(outputDir, {
|
|
334
|
-
source: messageTopicType(message) || 'feed',
|
|
335
|
-
sourceId: topicId,
|
|
336
|
-
title: `OpenDesign · ${topicId.slice(0, 8)}`,
|
|
337
|
-
entry: 'index.html',
|
|
338
|
-
type: 'opendesign',
|
|
339
|
-
metadata: { message_id: message.id || message.message_id || '', generated_by: adapter.name, opendesign_skills: skills },
|
|
340
|
-
});
|
|
341
|
-
const previewUrl = webPreviewUrl(artifact.preview_url || artifact.url);
|
|
342
|
-
if (previewUrl) {
|
|
343
|
-
const title = artifact.title || 'OpenDesign artifact';
|
|
344
|
-
await this.wtt.publish(topicId, `[opendesign:${title}](${previewUrl})`, 'TASK_ARTIFACT');
|
|
345
|
-
}
|
|
346
|
-
log('info', 'opendesign artifact completed', { topicId, preview: previewUrl });
|
|
347
|
-
} catch (err) {
|
|
348
|
-
log('warn', 'opendesign artifact failed', { topicId, error: err.message });
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
292
|
async publishGeneratedFileArtifacts(topicId, refs, context = {}) {
|
|
353
293
|
if (!topicId || !Array.isArray(refs) || !refs.length) return [];
|
|
354
294
|
const uploaded = [];
|
|
355
295
|
const seen = new Set();
|
|
296
|
+
const candidates = [];
|
|
356
297
|
for (const ref of refs) {
|
|
357
298
|
const resolved = resolveGeneratedFilePath(ref.path, this.config);
|
|
358
299
|
if (!resolved || seen.has(resolved)) continue;
|
|
@@ -360,16 +301,26 @@ export class Runner {
|
|
|
360
301
|
try {
|
|
361
302
|
const stat = await fsp.stat(resolved);
|
|
362
303
|
if (!stat.isFile()) continue;
|
|
304
|
+
candidates.push({ path: resolved, title: ref.title || path.basename(resolved), stat });
|
|
305
|
+
} catch (err) {
|
|
306
|
+
log('warn', 'generated file artifact skipped', { topicId, file: ref.path, error: err.message });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const publishTargets = await packageGeneratedFilesIfNeeded(candidates, this.config, context);
|
|
311
|
+
for (const ref of publishTargets) {
|
|
312
|
+
try {
|
|
363
313
|
await this.wtt.typing(topicId, 'start', {
|
|
364
|
-
statusText: `Agent 正在上传生成文件:${path.basename(
|
|
314
|
+
statusText: `Agent 正在上传生成文件:${path.basename(ref.path)}`,
|
|
365
315
|
statusKind: 'artifact_upload',
|
|
366
316
|
adapter: context.adapter,
|
|
367
317
|
ttlMs: 60000,
|
|
368
318
|
});
|
|
369
|
-
const asset = await this.artifacts.uploadFile(
|
|
319
|
+
const asset = await this.artifacts.uploadFile(ref.path, { force: true });
|
|
370
320
|
const url = asset?.url || asset?.public_url;
|
|
371
321
|
if (!url) continue;
|
|
372
|
-
const
|
|
322
|
+
const stat = ref.stat || await fsp.stat(ref.path);
|
|
323
|
+
const fileName = ref.title || path.basename(ref.path);
|
|
373
324
|
await this.wtt.publish(
|
|
374
325
|
topicId,
|
|
375
326
|
`[file:${fileName}](${url})`,
|
|
@@ -379,12 +330,14 @@ export class Runner {
|
|
|
379
330
|
source: context.source || 'agent',
|
|
380
331
|
source_id: context.sourceId || '',
|
|
381
332
|
file_name: fileName,
|
|
382
|
-
local_path:
|
|
333
|
+
local_path: ref.path,
|
|
383
334
|
mime_type: asset.mime_type || '',
|
|
384
335
|
size: asset.size || stat.size,
|
|
336
|
+
packaged: Boolean(ref.packaged),
|
|
337
|
+
original_files: ref.originalFiles || undefined,
|
|
385
338
|
},
|
|
386
339
|
);
|
|
387
|
-
uploaded.push({ path:
|
|
340
|
+
uploaded.push({ path: ref.path, asset });
|
|
388
341
|
} catch (err) {
|
|
389
342
|
log('warn', 'generated file artifact upload failed', { topicId, file: ref.path, error: err.message });
|
|
390
343
|
}
|
|
@@ -442,28 +395,44 @@ function adapterDisplayName(name) {
|
|
|
442
395
|
return name || 'Agent';
|
|
443
396
|
}
|
|
444
397
|
|
|
445
|
-
function
|
|
446
|
-
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
function webPreviewUrl(url) {
|
|
450
|
-
const text = String(url || '').trim();
|
|
451
|
-
if (!text) return '';
|
|
452
|
-
if (text.startsWith('/api/wtt/')) return text;
|
|
453
|
-
if (text.startsWith('/artifacts/')) return `/api/wtt${text}`;
|
|
454
|
-
return text;
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
function renderGeneratedFileArtifactInstruction(config) {
|
|
398
|
+
function renderGeneratedFileArtifactInstruction(config, topicId = '') {
|
|
399
|
+
const workDir = config.workDir || process.cwd();
|
|
400
|
+
const uploadCommand = `wtt-connect upload-file --topic-id ${topicId || '<topic_id>'} --title "optional display name" <file-or-files>`;
|
|
458
401
|
return [
|
|
459
402
|
'Generated file artifact rule, for internal execution only:',
|
|
460
|
-
`- If you create a user-facing file (.docx, .pptx, .xlsx, .pdf, .csv, .zip), save it under
|
|
461
|
-
|
|
403
|
+
`- If you create a user-facing file (.docx, .pptx, .xlsx, .pdf, .csv, .md, .zip, images, or video), save it under your current WTT agent workspace: ${workDir}.`,
|
|
404
|
+
`- After creating the file, immediately publish it to this WTT conversation by running: ${uploadCommand}`,
|
|
405
|
+
'- For multiple related files, pass all file paths to one upload-file command; wtt-connect will package them into one zip before publishing.',
|
|
406
|
+
'- If upload-file is unavailable, include one marker line per generated file in your final response, exactly like: [WTT_ARTIFACT_FILE path="relative/or/absolute/file.docx" title="optional display name"].',
|
|
462
407
|
'- Do not use this marker for source code files, temp files, private logs, credentials, or internal scratch files.',
|
|
463
|
-
'- wtt-connect will
|
|
408
|
+
'- wtt-connect will not scan your workspace automatically; only explicit upload-file commands or marker lines are published as WTT file cards.',
|
|
464
409
|
].join('\n');
|
|
465
410
|
}
|
|
466
411
|
|
|
412
|
+
async function packageGeneratedFilesIfNeeded(files, config, context = {}) {
|
|
413
|
+
if (!files.length) return [];
|
|
414
|
+
if (files.length === 1) return files;
|
|
415
|
+
const dir = config.artifactDir || path.join(config.workDir || process.cwd(), '.wtt-connect', 'artifacts');
|
|
416
|
+
await fsp.mkdir(dir, { recursive: true });
|
|
417
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
418
|
+
const sourceId = String(context.sourceId || 'chat').replace(/[^a-zA-Z0-9._-]+/g, '-').slice(0, 48) || 'chat';
|
|
419
|
+
const zipPath = path.join(dir, `wtt-generated-${sourceId}-${stamp}.zip`);
|
|
420
|
+
try {
|
|
421
|
+
await execFileAsync('zip', ['-j', '-q', zipPath, ...files.map((file) => file.path)], { timeout: 120000 });
|
|
422
|
+
const stat = await fsp.stat(zipPath);
|
|
423
|
+
return [{
|
|
424
|
+
path: zipPath,
|
|
425
|
+
title: `agent-files-${sourceId}.zip`,
|
|
426
|
+
stat,
|
|
427
|
+
packaged: true,
|
|
428
|
+
originalFiles: files.map((file) => path.basename(file.path)),
|
|
429
|
+
}];
|
|
430
|
+
} catch (err) {
|
|
431
|
+
log('warn', 'generated files zip packaging failed; uploading files individually', { error: err.message, files: files.length });
|
|
432
|
+
return files;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
467
436
|
function prepareGeneratedFileArtifacts(text) {
|
|
468
437
|
const input = String(text || '');
|
|
469
438
|
const refs = [];
|
|
@@ -477,7 +446,6 @@ function prepareGeneratedFileArtifacts(text) {
|
|
|
477
446
|
if (parsed.path) refs.push(parsed);
|
|
478
447
|
return '';
|
|
479
448
|
});
|
|
480
|
-
for (const ref of extractLocalFilePathRefs(cleaned)) refs.push(ref);
|
|
481
449
|
return { text: cleaned.replace(/\n{3,}/g, '\n\n').trim(), refs: dedupeArtifactRefs(refs) };
|
|
482
450
|
}
|
|
483
451
|
|
|
@@ -495,17 +463,6 @@ function parseArtifactLine(body) {
|
|
|
495
463
|
return { path: stripPathPunctuation(parts[0] || ''), title: String(parts[1] || '').trim() };
|
|
496
464
|
}
|
|
497
465
|
|
|
498
|
-
function extractLocalFilePathRefs(text) {
|
|
499
|
-
const refs = [];
|
|
500
|
-
const body = String(text || '');
|
|
501
|
-
const ext = AUTO_DETECTED_FILE_EXTENSIONS;
|
|
502
|
-
const pathLike = new RegExp(`(?:^|[\\s"'\\\`((])((?:~\\/|\\.{1,2}\\/|\\/)[^\\s"'\\\`<>]+?\\.${ext})(?=$|[\\s"'\\\`)).,;,。;])`, 'gi');
|
|
503
|
-
for (const m of body.matchAll(pathLike)) refs.push({ path: stripPathPunctuation(m[1]), title: '' });
|
|
504
|
-
const basename = new RegExp(`\\b([A-Za-z0-9][A-Za-z0-9._-]{0,160}\\.${ext})\\b`, 'gi');
|
|
505
|
-
for (const m of body.matchAll(basename)) refs.push({ path: stripPathPunctuation(m[1]), title: '' });
|
|
506
|
-
return refs;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
466
|
function stripPathPunctuation(value) {
|
|
510
467
|
return String(value || '').trim().replace(/^["'`]+|["'`.,;:,。;:))\]]+$/g, '');
|
|
511
468
|
}
|
|
@@ -532,7 +489,10 @@ function resolveGeneratedFilePath(filePath, config) {
|
|
|
532
489
|
|
|
533
490
|
const expanded = raw.startsWith('~/') ? path.join(process.env.HOME || '', raw.slice(2)) : raw;
|
|
534
491
|
const base = path.resolve(config.workDir || process.cwd());
|
|
535
|
-
const
|
|
492
|
+
const relativeRoots = [base, config.artifactDir].filter(Boolean).map((root) => path.resolve(root));
|
|
493
|
+
const resolved = path.resolve(path.isAbsolute(expanded)
|
|
494
|
+
? expanded
|
|
495
|
+
: (relativeRoots.map((root) => path.join(root, expanded)).find((candidate) => fs.existsSync(candidate)) || path.join(base, expanded)));
|
|
536
496
|
const roots = [
|
|
537
497
|
config.workDir,
|
|
538
498
|
config.artifactDir,
|
|
@@ -859,7 +819,7 @@ function buildTaskPrompt(task, config, staged = { promptBlock: '' }, transcripts
|
|
|
859
819
|
renderTranscriptBlock(transcripts),
|
|
860
820
|
'',
|
|
861
821
|
config.requireCommitPush ? 'For code changes, commit and push before final response, and include commit id.' : '',
|
|
862
|
-
renderGeneratedFileArtifactInstruction(config),
|
|
822
|
+
renderGeneratedFileArtifactInstruction(config, task.topic_id || task.topicId || ''),
|
|
863
823
|
'Return a concise final summary with evidence, changed files, tests, artifacts, and blockers.',
|
|
864
824
|
].filter(Boolean).join('\n');
|
|
865
825
|
}
|
package/src/wtt-client.js
CHANGED
|
@@ -59,6 +59,31 @@ export class WTTClient {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
async connectForActions(timeoutMs = 15000) {
|
|
63
|
+
if (!globalThis.WebSocket) throw new Error('Node.js global WebSocket is unavailable; use Node >=22');
|
|
64
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) return;
|
|
65
|
+
log('info', 'connecting WTT websocket for action', { url: this.config.wttWsUrl, agentId: this.config.agentId });
|
|
66
|
+
const ws = new WebSocket(this.config.wttWsUrl);
|
|
67
|
+
this.ws = ws;
|
|
68
|
+
await onceOpen(ws, timeoutMs);
|
|
69
|
+
ws.addEventListener('message', (event) => this.handleMessage(String(event.data)).catch((err) => {
|
|
70
|
+
log('warn', 'WTT websocket action message failed', { error: err?.message || err });
|
|
71
|
+
}));
|
|
72
|
+
ws.addEventListener('close', () => {
|
|
73
|
+
if (this.ws === ws) this.ws = null;
|
|
74
|
+
for (const [, p] of this.pending) p.reject(new Error('WTT websocket closed'));
|
|
75
|
+
this.pending.clear();
|
|
76
|
+
});
|
|
77
|
+
if (this.config.token) {
|
|
78
|
+
try {
|
|
79
|
+
await this.action('auth', { token: this.config.token }, timeoutMs);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
log('warn', 'WTT websocket action auth failed; continuing for agent-scoped actions', { error: err?.message || err });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
await this.sendHeartbeat();
|
|
85
|
+
}
|
|
86
|
+
|
|
62
87
|
async close() {
|
|
63
88
|
this.closed = true;
|
|
64
89
|
if (this.ws) this.ws.close();
|
|
@@ -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
|