Keep conversations, so a useful answer outlives the tab it was asked in
This commit is contained in:
@@ -42,7 +42,9 @@
|
||||
// History is validated per message, not trusted as a blob.
|
||||
// Role must be user or assistant, content must be a non-empty string, and each is
|
||||
// truncated. A crafted history could otherwise inject a system role or push the context
|
||||
// past the offload ceiling.
|
||||
// past the offload ceiling. chat_save applies the same validation, because a stored
|
||||
// conversation is replayed into a later prompt when it is reopened — an unchecked role
|
||||
// written there is an injection that survives a reload rather than one turn.
|
||||
//
|
||||
// The question is length-capped before it reaches a command line.
|
||||
// It is passed to the worker through escapeshellarg, but an unbounded string would still
|
||||
@@ -61,15 +63,21 @@
|
||||
// GET ?action=stats banner payload
|
||||
// GET ?action=poll&token=<hex32> job state
|
||||
// GET ?action=memory_get the operator memory file and its budget
|
||||
// GET ?action=chats stored conversations, newest first, metadata only
|
||||
// GET ?action=chat_get&id=<hex32> one stored conversation with its transcript
|
||||
// POST action=ask question=… [history=<JSON>] [kind=…] [think=0|1]
|
||||
// POST action=memory_set memory=… replace the memory file
|
||||
// POST action=clear token=<hex32> discard a finished job
|
||||
// POST action=chat_save [id=<hex32>] profile=… messages=<JSON>
|
||||
// POST action=chat_delete id=<hex32>
|
||||
//
|
||||
// RESPONSE
|
||||
// stats {"ok":true,"stats":{…}}
|
||||
// ask {"ok":true,"token":"<hex32>"}
|
||||
// poll {"ok":true,"job":{"status":"retrieving|generating|done|error",…}}
|
||||
// clear {"ok":true}
|
||||
// chats {"ok":true,"chats":[{id,ts,profile,title,turns}],"max":N}
|
||||
// chat_save {"ok":true,"id":"<hex32>","title":…}
|
||||
// {"ok":false,"error":…}
|
||||
//
|
||||
// DEPENDS ON
|
||||
@@ -201,6 +209,64 @@ if ($action === 'clear') {
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── chats ─────────────────────────────────────────────────────────────────────
|
||||
// Stored conversations. Listing and reading are GET because they change nothing; saving and
|
||||
// deleting are POST, so they ride Unraid's CSRF prepend like every other mutation here.
|
||||
//
|
||||
// Messages are validated per message on the way in, exactly as ask validates history and for
|
||||
// the same reason: a stored chat is replayed into a later prompt when the operator reopens it,
|
||||
// so a crafted role in the store would be an injection that survives a reload.
|
||||
if ($action === 'chats') {
|
||||
echo json_encode(['ok' => true, 'chats' => vv_ai_chats_list(), 'max' => vv_ai_chats_max()]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'chat_get') {
|
||||
$chat = vv_ai_chat_read(trim($_GET['id'] ?? ''));
|
||||
if ($chat === null) { echo json_encode(['ok' => false, 'error' => 'No such chat']); exit; }
|
||||
echo json_encode(['ok' => true, 'chat' => $chat]);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'chat_save') {
|
||||
if (!$isPost) { http_response_code(405); echo json_encode(['ok' => false, 'error' => 'POST only']); exit; }
|
||||
|
||||
$profile = trim($_POST['profile'] ?? 'chat');
|
||||
if (!isset(VV_AI_PROFILES[$profile])) {
|
||||
echo json_encode(['ok' => false, 'error' => 'Unknown profile: ' . $profile]); exit;
|
||||
}
|
||||
|
||||
$clean = [];
|
||||
$msgs = json_decode($_POST['messages'] ?? '[]', true);
|
||||
if (is_array($msgs)) {
|
||||
foreach ($msgs as $m) {
|
||||
$role = $m['role'] ?? '';
|
||||
$text = trim((string)($m['content'] ?? ''));
|
||||
if (!in_array($role, ['user', 'assistant'], true) || $text === '') continue;
|
||||
$clean[] = ['role' => $role, 'content' => mb_substr($text, 0, VV_AI_MAX_HIST_MSG)];
|
||||
}
|
||||
}
|
||||
// Capped at the deepest profile's window rather than that of the profile in hand. A chat
|
||||
// saved under one profile can be reopened under another, and the reopened turn is trimmed
|
||||
// again on the way back out by ask — so storing a little more than any single profile will
|
||||
// send costs nothing and keeps the transcript readable.
|
||||
$cap = max(VV_AI_PROFILES) * 2;
|
||||
if (count($clean) > $cap) $clean = array_slice($clean, -$cap);
|
||||
|
||||
$r = vv_ai_chat_save(trim($_POST['id'] ?? ''), $profile, $clean);
|
||||
vv_ai_log('chat_save ' . ($r['ok'] ? 'ok id=' . substr($r['id'], 0, 12)
|
||||
: 'FAILED: ' . $r['error']));
|
||||
echo json_encode($r);
|
||||
exit;
|
||||
}
|
||||
|
||||
if ($action === 'chat_delete') {
|
||||
if (!$isPost) { http_response_code(405); echo json_encode(['ok' => false, 'error' => 'POST only']); exit; }
|
||||
$ok = vv_ai_chat_delete(trim($_POST['id'] ?? ''));
|
||||
echo json_encode(['ok' => $ok, 'error' => $ok ? null : 'No such chat']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// ── bugs / bug_close ──────────────────────────────────────────────────────────
|
||||
// Reports the troubleshooter filed. Listing is a GET because it changes nothing; dismissing is
|
||||
// a POST, like every other mutation in this plugin.
|
||||
|
||||
Reference in New Issue
Block a user