u
This commit is contained in:
@@ -266,7 +266,6 @@ Generated from `@openapi` annotations in [`src/api/routes/`](../../src/api/route
|
|||||||
| [`routes/v1/product.md`](routes/v1/product.md) | `GET /v1/product` |
|
| [`routes/v1/product.md`](routes/v1/product.md) | `GET /v1/product` |
|
||||||
| [`routes/vfs/list.md`](routes/vfs/list.md) | `GET /vfs` |
|
| [`routes/vfs/list.md`](routes/vfs/list.md) | `GET /vfs` |
|
||||||
| [`routes/vfs/applets-restore.md`](routes/vfs/applets-restore.md) | `POST /vfs/applets/restore` |
|
| [`routes/vfs/applets-restore.md`](routes/vfs/applets-restore.md) | `POST /vfs/applets/restore` |
|
||||||
| [`routes/vfs/drafts/response.md`](routes/vfs/drafts/response.md) | `POST /vfs/drafts/response` |
|
|
||||||
| [`routes/vfs/mkdir.md`](routes/vfs/mkdir.md) | `POST /vfs/mkdir` |
|
| [`routes/vfs/mkdir.md`](routes/vfs/mkdir.md) | `POST /vfs/mkdir` |
|
||||||
| [`routes/vfs/rename.md`](routes/vfs/rename.md) | `POST /vfs/rename` |
|
| [`routes/vfs/rename.md`](routes/vfs/rename.md) | `POST /vfs/rename` |
|
||||||
| [`routes/vfs/skills-restore.md`](routes/vfs/skills-restore.md) | `POST /vfs/skills/restore` |
|
| [`routes/vfs/skills-restore.md`](routes/vfs/skills-restore.md) | `POST /vfs/skills/restore` |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Dash API Routes Overview (All)
|
# Dash API Routes Overview (All)
|
||||||
|
|
||||||
Overview of all 250 API endpoints across all modules.
|
Overview of all 249 API endpoints across all modules.
|
||||||
|
|
||||||
### Notation
|
### Notation
|
||||||
|
|
||||||
@@ -915,9 +915,6 @@ _none_
|
|||||||
#### Restore system default applets — `POST /vfs/applets/restore`
|
#### Restore system default applets — `POST /vfs/applets/restore`
|
||||||
_none_
|
_none_
|
||||||
- Returns: `200 { success: bool, restored: str[], count: int }`
|
- Returns: `200 { success: bool, restored: str[], count: int }`
|
||||||
#### Respond to a frontend draft request — `POST /vfs/drafts/response`
|
|
||||||
- Params: `body: { requestId: str, content?: any, error?: str, files?: any[] }`
|
|
||||||
- Returns: `200 { success: bool }`
|
|
||||||
#### Create directory in virtual filesystem — `POST /vfs/mkdir`
|
#### Create directory in virtual filesystem — `POST /vfs/mkdir`
|
||||||
- Params: `body: { path: str }`
|
- Params: `body: { path: str }`
|
||||||
- Returns: `200 { success: bool, dir: str }`
|
- Returns: `200 { success: bool, dir: str }`
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
# `POST /vfs/drafts/response`
|
|
||||||
|
|
||||||
Protected. Requires a valid session cookie (`credentials: 'include'`).
|
|
||||||
|
|
||||||
Respond to a frontend draft request
|
|
||||||
|
|
||||||
Used by the frontend to answer a server-initiated `draft:request` SSE event
|
|
||||||
when reading or listing virtual drafts (`drafts/quote`, `drafts/order`, `drafts/delivery`).
|
|
||||||
|
|
||||||
|
|
||||||
## Body (JSON)
|
|
||||||
|
|
||||||
| Field | Required | Type | Description |
|
|
||||||
|-------|----------|------|-------------|
|
|
||||||
| `requestId` | yes | string | Unique request ID from the draft:request SSE event |
|
|
||||||
| `content` | no | | File content when action was read |
|
|
||||||
| `error` | no | string | Error message if draft retrieval failed on frontend |
|
|
||||||
| `files` | no | any[] | File listing when action was list |
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"requestId": "…",
|
|
||||||
"content": "…",
|
|
||||||
"error": "…",
|
|
||||||
"files": []
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Response `200`
|
|
||||||
|
|
||||||
Response received and resolved
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"success": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Errors
|
|
||||||
|
|
||||||
| Status | Description |
|
|
||||||
|--------|-------------|
|
|
||||||
| 400 | Missing requestId or no company selected — `{"error":"…"}` |
|
|
||||||
| 404 | Request ID not found or already timed out — `{"error":"…"}` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
_Generated from `@openapi` JSDoc. Do not edit by hand — run `npm run docs:api`._
|
|
||||||
@@ -15,7 +15,6 @@ import {
|
|||||||
parseDraftPath,
|
parseDraftPath,
|
||||||
requestDraftRead,
|
requestDraftRead,
|
||||||
requestDraftList,
|
requestDraftList,
|
||||||
handleDraftResponse,
|
|
||||||
notifyDraftUpdate,
|
notifyDraftUpdate,
|
||||||
} from '../../services/draftManager.js';
|
} from '../../services/draftManager.js';
|
||||||
|
|
||||||
@@ -1319,8 +1318,7 @@ export function register(app, basePath = '') {
|
|||||||
res.setHeader('Content-Disposition', `inline; filename="${fallbackName}"`);
|
res.setHeader('Content-Disposition', `inline; filename="${fallbackName}"`);
|
||||||
return res.status(200).send(buffer);
|
return res.status(200).send(buffer);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const status = err.message?.includes('timed out') ? 504 : 404;
|
return res.status(404).json({ error: err.message });
|
||||||
return res.status(status).json({ error: err.message });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1399,78 +1397,7 @@ export function register(app, basePath = '') {
|
|||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|
||||||
/**
|
|
||||||
* @openapi
|
|
||||||
* /vfs/drafts/response:
|
|
||||||
* post:
|
|
||||||
* tags: [Vfs]
|
|
||||||
* summary: Respond to a frontend draft request
|
|
||||||
* description: |
|
|
||||||
* Used by the frontend to answer a server-initiated `draft:request` SSE event
|
|
||||||
* when reading or listing virtual drafts (`drafts/quote`, `drafts/order`, `drafts/delivery`).
|
|
||||||
* security:
|
|
||||||
* - cookieAuth: []
|
|
||||||
* requestBody:
|
|
||||||
* required: true
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* required: [requestId]
|
|
||||||
* properties:
|
|
||||||
* requestId:
|
|
||||||
* type: string
|
|
||||||
* description: Unique request ID from the draft:request SSE event
|
|
||||||
* content:
|
|
||||||
* description: File content when action was read
|
|
||||||
* error:
|
|
||||||
* type: string
|
|
||||||
* description: Error message if draft retrieval failed on frontend
|
|
||||||
* files:
|
|
||||||
* type: array
|
|
||||||
* description: File listing when action was list
|
|
||||||
* responses:
|
|
||||||
* 200:
|
|
||||||
* description: Response received and resolved
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* success:
|
|
||||||
* type: boolean
|
|
||||||
* 400:
|
|
||||||
* description: Missing requestId or no company selected
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* $ref: '#/components/schemas/Error'
|
|
||||||
* 404:
|
|
||||||
* description: Request ID not found or already timed out
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* $ref: '#/components/schemas/Error'
|
|
||||||
*/
|
|
||||||
// #region POST /vfs/drafts/response
|
|
||||||
|
|
||||||
app.post(`${basePath}/vfs/drafts/response`, async (req, res) => {
|
|
||||||
const selected = requireSelectedCompany(req);
|
|
||||||
if (selected.error) {
|
|
||||||
return res.status(400).json({ error: selected.error });
|
|
||||||
}
|
|
||||||
const { requestId, content, error, files } = req.body || {};
|
|
||||||
if (!requestId) {
|
|
||||||
return res.status(400).json({ error: 'requestId is required' });
|
|
||||||
}
|
|
||||||
const result = handleDraftResponse({ requestId, content, error, files });
|
|
||||||
if (!result.handled) {
|
|
||||||
return res.status(404).json({ error: result.error || 'Request expired or not found' });
|
|
||||||
}
|
|
||||||
return res.json({ success: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
// #endregion
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @openapi
|
* @openapi
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ process.on('SIGINT', () => {
|
|||||||
process.on('SIGTERM', () => {
|
process.on('SIGTERM', () => {
|
||||||
shutdown().then(() => process.exit(0));
|
shutdown().then(() => process.exit(0));
|
||||||
});
|
});
|
||||||
|
process.on('unhandledRejection', (reason) => {
|
||||||
|
sysLog.error('Unhandled rejection:', reason);
|
||||||
|
});
|
||||||
|
|
||||||
// MARK: calls startApi from ./api/index.js
|
// MARK: calls startApi from ./api/index.js
|
||||||
startApi().catch((err) => {
|
startApi().catch((err) => {
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { createApiSuggestionsOrIssuesTool } from '../tools/apisuggestionsorissue
|
|||||||
import { createFindFilesTool } from '../tools/find-files.js';
|
import { createFindFilesTool } from '../tools/find-files.js';
|
||||||
import { createReadFileTool } from '../tools/read-file.js';
|
import { createReadFileTool } from '../tools/read-file.js';
|
||||||
import { createPatchTool } from '../tools/patch.js';
|
import { createPatchTool } from '../tools/patch.js';
|
||||||
|
import { setCachedDraft, parseDraftPath } from '../services/draftManager.js';
|
||||||
import { createRmTool } from '../tools/rm.js';
|
import { createRmTool } from '../tools/rm.js';
|
||||||
import { createSearchImagesTool } from '../tools/search-images.js';
|
import { createSearchImagesTool } from '../tools/search-images.js';
|
||||||
import { createAnalyzeImageTool } from '../tools/analyze-image.js';
|
import { createAnalyzeImageTool } from '../tools/analyze-image.js';
|
||||||
@@ -501,6 +502,39 @@ export class ConversationManager {
|
|||||||
});
|
});
|
||||||
toolMsg = await db.addMessage(googleAuthId, this.conversationId, { message: payload, role: 'tool' });
|
toolMsg = await db.addMessage(googleAuthId, this.conversationId, { message: payload, role: 'tool' });
|
||||||
await db.setState(googleAuthId, this.conversationId, 'busy');
|
await db.setState(googleAuthId, this.conversationId, 'busy');
|
||||||
|
|
||||||
|
// If this tool response is answering a draft read, cache the draft
|
||||||
|
try {
|
||||||
|
let parsedContent = null;
|
||||||
|
try { parsedContent = JSON.parse(rawContent); } catch {}
|
||||||
|
if (parsedContent && (parsedContent.manifest || parsedContent.lines)) {
|
||||||
|
const messages = await db.listMessages(googleAuthId, this.conversationId);
|
||||||
|
for (let i = messages.length - 1; i >= 0; i--) {
|
||||||
|
const m = messages[i];
|
||||||
|
if (m.role === 'assistant' || m.role === 'model') {
|
||||||
|
let p = null;
|
||||||
|
try { p = typeof m.message === 'string' ? JSON.parse(m.message) : m.message; } catch {}
|
||||||
|
const tc = p?.tool_calls?.find((c) => c.id === tool_call_id);
|
||||||
|
if (tc) {
|
||||||
|
let tcArgs = {};
|
||||||
|
try { tcArgs = typeof tc.function?.arguments === 'string' ? JSON.parse(tc.function.arguments) : (tc.function?.arguments || {}); } catch {}
|
||||||
|
const draftInfo = parseDraftPath(tcArgs.filename || tcArgs.file || '');
|
||||||
|
if (draftInfo.isDraft) {
|
||||||
|
setCachedDraft({
|
||||||
|
companyId: this.companyId,
|
||||||
|
userId: googleAuthId,
|
||||||
|
name: draftInfo.name,
|
||||||
|
content: parsedContent,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('[conversationManager] could not cache draft from tool message:', err.message);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.isProcessing = false;
|
this.isProcessing = false;
|
||||||
throw err;
|
throw err;
|
||||||
@@ -739,8 +773,8 @@ export class ConversationManager {
|
|||||||
// can never end up pointed at different companies.
|
// can never end up pointed at different companies.
|
||||||
createJavascriptExecuteTool({ googleAuthId }),
|
createJavascriptExecuteTool({ googleAuthId }),
|
||||||
createFindFilesTool({ googleAuthId }),
|
createFindFilesTool({ googleAuthId }),
|
||||||
createReadFileTool({ googleAuthId }),
|
createReadFileTool({ googleAuthId, conversationId: this.conversationId }),
|
||||||
createPatchTool({ googleAuthId }),
|
createPatchTool({ googleAuthId, conversationId: this.conversationId }),
|
||||||
createRmTool({ googleAuthId }),
|
createRmTool({ googleAuthId }),
|
||||||
createSearchImagesTool({ googleAuthId }),
|
createSearchImagesTool({ googleAuthId }),
|
||||||
createAnalyzeImageTool({ googleAuthId, model }),
|
createAnalyzeImageTool({ googleAuthId, model }),
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { emit, hasActiveClient } from '../api/sse.js';
|
import { emit } from '../api/sse.js';
|
||||||
import { getStore } from '../db/store.js';
|
import { getStore } from '../db/store.js';
|
||||||
|
import { DirectReturnError } from 'libllmorbiter';
|
||||||
|
|
||||||
export const DRAFT_NAMES = new Set(['quote', 'order', 'delivery']);
|
export const DRAFT_NAMES = new Set(['quote', 'order', 'delivery']);
|
||||||
|
|
||||||
@@ -37,6 +38,27 @@ export function parseDraftPath(filePath) {
|
|||||||
return { isFolder: false, isDraft: false };
|
return { isFolder: false, isDraft: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default empty draft structure.
|
||||||
|
* @param {string} name
|
||||||
|
* @returns {object}
|
||||||
|
*/
|
||||||
|
export function defaultDraftContent(name) {
|
||||||
|
return {
|
||||||
|
manifest: {
|
||||||
|
kind: name,
|
||||||
|
mode: 'create',
|
||||||
|
id: null,
|
||||||
|
date: null,
|
||||||
|
partyId: null,
|
||||||
|
number: null,
|
||||||
|
externalNumber: null,
|
||||||
|
comment: null,
|
||||||
|
lines: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In-memory cache for drafts: key: `${companyId}:${userId}:${name}` -> { content, updatedAt, size }
|
* In-memory cache for drafts: key: `${companyId}:${userId}:${name}` -> { content, updatedAt, size }
|
||||||
*/
|
*/
|
||||||
@@ -111,200 +133,81 @@ export async function resolveCompanyForUser(googleAuthId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pending requests map: requestId -> { resolve, reject, timer, companyId, userId, action, name }
|
* Request a draft content.
|
||||||
*/
|
* Stateless model:
|
||||||
const pendingRequests = new Map();
|
* - If cached, returns immediately.
|
||||||
|
* - In conversation: emits `draft:request` with conversationId and raises DirectReturnError(..., { recordResult: false })
|
||||||
/**
|
* so the assistant tool call remains the last message in `modelmessage` until frontend responds.
|
||||||
* Request a draft content from the frontend via SSE and wait for its response.
|
* - Outside conversation: returns default empty draft envelope.
|
||||||
*
|
*
|
||||||
* @param {{ companyId?: number, userId: number, clientId?: string, name: string, timeoutMs?: number }} options
|
* @param {{ companyId?: number, userId: number, conversationId?: number, clientId?: string, name: string }} options
|
||||||
* @returns {Promise<any>}
|
* @returns {Promise<any>}
|
||||||
*/
|
*/
|
||||||
export async function requestDraftRead({ companyId, userId, clientId, name, timeoutMs = 15000 }) {
|
export async function requestDraftRead({ companyId, userId, conversationId, clientId, name }) {
|
||||||
const effectiveCompanyId = companyId != null ? companyId : await resolveCompanyForUser(userId);
|
const effectiveCompanyId = companyId != null ? companyId : await resolveCompanyForUser(userId);
|
||||||
if (effectiveCompanyId == null) {
|
if (effectiveCompanyId == null) {
|
||||||
throw new Error('No company selected');
|
throw new Error('No company selected');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if client is active; if not and we have cache, return cache immediately
|
const cached = getCachedDraft({ companyId: effectiveCompanyId, userId, name });
|
||||||
const hasClient = hasActiveClient({ companyId: effectiveCompanyId, userId, clientId });
|
if (cached) {
|
||||||
if (!hasClient) {
|
return cached.content;
|
||||||
const cached = getCachedDraft({ companyId: effectiveCompanyId, userId, name });
|
|
||||||
if (cached) {
|
|
||||||
return cached.content;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const requestId = `draft_req_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
if (conversationId) {
|
||||||
|
const requestId = `conv_${conversationId}_${name}`;
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
// Emit request over SSE with conversationId
|
||||||
const timer = setTimeout(() => {
|
|
||||||
pendingRequests.delete(requestId);
|
|
||||||
// Fallback to cache if available
|
|
||||||
const cached = getCachedDraft({ companyId: effectiveCompanyId, userId, name });
|
|
||||||
if (cached) {
|
|
||||||
return resolve(cached.content);
|
|
||||||
}
|
|
||||||
reject(new Error(`Frontend draft response timed out for "${name}"`));
|
|
||||||
}, timeoutMs);
|
|
||||||
|
|
||||||
pendingRequests.set(requestId, {
|
|
||||||
resolve,
|
|
||||||
reject,
|
|
||||||
timer,
|
|
||||||
companyId: effectiveCompanyId,
|
|
||||||
userId,
|
|
||||||
action: 'read',
|
|
||||||
name,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Emit request over SSE
|
|
||||||
emit('draft:request', {
|
emit('draft:request', {
|
||||||
companyId: effectiveCompanyId,
|
companyId: effectiveCompanyId,
|
||||||
targetUserId: userId,
|
targetUserId: userId,
|
||||||
targetClientId: clientId,
|
targetClientId: clientId,
|
||||||
data: {
|
data: {
|
||||||
requestId,
|
requestId,
|
||||||
|
conversationId,
|
||||||
action: 'read',
|
action: 'read',
|
||||||
name,
|
name,
|
||||||
target: name,
|
target: name,
|
||||||
sender: clientId || '',
|
sender: clientId || '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
// Halt turn statelessly; assistant tool call stays as the last message in modelmessage
|
||||||
|
throw new DirectReturnError({
|
||||||
|
action: 'draft_read',
|
||||||
|
draft: name,
|
||||||
|
conversationId,
|
||||||
|
}, { recordResult: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultDraftContent(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request draft listing from the frontend via SSE.
|
* Request draft listing for VFS.
|
||||||
* If frontend doesn't respond or times out, returns default metadata for all 3 drafts.
|
* Returns immediately from cache or default empty draft metadata. Completely stateless without timers or promises.
|
||||||
*
|
*
|
||||||
* @param {{ companyId?: number, userId: number, clientId?: string, timeoutMs?: number }} options
|
* @param {{ companyId?: number, userId: number }} options
|
||||||
* @returns {Promise<Array<object>>}
|
* @returns {Array<object>}
|
||||||
*/
|
*/
|
||||||
export async function requestDraftList({ companyId, userId, clientId, timeoutMs = 2500 }) {
|
export function requestDraftList({ companyId, userId }) {
|
||||||
const effectiveCompanyId = companyId != null ? companyId : await resolveCompanyForUser(userId);
|
const now = new Date().toISOString();
|
||||||
if (effectiveCompanyId == null) {
|
return Array.from(DRAFT_NAMES).map((name) => {
|
||||||
return [];
|
const cached = companyId ? getCachedDraft({ companyId, userId, name }) : null;
|
||||||
}
|
return {
|
||||||
|
name: `drafts/${name}`,
|
||||||
const defaultDrafts = () => {
|
size: cached ? cached.size : 0,
|
||||||
const now = new Date().toISOString();
|
mime: 'application/json; charset=utf-8',
|
||||||
return Array.from(DRAFT_NAMES).map((name) => {
|
updated_at: cached ? cached.updatedAt : now,
|
||||||
const cached = getCachedDraft({ companyId: effectiveCompanyId, userId, name });
|
created_at: cached ? cached.updatedAt : now,
|
||||||
return {
|
isText: true,
|
||||||
name: `drafts/${name}`,
|
isImage: false,
|
||||||
size: cached ? cached.size : 0,
|
isPdf: false,
|
||||||
mime: 'application/json; charset=utf-8',
|
isDraft: true,
|
||||||
updated_at: cached ? cached.updatedAt : now,
|
};
|
||||||
created_at: cached ? cached.updatedAt : now,
|
|
||||||
isText: true,
|
|
||||||
isImage: false,
|
|
||||||
isPdf: false,
|
|
||||||
isDraft: true,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// If no client is connected, return defaults immediately without delaying listing
|
|
||||||
const hasClient = hasActiveClient({ companyId: effectiveCompanyId, userId, clientId });
|
|
||||||
if (!hasClient) {
|
|
||||||
return defaultDrafts();
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestId = `draft_list_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
pendingRequests.delete(requestId);
|
|
||||||
resolve(defaultDrafts());
|
|
||||||
}, timeoutMs);
|
|
||||||
|
|
||||||
pendingRequests.set(requestId, {
|
|
||||||
resolve: (filesOrList) => {
|
|
||||||
if (Array.isArray(filesOrList)) {
|
|
||||||
const now = new Date().toISOString();
|
|
||||||
const normalized = filesOrList.map((item) => {
|
|
||||||
const rawName = typeof item === 'string' ? item : (item.name || item.filename || '');
|
|
||||||
const parsed = parseDraftPath(rawName.startsWith('drafts/') ? rawName : `drafts/${rawName}`);
|
|
||||||
const nameKey = parsed.name || rawName;
|
|
||||||
const cached = getCachedDraft({ companyId: effectiveCompanyId, userId, name: nameKey });
|
|
||||||
return {
|
|
||||||
name: `drafts/${nameKey}`,
|
|
||||||
size: typeof item === 'object' && item.size != null ? item.size : (cached ? cached.size : 0),
|
|
||||||
mime: 'application/json; charset=utf-8',
|
|
||||||
updated_at: typeof item === 'object' && item.updated_at ? item.updated_at : (cached ? cached.updatedAt : now),
|
|
||||||
created_at: typeof item === 'object' && item.created_at ? item.created_at : (cached ? cached.updatedAt : now),
|
|
||||||
isText: true,
|
|
||||||
isImage: false,
|
|
||||||
isPdf: false,
|
|
||||||
isDraft: true,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
resolve(normalized);
|
|
||||||
} else {
|
|
||||||
resolve(defaultDrafts());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
reject: () => {
|
|
||||||
resolve(defaultDrafts());
|
|
||||||
},
|
|
||||||
timer,
|
|
||||||
companyId: effectiveCompanyId,
|
|
||||||
userId,
|
|
||||||
action: 'list',
|
|
||||||
});
|
|
||||||
|
|
||||||
emit('draft:request', {
|
|
||||||
companyId: effectiveCompanyId,
|
|
||||||
targetUserId: userId,
|
|
||||||
targetClientId: clientId,
|
|
||||||
data: {
|
|
||||||
requestId,
|
|
||||||
action: 'list',
|
|
||||||
sender: clientId || '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle frontend response to a previous draft:request.
|
|
||||||
*
|
|
||||||
* @param {{ requestId: string, content?: any, error?: string, files?: any[] }} payload
|
|
||||||
* @returns {{ handled: boolean, error?: string }}
|
|
||||||
*/
|
|
||||||
export function handleDraftResponse({ requestId, content, error, files }) {
|
|
||||||
if (!requestId || !pendingRequests.has(requestId)) {
|
|
||||||
return { handled: false, error: 'Unknown or expired requestId' };
|
|
||||||
}
|
|
||||||
|
|
||||||
const pending = pendingRequests.get(requestId);
|
|
||||||
pendingRequests.delete(requestId);
|
|
||||||
clearTimeout(pending.timer);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
pending.reject(new Error(error));
|
|
||||||
return { handled: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pending.action === 'list') {
|
|
||||||
pending.resolve(files !== undefined ? files : content);
|
|
||||||
return { handled: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
// action === 'read'
|
|
||||||
const normalized = normalizeDraftContent(content);
|
|
||||||
setCachedDraft({
|
|
||||||
companyId: pending.companyId,
|
|
||||||
userId: pending.userId,
|
|
||||||
name: pending.name,
|
|
||||||
content: normalized,
|
|
||||||
});
|
|
||||||
pending.resolve(normalized);
|
|
||||||
return { handled: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notify frontend about a draft update via the normal /events SSE endpoint.
|
* Notify frontend about a draft update via the normal /events SSE endpoint.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { defineTool, ToolError } from 'libllmorbiter';
|
import { defineTool, ToolError, DirectReturnError } from 'libllmorbiter';
|
||||||
import { isReadOnlyDatabasePath } from './vfs.js';
|
import { isReadOnlyDatabasePath } from './vfs.js';
|
||||||
import { isSkillPath, renderSkill, parseSkill, SKILL_EXT, skillFileName } from './skill.js';
|
import { isSkillPath, renderSkill, parseSkill, SKILL_EXT, skillFileName } from './skill.js';
|
||||||
import { prepareVfs, syncVfsToDb } from './vfs.js';
|
import { prepareVfs, syncVfsToDb } from './vfs.js';
|
||||||
@@ -64,9 +64,9 @@ function toLines(text) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the patch tool bound to a company's virtual filesystem.
|
* Creates the patch tool bound to a company's virtual filesystem.
|
||||||
* @param {{ googleAuthId: number }} ctx
|
* @param {{ googleAuthId: number, conversationId?: number }} ctx
|
||||||
*/
|
*/
|
||||||
export function createPatchTool({ googleAuthId }) {
|
export function createPatchTool({ googleAuthId, conversationId }) {
|
||||||
return defineTool({
|
return defineTool({
|
||||||
name: 'patch',
|
name: 'patch',
|
||||||
description: TOOL_DESCRIPTIONS.patch,
|
description: TOOL_DESCRIPTIONS.patch,
|
||||||
@@ -197,9 +197,10 @@ export function createPatchTool({ googleAuthId }) {
|
|||||||
let original = '';
|
let original = '';
|
||||||
if (!isCreate) {
|
if (!isCreate) {
|
||||||
try {
|
try {
|
||||||
const readVal = await requestDraftRead({ userId: googleAuthId, name: draftInfo.name });
|
const readVal = await requestDraftRead({ userId: googleAuthId, conversationId, name: draftInfo.name });
|
||||||
original = typeof readVal === 'string' ? readVal : JSON.stringify(readVal ?? {}, null, 2);
|
original = typeof readVal === 'string' ? readVal : JSON.stringify(readVal ?? {}, null, 2);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (err instanceof DirectReturnError) throw err;
|
||||||
const cached = getCachedDraft({ userId: googleAuthId, name: draftInfo.name });
|
const cached = getCachedDraft({ userId: googleAuthId, name: draftInfo.name });
|
||||||
if (cached) {
|
if (cached) {
|
||||||
original = typeof cached.content === 'string' ? cached.content : JSON.stringify(cached.content ?? {}, null, 2);
|
original = typeof cached.content === 'string' ? cached.content : JSON.stringify(cached.content ?? {}, null, 2);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { defineTool, ToolError } from 'libllmorbiter';
|
import { defineTool, ToolError, DirectReturnError } from 'libllmorbiter';
|
||||||
import { isSkillPath, parseSkill, SKILL_EXT } from './skill.js';
|
import { isSkillPath, parseSkill, SKILL_EXT } from './skill.js';
|
||||||
import { getApiRouteSkill } from './routes-skills.js';
|
import { getApiRouteSkill } from './routes-skills.js';
|
||||||
import { prepareVfs } from './vfs.js';
|
import { prepareVfs } from './vfs.js';
|
||||||
@@ -58,9 +58,9 @@ function looksBinary(buf) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the read_file tool bound to a company's virtual filesystem.
|
* Creates the read_file tool bound to a company's virtual filesystem.
|
||||||
* @param {{ googleAuthId: number }} ctx
|
* @param {{ googleAuthId: number, conversationId?: number }} ctx
|
||||||
*/
|
*/
|
||||||
export function createReadFileTool({ googleAuthId }) {
|
export function createReadFileTool({ googleAuthId, conversationId }) {
|
||||||
return defineTool({
|
return defineTool({
|
||||||
name: 'read_file',
|
name: 'read_file',
|
||||||
description: TOOL_DESCRIPTIONS.read_file,
|
description: TOOL_DESCRIPTIONS.read_file,
|
||||||
@@ -120,11 +120,13 @@ export function createReadFileTool({ googleAuthId }) {
|
|||||||
try {
|
try {
|
||||||
const content = await requestDraftRead({
|
const content = await requestDraftRead({
|
||||||
userId: googleAuthId,
|
userId: googleAuthId,
|
||||||
|
conversationId,
|
||||||
name: draftInfo.name,
|
name: draftInfo.name,
|
||||||
});
|
});
|
||||||
const str = typeof content === 'string' ? content : JSON.stringify(content ?? {}, null, 2);
|
const str = typeof content === 'string' ? content : JSON.stringify(content ?? {}, null, 2);
|
||||||
raw = Buffer.from(str, 'utf8');
|
raw = Buffer.from(str, 'utf8');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (err instanceof DirectReturnError) throw err;
|
||||||
throw new ToolError(`Failed to read draft "${draftInfo.name}": ${err.message}`);
|
throw new ToolError(`Failed to read draft "${draftInfo.name}": ${err.message}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user