wtt-connect 0.2.5 → 0.2.7
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 +30 -0
- package/package.json +1 -1
- package/src/artifacts.js +2 -2
- package/src/mime.js +5 -1
- package/src/opendesign.js +23 -5
- package/src/runner.js +149 -2
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ Implemented production-oriented surfaces:
|
|
|
28
28
|
- HTTP task status update path
|
|
29
29
|
- Permission broker for Codex modes, including explicit opt-in for dangerous `yolo`
|
|
30
30
|
- Optional WTT media artifact upload path (`/media/sign` → direct upload → `/media/commit`)
|
|
31
|
+
- 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
|
|
31
32
|
- 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)
|
|
32
33
|
- STT extension point (`command` and OpenAI/Whisper providers) for audio attachments
|
|
33
34
|
- Setup/claim-code command, smoke-task/smoke-chat commands, start script, and macOS launchd installer
|
|
@@ -106,6 +107,35 @@ A JSON config may also be supplied:
|
|
|
106
107
|
node ./bin/wtt-connect.js start --config ./config.json
|
|
107
108
|
```
|
|
108
109
|
|
|
110
|
+
### Generated files in feed chat
|
|
111
|
+
|
|
112
|
+
Codex, Claude Code, and other adapters can generate user-facing files on the agent host and send them back to WTT feed chat as clickable file cards.
|
|
113
|
+
|
|
114
|
+
Supported generated artifact types:
|
|
115
|
+
|
|
116
|
+
```text
|
|
117
|
+
.doc .docx .ppt .pptx .xls .xlsx .pdf .csv .md .txt .zip
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
When an agent creates a deliverable file, it should save the file under its workspace or artifact directory and include one final-response marker per file:
|
|
121
|
+
|
|
122
|
+
```text
|
|
123
|
+
[WTT_ARTIFACT_FILE path="report.docx" title="Project Report"]
|
|
124
|
+
[WTT_ARTIFACT_FILE path="/absolute/workspace/slides.pptx" title="Review Slides"]
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`wtt-connect` removes those marker lines from the visible reply, uploads each file through WTT media, and publishes file cards like:
|
|
128
|
+
|
|
129
|
+
```text
|
|
130
|
+
[file:Project Report](https://...)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Notes:
|
|
134
|
+
|
|
135
|
+
- Generated file markers are explicit user-facing uploads; they are uploaded even when `WTT_CONNECT_UPLOAD_ARTIFACTS=0`.
|
|
136
|
+
- Files outside the configured workspace, state directory, inbox directory, or artifact directory are ignored to avoid accidental leakage.
|
|
137
|
+
- `WTT_CONNECT_UPLOAD_ARTIFACTS=1` still controls automatic summary/TTS uploads; it is not required for explicit generated file markers.
|
|
138
|
+
|
|
109
139
|
### One-command service binding
|
|
110
140
|
|
|
111
141
|
The recommended install path for users is npm + `up`:
|
package/package.json
CHANGED
package/src/artifacts.js
CHANGED
|
@@ -17,8 +17,8 @@ export class ArtifactManager {
|
|
|
17
17
|
return file;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
async uploadFile(filePath) {
|
|
21
|
-
if (!this.config.uploadArtifacts) return null;
|
|
20
|
+
async uploadFile(filePath, options = {}) {
|
|
21
|
+
if (!this.config.uploadArtifacts && !options.force) return null;
|
|
22
22
|
const stat = await fs.stat(filePath);
|
|
23
23
|
if (!stat.isFile()) return null;
|
|
24
24
|
const mimeType = lookupMime(filePath);
|
package/src/mime.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
const MAP = new Map([
|
|
2
2
|
['.txt', 'text/plain'], ['.md', 'text/markdown'], ['.json', 'application/json'],
|
|
3
|
-
['.html', 'text/html'], ['.pdf', 'application/pdf'], ['.zip', 'application/zip'],
|
|
3
|
+
['.html', 'text/html'], ['.htm', 'text/html'], ['.pdf', 'application/pdf'], ['.zip', 'application/zip'],
|
|
4
|
+
['.csv', 'text/csv'], ['.xml', 'text/xml'], ['.yml', 'application/x-yaml'], ['.yaml', 'application/x-yaml'],
|
|
5
|
+
['.doc', 'application/msword'], ['.docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
|
|
6
|
+
['.ppt', 'application/vnd.ms-powerpoint'], ['.pptx', 'application/vnd.openxmlformats-officedocument.presentationml.presentation'],
|
|
7
|
+
['.xls', 'application/vnd.ms-excel'], ['.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
|
|
4
8
|
['.png', 'image/png'], ['.jpg', 'image/jpeg'], ['.jpeg', 'image/jpeg'],
|
|
5
9
|
['.gif', 'image/gif'], ['.webp', 'image/webp'], ['.mp3', 'audio/mpeg'],
|
|
6
10
|
['.wav', 'audio/wav'], ['.ogg', 'audio/ogg'], ['.webm', 'audio/webm'], ['.mp4', 'video/mp4'], ['.mov', 'video/quicktime'],
|
package/src/opendesign.js
CHANGED
|
@@ -55,6 +55,8 @@ export async function buildOpenDesignPrompt({ config, outputDir, userMessage, re
|
|
|
55
55
|
'- Do not use external CDNs, remote images, external fonts, forms, cookies, localStorage, or network calls.',
|
|
56
56
|
'- Use inline SVG or local CSS/JS only.',
|
|
57
57
|
'- Treat this as a premium visual explainer, not a documentation page. The first viewport must look intentionally designed.',
|
|
58
|
+
'- Never rely on browser default fonts or default black SVG fills. Every SVG shape must intentionally set fill/stroke from named CSS variables.',
|
|
59
|
+
'- Do not produce black-on-white blob diagrams. Large black/dark fills, default #000 text-only SVGs, and unstyled Times/Arial pages are failed artifacts.',
|
|
58
60
|
'- For formula/principle explanations include: a main diagram, step animation, symbol/metric table, minimal concrete example, explanatory text beside visuals, and final compact summary.',
|
|
59
61
|
'- For UI/prototype requests include realistic fake data, hover states, and meaningful state transitions.',
|
|
60
62
|
'',
|
|
@@ -132,7 +134,22 @@ export async function evaluateOpenDesignArtifact(dir) {
|
|
|
132
134
|
if (/data-step|step-|timeline|progress|播放|replay|next/i.test(combined)) score += 1;
|
|
133
135
|
else issues.push('missing step-by-step interaction/state');
|
|
134
136
|
|
|
135
|
-
|
|
137
|
+
const lower = combined.toLowerCase();
|
|
138
|
+
const blackishTokens = (lower.match(/(?:#[0-2]{3,6}\b|rgb\(\s*(?:0|1?\d|2\d|3\d)\s*,\s*(?:0|1?\d|2\d|3\d)\s*,\s*(?:0|1?\d|2\d|3\d)\s*\)|fill=["'](?:black|#000|#111|#222)|background(?:-color)?:\s*(?:#000|#111|#121212|black))/g) || []).length;
|
|
139
|
+
const colorTokens = new Set((lower.match(/#[0-9a-f]{3,8}\b|oklch\([^)]+\)|hsl\([^)]+\)|rgb\([^)]+\)/g) || [])
|
|
140
|
+
.filter((token) => !/^#(?:000|111|222|333|fff|ffffff)\b/.test(token)));
|
|
141
|
+
if (blackishTokens <= 8 && colorTokens.size >= 8) score += 1;
|
|
142
|
+
else issues.push('visual palette is too black/default or lacks enough intentional color tokens; avoid black blob diagrams');
|
|
143
|
+
|
|
144
|
+
const fontFamilies = (combined.match(/font-family\s*:\s*([^;}]+)/gi) || []).join('\n').toLowerCase();
|
|
145
|
+
if (fontFamilies && !/inter|roboto|arial,\s*sans-serif|system-ui,\s*sans-serif/.test(fontFamilies)) score += 1;
|
|
146
|
+
else issues.push('typography is too default; use a distinctive local font stack, not Inter/Roboto/Arial/system-only');
|
|
147
|
+
|
|
148
|
+
const svgFills = (combined.match(/fill=["'][^"']+["']|fill:\s*[^;}]+/gi) || []).join('\n').toLowerCase();
|
|
149
|
+
if (svgFills && !/fill=["'](?:black|#000|#111|#222)["']/.test(svgFills)) score += 1;
|
|
150
|
+
else issues.push('SVG appears to rely on default/black fills; explicitly use colored fills and strokes for nodes, arrows, and highlights');
|
|
151
|
+
|
|
152
|
+
return { ok: score >= 8, score, issues };
|
|
136
153
|
}
|
|
137
154
|
|
|
138
155
|
async function loadSkillBlocks(config, skillNames) {
|
|
@@ -165,11 +182,12 @@ function visualQualityContract(zh) {
|
|
|
165
182
|
'- Canvas: design for a 1440x900 whiteboard/preview area, responsive down to tablet. Use full-width composition, not a narrow article.',
|
|
166
183
|
'- Visual hierarchy: strong title block, one dominant SVG/canvas diagram, one compact example panel, one formula/symbol table, one step timeline, and a final takeaway strip.',
|
|
167
184
|
'- Default aesthetic when the user did not request a theme: light editorial research board, warm off-white paper, precise ink lines, subtle grid/noise, restrained color accents, refined spacing. It should feel closer to a polished team workspace/design note than a dark coding console.',
|
|
168
|
-
'- Aesthetic: choose a committed direction such as editorial lab notebook, precision engineering console, warm classroom
|
|
169
|
-
'- Typography:
|
|
170
|
-
'- Color: define CSS variables in :root. Use a restrained but intentional palette with textured background, grid/noise/paper effect, or layered panels. Avoid flat dark gray, neon cyan buttons, purple gradients, and default dark-mode dashboards unless the content explicitly requires them.',
|
|
185
|
+
'- Aesthetic: choose a committed direction such as editorial lab notebook, precision engineering console, warm classroom board, industrial systems map, or high-contrast research poster. Avoid generic SaaS cards.',
|
|
186
|
+
'- Typography: define a non-default font system in :root/body. Recommended local stacks: "Avenir Next", "Helvetica Neue", sans-serif for UI; Georgia, "Times New Roman", serif for editorial formulas; "SFMono-Regular", "Cascadia Code", monospace for data. Do not use plain Inter/Roboto/Arial/default browser font stacks.',
|
|
187
|
+
'- Color: define CSS variables in :root and use at least 8 named color tokens. Use a restrained but intentional palette with textured background, grid/noise/paper effect, or layered panels. Avoid flat dark gray, neon cyan buttons, purple gradients, and default dark-mode dashboards unless the content explicitly requires them.',
|
|
188
|
+
'- No black blob rule: SVG rectangles, circles, arrows, and paths must not default to black. Use colored fills like parchment, blue, amber, green, coral, slate ink, and colored strokes. Large dark regions must be rare accents, never the dominant diagram body.',
|
|
171
189
|
'- Polish: use fine borders, soft shadows, intentional whitespace, aligned baselines, and varied panel sizes. Do not make every block the same rounded rectangle.',
|
|
172
|
-
'- SVG: include at least one large inline SVG with labeled nodes, arrows, and a visible animated path. Arrow animation must be obvious: stroke-dasharray/stroke-dashoffset or moving marker/dot.',
|
|
190
|
+
'- SVG: include at least one large inline SVG with labeled nodes, arrows, and a visible animated path. Every shape must have explicit fill/stroke. Arrow animation must be obvious: stroke-dasharray/stroke-dashoffset or moving marker/dot, and final arrow/path must remain visible after animation ends.',
|
|
173
191
|
'- Motion: include 2-4 purposeful animations: entry reveal, arrow/path draw, active step highlight, formula/example transition. Respect prefers-reduced-motion.',
|
|
174
192
|
'- Explanation: every diagram node must have adjacent text that explains why it matters. Do not rely on labels alone.',
|
|
175
193
|
'- Example: include a minimal concrete example with inputs, intermediate transformation, and output/result.',
|
package/src/runner.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { WTTClient } from './wtt-client.js';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
+
import fsp from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
3
5
|
import { WTTApi } from './wtt-api.js';
|
|
4
6
|
import { SessionManager } from './session-manager.js';
|
|
5
7
|
import { TTSManager } from './tts.js';
|
|
@@ -17,6 +19,8 @@ import { TerminalSessionManager } from './terminal-session.js';
|
|
|
17
19
|
import { buildOpenDesignPrompt, buildOpenDesignRepairPrompt, chooseOpenDesignSkills, evaluateOpenDesignArtifact, prepareOpenDesignDir, shouldRenderOpenDesignArtifact } from './opendesign.js';
|
|
18
20
|
|
|
19
21
|
const TERMINAL_STATUSES = new Set(['review', 'done', 'approved', 'cancelled']);
|
|
22
|
+
const GENERATED_FILE_EXTENSIONS = new Set(['.doc', '.docx', '.ppt', '.pptx', '.xls', '.xlsx', '.pdf', '.csv', '.md', '.txt', '.zip']);
|
|
23
|
+
const AUTO_DETECTED_FILE_EXTENSIONS = '(?:docx?|pptx?|xlsx?|pdf|csv|zip)';
|
|
20
24
|
|
|
21
25
|
export class Runner {
|
|
22
26
|
constructor(config) {
|
|
@@ -183,6 +187,7 @@ export class Runner {
|
|
|
183
187
|
'',
|
|
184
188
|
staged.promptBlock,
|
|
185
189
|
renderTranscriptBlock(transcripts),
|
|
190
|
+
renderGeneratedFileArtifactInstruction(this.config),
|
|
186
191
|
'Reply naturally and concisely unless the user asks for detail.',
|
|
187
192
|
].filter(Boolean).join('\n');
|
|
188
193
|
const output = await adapter.run(prompt, {
|
|
@@ -192,9 +197,11 @@ export class Runner {
|
|
|
192
197
|
images: staged.images,
|
|
193
198
|
onProgress: (event) => this.maybePublishProgress(topicId, event, adapter.name),
|
|
194
199
|
});
|
|
195
|
-
const
|
|
200
|
+
const prepared = prepareGeneratedFileArtifacts(stripHiddenContextLeak(output || '(empty response)') || '(empty response)');
|
|
201
|
+
const reply = prepared.text || '(empty response)';
|
|
196
202
|
await this.maybeAttachSpeech(topicId, reply, `chat-${topicId}`);
|
|
197
203
|
await this.wtt.publish(topicId, reply, 'CHAT_REPLY');
|
|
204
|
+
await this.publishGeneratedFileArtifacts(topicId, prepared.refs, { source: 'chat', sourceId: topicId, adapter: adapter.name });
|
|
198
205
|
await this.maybeRenderOpenDesignArtifact(m, reply, adapter);
|
|
199
206
|
log('info', 'chat replied', { topicId, chars: reply.length });
|
|
200
207
|
} catch (err) {
|
|
@@ -226,7 +233,8 @@ export class Runner {
|
|
|
226
233
|
images: staged.images,
|
|
227
234
|
onProgress: (ev) => topicId ? this.maybePublishProgress(topicId, ev, adapter.name) : Promise.resolve(),
|
|
228
235
|
});
|
|
229
|
-
const
|
|
236
|
+
const prepared = prepareGeneratedFileArtifacts(output || '(empty response)');
|
|
237
|
+
const summary = prepared.text || '(empty response)';
|
|
230
238
|
await this.maybeRelayArenaOpenCLResult(task, summary);
|
|
231
239
|
const artifact = await this.materializeTaskArtifact(taskId, summary);
|
|
232
240
|
const commitIds = extractCommitIds(summary);
|
|
@@ -235,6 +243,7 @@ export class Runner {
|
|
|
235
243
|
await this.api.patchTask(taskId, patch);
|
|
236
244
|
if (topicId) {
|
|
237
245
|
await this.wtt.publish(topicId, summary, 'TASK_SUMMARY');
|
|
246
|
+
await this.publishGeneratedFileArtifacts(topicId, prepared.refs, { source: 'task', sourceId: taskId, adapter: adapter.name });
|
|
238
247
|
if (artifact?.asset?.url) await this.wtt.publish(topicId, `Artifact: ${artifact.asset.url}`, 'TASK_ARTIFACT');
|
|
239
248
|
}
|
|
240
249
|
log('info', 'task completed', { taskId, chars: summary.length });
|
|
@@ -339,6 +348,49 @@ export class Runner {
|
|
|
339
348
|
}
|
|
340
349
|
}
|
|
341
350
|
|
|
351
|
+
async publishGeneratedFileArtifacts(topicId, refs, context = {}) {
|
|
352
|
+
if (!topicId || !Array.isArray(refs) || !refs.length) return [];
|
|
353
|
+
const uploaded = [];
|
|
354
|
+
const seen = new Set();
|
|
355
|
+
for (const ref of refs) {
|
|
356
|
+
const resolved = resolveGeneratedFilePath(ref.path, this.config);
|
|
357
|
+
if (!resolved || seen.has(resolved)) continue;
|
|
358
|
+
seen.add(resolved);
|
|
359
|
+
try {
|
|
360
|
+
const stat = await fsp.stat(resolved);
|
|
361
|
+
if (!stat.isFile()) continue;
|
|
362
|
+
await this.wtt.typing(topicId, 'start', {
|
|
363
|
+
statusText: `Agent 正在上传生成文件:${path.basename(resolved)}`,
|
|
364
|
+
statusKind: 'artifact_upload',
|
|
365
|
+
adapter: context.adapter,
|
|
366
|
+
ttlMs: 60000,
|
|
367
|
+
});
|
|
368
|
+
const asset = await this.artifacts.uploadFile(resolved, { force: true });
|
|
369
|
+
const url = asset?.url || asset?.public_url;
|
|
370
|
+
if (!url) continue;
|
|
371
|
+
const fileName = ref.title || path.basename(resolved);
|
|
372
|
+
await this.wtt.publish(
|
|
373
|
+
topicId,
|
|
374
|
+
`[file:${fileName}](${url})`,
|
|
375
|
+
'TASK_ARTIFACT',
|
|
376
|
+
{
|
|
377
|
+
type: 'generated_file',
|
|
378
|
+
source: context.source || 'agent',
|
|
379
|
+
source_id: context.sourceId || '',
|
|
380
|
+
file_name: fileName,
|
|
381
|
+
local_path: resolved,
|
|
382
|
+
mime_type: asset.mime_type || '',
|
|
383
|
+
size: asset.size || stat.size,
|
|
384
|
+
},
|
|
385
|
+
);
|
|
386
|
+
uploaded.push({ path: resolved, asset });
|
|
387
|
+
} catch (err) {
|
|
388
|
+
log('warn', 'generated file artifact upload failed', { topicId, file: ref.path, error: err.message });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return uploaded;
|
|
392
|
+
}
|
|
393
|
+
|
|
342
394
|
async materializeTaskArtifact(taskId, summary) {
|
|
343
395
|
try {
|
|
344
396
|
return await this.artifacts.uploadText(`task-${taskId}-summary.md`, summary);
|
|
@@ -401,6 +453,100 @@ function webPreviewUrl(url) {
|
|
|
401
453
|
return text;
|
|
402
454
|
}
|
|
403
455
|
|
|
456
|
+
function renderGeneratedFileArtifactInstruction(config) {
|
|
457
|
+
return [
|
|
458
|
+
'Generated file artifact rule, for internal execution only:',
|
|
459
|
+
`- If you create a user-facing file (.docx, .pptx, .xlsx, .pdf, .csv, .zip), save it under the workspace or artifact directory: ${config.artifactDir}.`,
|
|
460
|
+
'- 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"].',
|
|
461
|
+
'- Do not use this marker for source code files, temp files, private logs, credentials, or internal scratch files.',
|
|
462
|
+
'- wtt-connect will remove the marker from the visible chat and publish a WTT file card.',
|
|
463
|
+
].join('\n');
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function prepareGeneratedFileArtifacts(text) {
|
|
467
|
+
const input = String(text || '');
|
|
468
|
+
const refs = [];
|
|
469
|
+
let cleaned = input.replace(/^\s*\[WTT_ARTIFACT_FILE\s+([^\]]+)\]\s*$/gim, (_all, attrs) => {
|
|
470
|
+
const parsed = parseArtifactMarkerAttrs(attrs);
|
|
471
|
+
if (parsed.path) refs.push(parsed);
|
|
472
|
+
return '';
|
|
473
|
+
});
|
|
474
|
+
cleaned = cleaned.replace(/^\s*WTT_ARTIFACT_FILE\s*:\s*(.+?)\s*$/gim, (_all, body) => {
|
|
475
|
+
const parsed = parseArtifactLine(body);
|
|
476
|
+
if (parsed.path) refs.push(parsed);
|
|
477
|
+
return '';
|
|
478
|
+
});
|
|
479
|
+
for (const ref of extractLocalFilePathRefs(cleaned)) refs.push(ref);
|
|
480
|
+
return { text: cleaned.replace(/\n{3,}/g, '\n\n').trim(), refs: dedupeArtifactRefs(refs) };
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function parseArtifactMarkerAttrs(attrs) {
|
|
484
|
+
const out = {};
|
|
485
|
+
for (const m of String(attrs || '').matchAll(/([a-zA-Z_][\w-]*)\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s]+))/g)) {
|
|
486
|
+
out[m[1]] = m[2] || m[3] || m[4] || '';
|
|
487
|
+
}
|
|
488
|
+
return { path: String(out.path || out.file || '').trim(), title: String(out.title || out.name || '').trim() };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function parseArtifactLine(body) {
|
|
492
|
+
const text = String(body || '').trim();
|
|
493
|
+
const parts = text.split(/\s+\|\s+/, 2);
|
|
494
|
+
return { path: stripPathPunctuation(parts[0] || ''), title: String(parts[1] || '').trim() };
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function extractLocalFilePathRefs(text) {
|
|
498
|
+
const refs = [];
|
|
499
|
+
const body = String(text || '');
|
|
500
|
+
const ext = AUTO_DETECTED_FILE_EXTENSIONS;
|
|
501
|
+
const pathLike = new RegExp(`(?:^|[\\s"'\\\`((])((?:~\\/|\\.{1,2}\\/|\\/)[^\\s"'\\\`<>]+?\\.${ext})(?=$|[\\s"'\\\`)).,;,。;])`, 'gi');
|
|
502
|
+
for (const m of body.matchAll(pathLike)) refs.push({ path: stripPathPunctuation(m[1]), title: '' });
|
|
503
|
+
const basename = new RegExp(`\\b([A-Za-z0-9][A-Za-z0-9._-]{0,160}\\.${ext})\\b`, 'gi');
|
|
504
|
+
for (const m of body.matchAll(basename)) refs.push({ path: stripPathPunctuation(m[1]), title: '' });
|
|
505
|
+
return refs;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function stripPathPunctuation(value) {
|
|
509
|
+
return String(value || '').trim().replace(/^["'`]+|["'`.,;:,。;:))\]]+$/g, '');
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function dedupeArtifactRefs(refs) {
|
|
513
|
+
const out = [];
|
|
514
|
+
const seen = new Set();
|
|
515
|
+
for (const ref of refs) {
|
|
516
|
+
const filePath = stripPathPunctuation(ref?.path || '');
|
|
517
|
+
if (!filePath) continue;
|
|
518
|
+
const key = filePath.toLowerCase();
|
|
519
|
+
if (seen.has(key)) continue;
|
|
520
|
+
seen.add(key);
|
|
521
|
+
out.push({ path: filePath, title: String(ref?.title || '').trim() });
|
|
522
|
+
}
|
|
523
|
+
return out;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function resolveGeneratedFilePath(filePath, config) {
|
|
527
|
+
const raw = stripPathPunctuation(filePath);
|
|
528
|
+
if (!raw || /^https?:\/\//i.test(raw)) return null;
|
|
529
|
+
const ext = path.extname(raw).toLowerCase();
|
|
530
|
+
if (!GENERATED_FILE_EXTENSIONS.has(ext)) return null;
|
|
531
|
+
|
|
532
|
+
const expanded = raw.startsWith('~/') ? path.join(process.env.HOME || '', raw.slice(2)) : raw;
|
|
533
|
+
const base = path.resolve(config.workDir || process.cwd());
|
|
534
|
+
const resolved = path.resolve(path.isAbsolute(expanded) ? expanded : path.join(base, expanded));
|
|
535
|
+
const roots = [
|
|
536
|
+
config.workDir,
|
|
537
|
+
config.artifactDir,
|
|
538
|
+
config.storeFile ? path.dirname(config.storeFile) : '',
|
|
539
|
+
config.inboxDir,
|
|
540
|
+
].filter(Boolean).map((root) => path.resolve(root));
|
|
541
|
+
if (!roots.some((root) => isPathInside(resolved, root))) return null;
|
|
542
|
+
return resolved;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function isPathInside(candidate, root) {
|
|
546
|
+
const rel = path.relative(root, candidate);
|
|
547
|
+
return rel === '' || (rel && !rel.startsWith('..') && !path.isAbsolute(rel));
|
|
548
|
+
}
|
|
549
|
+
|
|
404
550
|
function messageTopicType(m) {
|
|
405
551
|
return String(m.topic_type || metadataValue(m.metadata, 'topic_type') || metadataValue(m.metadata, 'topicType') || '').toLowerCase();
|
|
406
552
|
}
|
|
@@ -696,6 +842,7 @@ function buildTaskPrompt(task, config, staged = { promptBlock: '' }, transcripts
|
|
|
696
842
|
renderTranscriptBlock(transcripts),
|
|
697
843
|
'',
|
|
698
844
|
config.requireCommitPush ? 'For code changes, commit and push before final response, and include commit id.' : '',
|
|
845
|
+
renderGeneratedFileArtifactInstruction(config),
|
|
699
846
|
'Return a concise final summary with evidence, changed files, tests, artifacts, and blockers.',
|
|
700
847
|
].filter(Boolean).join('\n');
|
|
701
848
|
}
|