@yeaft/webchat-agent 1.0.154 → 1.0.156
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
package/workbench/file-ops.js
CHANGED
|
@@ -103,7 +103,7 @@ export async function handleWriteFile(msg) {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
export async function handleListDirectory(msg) {
|
|
106
|
-
const { conversationId, dirPath, _requestUserId, _requestClientId } = msg;
|
|
106
|
+
const { conversationId, requestId, dirPath, _requestUserId, _requestClientId } = msg;
|
|
107
107
|
const conv = ctx.conversations.get(conversationId);
|
|
108
108
|
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
109
109
|
|
|
@@ -121,6 +121,7 @@ export async function handleListDirectory(msg) {
|
|
|
121
121
|
ctx.sendToServer({
|
|
122
122
|
type: 'directory_listing',
|
|
123
123
|
conversationId,
|
|
124
|
+
requestId,
|
|
124
125
|
_requestUserId,
|
|
125
126
|
_requestClientId,
|
|
126
127
|
dirPath: '',
|
|
@@ -137,6 +138,7 @@ export async function handleListDirectory(msg) {
|
|
|
137
138
|
ctx.sendToServer({
|
|
138
139
|
type: 'directory_listing',
|
|
139
140
|
conversationId,
|
|
141
|
+
requestId,
|
|
140
142
|
_requestUserId,
|
|
141
143
|
_requestClientId,
|
|
142
144
|
dirPath: '/',
|
|
@@ -147,6 +149,7 @@ export async function handleListDirectory(msg) {
|
|
|
147
149
|
ctx.sendToServer({
|
|
148
150
|
type: 'directory_listing',
|
|
149
151
|
conversationId,
|
|
152
|
+
requestId,
|
|
150
153
|
_requestUserId,
|
|
151
154
|
_requestClientId,
|
|
152
155
|
dirPath: '',
|
|
@@ -194,6 +197,7 @@ export async function handleListDirectory(msg) {
|
|
|
194
197
|
ctx.sendToServer({
|
|
195
198
|
type: 'directory_listing',
|
|
196
199
|
conversationId,
|
|
200
|
+
requestId,
|
|
197
201
|
_requestUserId,
|
|
198
202
|
_requestClientId,
|
|
199
203
|
dirPath: resolved,
|
|
@@ -203,6 +207,7 @@ export async function handleListDirectory(msg) {
|
|
|
203
207
|
ctx.sendToServer({
|
|
204
208
|
type: 'directory_listing',
|
|
205
209
|
conversationId,
|
|
210
|
+
requestId,
|
|
206
211
|
_requestUserId,
|
|
207
212
|
_requestClientId,
|
|
208
213
|
dirPath: dirPath || workDir,
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
readFileSync,
|
|
10
10
|
realpathSync,
|
|
11
11
|
rmSync,
|
|
12
|
+
unlinkSync,
|
|
12
13
|
writeFileSync,
|
|
13
14
|
} from 'node:fs';
|
|
14
15
|
import { createHash, randomUUID } from 'node:crypto';
|
|
@@ -175,6 +176,43 @@ function removeCreatedDirectory(state) {
|
|
|
175
176
|
}
|
|
176
177
|
}
|
|
177
178
|
|
|
179
|
+
function openAttachmentDirectory(root, workItemId) {
|
|
180
|
+
if (process.platform !== 'linux') {
|
|
181
|
+
throw new Error('Secure WorkItem attachment access requires Linux');
|
|
182
|
+
}
|
|
183
|
+
const { attachmentRoot, itemDirectory } = attachmentDirectory(root, workItemId);
|
|
184
|
+
const rootDescriptor = openDirectory(attachmentRoot, 'WorkItem attachment root');
|
|
185
|
+
try {
|
|
186
|
+
const ownerName = safeWorkItemId(workItemId);
|
|
187
|
+
const itemDescriptor = openDirectory(
|
|
188
|
+
`/proc/self/fd/${rootDescriptor}/${ownerName}`,
|
|
189
|
+
'WorkItem attachment owner directory',
|
|
190
|
+
);
|
|
191
|
+
try {
|
|
192
|
+
assertDescriptorMatchesPath(rootDescriptor, attachmentRoot, 'WorkItem attachment root');
|
|
193
|
+
assertDescriptorMatchesPath(itemDescriptor, itemDirectory, 'WorkItem attachment owner directory');
|
|
194
|
+
return { attachmentRoot, itemDirectory, ownerName, rootDescriptor, itemDescriptor };
|
|
195
|
+
} catch (error) {
|
|
196
|
+
closeSync(itemDescriptor);
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
} catch (error) {
|
|
200
|
+
closeSync(rootDescriptor);
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function removeAttachmentFile(directoryState, storageName) {
|
|
206
|
+
if (!/^[A-Za-z0-9_-]+(?:\.[a-z0-9]{1,10})?$/.test(storageName)) return;
|
|
207
|
+
assertDescriptorMatchesPath(directoryState.rootDescriptor, directoryState.attachmentRoot, 'WorkItem attachment root');
|
|
208
|
+
assertDescriptorMatchesPath(directoryState.itemDescriptor, directoryState.itemDirectory, 'WorkItem attachment owner directory');
|
|
209
|
+
try {
|
|
210
|
+
unlinkSync(`/proc/self/fd/${directoryState.itemDescriptor}/${storageName}`);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
178
216
|
export function persistWorkItemAttachments(files, options = {}) {
|
|
179
217
|
if (!Array.isArray(files) || files.length === 0) return [];
|
|
180
218
|
if (files.length > MAX_WORK_ITEM_ATTACHMENTS) {
|
|
@@ -226,6 +264,74 @@ export function persistWorkItemAttachments(files, options = {}) {
|
|
|
226
264
|
}
|
|
227
265
|
}
|
|
228
266
|
|
|
267
|
+
export function appendWorkItemAttachments(existing, files, options = {}) {
|
|
268
|
+
if (!Array.isArray(files) || files.length === 0) return [];
|
|
269
|
+
const current = Array.isArray(existing) ? existing : [];
|
|
270
|
+
if (current.length === 0) return persistWorkItemAttachments(files, options);
|
|
271
|
+
if (current.length + files.length > MAX_WORK_ITEM_ATTACHMENTS) {
|
|
272
|
+
throw new Error(`WorkItem supports at most ${MAX_WORK_ITEM_ATTACHMENTS} attachments`);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
let totalBytes = current.reduce((total, attachment) => total + (Number(attachment?.size) || 0), 0);
|
|
276
|
+
const prepared = files.map((file, index) => {
|
|
277
|
+
if (!file || typeof file !== 'object' || Array.isArray(file)) {
|
|
278
|
+
throw new Error(`Attachment ${index + 1} is invalid`);
|
|
279
|
+
}
|
|
280
|
+
const name = safeDisplayName(file.name, current.length + index);
|
|
281
|
+
const mimeType = typeof file.mimeType === 'string' && file.mimeType.trim()
|
|
282
|
+
? file.mimeType.trim().slice(0, 255)
|
|
283
|
+
: 'application/octet-stream';
|
|
284
|
+
const kind = assertSupportedWorkItemAttachment(name, mimeType);
|
|
285
|
+
const buffer = decodeBase64(file.data);
|
|
286
|
+
assertWorkItemAttachmentSize(buffer.length);
|
|
287
|
+
totalBytes += buffer.length;
|
|
288
|
+
if (totalBytes > MAX_WORK_ITEM_ATTACHMENT_BYTES) {
|
|
289
|
+
throw new Error(`WorkItem attachments exceed ${MAX_WORK_ITEM_ATTACHMENT_BYTES} bytes`);
|
|
290
|
+
}
|
|
291
|
+
const id = randomUUID();
|
|
292
|
+
return {
|
|
293
|
+
buffer,
|
|
294
|
+
attachment: {
|
|
295
|
+
id,
|
|
296
|
+
name,
|
|
297
|
+
storageName: `${id}${safeExtension(name)}`,
|
|
298
|
+
mimeType,
|
|
299
|
+
size: buffer.length,
|
|
300
|
+
sha256: digest(buffer),
|
|
301
|
+
kind,
|
|
302
|
+
isImage: kind === 'image',
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const directoryState = openAttachmentDirectory(options.root, options.workItemId);
|
|
308
|
+
const written = [];
|
|
309
|
+
try {
|
|
310
|
+
for (const entry of prepared) {
|
|
311
|
+
writeAttachmentFile(directoryState, entry.attachment.storageName, entry.buffer);
|
|
312
|
+
written.push(entry.attachment);
|
|
313
|
+
}
|
|
314
|
+
return written;
|
|
315
|
+
} catch (error) {
|
|
316
|
+
for (const attachment of written) {
|
|
317
|
+
try { removeAttachmentFile(directoryState, attachment.storageName); } catch {}
|
|
318
|
+
}
|
|
319
|
+
throw error;
|
|
320
|
+
} finally {
|
|
321
|
+
closeDirectoryState(directoryState);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function removeWorkItemAttachmentFiles(root, workItemId, attachments) {
|
|
326
|
+
if (!Array.isArray(attachments) || attachments.length === 0) return;
|
|
327
|
+
const directoryState = openAttachmentDirectory(root, workItemId);
|
|
328
|
+
try {
|
|
329
|
+
for (const attachment of attachments) removeAttachmentFile(directoryState, attachment?.storageName);
|
|
330
|
+
} finally {
|
|
331
|
+
closeDirectoryState(directoryState);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
229
335
|
export function removeWorkItemAttachments(root, workItemId, options = {}) {
|
|
230
336
|
if (!root || !workItemId) return;
|
|
231
337
|
if (process.platform !== 'linux') {
|
|
@@ -286,6 +392,26 @@ function resolveAttachmentPath(root, workItemId, attachment) {
|
|
|
286
392
|
return { filePath: actualPath, size: stat.size, itemDirectory: itemRoot };
|
|
287
393
|
}
|
|
288
394
|
|
|
395
|
+
export function readWorkItemAttachment(workItem, attachmentId, options = {}) {
|
|
396
|
+
const attachment = Array.isArray(workItem?.attachments)
|
|
397
|
+
? workItem.attachments.find(item => item?.id === attachmentId)
|
|
398
|
+
: null;
|
|
399
|
+
if (!attachment) throw new Error('WorkItem attachment not found');
|
|
400
|
+
const resolved = resolveAttachmentPath(options.root, workItem.id, attachment);
|
|
401
|
+
const buffer = readFileSync(resolved.filePath);
|
|
402
|
+
if (resolved.size !== Number(attachment.size) || digest(buffer) !== attachment.sha256) {
|
|
403
|
+
throw new Error(`WorkItem attachment changed after creation: ${attachment.name || attachment.id}`);
|
|
404
|
+
}
|
|
405
|
+
return {
|
|
406
|
+
id: attachment.id,
|
|
407
|
+
name: attachment.name,
|
|
408
|
+
mimeType: attachment.mimeType,
|
|
409
|
+
size: resolved.size,
|
|
410
|
+
isImage: attachment.isImage === true,
|
|
411
|
+
data: buffer.toString('base64'),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
289
415
|
function escapePromptText(value) {
|
|
290
416
|
return String(value || '')
|
|
291
417
|
.replace(/&/g, '&')
|
|
@@ -20,21 +20,18 @@ let serviceFactory = null;
|
|
|
20
20
|
const BROWSER_DETAIL_OPS = new Set(['get', 'create', 'update', 'start', 'cancel', 'guide', 'retry']);
|
|
21
21
|
// `files` is an internal server-to-Agent field. The browser relay rejects any
|
|
22
22
|
// client-supplied value and only emits files resolved from owned upload ids.
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
'
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
'start',
|
|
33
|
-
]);
|
|
34
|
-
|
|
35
|
-
function browserCreatePayload(value) {
|
|
23
|
+
const BROWSER_FILE_FIELDS = Object.freeze({
|
|
24
|
+
create: [
|
|
25
|
+
'title', 'goal', 'acceptanceCriteria', 'workDir', 'reuseMemory', 'origin',
|
|
26
|
+
'linkedSessionIds', 'files', 'start',
|
|
27
|
+
],
|
|
28
|
+
guide: ['id', 'guidance', 'actionId', 'revision', 'files'],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
function browserFilePayload(op, value) {
|
|
36
32
|
const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
37
|
-
|
|
33
|
+
const fields = BROWSER_FILE_FIELDS[op] || [];
|
|
34
|
+
return Object.fromEntries(fields
|
|
38
35
|
.filter(field => Object.prototype.hasOwnProperty.call(source, field))
|
|
39
36
|
.map(field => [field, source[field]]));
|
|
40
37
|
}
|
|
@@ -176,10 +173,10 @@ export async function handleWorkCenterRequest(msg) {
|
|
|
176
173
|
data = await readSettingsResponse();
|
|
177
174
|
} else {
|
|
178
175
|
const workCenter = await ensureWorkCenter();
|
|
179
|
-
|
|
180
|
-
op,
|
|
181
|
-
|
|
182
|
-
);
|
|
176
|
+
const payload = Object.hasOwn(BROWSER_FILE_FIELDS, op)
|
|
177
|
+
? browserFilePayload(op, msg.payload)
|
|
178
|
+
: (msg.payload || {});
|
|
179
|
+
data = await workCenter.handle(op, payload);
|
|
183
180
|
}
|
|
184
181
|
if (BROWSER_DETAIL_OPS.has(op)) data = projectWorkItemDetail(data);
|
|
185
182
|
send({
|
|
@@ -172,7 +172,9 @@ export class WorkflowController {
|
|
|
172
172
|
|
|
173
173
|
guide(id, input = {}) {
|
|
174
174
|
const guidance = typeof input.guidance === 'string' ? input.guidance.trim().slice(0, 8_000) : '';
|
|
175
|
-
|
|
175
|
+
const addedAttachmentCount = Math.max(0, Number(input.addedAttachmentCount) || 0);
|
|
176
|
+
if (!guidance && addedAttachmentCount === 0) throw new Error('guidance or attachments are required');
|
|
177
|
+
const guidanceSummary = guidance || `The user added ${addedAttachmentCount} attachment(s) as additional context for this Action.`;
|
|
176
178
|
const expected = {
|
|
177
179
|
actionId: typeof input.actionId === 'string' ? input.actionId : '',
|
|
178
180
|
revision: Number(input.revision),
|
|
@@ -180,11 +182,11 @@ export class WorkflowController {
|
|
|
180
182
|
if (!expected.actionId || !Number.isInteger(expected.revision)) {
|
|
181
183
|
throw new Error('actionId and revision are required for guidance');
|
|
182
184
|
}
|
|
183
|
-
const detail = this.store.addActionGuidance(id,
|
|
185
|
+
const detail = this.store.addActionGuidance(id, guidanceSummary, expected, (workItem, previous) => {
|
|
184
186
|
const context = [...(previous.context || []), {
|
|
185
187
|
type: 'guidance',
|
|
186
188
|
role: 'user',
|
|
187
|
-
summary:
|
|
189
|
+
summary: guidanceSummary,
|
|
188
190
|
evidence: [],
|
|
189
191
|
}];
|
|
190
192
|
const step = {
|
|
@@ -200,7 +202,7 @@ export class WorkflowController {
|
|
|
200
202
|
instruction: actionInstruction(step, workItem, context),
|
|
201
203
|
maxAttempts: previous.maxAttempts || 2,
|
|
202
204
|
};
|
|
203
|
-
});
|
|
205
|
+
}, input.attachments);
|
|
204
206
|
if (!detail) throw new Error(`WorkItem not found: ${id}`);
|
|
205
207
|
return detail;
|
|
206
208
|
}
|
|
@@ -5,7 +5,10 @@ import { WorkItemStore } from './store.js';
|
|
|
5
5
|
import { WorkflowController } from './controller.js';
|
|
6
6
|
import { WorkItemWatcher } from './watcher.js';
|
|
7
7
|
import {
|
|
8
|
+
appendWorkItemAttachments,
|
|
8
9
|
persistWorkItemAttachments,
|
|
10
|
+
readWorkItemAttachment,
|
|
11
|
+
removeWorkItemAttachmentFiles,
|
|
9
12
|
removeWorkItemAttachments,
|
|
10
13
|
} from './attachments.js';
|
|
11
14
|
import { projectWorkItemDetail, projectWorkItemSummary } from './projection.js';
|
|
@@ -157,15 +160,57 @@ export class WorkCenterService {
|
|
|
157
160
|
}
|
|
158
161
|
case 'guide': {
|
|
159
162
|
const id = requiredString(payload.id, 'id');
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
const workItem = this.#requiredItem(id);
|
|
164
|
+
let addedAttachments = [];
|
|
165
|
+
let detail;
|
|
166
|
+
try {
|
|
167
|
+
addedAttachments = appendWorkItemAttachments(workItem.attachments, payload.files, {
|
|
168
|
+
root: this.attachmentRoot,
|
|
169
|
+
workItemId: id,
|
|
170
|
+
});
|
|
171
|
+
detail = this.controller.guide(id, {
|
|
172
|
+
guidance: typeof payload.guidance === 'string' ? payload.guidance : '',
|
|
173
|
+
actionId: typeof payload.actionId === 'string' ? payload.actionId : '',
|
|
174
|
+
revision: payload.revision,
|
|
175
|
+
addedAttachmentCount: addedAttachments.length,
|
|
176
|
+
attachments: [...(workItem.attachments || []), ...addedAttachments],
|
|
177
|
+
});
|
|
178
|
+
} catch (error) {
|
|
179
|
+
try {
|
|
180
|
+
if ((workItem.attachments || []).length === 0 && addedAttachments.length > 0) {
|
|
181
|
+
removeWorkItemAttachments(this.attachmentRoot, id);
|
|
182
|
+
} else {
|
|
183
|
+
removeWorkItemAttachmentFiles(this.attachmentRoot, id, addedAttachments);
|
|
184
|
+
}
|
|
185
|
+
} catch {}
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
165
188
|
this.watcher.abortInvalidWorkItemRuns(id);
|
|
166
189
|
this.#emit({ type: 'action.guidance_added', workItem: detail });
|
|
167
190
|
return detail;
|
|
168
191
|
}
|
|
192
|
+
case 'preview_attachment': {
|
|
193
|
+
const id = requiredString(payload.id, 'id');
|
|
194
|
+
const attachment = readWorkItemAttachment(
|
|
195
|
+
this.#requiredItem(id),
|
|
196
|
+
requiredString(payload.attachmentId, 'attachmentId'),
|
|
197
|
+
{ root: this.attachmentRoot },
|
|
198
|
+
);
|
|
199
|
+
return {
|
|
200
|
+
attachment: {
|
|
201
|
+
id: attachment.id,
|
|
202
|
+
name: attachment.name,
|
|
203
|
+
mimeType: attachment.mimeType,
|
|
204
|
+
size: attachment.size,
|
|
205
|
+
isImage: attachment.isImage,
|
|
206
|
+
},
|
|
207
|
+
previewData: {
|
|
208
|
+
data: attachment.data,
|
|
209
|
+
mimeType: attachment.mimeType,
|
|
210
|
+
filename: attachment.name,
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
169
214
|
case 'retry': {
|
|
170
215
|
const detail = this.controller.retry(requiredString(payload.id, 'id'), {
|
|
171
216
|
answer: typeof payload.answer === 'string' ? payload.answer : '',
|
|
@@ -536,7 +536,7 @@ export class WorkItemStore {
|
|
|
536
536
|
});
|
|
537
537
|
}
|
|
538
538
|
|
|
539
|
-
addActionGuidance(id, guidance, expected, makeAction) {
|
|
539
|
+
addActionGuidance(id, guidance, expected, makeAction, attachments = null) {
|
|
540
540
|
return withTransaction(this.db, () => {
|
|
541
541
|
const workItem = this.getWorkItem(id);
|
|
542
542
|
if (!workItem) return null;
|
|
@@ -563,7 +563,12 @@ export class WorkItemStore {
|
|
|
563
563
|
contractRevision: workItem.revision,
|
|
564
564
|
}, this.#nextSequence(id), now);
|
|
565
565
|
this.db.prepare(`UPDATE work_items SET status = 'ready', current_action_id = ?,
|
|
566
|
-
current_run_id = NULL, updated_at = ? WHERE id = ?`).run(
|
|
566
|
+
current_run_id = NULL, attachments = ?, updated_at = ? WHERE id = ?`).run(
|
|
567
|
+
action.id,
|
|
568
|
+
stringify(Array.isArray(attachments) ? attachments : workItem.attachments),
|
|
569
|
+
now,
|
|
570
|
+
id,
|
|
571
|
+
);
|
|
567
572
|
this.appendEvent(id, 'action.guidance_added', { guidance }, { actionId: action.id });
|
|
568
573
|
return this.getWorkItemDetail(id);
|
|
569
574
|
});
|