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:
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "ias-http"
|
||||
version = "0.1.9"
|
||||
version = "0.1.13"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
+232
-16
@@ -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, "邮箱目录"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user