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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wtt-connect",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
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/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