feat: add Kimi Code and web APIs

This commit is contained in:
WHC
2026-10-19 10:41:00 +08:00
parent 60047ca5ce
commit 235652604d
21 changed files with 1189 additions and 49 deletions
+8
View File
@@ -159,6 +159,14 @@ static const cst_tool_def_t TOOLS[] = {
NULL,
{{"todos", S, 1, "the whole list, one item per line"}},
CST_TP_META, 0, cst_tool_todo_write, NULL, NO_DYN, NO_META},
{"web_search",
"Search the live web through the current Kimi or DeepSeek provider and "
"return titles, URLs and snippets. Use web_fetch to read a result.",
NULL,
{{"query", S, 1, "search query"},
{"count", I, 0, "number of results (default 5, max 20)"},
{"include_content", B, 0, "include provider-returned page content"}},
CST_TP_NET, 0, cst_tool_web_search, NULL, NO_DYN, NO_META},
{"web_fetch",
"Fetch an http(s) URL (docs, issues, APIs) and return its text; HTML "
"is reduced to readable text.",
+2
View File
@@ -55,6 +55,8 @@ int cst_preview_clipboard_write(const cst_tool_ctx_t *ctx,
const ctl_tool_call_t *c, char **out);
int cst_tool_web_fetch(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
cst_tool_result_t *r);
int cst_tool_web_search(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
cst_tool_result_t *r);
int cst_tool_download(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
cst_tool_result_t *r);
int cst_tool_md_to_html(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
+209
View File
@@ -6,6 +6,7 @@
#include <curl/curl.h>
#include "cJSON.h"
#include "cst_agent.h"
#include "cst_attach.h"
#include "cst_tools.h"
@@ -23,6 +24,211 @@ typedef struct {
int overflow;
} fetch_t;
static size_t on_body(char *p, size_t sz, size_t n, void *ud);
static const cst_agent_config_t *web_cfg(const cst_tool_ctx_t *ctx) {
return ctx && ctx->agent ? cst_agent_config(ctx->agent) : NULL;
}
static int provider_api(const cst_tool_ctx_t *ctx, const char **kind,
const char **base, const char **key) {
const cst_agent_config_t *cfg = web_cfg(ctx);
if (!cfg || !cfg->api_key[0])
return 0;
if (strcmp(cfg->provider, "deepseek") == 0 ||
(cfg->api_base[0] && strstr(cfg->api_base, "api.deepseek.com"))) {
*kind = "deepseek";
} else if (strcmp(cfg->provider, "kimi") == 0 ||
(cfg->api_base[0] && strstr(cfg->api_base, "moonshot"))) {
*kind = "kimi";
} else {
return 0;
}
*base = cfg->api_base;
*key = cfg->api_key;
return 1;
}
static int api_post_json(const char *url, const char *key, const char *body,
cst_buf_t *out, long *http) {
CURL *curl = curl_easy_init();
if (!curl)
return -1;
char auth[700];
snprintf(auth, sizeof(auth), "Authorization: Bearer %s", key);
struct curl_slist *hdrs = NULL;
hdrs = curl_slist_append(hdrs, "Content-Type: application/json");
hdrs = curl_slist_append(hdrs, "Accept: application/json");
hdrs = curl_slist_append(hdrs, auth);
fetch_t f = {0};
cst_curl_apply_ca(curl);
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, hdrs);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, body);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 15L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 90L);
curl_easy_setopt(curl, CURLOPT_NOSIGNAL, 1L);
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, on_body);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &f);
CURLcode rc = curl_easy_perform(curl);
*http = 0;
curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, http);
if (f.body.buf)
cst_buf_append(out, f.body.buf, f.body.len);
cst_buf_free(&f.body);
curl_slist_free_all(hdrs);
curl_easy_cleanup(curl);
return rc == CURLE_OK ? 0 : -1;
}
static void api_path(char *out, size_t n, const char *base, const char *path) {
size_t len = strlen(base);
while (len && base[len - 1] == '/')
len--;
snprintf(out, n, "%.*s/%s", (int)len, base, path);
}
static int api_error(cst_tool_result_t *r, long http, const cst_buf_t *raw) {
cJSON *j = cJSON_Parse(raw->buf ? raw->buf : "");
const cJSON *e = j ? cJSON_GetObjectItemCaseSensitive(j, "error") : NULL;
const cJSON *m = e ? cJSON_GetObjectItemCaseSensitive(e, "message") : NULL;
int rc;
if (cJSON_IsString(m))
rc = tool_set_resultf(r, 1, "error: HTTP %ld: %s", http, m->valuestring);
else
rc = tool_set_resultf(r, 1, "error: HTTP %ld: %.400s", http,
raw->buf ? raw->buf : "no response");
cJSON_Delete(j);
return rc;
}
static int online_fetch(const cst_tool_ctx_t *ctx, const char *url,
long max_out, cst_tool_result_t *r) {
const char *kind, *base, *key;
if (!provider_api(ctx, &kind, &base, &key))
return 0;
if (max_out <= 0)
max_out = FETCH_DEFAULT_OUT;
if (max_out > FETCH_MAX_OUT)
max_out = FETCH_MAX_OUT;
char endpoint[1024];
api_path(endpoint, sizeof(endpoint), base, "tools/fetch");
cJSON *body = cJSON_CreateObject();
cJSON_AddStringToObject(body, "url", url);
char *json = cJSON_PrintUnformatted(body);
cJSON_Delete(body);
cst_buf_t raw = {0};
long http = 0;
int rc = json ? api_post_json(endpoint, key, json, &raw, &http) : -1;
free(json);
if (rc != 0) {
cst_buf_free(&raw);
return tool_set_result(r, "error: online fetch request failed", 1);
}
if (http >= 400) {
int e = api_error(r, http, &raw);
cst_buf_free(&raw);
return e;
}
cJSON *doc = cJSON_Parse(raw.buf ? raw.buf : "");
if (!doc) {
cst_buf_free(&raw);
return tool_set_result(r, "error: online fetch returned invalid JSON", 1);
}
const cJSON *text = cJSON_GetObjectItemCaseSensitive(
doc, strcmp(kind, "kimi") == 0 ? "markdown" : "content");
const cJSON *title = cJSON_GetObjectItemCaseSensitive(doc, "title");
if (!cJSON_IsString(text)) {
cJSON_Delete(doc);
cst_buf_free(&raw);
return tool_set_result(r, "error: online fetch returned no readable content", 1);
}
cst_buf_t out = {0};
cst_buf_printf(&out, "%s\n\n%s", cJSON_IsString(title) ? title->valuestring : url,
cJSON_IsString(text) ? text->valuestring : "");
size_t keep = out.len > (size_t)max_out ? cst_utf8_floor(out.buf, (size_t)max_out)
: out.len;
cst_buf_t clipped = {0};
cst_buf_append(&clipped, out.buf ? out.buf : "", keep);
if (keep < out.len)
cst_buf_puts(&clipped, "\n...(truncated)\n");
int result = tool_take_buf(r, &clipped, 0);
cst_buf_free(&out);
cJSON_Delete(doc);
cst_buf_free(&raw);
return result;
}
int cst_tool_web_search(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
cst_tool_result_t *r) {
const char *query = tool_arg(c, "query");
const char *kind, *base, *key;
if (!query || !query[0])
return tool_set_result(r, "error: missing query", 1);
if (!provider_api(ctx, &kind, &base, &key))
return tool_set_result(
r, "error: web_search needs the kimi or deepseek provider", 1);
long count = tool_arg_long(c, "count", 5);
if (count < 1) count = 1;
if (count > 20) count = 20;
int content = tool_arg_bool(c, "include_content", 0);
cJSON *body = cJSON_CreateObject();
cJSON_AddStringToObject(body, strcmp(kind, "kimi") == 0 ? "text_query" : "query", query);
cJSON_AddNumberToObject(body, strcmp(kind, "kimi") == 0 ? "limit" : "count", count);
if (strcmp(kind, "kimi") == 0)
cJSON_AddBoolToObject(body, "include_content", content);
else
cJSON_AddBoolToObject(body, "need_content", content);
char *json = cJSON_PrintUnformatted(body);
cJSON_Delete(body);
char endpoint[1024];
api_path(endpoint, sizeof(endpoint), base, "tools/search");
cst_buf_t raw = {0};
long http = 0;
int rc = json ? api_post_json(endpoint, key, json, &raw, &http) : -1;
free(json);
if (rc != 0) {
cst_buf_free(&raw);
return tool_set_result(r, "error: web search request failed", 1);
}
if (http >= 400) {
int e = api_error(r, http, &raw);
cst_buf_free(&raw);
return e;
}
cJSON *doc = cJSON_Parse(raw.buf ? raw.buf : "");
if (!doc) {
cst_buf_free(&raw);
return tool_set_result(r, "error: web search returned invalid JSON", 1);
}
const cJSON *arr = cJSON_GetObjectItemCaseSensitive(
doc, strcmp(kind, "kimi") == 0 ? "search_results" : "results");
if (!cJSON_IsArray(arr)) {
cJSON_Delete(doc);
cst_buf_free(&raw);
return tool_set_result(r, "error: web search returned no result list", 1);
}
cst_buf_t out = {0};
int n = cJSON_GetArraySize(arr);
cst_buf_printf(&out, "web search (%s): %d result%s\n", kind, n, n == 1 ? "" : "s");
for (int i = 0; i < n; i++) {
const cJSON *item = cJSON_GetArrayItem(arr, i);
const cJSON *title = cJSON_GetObjectItemCaseSensitive(item, "title");
const cJSON *url = cJSON_GetObjectItemCaseSensitive(item, "url");
const cJSON *snippet = cJSON_GetObjectItemCaseSensitive(
item, strcmp(kind, "kimi") == 0 ? (content ? "text" : "snippet") :
(content ? "summary" : "snippet"));
cst_buf_printf(&out, "\n[%d] %s\n%s\n%s\n", i + 1,
cJSON_IsString(title) ? title->valuestring : "(untitled)",
cJSON_IsString(url) ? url->valuestring : "",
cJSON_IsString(snippet) ? snippet->valuestring : "");
}
int result = tool_take_buf(r, &out, 0);
cJSON_Delete(doc);
cst_buf_free(&raw);
return result;
}
static size_t on_body(char *p, size_t sz, size_t n, void *ud) {
fetch_t *f = ud;
size_t len = sz * n;
@@ -206,6 +412,9 @@ int cst_tool_web_fetch(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
if (strncasecmp(url, "http://", 7) != 0 &&
strncasecmp(url, "https://", 8) != 0)
return tool_set_result(r, "error: only http:// and https:// URLs", 1);
const char *online_kind, *online_base, *online_key;
if (provider_api(ctx, &online_kind, &online_base, &online_key))
return online_fetch(ctx, url, tool_arg_long(c, "max_bytes", FETCH_DEFAULT_OUT), r);
long max_out = tool_arg_long(c, "max_bytes", FETCH_DEFAULT_OUT);
if (max_out <= 0)
max_out = FETCH_DEFAULT_OUT;