fluxy-bot 0.3.20 → 0.3.22
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/dist-fluxy/assets/{fluxy-sbmFOV3Z.js → fluxy-D_vy8fea.js} +26 -26
- package/dist-fluxy/fluxy.html +1 -1
- package/package.json +1 -1
- package/shared/paths.ts +5 -4
- package/supervisor/chat/ARCHITECTURE.md +89 -0
- package/supervisor/chat/fluxy-main.tsx +19 -1
- package/supervisor/chat/src/components/Chat/InputBar.tsx +14 -8
- package/supervisor/file-saver.ts +50 -0
- package/supervisor/fluxy-agent.ts +19 -3
- package/supervisor/index.ts +42 -4
- package/worker/index.ts +2 -3
- package/workspace/client/public/fluxy-icon-192.png +0 -0
- package/workspace/client/public/fluxy-icon-512.png +0 -0
- package/workspace/client/public/sw.js +1 -0
- package/worker/file-storage.ts +0 -25
package/dist-fluxy/fluxy.html
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content" />
|
|
6
6
|
<title>Fluxy Chat</title>
|
|
7
|
-
<script type="module" crossorigin src="/fluxy/assets/fluxy-
|
|
7
|
+
<script type="module" crossorigin src="/fluxy/assets/fluxy-D_vy8fea.js"></script>
|
|
8
8
|
<link rel="modulepreload" crossorigin href="/fluxy/assets/globals-BmcQJ_1f.js">
|
|
9
9
|
<link rel="stylesheet" crossorigin href="/fluxy/assets/globals-BdY9BJIP.css">
|
|
10
10
|
</head>
|
package/package.json
CHANGED
package/shared/paths.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from 'url';
|
|
|
4
4
|
|
|
5
5
|
export const PKG_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
6
6
|
export const DATA_DIR = path.join(os.homedir(), '.fluxy');
|
|
7
|
+
export const WORKSPACE_DIR = path.join(PKG_DIR, 'workspace');
|
|
7
8
|
|
|
8
9
|
const cfName = process.platform === 'win32' ? 'cloudflared.exe' : 'cloudflared';
|
|
9
10
|
|
|
@@ -12,8 +13,8 @@ export const paths = {
|
|
|
12
13
|
db: path.join(DATA_DIR, 'memory.db'),
|
|
13
14
|
widgetJs: path.join(PKG_DIR, 'supervisor', 'widget.js'),
|
|
14
15
|
cloudflared: path.join(DATA_DIR, 'bin', cfName),
|
|
15
|
-
files: path.join(
|
|
16
|
-
filesAudio: path.join(
|
|
17
|
-
filesImages: path.join(
|
|
18
|
-
filesDocuments: path.join(
|
|
16
|
+
files: path.join(WORKSPACE_DIR, 'files'),
|
|
17
|
+
filesAudio: path.join(WORKSPACE_DIR, 'files', 'audio'),
|
|
18
|
+
filesImages: path.join(WORKSPACE_DIR, 'files', 'images'),
|
|
19
|
+
filesDocuments: path.join(WORKSPACE_DIR, 'files', 'documents'),
|
|
19
20
|
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# Fluxy Chat Architecture
|
|
2
|
+
|
|
3
|
+
## Network Topology
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
Browser
|
|
7
|
+
|
|
|
8
|
+
| https://bot-name.at.fluxy.bot
|
|
9
|
+
v
|
|
10
|
+
Fluxy Relay (cloud)
|
|
11
|
+
|
|
|
12
|
+
| Cloudflare Quick Tunnel
|
|
13
|
+
v
|
|
14
|
+
Supervisor (localhost:3000)
|
|
15
|
+
|
|
|
16
|
+
├── Worker (localhost:3001) — DB, settings, AI providers
|
|
17
|
+
├── Dashboard Vite (localhost:3002) — dev server for workspace/client
|
|
18
|
+
└── Backend (localhost:3004) — user's app backend
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## How the Chat Loads
|
|
22
|
+
|
|
23
|
+
The dashboard (`workspace/client/`) loads at the root URL. It injects `widget.js`, which creates:
|
|
24
|
+
|
|
25
|
+
1. A floating bubble (the Fluxy avatar)
|
|
26
|
+
2. A slide-out panel containing an **iframe** at `/fluxy/`
|
|
27
|
+
|
|
28
|
+
The iframe loads `dist-fluxy/fluxy.html`, which is the pre-built Fluxy chat SPA. This SPA is completely independent from the dashboard — it has its own React tree, its own styles, and its own WebSocket connection.
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
Dashboard (main page)
|
|
32
|
+
└── widget.js
|
|
33
|
+
├── Bubble (click to open)
|
|
34
|
+
└── Panel
|
|
35
|
+
└── <iframe src="/fluxy/"> ← Fluxy chat lives here
|
|
36
|
+
├── fluxy-main.tsx
|
|
37
|
+
├── InputBar, MessageList, etc.
|
|
38
|
+
└── OnboardWizard (opened from settings menu)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## The Relay Problem
|
|
42
|
+
|
|
43
|
+
When a user registers a handle (e.g. `bug.at.fluxy.bot`), the relay proxies all HTTP traffic through Cloudflare's tunnel to the local supervisor. This works transparently for:
|
|
44
|
+
|
|
45
|
+
- **GET requests** — settings, onboard status, static assets
|
|
46
|
+
- **WebSocket** — chat messages, heartbeats, real-time events
|
|
47
|
+
|
|
48
|
+
However, **POST requests from the chat iframe fail** (502 / timeout). The relay + tunnel chain cannot reliably forward HTTP POST bodies originating from an iframe context. The exact cause is in the relay/tunnel infrastructure — the request never reaches the supervisor.
|
|
49
|
+
|
|
50
|
+
This does NOT affect:
|
|
51
|
+
- The initial onboard (`/fluxy/onboard.html` in a separate iframe, runs before the relay is configured)
|
|
52
|
+
- Direct `localhost` access
|
|
53
|
+
- GET requests from the chat iframe
|
|
54
|
+
- WebSocket messages from the chat iframe
|
|
55
|
+
|
|
56
|
+
## The Fix: WebSocket as a Sidecar Channel
|
|
57
|
+
|
|
58
|
+
Since the WebSocket connection works reliably from the chat iframe, any mutation that the chat needs to perform (saving settings, etc.) is sent over WebSocket instead of HTTP POST.
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
Chat Wizard Supervisor WS Handler Worker
|
|
62
|
+
| | |
|
|
63
|
+
|-- ws: settings:save --------->| |
|
|
64
|
+
| |-- POST /api/onboard ----->|
|
|
65
|
+
| | (localhost, no relay) |
|
|
66
|
+
| |<-- { ok: true } ---------|
|
|
67
|
+
|<-- ws: settings:saved --------| |
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The supervisor's WebSocket handler receives the `settings:save` message and makes a **local** HTTP POST to the worker (`localhost:3001`). This completely bypasses the relay.
|
|
71
|
+
|
|
72
|
+
### When to use WebSocket vs HTTP
|
|
73
|
+
|
|
74
|
+
| From where | GET | POST/PUT | Notes |
|
|
75
|
+
|---|---|---|---|
|
|
76
|
+
| Initial onboard | `fetch()` | `fetch()` | Runs before relay exists, direct tunnel works |
|
|
77
|
+
| Chat iframe | `fetch()` / `authFetch()` | **WebSocket** | POST through relay fails |
|
|
78
|
+
| Dashboard | `fetch()` | `fetch()` | Not in an iframe, relay handles it fine |
|
|
79
|
+
|
|
80
|
+
## Key Files
|
|
81
|
+
|
|
82
|
+
| File | Purpose |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `supervisor/widget.js` | Creates the chat iframe + bubble on the dashboard |
|
|
85
|
+
| `supervisor/chat/fluxy-main.tsx` | Chat app entry point, WS connection, wizard integration |
|
|
86
|
+
| `supervisor/chat/OnboardWizard.tsx` | Setup wizard (uses `onSave` prop for WS saves) |
|
|
87
|
+
| `supervisor/chat/src/lib/ws-client.ts` | WebSocket client with reconnect + message queue |
|
|
88
|
+
| `supervisor/index.ts` | Supervisor HTTP server + WS handler (`settings:save`) |
|
|
89
|
+
| `worker/index.ts` | Worker API (`POST /api/onboard`, settings DB) |
|
|
@@ -246,7 +246,25 @@ function FluxyApp() {
|
|
|
246
246
|
{/* Chat body */}
|
|
247
247
|
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
|
248
248
|
<MessageList messages={messages} streaming={streaming} streamBuffer={streamBuffer} tools={tools} />
|
|
249
|
-
<InputBar
|
|
249
|
+
<InputBar
|
|
250
|
+
onSend={sendMessage}
|
|
251
|
+
onStop={stopStreaming}
|
|
252
|
+
streaming={streaming}
|
|
253
|
+
whisperEnabled={whisperEnabled}
|
|
254
|
+
onTranscribe={(audio) => {
|
|
255
|
+
return new Promise((resolve, reject) => {
|
|
256
|
+
const client = clientRef.current;
|
|
257
|
+
if (!client?.connected) { reject(new Error('WebSocket not connected')); return; }
|
|
258
|
+
const unsub = client.on('whisper:result', (data) => {
|
|
259
|
+
unsub();
|
|
260
|
+
clearTimeout(timer);
|
|
261
|
+
resolve(data);
|
|
262
|
+
});
|
|
263
|
+
const timer = setTimeout(() => { unsub(); reject(new Error('Transcription timeout')); }, 30000);
|
|
264
|
+
client.send('whisper:transcribe', { audio });
|
|
265
|
+
});
|
|
266
|
+
}}
|
|
267
|
+
/>
|
|
250
268
|
</div>
|
|
251
269
|
|
|
252
270
|
{/* Setup Wizard overlay */}
|
|
@@ -8,6 +8,7 @@ interface Props {
|
|
|
8
8
|
onStop: () => void;
|
|
9
9
|
streaming: boolean;
|
|
10
10
|
whisperEnabled?: boolean;
|
|
11
|
+
onTranscribe?: (audio: string) => Promise<{ transcript?: string }>;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
function formatTime(s: number) {
|
|
@@ -53,7 +54,7 @@ function compressImage(dataUrl: string, maxBytes = 4 * 1024 * 1024): Promise<str
|
|
|
53
54
|
|
|
54
55
|
const DRAFT_KEY = 'fluxy_draft';
|
|
55
56
|
|
|
56
|
-
export default function InputBar({ onSend, onStop, streaming, whisperEnabled }: Props) {
|
|
57
|
+
export default function InputBar({ onSend, onStop, streaming, whisperEnabled, onTranscribe }: Props) {
|
|
57
58
|
const [text, setText] = useState(() => {
|
|
58
59
|
try { return localStorage.getItem(DRAFT_KEY) || ''; } catch { return ''; }
|
|
59
60
|
});
|
|
@@ -136,12 +137,17 @@ export default function InputBar({ onSend, onStop, streaming, whisperEnabled }:
|
|
|
136
137
|
if (!base64) return;
|
|
137
138
|
|
|
138
139
|
try {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
140
|
+
let data: { transcript?: string };
|
|
141
|
+
if (onTranscribe) {
|
|
142
|
+
data = await onTranscribe(base64);
|
|
143
|
+
} else {
|
|
144
|
+
const res = await fetch('/api/whisper/transcribe', {
|
|
145
|
+
method: 'POST',
|
|
146
|
+
headers: { 'Content-Type': 'application/json' },
|
|
147
|
+
body: JSON.stringify({ audio: base64 }),
|
|
148
|
+
});
|
|
149
|
+
data = await res.json();
|
|
150
|
+
}
|
|
145
151
|
if (data.transcript?.trim()) {
|
|
146
152
|
onSend(data.transcript.trim(), undefined, dataUrl);
|
|
147
153
|
}
|
|
@@ -158,7 +164,7 @@ export default function InputBar({ onSend, onStop, streaming, whisperEnabled }:
|
|
|
158
164
|
setIsRecording(false);
|
|
159
165
|
setRecordingTime(0);
|
|
160
166
|
dragRef.current = 0;
|
|
161
|
-
}, [onSend]);
|
|
167
|
+
}, [onSend, onTranscribe]);
|
|
162
168
|
|
|
163
169
|
// ── File handling ──
|
|
164
170
|
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import { paths } from '../shared/paths.js';
|
|
4
|
+
|
|
5
|
+
export interface SavedFile {
|
|
6
|
+
type: 'image' | 'document';
|
|
7
|
+
name: string;
|
|
8
|
+
mediaType: string;
|
|
9
|
+
relPath: string;
|
|
10
|
+
absPath: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ensureFileDirs(): void {
|
|
14
|
+
fs.mkdirSync(paths.filesAudio, { recursive: true });
|
|
15
|
+
fs.mkdirSync(paths.filesImages, { recursive: true });
|
|
16
|
+
fs.mkdirSync(paths.filesDocuments, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function saveAttachment(att: { type: 'image' | 'file'; name: string; mediaType: string; data: string }): SavedFile {
|
|
20
|
+
const category = att.type === 'image' ? 'images' : 'documents';
|
|
21
|
+
|
|
22
|
+
const now = new Date();
|
|
23
|
+
const ts = now.toISOString().replace(/[-:T]/g, '').slice(0, 14);
|
|
24
|
+
const stamp = `${ts.slice(0, 8)}_${ts.slice(8, 14)}`;
|
|
25
|
+
const rand = crypto.randomBytes(3).toString('hex');
|
|
26
|
+
|
|
27
|
+
// Extract extension from original name or mediaType
|
|
28
|
+
const extFromName = att.name?.includes('.') ? att.name.split('.').pop()! : '';
|
|
29
|
+
const extFromMime: Record<string, string> = {
|
|
30
|
+
'image/png': 'png', 'image/jpeg': 'jpg', 'image/gif': 'gif', 'image/webp': 'webp',
|
|
31
|
+
'application/pdf': 'pdf', 'text/plain': 'txt', 'text/csv': 'csv',
|
|
32
|
+
};
|
|
33
|
+
const ext = extFromName || extFromMime[att.mediaType] || 'bin';
|
|
34
|
+
|
|
35
|
+
const filename = `${stamp}_${rand}.${ext}`;
|
|
36
|
+
const relPath = `files/${category}/${filename}`;
|
|
37
|
+
|
|
38
|
+
const dir = category === 'images' ? paths.filesImages : paths.filesDocuments;
|
|
39
|
+
const absPath = `${dir}/${filename}`;
|
|
40
|
+
|
|
41
|
+
fs.writeFileSync(absPath, Buffer.from(att.data, 'base64'));
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
type: att.type === 'image' ? 'image' : 'document',
|
|
45
|
+
name: att.name,
|
|
46
|
+
mediaType: att.mediaType,
|
|
47
|
+
relPath,
|
|
48
|
+
absPath,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -8,6 +8,7 @@ import fs from 'fs';
|
|
|
8
8
|
import path from 'path';
|
|
9
9
|
import { log } from '../shared/logger.js';
|
|
10
10
|
import { DATA_DIR, PKG_DIR } from '../shared/paths.js';
|
|
11
|
+
import type { SavedFile } from './file-saver.js';
|
|
11
12
|
import { getClaudeAccessToken } from '../worker/claude-auth.js';
|
|
12
13
|
|
|
13
14
|
const PROMPT_FILE = path.join(import.meta.dirname, '..', 'worker', 'prompts', 'fluxy-system-prompt.txt');
|
|
@@ -29,7 +30,7 @@ export interface AgentAttachment {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
/** Build a multi-part prompt with attachments for the SDK */
|
|
32
|
-
function buildMultiPartPrompt(text: string, attachments: AgentAttachment[]): AsyncIterable<SDKUserMessage> {
|
|
33
|
+
function buildMultiPartPrompt(text: string, attachments: AgentAttachment[], savedFiles?: SavedFile[]): AsyncIterable<SDKUserMessage> {
|
|
33
34
|
return (async function* () {
|
|
34
35
|
const content: any[] = [];
|
|
35
36
|
|
|
@@ -47,7 +48,14 @@ function buildMultiPartPrompt(text: string, attachments: AgentAttachment[]): Asy
|
|
|
47
48
|
}
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
// Append local file paths so Claude can reference them with tools
|
|
52
|
+
let promptText = text || '(attached files)';
|
|
53
|
+
if (savedFiles?.length) {
|
|
54
|
+
const lines = savedFiles.map((f) => `- ${f.name} -> ${f.relPath}`);
|
|
55
|
+
promptText += `\n\n[Attached files saved to disk]\n${lines.join('\n')}\nYou can read or reference these files using the paths above (relative to your cwd).`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
content.push({ type: 'text', text: promptText });
|
|
51
59
|
|
|
52
60
|
yield {
|
|
53
61
|
type: 'user' as const,
|
|
@@ -76,6 +84,7 @@ export async function startFluxyAgentQuery(
|
|
|
76
84
|
model: string,
|
|
77
85
|
onMessage: (type: string, data: any) => void,
|
|
78
86
|
attachments?: AgentAttachment[],
|
|
87
|
+
savedFiles?: SavedFile[],
|
|
79
88
|
): Promise<void> {
|
|
80
89
|
const oauthToken = await getClaudeAccessToken();
|
|
81
90
|
if (!oauthToken) {
|
|
@@ -92,8 +101,15 @@ export async function startFluxyAgentQuery(
|
|
|
92
101
|
let fullText = '';
|
|
93
102
|
const usedTools = new Set<string>();
|
|
94
103
|
|
|
104
|
+
// If there are saved files but no inline attachments, append path info to plain text prompt
|
|
105
|
+
let plainPrompt = prompt;
|
|
106
|
+
if (savedFiles?.length && !attachments?.length) {
|
|
107
|
+
const lines = savedFiles.map((f) => `- ${f.name} -> ${f.relPath}`);
|
|
108
|
+
plainPrompt += `\n\n[Attached files saved to disk]\n${lines.join('\n')}\nYou can read or reference these files using the paths above (relative to your cwd).`;
|
|
109
|
+
}
|
|
110
|
+
|
|
95
111
|
const sdkPrompt: string | AsyncIterable<SDKUserMessage> =
|
|
96
|
-
attachments?.length ? buildMultiPartPrompt(prompt, attachments) :
|
|
112
|
+
attachments?.length ? buildMultiPartPrompt(prompt, attachments, savedFiles) : plainPrompt;
|
|
97
113
|
|
|
98
114
|
try {
|
|
99
115
|
const claudeQuery = query({
|
package/supervisor/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { spawnWorker, stopWorker, getWorkerPort, isWorkerAlive } from './worker.
|
|
|
13
13
|
import { spawnBackend, stopBackend, getBackendPort, isBackendAlive, resetBackendRestarts } from './backend.js';
|
|
14
14
|
import { updateTunnelUrl, startHeartbeat, stopHeartbeat, disconnect } from '../shared/relay.js';
|
|
15
15
|
import { startFluxyAgentQuery, stopFluxyAgentQuery, clearFluxySession } from './fluxy-agent.js';
|
|
16
|
+
import { ensureFileDirs, saveAttachment, type SavedFile } from './file-saver.js';
|
|
16
17
|
import { startViteDevServers, stopViteDevServers } from './vite-dev.js';
|
|
17
18
|
import { execSync } from 'child_process';
|
|
18
19
|
|
|
@@ -61,6 +62,9 @@ export async function startSupervisor() {
|
|
|
61
62
|
const vitePorts = await startViteDevServers(config.port);
|
|
62
63
|
console.log(`[supervisor] Vite ready — dashboard :${vitePorts.dashboard}`);
|
|
63
64
|
|
|
65
|
+
// Ensure file storage dirs exist
|
|
66
|
+
ensureFileDirs();
|
|
67
|
+
|
|
64
68
|
// Fluxy's AI brain
|
|
65
69
|
let ai: AiProvider | null = null;
|
|
66
70
|
if (config.ai.provider && (config.ai.apiKey || config.ai.provider === 'ollama')) {
|
|
@@ -150,7 +154,6 @@ export async function startSupervisor() {
|
|
|
150
154
|
|
|
151
155
|
// HTTP server — proxies to Vite dev servers + worker API
|
|
152
156
|
const server = http.createServer(async (req, res) => {
|
|
153
|
-
if (req.method === 'POST') console.log(`[supervisor] POST received: ${req.url}`);
|
|
154
157
|
// Fluxy widget — served directly (not part of Vite build)
|
|
155
158
|
if (req.url === '/fluxy/widget.js') {
|
|
156
159
|
console.log('[supervisor] Serving /fluxy/widget.js directly');
|
|
@@ -305,6 +308,23 @@ export async function startSupervisor() {
|
|
|
305
308
|
|
|
306
309
|
const msg = JSON.parse(rawStr);
|
|
307
310
|
|
|
311
|
+
// Whisper transcription via WebSocket (bypasses relay POST issues)
|
|
312
|
+
if (msg.type === 'whisper:transcribe') {
|
|
313
|
+
(async () => {
|
|
314
|
+
try {
|
|
315
|
+
const result = await workerApi('/api/whisper/transcribe', 'POST', { audio: msg.data.audio });
|
|
316
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
317
|
+
ws.send(JSON.stringify({ type: 'whisper:result', data: result }));
|
|
318
|
+
}
|
|
319
|
+
} catch (err: any) {
|
|
320
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
321
|
+
ws.send(JSON.stringify({ type: 'whisper:result', data: { error: err.message } }));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
})();
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
308
328
|
// Save settings via WebSocket (bypasses relay POST issues)
|
|
309
329
|
if (msg.type === 'settings:save') {
|
|
310
330
|
(async () => {
|
|
@@ -364,9 +384,27 @@ export async function startSupervisor() {
|
|
|
364
384
|
}
|
|
365
385
|
convId = dbConvId!;
|
|
366
386
|
|
|
367
|
-
// Save
|
|
387
|
+
// Save attachments to disk
|
|
388
|
+
let savedFiles: SavedFile[] = [];
|
|
389
|
+
if (data.attachments?.length) {
|
|
390
|
+
for (const att of data.attachments) {
|
|
391
|
+
try {
|
|
392
|
+
savedFiles.push(saveAttachment(att));
|
|
393
|
+
} catch (err: any) {
|
|
394
|
+
log.warn(`[fluxy] File save error: ${err.message}`);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Save user message to DB (include attachment metadata)
|
|
400
|
+
const meta: any = { model: freshConfig.ai.model };
|
|
401
|
+
if (savedFiles.length) {
|
|
402
|
+
meta.attachments = savedFiles.map((f) => ({
|
|
403
|
+
type: f.type, name: f.name, mediaType: f.mediaType, path: f.relPath,
|
|
404
|
+
}));
|
|
405
|
+
}
|
|
368
406
|
await workerApi(`/api/conversations/${convId}/messages`, 'POST', {
|
|
369
|
-
role: 'user', content, meta
|
|
407
|
+
role: 'user', content, meta,
|
|
370
408
|
});
|
|
371
409
|
|
|
372
410
|
// Broadcast user message to other clients
|
|
@@ -414,7 +452,7 @@ export async function startSupervisor() {
|
|
|
414
452
|
if (ws.readyState === WebSocket.OPEN) {
|
|
415
453
|
ws.send(JSON.stringify({ type, data: eventData }));
|
|
416
454
|
}
|
|
417
|
-
}, data.attachments);
|
|
455
|
+
}, data.attachments, savedFiles);
|
|
418
456
|
})();
|
|
419
457
|
return;
|
|
420
458
|
}
|
package/worker/index.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { initDb, closeDb, listConversations, createConversation, deleteConversat
|
|
|
7
7
|
import { startCodexOAuth, cancelCodexOAuth, getCodexAuthStatus, readCodexAccessToken } from './codex-auth.js';
|
|
8
8
|
import { startClaudeOAuth, exchangeClaudeCode, getClaudeAuthStatus, readClaudeAccessToken } from './claude-auth.js';
|
|
9
9
|
import { checkAvailability, registerHandle, releaseHandle, updateTunnelUrl, startHeartbeat, stopHeartbeat } from '../shared/relay.js';
|
|
10
|
-
import { ensureFileDirs } from '
|
|
10
|
+
import { ensureFileDirs } from '../supervisor/file-saver.js';
|
|
11
11
|
|
|
12
12
|
// ── Password hashing (scrypt) ──
|
|
13
13
|
|
|
@@ -34,7 +34,7 @@ ensureFileDirs();
|
|
|
34
34
|
|
|
35
35
|
// Express
|
|
36
36
|
const app = express();
|
|
37
|
-
app.use(express.json());
|
|
37
|
+
app.use(express.json({ limit: '10mb' }));
|
|
38
38
|
|
|
39
39
|
app.get('/api/health', (_, res) => res.json({ status: 'ok' }));
|
|
40
40
|
app.get('/api/conversations', (_, res) => res.json(listConversations()));
|
|
@@ -297,7 +297,6 @@ app.get('/api/portal/validate-token', (req, res) => {
|
|
|
297
297
|
});
|
|
298
298
|
|
|
299
299
|
app.post('/api/onboard', (req, res) => {
|
|
300
|
-
console.log('[worker] POST /api/onboard body:', JSON.stringify(req.body));
|
|
301
300
|
const { userName, agentName, provider, model, apiKey, baseUrl, portalUser, portalPass, whisperEnabled, whisperKey } = req.body;
|
|
302
301
|
setSetting('user_name', userName || '');
|
|
303
302
|
setSetting('agent_name', agentName || 'Fluxy');
|
|
Binary file
|
|
Binary file
|
package/worker/file-storage.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import crypto from 'crypto';
|
|
3
|
-
import { paths } from '../shared/paths.js';
|
|
4
|
-
|
|
5
|
-
export function ensureFileDirs(): void {
|
|
6
|
-
fs.mkdirSync(paths.filesAudio, { recursive: true });
|
|
7
|
-
fs.mkdirSync(paths.filesImages, { recursive: true });
|
|
8
|
-
fs.mkdirSync(paths.filesDocuments, { recursive: true });
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function saveFile(base64: string, category: 'audio' | 'images' | 'documents', ext: string): string {
|
|
12
|
-
const now = new Date();
|
|
13
|
-
const ts = now.toISOString().replace(/[-:T]/g, '').slice(0, 14); // YYYYMMDD_HHmmss → YYYYMMDDHHmmss
|
|
14
|
-
const stamp = `${ts.slice(0, 8)}_${ts.slice(8, 14)}`;
|
|
15
|
-
const rand = crypto.randomBytes(3).toString('hex');
|
|
16
|
-
const filename = `${stamp}_${rand}.${ext}`;
|
|
17
|
-
const relPath = `${category}/${filename}`;
|
|
18
|
-
|
|
19
|
-
const dir = category === 'audio' ? paths.filesAudio
|
|
20
|
-
: category === 'images' ? paths.filesImages
|
|
21
|
-
: paths.filesDocuments;
|
|
22
|
-
|
|
23
|
-
fs.writeFileSync(`${dir}/${filename}`, Buffer.from(base64, 'base64'));
|
|
24
|
-
return relPath;
|
|
25
|
-
}
|