@chatpanel/bridge 0.3.3 → 0.4.0
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 +1 -1
- package/src/engines/claude.js +41 -5
- package/src/engines/codex.js +23 -2
- package/src/engines/custom.js +62 -2
- package/src/engines/gemini.js +35 -3
- package/src/server.js +5 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Local bridge that exposes the AI coding agents installed on your machine — Claude Code (CLI), Codex (CLI), and Gemini CLI — to the ChatPanel Chrome extension over a localhost SSE endpoint. Bring your own agent.",
|
|
6
6
|
"keywords": [
|
package/src/engines/claude.js
CHANGED
|
@@ -14,10 +14,27 @@
|
|
|
14
14
|
// permissionMode is 'acceptEdits'/'bypassPermissions' in ChatPanel Settings.
|
|
15
15
|
|
|
16
16
|
import { spawn } from 'node:child_process';
|
|
17
|
+
import { writeFile, unlink } from 'node:fs/promises';
|
|
17
18
|
import os from 'node:os';
|
|
18
19
|
import path from 'node:path';
|
|
19
20
|
import { resolveClaude, buildSpawnSpec, isCompiledBinary } from '../env.js';
|
|
20
21
|
|
|
22
|
+
// Write base64 data-URL images to temp files. Claude Code reads them with its
|
|
23
|
+
// Read tool (which feeds images to the model as vision), so we just reference the
|
|
24
|
+
// paths in the prompt — no custom image flag needed. Returns paths (caller cleans).
|
|
25
|
+
async function writeImages(images, tag) {
|
|
26
|
+
const files = [];
|
|
27
|
+
for (let i = 0; i < (images?.length || 0); i++) {
|
|
28
|
+
const m = /^data:([^;]+);base64,(.+)$/s.exec(images[i]?.dataUrl || '');
|
|
29
|
+
if (!m) continue;
|
|
30
|
+
const ext = (m[1].split('/')[1] || 'png').replace(/[^a-z0-9]/gi, '').slice(0, 5) || 'png';
|
|
31
|
+
const file = path.join(os.tmpdir(), `chatpanel-claude-img-${tag}-${i}.${ext}`);
|
|
32
|
+
await writeFile(file, Buffer.from(m[2], 'base64'));
|
|
33
|
+
files.push(file);
|
|
34
|
+
}
|
|
35
|
+
return files;
|
|
36
|
+
}
|
|
37
|
+
|
|
21
38
|
// Idle timeout: kill the run only after this long with NO output. The timer
|
|
22
39
|
// re-arms on every stdout/stderr chunk, so a task that keeps streaming can run
|
|
23
40
|
// indefinitely — only a truly stuck/silent process is killed. Override with
|
|
@@ -173,7 +190,7 @@ export function handleMessage(msg, emit, alreadyStreamed) {
|
|
|
173
190
|
return out;
|
|
174
191
|
}
|
|
175
192
|
|
|
176
|
-
export async function chat({ messages, system, options }, emit) {
|
|
193
|
+
export async function chat({ messages, system, options, images }, emit) {
|
|
177
194
|
const permissionMode = options.permissionMode || 'default';
|
|
178
195
|
// Explicit project dir, else null → CLI runs in home (or WSL home).
|
|
179
196
|
const cwd = options.workingDir ? path.resolve(options.workingDir) : null;
|
|
@@ -194,10 +211,29 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
194
211
|
// "Use my local skills & config" off → run clean.
|
|
195
212
|
if (options.useLocalConfig === false) args.push('--setting-sources', '');
|
|
196
213
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
|
|
214
|
+
// Attach images by writing them to temp files and asking Claude Code to Read
|
|
215
|
+
// them — its Read tool loads images as vision (no special flag needed).
|
|
216
|
+
const tag = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
217
|
+
const imageFiles = await writeImages(images, tag);
|
|
218
|
+
const cleanup = () => imageFiles.forEach((f) => unlink(f).catch(() => {}));
|
|
219
|
+
let prompt = buildPrompt(messages);
|
|
220
|
+
if (imageFiles.length) {
|
|
221
|
+
prompt += `\n\nThe user attached ${imageFiles.length} image file(s). Use the Read tool to view ${
|
|
222
|
+
imageFiles.length === 1 ? 'it' : 'them'
|
|
223
|
+
}: ${imageFiles.join(', ')}`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const run = runClaude({ prompt, args, cwd, emit });
|
|
227
|
+
if (run === null) {
|
|
228
|
+
cleanup(); // SDK fallback doesn't take images yet
|
|
229
|
+
return sdkChat({ messages, system, options }, emit);
|
|
230
|
+
}
|
|
231
|
+
try {
|
|
232
|
+
const { streamedAny, resultText } = await run;
|
|
233
|
+
emit({ type: 'done', text: streamedAny ? '' : resultText });
|
|
234
|
+
} finally {
|
|
235
|
+
cleanup();
|
|
236
|
+
}
|
|
201
237
|
}
|
|
202
238
|
|
|
203
239
|
// A fast, tool-free single-shot completion — used for prompt autocomplete. No
|
package/src/engines/codex.js
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// the agent to point it at a real project.
|
|
16
16
|
|
|
17
17
|
import { spawn, spawnSync } from 'node:child_process';
|
|
18
|
-
import { readFile, unlink } from 'node:fs/promises';
|
|
18
|
+
import { readFile, unlink, writeFile } from 'node:fs/promises';
|
|
19
19
|
import { existsSync, mkdirSync, symlinkSync, readFileSync } from 'node:fs';
|
|
20
20
|
import os from 'node:os';
|
|
21
21
|
import path from 'node:path';
|
|
@@ -110,10 +110,27 @@ function buildPrompt(messages, system) {
|
|
|
110
110
|
return p;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
// Write base64 data-URL images to temp files so `codex exec -i <file>` can
|
|
114
|
+
// attach them to the prompt as vision input. Returns the paths (caller cleans up).
|
|
115
|
+
async function writeImages(images, tag) {
|
|
116
|
+
const files = [];
|
|
117
|
+
for (let i = 0; i < (images?.length || 0); i++) {
|
|
118
|
+
const m = /^data:([^;]+);base64,(.+)$/s.exec(images[i]?.dataUrl || '');
|
|
119
|
+
if (!m) continue;
|
|
120
|
+
const ext = (m[1].split('/')[1] || 'png').replace(/[^a-z0-9]/gi, '').slice(0, 5) || 'png';
|
|
121
|
+
const file = path.join(os.tmpdir(), `chatpanel-codex-img-${tag}-${i}.${ext}`);
|
|
122
|
+
await writeFile(file, Buffer.from(m[2], 'base64'));
|
|
123
|
+
files.push(file);
|
|
124
|
+
}
|
|
125
|
+
return files;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function chat({ messages, system, options, images }, emit) {
|
|
114
129
|
ensureScratch();
|
|
115
130
|
const tag = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
116
131
|
const outFile = path.join(os.tmpdir(), `chatpanel-codex-${tag}.txt`);
|
|
132
|
+
const imageFiles = await writeImages(images, tag);
|
|
133
|
+
const cleanupImages = () => imageFiles.forEach((f) => unlink(f).catch(() => {}));
|
|
117
134
|
|
|
118
135
|
const cwd = options.workingDir ? path.resolve(options.workingDir) : SCRATCH;
|
|
119
136
|
const sandbox =
|
|
@@ -130,6 +147,7 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
130
147
|
args.push('-c', 'approval_policy=never');
|
|
131
148
|
if (REASONING) args.push('-c', `model_reasoning_effort=${REASONING}`);
|
|
132
149
|
if (options.model) args.push('-m', options.model);
|
|
150
|
+
for (const f of imageFiles) args.push('-i', f); // attach images to the initial prompt
|
|
133
151
|
args.push('-');
|
|
134
152
|
|
|
135
153
|
// Default: use the user's skills/config. Opt-out → isolated home.
|
|
@@ -145,6 +163,7 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
145
163
|
try {
|
|
146
164
|
child = spawn('codex', args, { cwd, stdio: ['pipe', 'pipe', 'pipe'], env });
|
|
147
165
|
} catch (e) {
|
|
166
|
+
cleanupImages();
|
|
148
167
|
return reject(new Error(`Failed to start codex: ${e.message}`));
|
|
149
168
|
}
|
|
150
169
|
|
|
@@ -178,6 +197,7 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
178
197
|
child.stderr.on('data', (d) => { armIdle(); stderr += d.toString(); });
|
|
179
198
|
child.on('error', (e) => {
|
|
180
199
|
clearTimeout(idleTimer);
|
|
200
|
+
cleanupImages();
|
|
181
201
|
reject(e);
|
|
182
202
|
});
|
|
183
203
|
child.on('close', async (code) => {
|
|
@@ -189,6 +209,7 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
189
209
|
/* no message file */
|
|
190
210
|
}
|
|
191
211
|
unlink(outFile).catch(() => {});
|
|
212
|
+
cleanupImages();
|
|
192
213
|
if (code === 0) {
|
|
193
214
|
emit({ type: 'delta', text: text || '(no output)' });
|
|
194
215
|
emit({ type: 'done', text: '' });
|
package/src/engines/custom.js
CHANGED
|
@@ -16,11 +16,44 @@
|
|
|
16
16
|
// speak it), reusing the Claude engine's parser.
|
|
17
17
|
|
|
18
18
|
import { spawn } from 'node:child_process';
|
|
19
|
+
import { writeFile, unlink } from 'node:fs/promises';
|
|
20
|
+
import os from 'node:os';
|
|
19
21
|
import path from 'node:path';
|
|
20
22
|
import { resolveCommand, buildSpawnSpec } from '../env.js';
|
|
21
23
|
import { isProEntitled } from '../entitlement.js';
|
|
22
24
|
import { handleMessage } from './claude.js';
|
|
23
25
|
|
|
26
|
+
// Write base64 data-URL images to temp files so a custom CLI can take them via
|
|
27
|
+
// its configured `imageArg` template (e.g. "-i {path}", "@{path}"). Returns paths.
|
|
28
|
+
async function writeImages(images, tag) {
|
|
29
|
+
const files = [];
|
|
30
|
+
for (let i = 0; i < (images?.length || 0); i++) {
|
|
31
|
+
const m = /^data:([^;]+);base64,(.+)$/s.exec(images[i]?.dataUrl || '');
|
|
32
|
+
if (!m) continue;
|
|
33
|
+
const ext = (m[1].split('/')[1] || 'png').replace(/[^a-z0-9]/gi, '').slice(0, 5) || 'png';
|
|
34
|
+
const file = path.join(os.tmpdir(), `chatpanel-custom-img-${tag}-${i}.${ext}`);
|
|
35
|
+
await writeFile(file, Buffer.from(m[2], 'base64'));
|
|
36
|
+
files.push(file);
|
|
37
|
+
}
|
|
38
|
+
return files;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Expand the user's `imageArg` template across the image files into argv tokens.
|
|
42
|
+
// "{path}" is substituted per image; a template without it appends the path.
|
|
43
|
+
function imageTokensFor(imageArg, files) {
|
|
44
|
+
const tmpl = String(imageArg || '').trim();
|
|
45
|
+
if (!tmpl || !files.length) return [];
|
|
46
|
+
const tokens = [];
|
|
47
|
+
for (const f of files) {
|
|
48
|
+
tokens.push(
|
|
49
|
+
...(tmpl.includes('{path}')
|
|
50
|
+
? tmpl.replaceAll('{path}', f).split(/\s+/).filter(Boolean)
|
|
51
|
+
: [...tmpl.split(/\s+/).filter(Boolean), f]),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return tokens;
|
|
55
|
+
}
|
|
56
|
+
|
|
24
57
|
// Idle timeout: re-armed on every stdout/stderr chunk, so a long run that keeps
|
|
25
58
|
// streaming never trips it — only true silence does. Override with
|
|
26
59
|
// CHATPANEL_CUSTOM_TIMEOUT_MS (ms).
|
|
@@ -92,7 +125,7 @@ function buildPrompt(messages, system) {
|
|
|
92
125
|
return p;
|
|
93
126
|
}
|
|
94
127
|
|
|
95
|
-
export async function chat({ messages, system, options }, emit) {
|
|
128
|
+
export async function chat({ messages, system, options, images }, emit) {
|
|
96
129
|
// Pro gate — verified, not just UI. No valid signed entitlement → no run.
|
|
97
130
|
if (!(await isProEntitled(options.entitlement))) {
|
|
98
131
|
throw new Error('Custom agents require ChatPanel Pro. Upgrade in Settings to bring your own CLI agent.');
|
|
@@ -131,6 +164,24 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
131
164
|
: [...tmpl.split(/\s+/).filter(Boolean), options.model];
|
|
132
165
|
args = [...injected, ...args];
|
|
133
166
|
}
|
|
167
|
+
// Images: write to temp files, expand the agent's imageArg template, then place
|
|
168
|
+
// the tokens. An explicit {images} placeholder in args wins; otherwise they go
|
|
169
|
+
// just before the prompt (arg mode) or get appended (stdin mode).
|
|
170
|
+
const tag = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
171
|
+
const imageFiles = spec.imageArg ? await writeImages(images, tag) : [];
|
|
172
|
+
const cleanup = () => imageFiles.forEach((f) => unlink(f).catch(() => {}));
|
|
173
|
+
const imageTokens = imageTokensFor(spec.imageArg, imageFiles);
|
|
174
|
+
let placedImages = false;
|
|
175
|
+
if (imageTokens.length) {
|
|
176
|
+
args = args.flatMap((a) => {
|
|
177
|
+
if (a === '{images}') {
|
|
178
|
+
placedImages = true;
|
|
179
|
+
return imageTokens;
|
|
180
|
+
}
|
|
181
|
+
return [a];
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
134
185
|
if (promptVia === 'arg') {
|
|
135
186
|
let placed = false;
|
|
136
187
|
args = args.map((a) => {
|
|
@@ -140,7 +191,12 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
140
191
|
}
|
|
141
192
|
return a;
|
|
142
193
|
});
|
|
143
|
-
if (!placed)
|
|
194
|
+
if (!placed) {
|
|
195
|
+
if (imageTokens.length && !placedImages) args.push(...imageTokens); // images, then prompt
|
|
196
|
+
args.push(prompt);
|
|
197
|
+
}
|
|
198
|
+
} else if (imageTokens.length && !placedImages) {
|
|
199
|
+
args.push(...imageTokens); // stdin prompt: image tokens go on argv
|
|
144
200
|
}
|
|
145
201
|
|
|
146
202
|
const [bin, argv, opts] = buildSpawnSpec(resolved, args, cwd);
|
|
@@ -150,6 +206,7 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
150
206
|
try {
|
|
151
207
|
child = spawn(bin, argv, opts);
|
|
152
208
|
} catch (e) {
|
|
209
|
+
cleanup();
|
|
153
210
|
return reject(new Error(`Failed to start ${label}: ${e.message}`));
|
|
154
211
|
}
|
|
155
212
|
|
|
@@ -163,6 +220,7 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
163
220
|
clearTimeout(idleTimer);
|
|
164
221
|
idleTimer = setTimeout(() => {
|
|
165
222
|
child.kill('SIGKILL');
|
|
223
|
+
cleanup();
|
|
166
224
|
reject(new Error(`${label} timed out — no output for ${Math.round(IDLE_MS / 1000)}s.`));
|
|
167
225
|
}, IDLE_MS);
|
|
168
226
|
};
|
|
@@ -196,10 +254,12 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
196
254
|
child.stderr.on('data', (d) => { armIdle(); stderr += d.toString(); });
|
|
197
255
|
child.on('error', (e) => {
|
|
198
256
|
clearTimeout(idleTimer);
|
|
257
|
+
cleanup();
|
|
199
258
|
reject(new Error(`Failed to start ${label}: ${e.message}`));
|
|
200
259
|
});
|
|
201
260
|
child.on('close', (code) => {
|
|
202
261
|
clearTimeout(idleTimer);
|
|
262
|
+
cleanup();
|
|
203
263
|
if (code === 0) {
|
|
204
264
|
emit({ type: 'done', text: streamedAny ? '' : resultText });
|
|
205
265
|
resolve();
|
package/src/engines/gemini.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// Install: `npm i -g @google/gemini-cli`, then run `gemini` once to sign in.
|
|
10
10
|
|
|
11
11
|
import { spawn, spawnSync } from 'node:child_process';
|
|
12
|
-
import { mkdirSync } from 'node:fs';
|
|
12
|
+
import { mkdirSync, writeFileSync, unlinkSync } from 'node:fs';
|
|
13
13
|
import os from 'node:os';
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import { findAgentBin } from '../env.js';
|
|
@@ -45,6 +45,24 @@ export async function available() {
|
|
|
45
45
|
: { ok: false, reason: 'gemini not found on PATH. Install @google/gemini-cli, then run `gemini` once to sign in.' };
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// Write base64 data-URL images into `dir` so Gemini's `@<file>` can read them.
|
|
49
|
+
function writeImages(images, dir) {
|
|
50
|
+
const files = [];
|
|
51
|
+
for (let i = 0; i < (images?.length || 0); i++) {
|
|
52
|
+
const m = /^data:([^;]+);base64,(.+)$/s.exec(images[i]?.dataUrl || '');
|
|
53
|
+
if (!m) continue;
|
|
54
|
+
const ext = (m[1].split('/')[1] || 'png').replace(/[^a-z0-9]/gi, '').slice(0, 5) || 'png';
|
|
55
|
+
const file = path.join(dir, `chatpanel-img-${Date.now()}-${i}.${ext}`);
|
|
56
|
+
try {
|
|
57
|
+
writeFileSync(file, Buffer.from(m[2], 'base64'));
|
|
58
|
+
files.push(file);
|
|
59
|
+
} catch {
|
|
60
|
+
/* skip unwritable */
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return files;
|
|
64
|
+
}
|
|
65
|
+
|
|
48
66
|
function buildPrompt(messages, system) {
|
|
49
67
|
let p = system ? `${system}\n\n` : '';
|
|
50
68
|
const history = messages.slice(0, -1);
|
|
@@ -58,7 +76,7 @@ function buildPrompt(messages, system) {
|
|
|
58
76
|
return p;
|
|
59
77
|
}
|
|
60
78
|
|
|
61
|
-
export async function chat({ messages, system, options }, emit) {
|
|
79
|
+
export async function chat({ messages, system, options, images }, emit) {
|
|
62
80
|
try {
|
|
63
81
|
mkdirSync(SCRATCH, { recursive: true });
|
|
64
82
|
} catch {
|
|
@@ -66,10 +84,20 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
66
84
|
}
|
|
67
85
|
const cwd = options.workingDir ? path.resolve(options.workingDir) : SCRATCH;
|
|
68
86
|
|
|
87
|
+
// Images: write them into the cwd so Gemini's `@file` reference (which reads
|
|
88
|
+
// files — including images — as multimodal input) can resolve them. Cleaned up
|
|
89
|
+
// after the run. Written into cwd (not tmp) because `@` resolves to the workspace.
|
|
90
|
+
const imageFiles = writeImages(images, cwd);
|
|
91
|
+
const cleanup = () => imageFiles.forEach((f) => { try { unlinkSync(f); } catch { /* gone */ } });
|
|
92
|
+
let prompt = buildPrompt(messages, system);
|
|
93
|
+
if (imageFiles.length) {
|
|
94
|
+
prompt += `\n\nThe user attached image(s): ${imageFiles.map((f) => '@' + path.basename(f)).join(' ')}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
69
97
|
// `-p` is non-interactive (no TTY prompts). `-m` picks the model. `-y` (yolo)
|
|
70
98
|
// auto-approves tool calls when the user opted into bypassPermissions — without
|
|
71
99
|
// it Gemini would block on an approval it can't show in a headless run.
|
|
72
|
-
const args = ['-p',
|
|
100
|
+
const args = ['-p', prompt];
|
|
73
101
|
if (options.model) args.push('-m', options.model);
|
|
74
102
|
if (options.permissionMode === 'bypassPermissions') args.push('-y');
|
|
75
103
|
|
|
@@ -80,6 +108,7 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
80
108
|
// interactive "trust this folder?" dialog can block us.
|
|
81
109
|
child = spawn('gemini', args, { cwd, stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env } });
|
|
82
110
|
} catch (e) {
|
|
111
|
+
cleanup();
|
|
83
112
|
return reject(new Error(`Failed to start gemini: ${e.message}`));
|
|
84
113
|
}
|
|
85
114
|
|
|
@@ -91,6 +120,7 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
91
120
|
clearTimeout(idleTimer);
|
|
92
121
|
idleTimer = setTimeout(() => {
|
|
93
122
|
child.kill('SIGKILL');
|
|
123
|
+
cleanup();
|
|
94
124
|
reject(new Error(`Gemini timed out — no output for ${Math.round(IDLE_MS / 1000)}s.`));
|
|
95
125
|
}, IDLE_MS);
|
|
96
126
|
};
|
|
@@ -106,10 +136,12 @@ export async function chat({ messages, system, options }, emit) {
|
|
|
106
136
|
child.stderr.on('data', (d) => { armIdle(); err += d.toString(); });
|
|
107
137
|
child.on('error', (e) => {
|
|
108
138
|
clearTimeout(idleTimer);
|
|
139
|
+
cleanup();
|
|
109
140
|
reject(new Error(`Failed to start gemini: ${e.message}`));
|
|
110
141
|
});
|
|
111
142
|
child.on('close', (code) => {
|
|
112
143
|
clearTimeout(idleTimer);
|
|
144
|
+
cleanup();
|
|
113
145
|
if (code === 0) {
|
|
114
146
|
if (!streamed) emit({ type: 'delta', text: out.trim() || '(no output)' });
|
|
115
147
|
emit({ type: 'done', text: '' });
|
package/src/server.js
CHANGED
|
@@ -27,7 +27,7 @@ import { checkForUpdate, selfUpdate } from './update.js';
|
|
|
27
27
|
// Hardcoded (not read from package.json) so it survives Bun's single-file
|
|
28
28
|
// --compile, where package.json isn't on a readable FS. CI fails the publish if
|
|
29
29
|
// this drifts from package.json, so the two can't silently diverge.
|
|
30
|
-
const VERSION = '0.
|
|
30
|
+
const VERSION = '0.4.0';
|
|
31
31
|
const HOST = process.env.CHATPANEL_BRIDGE_HOST || '127.0.0.1';
|
|
32
32
|
const PORT = Number(process.env.CHATPANEL_BRIDGE_PORT) || 4319;
|
|
33
33
|
|
|
@@ -117,6 +117,9 @@ async function handleChat(req, res) {
|
|
|
117
117
|
}
|
|
118
118
|
const target = ENGINES[body.agent];
|
|
119
119
|
if (!target) return json(res, 404, { error: `Unknown agent "${body.agent}"` });
|
|
120
|
+
if (Array.isArray(body.images) && body.images.length) {
|
|
121
|
+
log('info', `chat: ${body.agent} received ${body.images.length} image(s)`);
|
|
122
|
+
}
|
|
120
123
|
|
|
121
124
|
// Open the SSE stream.
|
|
122
125
|
res.writeHead(200, {
|
|
@@ -138,6 +141,7 @@ async function handleChat(req, res) {
|
|
|
138
141
|
messages: Array.isArray(body.messages) ? body.messages : [],
|
|
139
142
|
system: body.system || '',
|
|
140
143
|
options: body.options || {},
|
|
144
|
+
images: Array.isArray(body.images) ? body.images : [],
|
|
141
145
|
},
|
|
142
146
|
(obj) => {
|
|
143
147
|
if (!closed) emit(obj);
|