wtt-connect 0.2.10 → 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 +17 -10
- package/package.json +1 -1
- package/src/adapters/claude-code.js +2 -2
- package/src/main.js +91 -0
- package/src/permissions.js +2 -1
- 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,12 +21,11 @@ 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
|
|
28
27
|
- HTTP task status update path
|
|
29
|
-
- Permission broker for
|
|
28
|
+
- Permission broker for agent modes; `full-auto` is the default no-approval mode for Codex and Claude Code
|
|
30
29
|
- Optional WTT media artifact upload path (`/media/sign` → direct upload → `/media/commit`)
|
|
31
30
|
- Agent-generated file artifacts for WTT feed chat (`.docx`, `.pptx`, `.xlsx`, `.pdf`, `.csv`, `.zip`) using explicit final-response markers that are converted into WTT file cards
|
|
32
31
|
- Attachment staging for WTT message media/files; image attachments are passed to Codex with `--image` (including Claude Code image fallback when the Claude provider cannot view images directly)
|
|
@@ -90,6 +89,7 @@ Important settings:
|
|
|
90
89
|
- `WTT_CONNECT_ADAPTERS=codex,claude-code,cursor,gemini,qoder,opencode,iflow,kimi,pi,acp,devin`
|
|
91
90
|
- `WTT_CONNECT_WORKDIR`
|
|
92
91
|
- `WTT_CONNECT_MODE=full-auto|auto-edit|yolo|suggest`
|
|
92
|
+
- `full-auto` is the default no-approval mode: Codex runs with `--dangerously-bypass-approvals-and-sandbox`; Claude Code runs with `--dangerously-skip-permissions --permission-mode bypassPermissions`
|
|
93
93
|
- `WTT_CONNECT_AGENT_ALIASES=alias1,alias2` for extra @mention names in discussion/collaborative topics; `WTT_AGENT_ID` always works
|
|
94
94
|
- `WTT_CONNECT_ALLOW_YOLO=1` only when intentionally using `yolo`
|
|
95
95
|
- `WTT_CONNECT_STATE_DIR` / `WTT_CONNECT_STORE_FILE` for durable sessions
|
|
@@ -121,10 +121,19 @@ Codex, Claude Code, and other adapters can generate user-facing files on the age
|
|
|
121
121
|
Supported generated artifact types:
|
|
122
122
|
|
|
123
123
|
```text
|
|
124
|
-
.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
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
-
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:
|
|
128
137
|
|
|
129
138
|
```text
|
|
130
139
|
[WTT_ARTIFACT_FILE path="report.docx" title="Project Report"]
|
|
@@ -139,7 +148,7 @@ When an agent creates a deliverable file, it should save the file under its work
|
|
|
139
148
|
|
|
140
149
|
Notes:
|
|
141
150
|
|
|
142
|
-
- 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`.
|
|
143
152
|
- Files outside the configured workspace, state directory, inbox directory, or artifact directory are ignored to avoid accidental leakage.
|
|
144
153
|
- `WTT_CONNECT_UPLOAD_ARTIFACTS=1` still controls automatic summary/TTS uploads; it is not required for explicit generated file markers.
|
|
145
154
|
|
|
@@ -267,7 +276,7 @@ wtt-connect up codex agent-codex '***'
|
|
|
267
276
|
wtt-connect up claude-code agent-claude '***'
|
|
268
277
|
```
|
|
269
278
|
|
|
270
|
-
Use named options when you need a custom service name, permission mode, or boot-before-login behavior:
|
|
279
|
+
Use named options when you need a custom service name, a stricter permission mode, or boot-before-login behavior:
|
|
271
280
|
|
|
272
281
|
```bash
|
|
273
282
|
./scripts/install-systemd-user.sh \
|
|
@@ -275,8 +284,7 @@ Use named options when you need a custom service name, permission mode, or boot-
|
|
|
275
284
|
--agent-id agent-codex \
|
|
276
285
|
--token '***' \
|
|
277
286
|
--adapter codex \
|
|
278
|
-
--mode
|
|
279
|
-
--allow-yolo \
|
|
287
|
+
--mode full-auto \
|
|
280
288
|
--publish-progress \
|
|
281
289
|
--enable-linger
|
|
282
290
|
|
|
@@ -285,8 +293,7 @@ Use named options when you need a custom service name, permission mode, or boot-
|
|
|
285
293
|
--agent-id agent-claude \
|
|
286
294
|
--token '***' \
|
|
287
295
|
--adapter claude-code \
|
|
288
|
-
--mode
|
|
289
|
-
--allow-yolo \
|
|
296
|
+
--mode full-auto \
|
|
290
297
|
--publish-progress \
|
|
291
298
|
--enable-linger
|
|
292
299
|
```
|
package/package.json
CHANGED
|
@@ -42,7 +42,7 @@ export class ClaudeCodeAdapter {
|
|
|
42
42
|
const args = [];
|
|
43
43
|
if (sessionId) args.push('--resume', sessionId);
|
|
44
44
|
args.push('-p', prompt, '--output-format', 'stream-json', '--verbose');
|
|
45
|
-
if (this.config.mode
|
|
45
|
+
if (['full-auto', 'yolo'].includes(this.config.mode)) args.push('--dangerously-skip-permissions', '--permission-mode', 'bypassPermissions');
|
|
46
46
|
if (this.config.model) args.push('--model', this.config.model);
|
|
47
47
|
return args;
|
|
48
48
|
}
|
|
@@ -118,7 +118,7 @@ function extractSessionId(ev) {
|
|
|
118
118
|
|
|
119
119
|
async function runCodexVision(bin, prompt, images, cwd, timeoutMs, config = {}, onEvent) {
|
|
120
120
|
const args = ['exec', '--skip-git-repo-check', '--json', '--cd', cwd];
|
|
121
|
-
if (
|
|
121
|
+
if (['full-auto', 'yolo'].includes(config.mode)) args.push('--dangerously-bypass-approvals-and-sandbox');
|
|
122
122
|
for (const img of images || []) args.push('--image', img);
|
|
123
123
|
args.push('-');
|
|
124
124
|
return new Promise((resolve, reject) => {
|
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/permissions.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const MODE_TO_CODEX = {
|
|
2
2
|
suggest: [],
|
|
3
3
|
'auto-edit': ['--full-auto'],
|
|
4
|
-
'full-auto': ['--
|
|
4
|
+
'full-auto': ['--dangerously-bypass-approvals-and-sandbox'],
|
|
5
5
|
yolo: ['--dangerously-bypass-approvals-and-sandbox'],
|
|
6
6
|
};
|
|
7
7
|
|
|
@@ -23,6 +23,7 @@ export class PermissionBroker {
|
|
|
23
23
|
|
|
24
24
|
describe() {
|
|
25
25
|
if (this.config.mode === 'yolo') return 'yolo/dangerously-bypass-approvals-and-sandbox';
|
|
26
|
+
if (this.config.mode === 'full-auto') return 'full-auto/dangerously-bypass-approvals-and-sandbox';
|
|
26
27
|
return this.config.mode;
|
|
27
28
|
}
|
|
28
29
|
}
|
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
|