Files
Hermit/src/util.c
T
WHC 2d1052fd5f
ci / linux (clang) (push) Successful in 5m56s
ci / linux (gcc) (push) Successful in 8m38s
ci / musl (push) Successful in 7m22s
ci / windows-cross (push) Successful in 9m21s
v0.8.3: WebUI models, providers and keys; garbled thinking fixed
Streaming: Chinese reasoning showed up garbled in the WebUI. The agent
loop now feeds visible/thinking text in runs instead of byte by byte,
and the WebUI holds an incomplete UTF-8 tail until the next chunk
(cst_utf8_hold_feed), so every think_delta/assistant_delta is whole.

src/provider_switch.c: model list fetch/cache, key verify-and-save and
provider switching moved out of tui.c and shared by TUI and WebUI.

WebUI API:
- GET /api/providers, GET /api/models[?refresh=1] (401 on a rejected
  key, still listing built-ins), POST /api/provider, POST /api/key
  (key checked against /v1/models, saved to credentials 0600, never
  echoed in replies, events or logs)
- /api/config validates everything before applying, persists model,
  effort and approval (not yolo), warns when effort does not apply;
  plan_mode no longer forces read-only
- state: turn_usage, api_base, key_set/key_from, model_info,
  effort_applies, sees_images, max_steps, tool_profile, perm_timeout;
  permission events carry their timeout

Frontend: searchable model picker with refresh and manual entry,
provider picker and key dialog, effort/vision hints, toasts instead of
silent failures, Chinese error text, real protocol in the settings and
usage panels, approval countdown from the server's timeout.

Tests: utf8 hold unit test, scripts/e2e_webui_settings.py (fake
/v1/models, wrong key, switching, persistence, Chinese thinking) run
from e2e_webui.sh, scripts/tui_keys/provider.py.
2026-10-09 15:54:53 +08:00

393 lines
8.5 KiB
C

#include "cst_util.h"
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <wchar.h>
#include <wctype.h>
static int buf_reserve(cst_buf_t *b, size_t extra) {
if (b->len + extra + 1 <= b->cap)
return 0;
size_t nc = b->cap ? b->cap : 256;
while (nc < b->len + extra + 1)
nc *= 2;
char *nb = realloc(b->buf, nc);
if (!nb)
return -1;
b->buf = nb;
b->cap = nc;
return 0;
}
int cst_buf_append(cst_buf_t *b, const char *s, size_t n) {
if (!b)
return -1;
if (buf_reserve(b, n) != 0)
return -1;
if (n)
memcpy(b->buf + b->len, s, n);
b->len += n;
b->buf[b->len] = 0;
return 0;
}
int cst_buf_puts(cst_buf_t *b, const char *s) {
return cst_buf_append(b, s ? s : "", s ? strlen(s) : 0);
}
int cst_buf_printf(cst_buf_t *b, const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
va_list ap2;
va_copy(ap2, ap);
int n = vsnprintf(NULL, 0, fmt, ap);
va_end(ap);
if (n < 0 || buf_reserve(b, (size_t)n) != 0) {
va_end(ap2);
return -1;
}
vsnprintf(b->buf + b->len, (size_t)n + 1, fmt, ap2);
va_end(ap2);
b->len += (size_t)n;
return 0;
}
void cst_buf_free(cst_buf_t *b) {
if (!b)
return;
free(b->buf);
b->buf = NULL;
b->len = b->cap = 0;
}
char *cst_buf_steal(cst_buf_t *b) {
if (buf_reserve(b, 0) != 0)
return NULL;
char *s = b->buf;
b->buf = NULL;
b->len = b->cap = 0;
return s;
}
size_t cst_utf8_floor(const char *s, size_t n) {
if (!s || n == 0)
return 0;
size_t i = n;
/* walk back over at most 3 continuation bytes to the lead byte */
size_t back = 0;
while (i > 0 && back < 3 && ((unsigned char)s[i - 1] & 0xC0) == 0x80) {
i--;
back++;
}
if (i == 0)
return n;
unsigned char lead = (unsigned char)s[i - 1];
size_t need = 1;
if (lead >= 0xF0)
need = 4;
else if (lead >= 0xE0)
need = 3;
else if (lead >= 0xC0)
need = 2;
if (need == 1)
return n; /* ASCII lead; any continuation bytes after it are invalid
anyway and get sanitized later */
if (back + 1 >= need)
return n;
return i - 1; /* sequence incomplete: cut before lead byte */
}
char *cst_utf8_hold_feed(cst_utf8_hold_t *h, const char *s, size_t n,
size_t *out_len) {
size_t have = h->n;
char *all = malloc(have + n + 1);
if (!all)
return NULL;
memcpy(all, h->b, have);
if (n)
memcpy(all + have, s, n);
size_t len = have + n;
size_t cut = cst_utf8_floor(all, len);
if (len - cut > sizeof(h->b))
cut = len;
h->n = len - cut;
memcpy(h->b, all + cut, h->n);
all[cut] = 0;
*out_len = cut;
return all;
}
static size_t utf8_seq_ok(const unsigned char *p, size_t left) {
unsigned char c = p[0];
size_t need;
unsigned cp;
if (c < 0x80)
return 1;
if ((c & 0xE0) == 0xC0) {
need = 2;
cp = c & 0x1F;
} else if ((c & 0xF0) == 0xE0) {
need = 3;
cp = c & 0x0F;
} else if ((c & 0xF8) == 0xF0) {
need = 4;
cp = c & 0x07;
} else {
return 0;
}
if (left < need)
return 0;
for (size_t i = 1; i < need; i++) {
if ((p[i] & 0xC0) != 0x80)
return 0;
cp = (cp << 6) | (p[i] & 0x3F);
}
if ((need == 2 && cp < 0x80) || (need == 3 && cp < 0x800) ||
(need == 4 && (cp < 0x10000 || cp > 0x10FFFF)) ||
(cp >= 0xD800 && cp <= 0xDFFF))
return 0;
return need;
}
char *cst_utf8_sanitize(const char *s, size_t n, size_t *out_len) {
char *out = malloc(n + 1);
if (!out)
return NULL;
size_t o = 0;
const unsigned char *p = (const unsigned char *)(s ? s : "");
size_t i = 0;
while (i < n) {
size_t k = utf8_seq_ok(p + i, n - i);
if (k == 0) {
out[o++] = '?';
i++;
continue;
}
memcpy(out + o, p + i, k);
o += k;
i += k;
}
out[o] = 0;
if (out_len)
*out_len = o;
return out;
}
int cst_utf8_valid(const char *s, size_t n) {
const unsigned char *p = (const unsigned char *)s;
for (size_t i = 0; i < n;) {
size_t k = utf8_seq_ok(p + i, n - i);
if (k == 0)
return 0;
i += k;
}
return 1;
}
size_t cst_bom_len(const char *s, size_t n) {
return n >= 3 && memcmp(s, "\xEF\xBB\xBF", 3) == 0 ? 3 : 0;
}
void cst_strip_bom(char *s, size_t *len) {
if (!s)
return;
size_t n = len ? *len : strlen(s);
if (!cst_bom_len(s, n))
return;
memmove(s, s + 3, n - 3 + 1);
if (len)
*len = n - 3;
}
size_t cst_crlf_to_lf(char *s, size_t n) {
size_t o = 0;
for (size_t i = 0; i < n; i++)
if (!(s[i] == '\r' && i + 1 < n && s[i + 1] == '\n'))
s[o++] = s[i];
if (o < n)
s[o] = 0;
return o;
}
int cst_text_is_crlf(const char *s, size_t n) {
int crlf = 0;
for (size_t i = 0; s && i < n; i++) {
if (s[i] != '\n')
continue;
if (i == 0 || s[i - 1] != '\r')
return 0;
crlf = 1;
}
return crlf;
}
char *cst_text_restyle(const char *s, size_t n, int crlf, int bom,
size_t *out_len) {
size_t skip = cst_bom_len(s, n), extra = bom ? 3 : 0;
for (size_t i = skip; crlf && i < n; i++)
extra += s[i] == '\n' && (i == skip || s[i - 1] != '\r');
char *o = malloc(n - skip + extra + 1);
if (!o)
return NULL;
size_t k = 0;
if (bom) {
memcpy(o, "\xEF\xBB\xBF", 3);
k = 3;
}
for (size_t i = skip; i < n; i++) {
if (crlf && s[i] == '\n' && (i == skip || s[i - 1] != '\r'))
o[k++] = '\r';
o[k++] = s[i];
}
o[k] = 0;
if (out_len)
*out_len = k;
return o;
}
/* Next code point of a UTF-8 string; a stray byte stands for itself. */
static int utf8_next(const char **ps) {
const unsigned char *p = (const unsigned char *)*ps;
size_t left = 0;
while (left < 4 && p[left])
left++;
size_t k = utf8_seq_ok(p, left);
if (k <= 1) {
*ps += 1;
return p[0];
}
int c = p[0] & (0x7F >> k);
for (size_t i = 1; i < k; i++)
c = (c << 6) | (p[i] & 0x3F);
*ps += k;
return c;
}
static int fold_cp(int c) {
if (c >= 'A' && c <= 'Z')
return c + 32;
if (c > 0x7F && c <= 0xFFFF)
return (int)towlower((wint_t)c);
return c;
}
/* p points just past '['. 1/0 = (no) match with *pp moved past ']';
* -1 = no closing ']' (the '[' is then a literal). */
static int fnm_bracket(const char **pp, int c, int fold) {
const char *p = *pp;
int neg = 0, hit = 0;
if (*p == '!' || *p == '^') {
neg = 1;
p++;
}
int fc = fold ? fold_cp(c) : c;
for (int first = 1; *p && (*p != ']' || first); first = 0) {
if (p[0] == '[' && p[1] == ':') {
const char *e = strstr(p + 2, ":]");
if (e) {
char name[16];
size_t nl = (size_t)(e - (p + 2));
if (nl < sizeof(name)) {
memcpy(name, p + 2, nl);
name[nl] = 0;
wctype_t t = wctype(name);
if (t && c <= 0xFFFF && iswctype((wint_t)c, t))
hit = 1;
}
p = e + 2;
continue;
}
}
if (*p == '\\' && p[1])
p++;
int lo = utf8_next(&p), hi = lo;
if (p[0] == '-' && p[1] && p[1] != ']') {
p++;
if (*p == '\\' && p[1])
p++;
hi = utf8_next(&p);
}
if ((c >= lo && c <= hi) ||
(fold && fc >= fold_cp(lo) && fc <= fold_cp(hi)))
hit = 1;
}
if (*p != ']')
return -1;
*pp = p + 1;
return hit != neg;
}
int cst_fnmatch(const char *pat, const char *str, int flags) {
#ifdef _WIN32
flags |= CST_FNM_CASEFOLD;
#endif
int fold = (flags & CST_FNM_CASEFOLD) != 0;
const char *p = pat, *s = str;
const char *star_p = NULL, *star_s = NULL;
if ((flags & CST_FNM_PERIOD) && s[0] == '.' && p[0] != '.' &&
!(p[0] == '\\' && p[1] == '.'))
return 1;
while (*s) {
if (*p == '*') {
while (*p == '*')
p++;
if (!*p)
return 0;
star_p = p;
star_s = s;
continue;
}
const char *sn = s;
int c = utf8_next(&sn);
if (*p == '?') {
p++;
s = sn;
continue;
}
if (*p == '[') {
const char *q = p + 1;
int r = fnm_bracket(&q, c, fold);
if (r == 1) {
p = q;
s = sn;
continue;
}
if (r == 0)
goto backtrack;
}
{
const char *q = p;
if (*q == '\\' && q[1])
q++;
if (*q) {
int pc = utf8_next(&q);
if (pc == c || (fold && fold_cp(pc) == fold_cp(c))) {
p = q;
s = sn;
continue;
}
}
}
backtrack:
if (!star_p)
return 1;
utf8_next(&star_s);
s = star_s;
p = star_p;
}
while (*p == '*')
p++;
return *p ? 1 : 0;
}
uint64_t cst_fnv1a64(uint64_t h, const void *data, size_t n) {
const unsigned char *p = data;
for (size_t i = 0; i < n; i++) {
h ^= p[i];
h *= 1099511628211ULL;
}
return h;
}