v0.8.6: general mode, clipboard text, PDF text extraction

Working mode: --mode coding|general (CST_MODE, settings.json, /mode,
WebUI run settings). general swaps the default framing for a general
assistant one -- workflow, safety notes, tool wording (run_shell, the
subagents) -- and /status shows it; minimal (the paper profile) stays
byte-frozen. The prompt state carries the mode so a switch leaves a
system note in history.

Clipboard: clipboard_read / clipboard_write tools (16 MiB cap read,
default 64 KiB window; writing is a WRITE permission with a preview) on
pbcopy / wl-copy / xclip / pbpaste / wl-paste / xclip and the Win32
clipboard. In the TUI, /attach paste falls back to clipboard text when
there is no image on it.

PDF: a dependency-free text extractor (src/pdf_text.c): inflate,
Flate/ASCIIHex/ASCII85, PNG/TIFF predictors, layout-independent object
scan, object streams, page tree walk with inherited resources, content
lexer (Tj/TJ/'/" and the Td/TD/T*/Tm spacing heuristics), per-font
ToUnicode CMaps and inline images skipped. Encrypted files and pages
with no text layer are refused with the reason.

Attachments: /attach (a path or files dropped on the terminal) now take
images, PDFs and text. A PDF is stored as its text layer with the mime
still saying application/pdf and its page count; anything else has to be
UTF-8 text. Documents travel inline as text parts, so CST_NO_IMAGES and
text-only models no longer drop them, while images keep their old path.

Tests: test_pdf_text (plain, bare and zlib-framed Flate, inline image,
CID + ToUnicode, object stream, error paths), documents in test_attach,
inlined documents in test_llm_body, mode in test_config.
This commit is contained in:
WHC
2026-10-11 11:09:39 +08:00
parent 50abcf1a91
commit e0b0eddf3a
28 changed files with 3291 additions and 111 deletions
+16
View File
@@ -263,6 +263,7 @@ static cJSON *wui_state_new(wui_t *w) {
cJSON_AddStringToObject(o, "provider", cfg->provider);
cJSON_AddStringToObject(o, "model", cfg->model);
cJSON_AddStringToObject(o, "effort", cfg->effort);
cJSON_AddStringToObject(o, "mode", cst_mode_name(cfg->mode));
cJSON_AddStringToObject(o, "approval", cst_approval_name(cfg->approval));
cJSON_AddStringToObject(o, "protocol_tools", cst_protocol_name(cfg->protocol));
cJSON_AddBoolToObject(o, "plan_mode", cfg->plan_mode);
@@ -1670,6 +1671,12 @@ static int h_settings_get(cst_http_conn_t *c, void *ud) {
cJSON *pr = cJSON_AddArrayToObject(o, "profiles");
for (int p = CST_TOOLS_DEFAULT; pr && p <= CST_TOOLS_MINIMAL; p++)
cJSON_AddItemToArray(pr, cJSON_CreateString(cst_tool_profile_name(p)));
cJSON_AddStringToObject(o, "mode", cst_mode_name(cfg->mode));
cJSON *md = cJSON_AddArrayToObject(o, "modes");
if (md) {
cJSON_AddItemToArray(md, cJSON_CreateString("coding"));
cJSON_AddItemToArray(md, cJSON_CreateString("general"));
}
cJSON *cf = cJSON_AddObjectToObject(o, "confirm");
if (cf) {
cJSON_AddBoolToObject(cf, "shell", cfg->confirm_shell);
@@ -1728,6 +1735,7 @@ static int h_settings_x(cst_http_conn_t *c, void *ud) {
const cJSON *proto = cJSON_GetObjectItem(j, "protocol");
const cJSON *steps = cJSON_GetObjectItem(j, "max_steps");
const cJSON *prof = cJSON_GetObjectItem(j, "tool_profile");
const cJSON *mode = cJSON_GetObjectItem(j, "mode");
const cJSON *tools = cJSON_GetObjectItem(j, "tools");
const cJSON *conf = cJSON_GetObjectItem(j, "confirm");
cst_protocol_t p = cfg->protocol;
@@ -1742,6 +1750,9 @@ static int h_settings_x(cst_http_conn_t *c, void *ud) {
else if (prof && (!cJSON_IsString(prof) ||
cst_tool_profile_parse(prof->valuestring, &profile) != 0))
snprintf(bad, sizeof(bad), "未知的工具集(default 或 minimal)");
else if (mode && (!cJSON_IsString(mode) ||
!cst_mode_valid(mode->valuestring)))
snprintf(bad, sizeof(bad), "工作模式只能是 coding / general");
else if (tools && !cJSON_IsObject(tools))
snprintf(bad, sizeof(bad), "tools 要是 {\"名字\": true/false}");
else if (conf && !cJSON_IsObject(conf))
@@ -1760,6 +1771,11 @@ static int h_settings_x(cst_http_conn_t *c, void *ud) {
}
cfg->protocol = p;
cfg->tool_profile = profile;
if (mode) {
snprintf(cfg->mode, sizeof(cfg->mode), "%s",
cst_mode_name(mode->valuestring));
cst_settings_remember_mode(cfg->mode);
}
if (steps)
cfg->max_steps = (int)steps->valuedouble;
for (const cJSON *t = tools ? tools->child : NULL; t; t = t->next)