feat: AI 工具架构重构与微信增强 (0.2.17 → 0.2.22)

0.2.17 收束模型初始工具列表:
- 仅暴露 query_capabilities/query_capability_detail/call_capability 三个入口
- API 能力统一通过 iAs HTTP API 封装,删除重复 YAML 工具

0.2.18 系统提示词拆分:
- 固定系统提示词硬编码,动态附加部分 AI 可维护
- 新增 update_additional_system_prompt 能力和持久化

0.2.19 上下文增强与打断语义:
- /new 拥有最高打断语义,取消运行中任务并清除当前 turn
- raw_message JSONB 原文入库,群聊按 group_id 隔离作用域
- 工具调用说明替代动态附加系统提示词

0.2.20 微信输入中状态:
- 改用官方 getconfig + sendtyping 流程,显式管理 typing 状态

0.2.21 API 文档功能分组:
- /v1/api-docs 返回 groups,帮助 AI 按功能面发现接口

0.2.22 AI API 能力去重:
- 移除与 HTTP endpoint 重复的专用能力,统一用 query_api_docs + call_http_api
- 删除不再编译的 web/memory 工具文件
This commit is contained in:
2026-07-08 14:58:13 +08:00
parent 279d89b87a
commit 7d5140c8d2
49 changed files with 2330 additions and 1185 deletions
+232 -16
View File
@@ -4,6 +4,7 @@ use axum::routing::get;
use axum::{Json, Router};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use std::collections::BTreeSet;
type ApiError = (StatusCode, Json<Value>);
@@ -23,6 +24,15 @@ pub struct ApiDoc {
pub notes: &'static [&'static str],
}
#[derive(Debug, Clone, Copy, Serialize)]
pub struct ApiGroupDoc {
pub id: &'static str,
pub title: &'static str,
pub description: &'static str,
pub aspects: &'static [&'static str],
pub keywords: &'static [&'static str],
}
#[derive(Debug, Deserialize)]
struct ApiDocQuery {
q: Option<String>,
@@ -51,15 +61,29 @@ async fn list_api_docs(Query(query): Query<ApiDocQuery>) -> Json<Value> {
.as_deref()
.map_or(true, |group| doc.group.eq_ignore_ascii_case(group))
})
.filter(|doc| q.as_deref().map_or(true, |q| doc_matches(doc, q)))
.filter(|doc| q.as_deref().map_or(true, |q| doc_or_group_matches(doc, q)))
.collect::<Vec<_>>();
let total = docs.len();
let matched_groups = docs.iter().map(|doc| doc.group).collect::<BTreeSet<_>>();
let groups = api_groups()
.into_iter()
.filter(|api_group| {
group
.as_deref()
.map_or(true, |group| api_group.id.eq_ignore_ascii_case(group))
})
.filter(|api_group| {
q.as_deref().map_or(true, |q| group_matches(api_group, q))
|| matched_groups.contains(api_group.id)
})
.collect::<Vec<_>>();
docs.truncate(limit);
Json(json!({
"success": true,
"total": total,
"limit": limit,
"groups": groups,
"docs": docs,
}))
}
@@ -72,10 +96,121 @@ async fn get_api_doc(Path(id): Path<String>) -> Result<Json<Value>, ApiError> {
Ok(Json(json!({
"success": true,
"group_doc": api_group(doc.group),
"doc": doc,
})))
}
fn api_groups() -> Vec<ApiGroupDoc> {
vec![
ApiGroupDoc {
id: "system",
title: "系统状态",
description: "用于判断 iAs HTTP 服务是否可访问,适合健康检查、启动验证和连通性排障。",
aspects: &["服务健康检查", "本地 HTTP 连通性验证"],
keywords: &["health", "健康", "存活", "连通", "状态"],
},
ApiGroupDoc {
id: "updates",
title: "版本与升级日志",
description: "用于查询当前项目版本、历史升级记录、模块版本和用户可感知变更。",
aspects: &[
"升级日志列表",
"单版本升级详情",
"模块版本记录",
"数据库迁移说明",
],
keywords: &["version", "版本", "升级", "更新", "changelog", "migration"],
},
ApiGroupDoc {
id: "api-docs",
title: "HTTP API 自描述",
description: "用于让控制台、调试器和 AI 查询 iAs HTTP API 的功能面、接口路径、参数、请求体、响应示例和备注。",
aspects: &[
"API 功能分组",
"接口搜索",
"接口详情",
"请求参数说明",
"响应示例",
],
keywords: &["api", "接口", "文档", "路径", "参数", "请求体", "响应"],
},
ApiGroupDoc {
id: "web",
title: "动态页面与预览",
description: "用于创建、查询和读取可分享的动态内容页面,包括普通页面、Markdown 预览和邮件预览页面。",
aspects: &[
"动态页面列表",
"动态页面创建",
"页面详情读取",
"邮件预览页",
"Markdown 预览",
],
keywords: &[
"web",
"页面",
"链接",
"预览",
"markdown",
"mail page",
"分享",
],
},
ApiGroupDoc {
id: "mail",
title: "邮件记录",
description: "用于查询已拉取邮件、搜索邮件、读取邮件详情,并在读取未读邮件时同步标记已读。",
aspects: &[
"邮件列表",
"邮件搜索",
"邮件详情",
"已读状态",
"邮箱目录过滤",
],
keywords: &["mail", "email", "邮件", "邮箱", "未读", "已读", "搜索邮件"],
},
ApiGroupDoc {
id: "console",
title: "控制台配置与能力清单",
description: "用于查看当前模型入口工具、API 能力、外部 HTTP 能力,以及微信/邮件账号的非敏感配置概览。",
aspects: &[
"工具与能力清单",
"API 能力列表",
"HTTP YAML 能力列表",
"微信账号配置",
"邮件账号配置",
],
keywords: &["console", "工具", "能力", "配置", "账号", "wechat", "yaml"],
},
ApiGroupDoc {
id: "context",
title: "上下文、记忆与工具调用说明",
description: "用于维护会话作用域内的长期记忆、工具调用说明,以及查询上下文模拟器数据辅助调试模型输入。",
aspects: &[
"长期记忆写入",
"长期记忆查询",
"长期记忆更新",
"长期记忆删除",
"工具调用说明",
"上下文模拟器",
],
keywords: &[
"context",
"上下文",
"记忆",
"memory",
"工具调用说明",
"模拟器",
"scope",
],
},
]
}
fn api_group(id: &str) -> Option<ApiGroupDoc> {
api_groups().into_iter().find(|group| group.id == id)
}
fn api_docs() -> Vec<ApiDoc> {
vec![
ApiDoc {
@@ -126,17 +261,20 @@ fn api_docs() -> Vec<ApiDoc> {
method: "GET",
path: "/v1/api-docs",
summary: "搜索或列出 HTTP API 文档",
description: "查询随服务发布的结构化 API 文档,供控制台、调试器和 AI 工具使用",
description: "查询随服务发布的结构化 API 文档和功能分组说明,供控制台、调试器和 AI 工具先判断有哪些 API 能力面向,再按关键词或 endpoint id 查询具体接口",
auth: "",
query_params: &[
"q: 可选关键词,会匹配 id、分组、路径、摘要、说明、参数、响应和备注",
"q: 可选关键词,会匹配功能分组、id、路径、摘要、说明、参数、响应和备注",
"group: 可选分组,例如 web、mail、context、updates、console",
"limit: 可选返回数量,默认 50,范围 1..100",
],
path_params: &[],
body: None,
response: r#"{"success":true,"total":12,"limit":50,"docs":[{"id":"web.pages.list","method":"GET","path":"/v1/web/pages",...}]}"#,
notes: &["该接口只返回 API 文档,不返回 React 页面内容。"],
response: r#"{"success":true,"total":12,"limit":50,"groups":[{"id":"web","title":"","description":"...","aspects":["",...]}],"docs":[{"id":"web.pages.list","method":"GET","path":"/v1/web/pages",...}]}"#,
notes: &[
"groups 描述 HTTP API 的功能面,AI 不确定该查哪个接口时应先看 groups。",
"docs 描述具体 endpoint;需要完整请求体、响应示例或备注时再查 /v1/api-docs/{id}。",
],
},
ApiDoc {
id: "api-docs.get",
@@ -144,12 +282,12 @@ fn api_docs() -> Vec<ApiDoc> {
method: "GET",
path: "/v1/api-docs/{id}",
summary: "查询单个 HTTP API 文档",
description: "根据 endpoint id 返回完整 API 文档。",
description: "根据 endpoint id 返回完整 API 文档,并附带该接口所属功能分组的说明",
auth: "",
query_params: &[],
path_params: &["id: API 文档 id,例如 web.pages.list"],
body: None,
response: r#"{"success":true,"doc":{"id":"web.pages.list","method":"GET","path":"/v1/web/pages",...}}"#,
response: r#"{"success":true,"group_doc":{"id":"web","title":"",...},"doc":{"id":"web.pages.list","method":"GET","path":"/v1/web/pages",...}}"#,
notes: &["id 不存在时返回 404。"],
},
ApiDoc {
@@ -281,13 +419,16 @@ fn api_docs() -> Vec<ApiDoc> {
method: "GET",
path: "/v1/tools",
summary: "查询可用工具列表",
description: "返回内置 AI 工具和外部 capability 工具的名称、参数、来源和警告信息。",
description: "返回模型入口工具、API 能力和 YAML HTTP 能力的名称、参数、来源和警告信息。",
auth: "",
query_params: &[],
path_params: &[],
body: None,
response: r#"{"success":true,"total":1,"tools_path":"...","builtin_tools":[...],"capability_tools":[...],"warnings":[]}"#,
notes: &["该接口用于控制台展示和调试工具能力。"],
response: r#"{"success":true,"total":3,"tools_path":"...","entry_tools":[...],"api_tools":[...],"http_tools":[...],"warnings":[]}"#,
notes: &[
"entry_tools 是初始暴露给模型的三入口工具。",
"api_tools 来自 iAs HTTP API 封装;http_tools 来自 tools/*.yaml 外部 API 配置。",
],
},
ApiDoc {
id: "console.user-configs.list",
@@ -314,7 +455,7 @@ fn api_docs() -> Vec<ApiDoc> {
query_params: &[],
path_params: &[],
body: Some(
r#"{"channel":"wechat","account_id":"...","from_user_id":"...","context":{},"content":"记忆内容","metadata":{}}"#,
r#"{"channel":"wechat","account_id":"...","from_user_id":"...","group_id":"...","context":{},"content":"记忆内容","metadata":{}}"#,
),
response: r#"{"success":true,"memory":{"id":1,"scope_key":"wechat:...","content":"...","metadata":{},"created_at":"...","updated_at":"..."}}"#,
notes: &["content 必填且不能为空。"],
@@ -330,7 +471,7 @@ fn api_docs() -> Vec<ApiDoc> {
query_params: &[],
path_params: &[],
body: Some(
r#"{"channel":"wechat","account_id":"...","from_user_id":"...","context":{},"query":"关键词","limit":20}"#,
r#"{"channel":"wechat","account_id":"...","from_user_id":"...","group_id":"...","context":{},"query":"关键词","limit":20}"#,
),
response: r#"{"success":true,"memories":[{"id":1,"content":"...","created_at":"...","updated_at":"..."}]}"#,
notes: &["limit 默认 20,范围 1..50。"],
@@ -346,7 +487,7 @@ fn api_docs() -> Vec<ApiDoc> {
query_params: &[],
path_params: &["id: 长期记忆 id"],
body: Some(
r#"{"channel":"wechat","account_id":"...","from_user_id":"...","context":{},"content":"新内容","metadata":{}}"#,
r#"{"channel":"wechat","account_id":"...","from_user_id":"...","group_id":"...","context":{},"content":"新内容","metadata":{}}"#,
),
response: r#"{"success":true,"memory":{"id":1,"content":"","updated_at":"..."}}"#,
notes: &["content 必填且不能为空;记忆不存在时返回 404。"],
@@ -362,27 +503,63 @@ fn api_docs() -> Vec<ApiDoc> {
query_params: &[],
path_params: &["id: 长期记忆 id"],
body: Some(
r#"{"channel":"wechat","account_id":"...","from_user_id":"...","context":{}}"#,
r#"{"channel":"wechat","account_id":"...","from_user_id":"...","group_id":"...","context":{}}"#,
),
response: r#"{"success":true,"deleted":true}"#,
notes: &["找不到可删除记忆时 deleted 为 false。"],
},
ApiDoc {
id: "context.tool-instructions.get",
group: "context",
method: "POST",
path: "/v1/context/tool-instructions",
summary: "查询工具调用说明",
description: "查询指定作用域下的工具调用说明。该说明用于 query_capabilities 展示工具调用规则、调用顺序和偏好,不进入系统提示词。",
auth: "需要 x-ias-context-token 或 Authorization: Bearer",
query_params: &[],
path_params: &[],
body: Some(
r#"{"channel":"wechat","account_id":"...","from_user_id":"...","group_id":"...","context":{}}"#,
),
response: r#"{"success":true,"instruction":{"scope_key":"wechat:...","content":"...","updated_at":"..."}}"#,
notes: &["没有配置时 instruction 为 null。"],
},
ApiDoc {
id: "context.tool-instructions.update",
group: "context",
method: "PATCH",
path: "/v1/context/tool-instructions",
summary: "记录工具调用说明",
description: "在指定作用域下写入或清空工具调用说明,用于保存用户提供的工具调用规则、稳定调用顺序、优先使用内部 API 等偏好,或 AI 在调用中总结出的可复用经验。",
auth: "需要 x-ias-context-token 或 Authorization: Bearer",
query_params: &[],
path_params: &[],
body: Some(
r#"{"channel":"wechat","account_id":"...","from_user_id":"...","group_id":"...","context":{},"content":"先查时间再查天气;优先使用内部 API。","reason":"用户要求"}"#,
),
response: r#"{"success":true,"instruction":{"scope_key":"wechat:...","content":"...","updated_at":"..."}}"#,
notes: &[
"content 传空字符串表示清空工具调用说明。",
"工具调用说明只随 query_capabilities 展示,不会插入系统提示词。",
],
},
ApiDoc {
id: "context.simulator.get",
group: "context",
method: "GET",
path: "/v1/context/simulator",
summary: "查询上下文模拟器数据",
description: "返回调试上下文所需的工具列表、默认消息、会话、消息、记忆和摘要数据。",
description: "返回调试上下文所需的入口工具列表、默认消息、会话、消息、记忆和摘要数据。",
auth: "",
query_params: &[
"channel: 可选渠道,默认 wechat",
"account_id: 可选账号 id",
"from_user_id: 可选用户 id",
"group_id: 可选群聊 id;提供时按群聊 scope 查询",
],
path_params: &[],
body: None,
response: r#"{"success":true,"has_query":true,"scope_key":"wechat:...","tools":[...],"default_messages":[...],"sessions":[...],"messages":[...],"memories":[...],"summaries":[...]}"#,
response: r#"{"success":true,"has_query":true,"scope_key":"wechat:...","tools":[...],"default_system_prompt":"...","tool_call_instruction":"...","default_messages":[...],"sessions":[...],"messages":[...],"memories":[...],"summaries":[...]}"#,
notes: &["未提供 account_id/from_user_id 时只返回默认调试数据。"],
},
]
@@ -419,6 +596,23 @@ fn doc_matches(doc: &ApiDoc, query: &str) -> bool {
.any(|field| field.to_ascii_lowercase().contains(&query))
}
fn doc_or_group_matches(doc: &ApiDoc, query: &str) -> bool {
doc_matches(doc, query)
|| api_group(doc.group).map_or(false, |group| group_matches(&group, query))
}
fn group_matches(group: &ApiGroupDoc, query: &str) -> bool {
let query = query.to_ascii_lowercase();
[group.id, group.title, group.description]
.iter()
.any(|field| field.to_ascii_lowercase().contains(&query))
|| group
.aspects
.iter()
.chain(group.keywords.iter())
.any(|field| field.to_ascii_lowercase().contains(&query))
}
fn api_not_found(message: impl Into<String>) -> ApiError {
(
StatusCode::NOT_FOUND,
@@ -449,4 +643,26 @@ mod tests {
assert!(api_docs().iter().any(|doc| doc.id == "api-docs.list"));
assert!(api_docs().iter().any(|doc| doc.id == "api-docs.get"));
}
#[test]
fn every_doc_has_group_description() {
for doc in api_docs() {
assert!(
api_group(doc.group).is_some(),
"missing group {}",
doc.group
);
}
}
#[test]
fn group_description_makes_docs_discoverable() {
let mail_doc = api_docs()
.into_iter()
.find(|doc| doc.id == "mail.messages.list")
.expect("mail list doc exists");
assert!(doc_or_group_matches(&mail_doc, "未读"));
assert!(doc_or_group_matches(&mail_doc, "邮箱目录"));
}
}