wtt-connect 0.1.7 → 0.1.9
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/artifacts.js +63 -0
- package/src/main.js +28 -0
- package/src/opendesign.js +68 -0
- package/src/runner.js +56 -0
- package/systemd/wtt-connect-claude.service +0 -14
- package/systemd/wtt-connect-codex.service +0 -14
package/package.json
CHANGED
package/src/artifacts.js
CHANGED
|
@@ -47,6 +47,39 @@ export class ArtifactManager {
|
|
|
47
47
|
return { file, asset: await this.uploadFile(file) };
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
async uploadDirectory(dirPath, options = {}) {
|
|
51
|
+
const dir = path.resolve(dirPath);
|
|
52
|
+
const files = await collectFiles(dir);
|
|
53
|
+
if (!files.length) throw new Error(`artifact directory has no files: ${dir}`);
|
|
54
|
+
const payloadFiles = [];
|
|
55
|
+
for (const file of files) {
|
|
56
|
+
const rel = path.relative(dir, file).split(path.sep).join('/');
|
|
57
|
+
const bytes = await fs.readFile(file);
|
|
58
|
+
payloadFiles.push({
|
|
59
|
+
path: rel,
|
|
60
|
+
content_base64: bytes.toString('base64'),
|
|
61
|
+
mime_type: lookupMime(file),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const artifact = await this.fetchJson('/artifacts', {
|
|
65
|
+
method: 'POST',
|
|
66
|
+
headers: { 'content-type': 'application/json', ...this.authHeaders(), ...this.agentHeaders() },
|
|
67
|
+
body: JSON.stringify({
|
|
68
|
+
source: options.source || 'feed',
|
|
69
|
+
source_id: options.sourceId || '',
|
|
70
|
+
agent_id: this.config.agentId || '',
|
|
71
|
+
title: options.title || path.basename(dir),
|
|
72
|
+
type: options.type || 'opendesign',
|
|
73
|
+
entry_file: options.entry || inferEntry(files, dir),
|
|
74
|
+
metadata: options.metadata || {},
|
|
75
|
+
files: payloadFiles,
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
this.store?.addArtifact({ filePath: dir, asset: artifact });
|
|
79
|
+
log('info', 'artifact directory uploaded', { dir, files: files.length, preview: artifact.preview_url });
|
|
80
|
+
return artifact;
|
|
81
|
+
}
|
|
82
|
+
|
|
50
83
|
async fetchJson(url, options) {
|
|
51
84
|
const r = await fetch(absoluteUrl(this.config.wttBaseUrl, url), options);
|
|
52
85
|
if (!r.ok) throw new Error(`WTT media API failed: ${r.status} ${await r.text()}`);
|
|
@@ -56,6 +89,10 @@ export class ArtifactManager {
|
|
|
56
89
|
authHeaders() {
|
|
57
90
|
return this.config.httpToken ? { Authorization: `Bearer ${this.config.httpToken}` } : {};
|
|
58
91
|
}
|
|
92
|
+
|
|
93
|
+
agentHeaders() {
|
|
94
|
+
return this.config.token ? { 'X-Agent-Token': this.config.token } : {};
|
|
95
|
+
}
|
|
59
96
|
}
|
|
60
97
|
|
|
61
98
|
function absoluteUrl(base, maybeRelative) {
|
|
@@ -66,3 +103,29 @@ function absoluteUrl(base, maybeRelative) {
|
|
|
66
103
|
function safeName(name) {
|
|
67
104
|
return String(name || 'artifact.txt').replace(/[^a-zA-Z0-9._-]+/g, '-').slice(0, 120) || 'artifact.txt';
|
|
68
105
|
}
|
|
106
|
+
|
|
107
|
+
async function collectFiles(root) {
|
|
108
|
+
const out = [];
|
|
109
|
+
async function walk(dir) {
|
|
110
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
111
|
+
for (const entry of entries) {
|
|
112
|
+
if (entry.name.startsWith('.')) continue;
|
|
113
|
+
const full = path.join(dir, entry.name);
|
|
114
|
+
if (entry.isDirectory()) {
|
|
115
|
+
await walk(full);
|
|
116
|
+
} else if (entry.isFile()) {
|
|
117
|
+
out.push(full);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
await walk(root);
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function inferEntry(files, root) {
|
|
126
|
+
const rels = files.map((file) => path.relative(root, file).split(path.sep).join('/'));
|
|
127
|
+
return rels.find((file) => file === 'index.html')
|
|
128
|
+
|| rels.find((file) => file.endsWith('/index.html'))
|
|
129
|
+
|| rels.find((file) => file.toLowerCase().endsWith('.html'))
|
|
130
|
+
|| rels[0];
|
|
131
|
+
}
|
package/src/main.js
CHANGED
|
@@ -5,6 +5,8 @@ import { Runner } from './runner.js';
|
|
|
5
5
|
import { PermissionBroker } from './permissions.js';
|
|
6
6
|
import { setup } from './setup.js';
|
|
7
7
|
import { smokeChat, smokeTask } from './smoke.js';
|
|
8
|
+
import { ArtifactManager } from './artifacts.js';
|
|
9
|
+
import { DurableStore } from './store.js';
|
|
8
10
|
import { log, redact } from './logger.js';
|
|
9
11
|
import { adapterBin, normalizeProfileName } from './adapters/generic-cli.js';
|
|
10
12
|
import { normalizeAdapterName } from './adapters/index.js';
|
|
@@ -27,6 +29,7 @@ export async function main(args) {
|
|
|
27
29
|
if (cmd === 'claim-code') return setup(loadConfig(argv), { ...argv, noRegister: true, claimCode: true });
|
|
28
30
|
if (cmd === 'smoke-task') return smokeTask(loadConfig(argv), argv);
|
|
29
31
|
if (cmd === 'smoke-chat') return smokeChat(loadConfig(argv), argv);
|
|
32
|
+
if (cmd === 'upload-artifact' || cmd === 'opendesign-upload') return uploadArtifact(loadConfig(argv), argv);
|
|
30
33
|
if (cmd === 'start') {
|
|
31
34
|
const config = loadConfig(argv);
|
|
32
35
|
if (!config.agentId) throw new Error('WTT_AGENT_ID is required');
|
|
@@ -68,6 +71,12 @@ function parseArgs(args) {
|
|
|
68
71
|
else if (a === '--expected') out.expected = args[++i];
|
|
69
72
|
else if (a === '--prompt') out.prompt = args[++i];
|
|
70
73
|
else if (a === '--message') out.message = args[++i];
|
|
74
|
+
else if (a === '--dir') out.dir = args[++i];
|
|
75
|
+
else if (a === '--source') out.source = args[++i];
|
|
76
|
+
else if (a === '--source-id') out.sourceId = args[++i];
|
|
77
|
+
else if (a === '--title') out.title = args[++i];
|
|
78
|
+
else if (a === '--entry') out.entry = args[++i];
|
|
79
|
+
else if (a === '--type') out.type = args[++i];
|
|
71
80
|
else if (a === '--timeout') out.timeout = Number(args[++i]) * 1000;
|
|
72
81
|
else if (a === '--sender-agent-id') out.senderAgentId = args[++i];
|
|
73
82
|
else if (a === '--sender-token') out.senderToken = args[++i];
|
|
@@ -98,10 +107,29 @@ Commands:
|
|
|
98
107
|
smoke-task Create/run a WTT task and wait for connector result
|
|
99
108
|
smoke-chat Send P2P chat from smoke sender and wait for reply
|
|
100
109
|
claim-code Print claim code for current WTT_AGENT_ID/TOKEN
|
|
110
|
+
upload-artifact --dir <path> Upload an OpenDesign/artifact directory to WTT
|
|
111
|
+
opendesign-upload --dir <path>
|
|
112
|
+
Alias for upload-artifact
|
|
101
113
|
help Show this help
|
|
102
114
|
`);
|
|
103
115
|
}
|
|
104
116
|
|
|
117
|
+
async function uploadArtifact(config, argv) {
|
|
118
|
+
const dir = argv.dir || argv._[0];
|
|
119
|
+
if (!dir) throw new Error('upload-artifact requires --dir <path>');
|
|
120
|
+
const store = new DurableStore(config.storeFile).load();
|
|
121
|
+
const artifacts = new ArtifactManager(config, store);
|
|
122
|
+
const artifact = await artifacts.uploadDirectory(dir, {
|
|
123
|
+
source: argv.source || 'feed',
|
|
124
|
+
sourceId: argv.sourceId || '',
|
|
125
|
+
title: argv.title || 'OpenDesign artifact',
|
|
126
|
+
entry: argv.entry || '',
|
|
127
|
+
type: argv.type || 'opendesign',
|
|
128
|
+
metadata: { uploaded_by: 'wtt-connect', command: 'upload-artifact' },
|
|
129
|
+
});
|
|
130
|
+
console.log(JSON.stringify(artifact, null, 2));
|
|
131
|
+
}
|
|
132
|
+
|
|
105
133
|
|
|
106
134
|
function doctor(config) {
|
|
107
135
|
log('info', 'wtt-connect doctor', {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
const DESIGN_KEYWORDS = /动画|动效|原理图|流程图|架构图|可视化|页面|UI|界面|原型|设计|白板|公式|推导|diagram|animation|visual|prototype|wireframe|design|ui|dashboard|deck|formula|architecture/i;
|
|
5
|
+
|
|
6
|
+
export function shouldRenderOpenDesignArtifact(message, reply) {
|
|
7
|
+
const meta = parseMetadata(message?.metadata);
|
|
8
|
+
if (meta?.opendesign_render === true || meta?.open_design_render === true) return true;
|
|
9
|
+
if (meta?.whiteboard_required === true || meta?.whiteboard_require_html === true) return true;
|
|
10
|
+
const text = `${message?.content || ''}\n${reply || ''}`;
|
|
11
|
+
return DESIGN_KEYWORDS.test(text);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function prepareOpenDesignDir(config, topicId) {
|
|
15
|
+
const dir = path.join(config.artifactDir, 'opendesign', `${Date.now()}-${safeSegment(topicId || 'topic')}`);
|
|
16
|
+
await fs.mkdir(dir, { recursive: true });
|
|
17
|
+
return dir;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function buildOpenDesignPrompt({ outputDir, userMessage, reply, topicId, locale = 'zh' }) {
|
|
21
|
+
const zh = locale === 'zh';
|
|
22
|
+
return [
|
|
23
|
+
'You are running the OpenDesign workflow inside wtt-connect.',
|
|
24
|
+
'Use OpenDesign skills as the governing design rules: senior designer role, HTML as output medium, context-first, anti-slop, committed aesthetic direction, artifact-first delivery.',
|
|
25
|
+
'',
|
|
26
|
+
'OpenDesign rules to follow strictly:',
|
|
27
|
+
'- Output a real design artifact, not chat prose.',
|
|
28
|
+
'- Avoid generic AI gradients, emoji icons, unearned cards, and vague placeholder copy.',
|
|
29
|
+
'- Use distinctive typography and a committed visual direction.',
|
|
30
|
+
'- Build a self-contained HTML artifact with CSS/SVG animation when explaining principles, formulas, flows, architecture, or UI.',
|
|
31
|
+
'- Every visible control or animation must have a purpose.',
|
|
32
|
+
'- Use labeled placeholders only when a real asset is unavailable.',
|
|
33
|
+
'- Close every non-void tag and keep the page runnable without network access.',
|
|
34
|
+
'',
|
|
35
|
+
`Write the artifact files directly into this directory: ${outputDir}`,
|
|
36
|
+
'Required files:',
|
|
37
|
+
'- index.html',
|
|
38
|
+
'- style.css if useful',
|
|
39
|
+
'- main.js only if interaction/state is genuinely needed',
|
|
40
|
+
'',
|
|
41
|
+
'Hard requirements:',
|
|
42
|
+
'- index.html must be complete and runnable in a sandbox iframe.',
|
|
43
|
+
'- Do not use external CDNs, remote images, external fonts, forms, cookies, localStorage, or network calls.',
|
|
44
|
+
'- Use inline SVG or local CSS/JS only.',
|
|
45
|
+
'- For formula/principle explanations include: a main diagram, step animation, symbol/metric table, minimal concrete example, explanatory text beside visuals, and final compact summary.',
|
|
46
|
+
'- For UI/prototype requests include realistic fake data, hover states, and meaningful state transitions.',
|
|
47
|
+
'',
|
|
48
|
+
`WTT topic_id: ${topicId || 'unknown'}`,
|
|
49
|
+
'',
|
|
50
|
+
zh ? '用户原始消息:' : 'Original user message:',
|
|
51
|
+
String(userMessage || '').slice(0, 5000),
|
|
52
|
+
'',
|
|
53
|
+
zh ? 'Agent 正文回答,OpenDesign artifact 必须基于这段内容重新设计,不要照抄:' : 'Agent answer to transform into the artifact:',
|
|
54
|
+
String(reply || '').slice(0, 12000),
|
|
55
|
+
'',
|
|
56
|
+
'After writing the files, reply with only a short one-line summary and the relative entry file name.',
|
|
57
|
+
].join('\n');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parseMetadata(metadata) {
|
|
61
|
+
if (!metadata) return null;
|
|
62
|
+
if (typeof metadata === 'object') return metadata;
|
|
63
|
+
try { return JSON.parse(String(metadata)); } catch { return null; }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function safeSegment(value) {
|
|
67
|
+
return String(value || 'artifact').replace(/[^a-zA-Z0-9._-]+/g, '-').slice(0, 80) || 'artifact';
|
|
68
|
+
}
|
package/src/runner.js
CHANGED
|
@@ -13,6 +13,7 @@ import { log } from './logger.js';
|
|
|
13
13
|
import { buildRuntimeInfo } from './runtime-info.js';
|
|
14
14
|
import { runShellCommand } from './shell-runner.js';
|
|
15
15
|
import { TerminalSessionManager } from './terminal-session.js';
|
|
16
|
+
import { buildOpenDesignPrompt, prepareOpenDesignDir, shouldRenderOpenDesignArtifact } from './opendesign.js';
|
|
16
17
|
|
|
17
18
|
const TERMINAL_STATUSES = new Set(['review', 'done', 'approved', 'cancelled']);
|
|
18
19
|
|
|
@@ -192,6 +193,7 @@ export class Runner {
|
|
|
192
193
|
const reply = stripHiddenContextLeak(output || '(empty response)') || '(empty response)';
|
|
193
194
|
await this.maybeAttachSpeech(topicId, reply, `chat-${topicId}`);
|
|
194
195
|
await this.wtt.publish(topicId, reply, 'CHAT_REPLY');
|
|
196
|
+
await this.maybeRenderOpenDesignArtifact(m, reply, adapter);
|
|
195
197
|
log('info', 'chat replied', { topicId, chars: reply.length });
|
|
196
198
|
} catch (err) {
|
|
197
199
|
await this.wtt.publish(topicId, `执行失败:${err.message}`, 'CHAT_REPLY');
|
|
@@ -272,6 +274,48 @@ export class Runner {
|
|
|
272
274
|
}
|
|
273
275
|
}
|
|
274
276
|
|
|
277
|
+
async maybeRenderOpenDesignArtifact(message, reply, adapter) {
|
|
278
|
+
const topicId = String(message.topic_id || '');
|
|
279
|
+
if (!topicId || !shouldRenderOpenDesignArtifact(message, reply)) return;
|
|
280
|
+
try {
|
|
281
|
+
await this.wtt.typing(topicId, 'start', {
|
|
282
|
+
statusText: 'OpenDesign 正在生成可视化 artifact',
|
|
283
|
+
statusKind: 'opendesign',
|
|
284
|
+
adapter: adapter.name,
|
|
285
|
+
ttlMs: 30000,
|
|
286
|
+
});
|
|
287
|
+
const outputDir = await prepareOpenDesignDir(this.config, topicId);
|
|
288
|
+
const prompt = buildOpenDesignPrompt({
|
|
289
|
+
outputDir,
|
|
290
|
+
userMessage: message.content || '',
|
|
291
|
+
reply,
|
|
292
|
+
topicId,
|
|
293
|
+
locale: inferLocale(`${message.content || ''}\n${reply || ''}`),
|
|
294
|
+
});
|
|
295
|
+
await adapter.run(prompt, {
|
|
296
|
+
sessionKey: `wtt:opendesign:${topicId}`,
|
|
297
|
+
topicId,
|
|
298
|
+
onProgress: (event) => this.maybePublishProgress(topicId, event, adapter.name),
|
|
299
|
+
});
|
|
300
|
+
const artifact = await this.artifacts.uploadDirectory(outputDir, {
|
|
301
|
+
source: message.topic_type || 'feed',
|
|
302
|
+
sourceId: topicId,
|
|
303
|
+
title: `OpenDesign · ${topicId.slice(0, 8)}`,
|
|
304
|
+
entry: 'index.html',
|
|
305
|
+
type: 'opendesign',
|
|
306
|
+
metadata: { message_id: message.id || message.message_id || '', generated_by: adapter.name },
|
|
307
|
+
});
|
|
308
|
+
const previewUrl = webPreviewUrl(artifact.preview_url || artifact.url);
|
|
309
|
+
if (previewUrl) {
|
|
310
|
+
const title = artifact.title || 'OpenDesign artifact';
|
|
311
|
+
await this.wtt.publish(topicId, `[opendesign:${title}](${previewUrl})`, 'TASK_ARTIFACT');
|
|
312
|
+
}
|
|
313
|
+
log('info', 'opendesign artifact completed', { topicId, preview: previewUrl });
|
|
314
|
+
} catch (err) {
|
|
315
|
+
log('warn', 'opendesign artifact failed', { topicId, error: err.message });
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
275
319
|
async materializeTaskArtifact(taskId, summary) {
|
|
276
320
|
try {
|
|
277
321
|
return await this.artifacts.uploadText(`task-${taskId}-summary.md`, summary);
|
|
@@ -322,6 +366,18 @@ function adapterDisplayName(name) {
|
|
|
322
366
|
return name || 'Agent';
|
|
323
367
|
}
|
|
324
368
|
|
|
369
|
+
function inferLocale(text) {
|
|
370
|
+
return /[\u4e00-\u9fff]/.test(String(text || '')) ? 'zh' : 'en';
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function webPreviewUrl(url) {
|
|
374
|
+
const text = String(url || '').trim();
|
|
375
|
+
if (!text) return '';
|
|
376
|
+
if (text.startsWith('/api/wtt/')) return text;
|
|
377
|
+
if (text.startsWith('/artifacts/')) return `/api/wtt${text}`;
|
|
378
|
+
return text;
|
|
379
|
+
}
|
|
380
|
+
|
|
325
381
|
function messageTopicType(m) {
|
|
326
382
|
return String(m.topic_type || metadataValue(m.metadata, 'topic_type') || metadataValue(m.metadata, 'topicType') || '').toLowerCase();
|
|
327
383
|
}
|
|
@@ -1,14 +0,0 @@
|
|
|
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
|
|
@@ -1,14 +0,0 @@
|
|
|
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
|