Compare commits
4 Commits
feat/no-db
...
fix/ai-jso
| Author | SHA1 | Date | |
|---|---|---|---|
|
53cdf488b6
|
|||
|
4776c1f1ec
|
|||
|
a3a45ad477
|
|||
|
2952466abd
|
@@ -186,14 +186,65 @@ ai_json_escape(const gchar* str)
|
||||
return g_string_free(result, FALSE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a 4-hex-digit sequence at @p to a 16-bit code point.
|
||||
* Returns -1 on invalid hex.
|
||||
*/
|
||||
static int
|
||||
_hex4_to_int(const gchar* p)
|
||||
{
|
||||
int val = 0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
char c = p[i];
|
||||
val <<= 4;
|
||||
if (c >= '0' && c <= '9')
|
||||
val |= c - '0';
|
||||
else if (c >= 'a' && c <= 'f')
|
||||
val |= c - 'a' + 10;
|
||||
else if (c >= 'A' && c <= 'F')
|
||||
val |= c - 'A' + 10;
|
||||
else
|
||||
return -1;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a UTF-8 encoding of @cp to @w, advancing @w.
|
||||
* Returns the updated pointer.
|
||||
*/
|
||||
static gchar*
|
||||
_write_utf8(gchar* w, unsigned int cp)
|
||||
{
|
||||
if (cp <= 0x7F) {
|
||||
*w++ = (char)cp;
|
||||
} else if (cp <= 0x7FF) {
|
||||
*w++ = (char)(0xC0 | (cp >> 6));
|
||||
*w++ = (char)(0x80 | (cp & 0x3F));
|
||||
} else if (cp <= 0xFFFF) {
|
||||
*w++ = (char)(0xE0 | (cp >> 12));
|
||||
*w++ = (char)(0x80 | ((cp >> 6) & 0x3F));
|
||||
*w++ = (char)(0x80 | (cp & 0x3F));
|
||||
} else if (cp <= 0x10FFFF) {
|
||||
*w++ = (char)(0xF0 | (cp >> 18));
|
||||
*w++ = (char)(0x80 | ((cp >> 12) & 0x3F));
|
||||
*w++ = (char)(0x80 | ((cp >> 6) & 0x3F));
|
||||
*w++ = (char)(0x80 | (cp & 0x3F));
|
||||
}
|
||||
/* Invalid code point: silently skip */
|
||||
return w;
|
||||
}
|
||||
|
||||
/* Decode JSON string escapes in [start, end) into a freshly allocated buffer.
|
||||
* Handles \" \\ \/ \n \t \r \b \f. \uXXXX sequences are passed through verbatim
|
||||
* since the rest of the AI pipeline already deals in UTF-8 byte streams. */
|
||||
* Handles \" \\ \/ \n \t \r \b \f and \uXXXX (including surrogate pairs). */
|
||||
static gchar*
|
||||
_json_unescape_substring(const gchar* start, const gchar* end)
|
||||
{
|
||||
gchar* out = g_new0(gchar, (end - start) + 1);
|
||||
/* Estimate upper bound: each \uXXXX becomes up to 6 UTF-8 bytes. */
|
||||
gsize max_len = (end - start) * 6 + 1;
|
||||
gchar* out = g_new0(gchar, max_len);
|
||||
gchar* w = out;
|
||||
|
||||
for (const gchar* in = start; in < end;) {
|
||||
if (*in == '\\' && in + 1 < end) {
|
||||
switch (in[1]) {
|
||||
@@ -229,6 +280,64 @@ _json_unescape_substring(const gchar* start, const gchar* end)
|
||||
*w++ = '\f';
|
||||
in += 2;
|
||||
break;
|
||||
case 'u':
|
||||
/* Handle \uXXXX or surrogate pair \uHHHH\uLLLL */
|
||||
if (in + 6 > end) {
|
||||
/* Not enough characters — emit literal */
|
||||
*w++ = *in++;
|
||||
break;
|
||||
}
|
||||
if (in[2] != '\\' || in[3] != 'u') {
|
||||
/* Single \uXXXX */
|
||||
int cp = _hex4_to_int(in + 2);
|
||||
if (cp < 0) {
|
||||
/* Invalid hex — emit literal backslash */
|
||||
*w++ = *in++;
|
||||
} else if (cp < 0xD800 || cp > 0xDFFF) {
|
||||
/* Not a surrogate — encode directly */
|
||||
w = _write_utf8(w, (unsigned int)cp);
|
||||
in += 6;
|
||||
} else {
|
||||
/* High or low surrogate — expect pair */
|
||||
unsigned int high = (unsigned int)cp;
|
||||
if ((high >= 0xD800 && high <= 0xDBFF) && in + 12 <= end && in[6] == '\\' && in[7] == 'u') {
|
||||
/* High surrogate followed by \u */
|
||||
int low = _hex4_to_int(in + 8);
|
||||
if (low >= 0xDC00 && low <= 0xDFFF) {
|
||||
/* Valid surrogate pair */
|
||||
unsigned int cp2 = (0x10000 + ((high & 0x3FF) << 10)) | (low & 0x3FF);
|
||||
w = _write_utf8(w, cp2);
|
||||
in += 12;
|
||||
} else {
|
||||
/* Invalid low surrogate — emit replacement */
|
||||
w = _write_utf8(w, 0xFFFD);
|
||||
in += 6;
|
||||
}
|
||||
} else {
|
||||
/* Orphan high surrogate or lone low surrogate */
|
||||
w = _write_utf8(w, 0xFFFD);
|
||||
in += 6;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* \uHHHH\uLLLL — surrogate pair */
|
||||
int high = _hex4_to_int(in + 2);
|
||||
if (high < 0) {
|
||||
*w++ = *in++;
|
||||
break;
|
||||
}
|
||||
int low = _hex4_to_int(in + 8);
|
||||
if ((high >= 0xD800 && high <= 0xDBFF) && (low >= 0xDC00 && low <= 0xDFFF)) {
|
||||
unsigned int cp2 = (0x10000 + ((unsigned int)(high & 0x3FF) << 10)) | (unsigned int)(low & 0x3FF);
|
||||
w = _write_utf8(w, cp2);
|
||||
in += 12;
|
||||
} else {
|
||||
/* Invalid pair — emit replacement for high */
|
||||
w = _write_utf8(w, 0xFFFD);
|
||||
in += 6;
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
*w++ = *in++;
|
||||
break;
|
||||
@@ -238,7 +347,9 @@ _json_unescape_substring(const gchar* start, const gchar* end)
|
||||
}
|
||||
}
|
||||
*w = '\0';
|
||||
return out;
|
||||
|
||||
/* Realloc to exact size */
|
||||
return g_realloc(out, w - out + 1);
|
||||
}
|
||||
|
||||
/* Locate "field":"...value..." in json and return the unescaped value.
|
||||
|
||||
@@ -10932,21 +10932,14 @@ cmd_ai_start(ProfWin* window, const char* const command, gchar** args)
|
||||
if (g_strv_length(args) >= 2) {
|
||||
provider_name = args[1];
|
||||
}
|
||||
if (g_strv_length(args) >= 3) {
|
||||
model = args[2];
|
||||
|
||||
if (!provider_name) {
|
||||
cons_show_error("Please specify provider name.");
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
// Resolve defaults
|
||||
auto_gchar gchar* owned_provider_name = NULL;
|
||||
if (!provider_name) {
|
||||
// Use default provider from preferences
|
||||
auto_gchar gchar* default_provider = prefs_get_string(PREF_AI_PROVIDER);
|
||||
if (default_provider && strlen(default_provider) > 0) {
|
||||
provider_name = default_provider;
|
||||
}
|
||||
}
|
||||
if (!provider_name) {
|
||||
provider_name = "openai"; // Fallback
|
||||
if (g_strv_length(args) >= 3) {
|
||||
model = args[2];
|
||||
}
|
||||
|
||||
AIProvider* provider = ai_get_provider(provider_name);
|
||||
|
||||
@@ -61,6 +61,22 @@ integrity_issue_free(integrity_issue_t* issue)
|
||||
}
|
||||
}
|
||||
|
||||
gboolean
|
||||
log_database_can_recover_messages(void)
|
||||
{
|
||||
if (!active_db_backend) {
|
||||
return FALSE;
|
||||
}
|
||||
auto_gchar gchar* pref_dblog = prefs_get_string(PREF_DBLOG);
|
||||
if (g_strcmp0(pref_dblog, "off") == 0) {
|
||||
return FALSE;
|
||||
}
|
||||
if (g_strcmp0(pref_dblog, "redact") == 0) {
|
||||
return FALSE;
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
gboolean
|
||||
log_database_init(ProfAccount* account)
|
||||
{
|
||||
|
||||
@@ -109,6 +109,9 @@ ProfMessage* log_database_get_limits_info(const gchar* const contact_barejid, gb
|
||||
void log_database_close(void);
|
||||
GSList* log_database_verify_integrity(const gchar* const contact_barejid);
|
||||
|
||||
// FALSE if no backend, or PREF_DBLOG is "off" / "redact".
|
||||
gboolean log_database_can_recover_messages(void);
|
||||
|
||||
// Cross-backend export/import (requires HAVE_SQLITE)
|
||||
#ifdef HAVE_SQLITE
|
||||
int log_database_export_to_flatfile(const gchar* const contact_jid);
|
||||
|
||||
@@ -301,12 +301,6 @@ cl_ev_send_ai_msg(ProfAiWin* aiwin, const char* const message, const char* const
|
||||
return;
|
||||
}
|
||||
|
||||
/* Reset paged flag before printing user message.
|
||||
* If the user scrolled up to view history, paged=1 would suppress
|
||||
* the message in _win_printf(). Reset it here so the message displays. */
|
||||
aiwin->window.layout->paged = 0;
|
||||
aiwin->window.layout->unread_msg = 0;
|
||||
|
||||
// Display user message in AI window.
|
||||
win_print_outgoing(&aiwin->window, ">>", id, NULL, message);
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
#include "ui/ui.h"
|
||||
#include "ui/titlebar.h"
|
||||
#include "ui/inputwin.h"
|
||||
#include "ui/win_types.h"
|
||||
#include "ui/window_list.h"
|
||||
#include "ui/window.h"
|
||||
#include "ui/screen.h"
|
||||
@@ -252,6 +253,10 @@ _title_bar_draw(void)
|
||||
_show_muc_privacy(mucwin);
|
||||
_show_attention(current, mucwin->has_attention);
|
||||
_show_scrolled(current);
|
||||
} else if (current && current->type == WIN_AI) {
|
||||
ProfAiWin* aiwin = (ProfAiWin*)current;
|
||||
assert(aiwin->memcheck == PROFAIWIN_MEMCHECK);
|
||||
_show_scrolled(current);
|
||||
}
|
||||
|
||||
_show_self_presence();
|
||||
|
||||
@@ -848,6 +848,14 @@ win_page_down(ProfWin* window, int scroll_size)
|
||||
window->layout->paged = 0;
|
||||
window->layout->unread_msg = 0;
|
||||
}
|
||||
|
||||
// Non-chat windows have no DB to fetch from, so WIN_SCROLL_REACHED_BOTTOM
|
||||
// never fires; reset paged once the buffer's last line is on screen.
|
||||
if (window->type != WIN_CHAT
|
||||
&& (total_rows - *page_start) <= page_space + 1) {
|
||||
window->layout->paged = 0;
|
||||
window->layout->unread_msg = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
@@ -1801,10 +1809,12 @@ static void
|
||||
_win_printf(ProfWin* window, const char* show_char, int pad_indent, GDateTime* timestamp, int flags, theme_item_t theme_item, const char* const display_from, const char* const from_jid, const char* const message_id, const char* const message, ...)
|
||||
{
|
||||
|
||||
/* Prevent printing and buffer update when user is viewing message history [SCROLLING]*/
|
||||
if (window->layout->paged && wins_is_current(window)) {
|
||||
window->layout->unread_msg++;
|
||||
return;
|
||||
// Skip buffer only when DB can replay the message on scroll-down.
|
||||
if (window->type == WIN_CHAT && log_database_can_recover_messages()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (timestamp == NULL) {
|
||||
|
||||
Reference in New Issue
Block a user