基于对项目全部核心源码的深度审查,从架构设计、代码质量、性能、安全、可维护性、功能完善六个维度给出具体的优化建议。
审查日期:2026-02-07 审查版本:v1.1
涉及文件:src/controllers/AiApi.cc:146-241, src/controllers/AiApi.cc:1009-1120
流式请求(stream=true)的处理流程是:
1. 创建 CollectorSink(内存收集器)
2. 调用 genService.runGuarded(genReq, collector, ...) ← 同步等待整个生成完成
3. 将 CollectorSink 中收集的事件复放给 ChatSseSink/ResponsesSseSink
4. 将 SSE payload 一次性通过 newStreamResponse 返回
实际代码:
// AiApi.cc:152-157
CollectorSink collector;
GenerationService genService;
auto gateErr = genService.runGuarded(
genReq, collector,
session::ConcurrencyPolicy::RejectConcurrent
);
// AiApi.cc:196-199 — 事件复放
for (const auto& ev : collector.getEvents()) {
sseSink.onEvent(ev);
}
sseSink.onClose();这意味着用户在等待首个 token 时,实际需要等待整个生成过程完成,完全丧失了流式输出的核心价值——低首 token 延迟(Time to First Token, TTFT)。
- 用户体验:用户看不到"逐字打印"效果,感觉响应很慢
- 超时风险:长回答时,客户端可能在收到任何数据前就超时
- 内存开销:CollectorSink 需要在内存中缓存完整响应
方案 A:Provider 层支持真正的流式回调(推荐)
// 新增 Provider 接口
class APIinterface {
public:
// 新增流式接口
virtual void generateStream(
session_st& session,
std::function<void(const std::string& chunk)> onChunk,
std::function<void()> onComplete,
std::function<void(const std::string& error)> onError
) = 0;
};方案 B:使用 Drogon AsyncStreamResponse(最小改动)
// Controller 中
auto [writerPtr, resp] = drogon::HttpResponse::newAsyncStreamResponse();
callback(resp); // 立即返回响应头
// 创建直接推送的 Sink
DirectSseSink sseSink(writerPtr);
genService.runGuarded(genReq, sseSink, ...); // Sink 直接推送每个事件方案 C:chaynsapi 轮询增量推送(渐进式改进)
chaynsapi 当前通过轮询上游 API 获取结果。可以在轮询过程中,每次获取到新内容时立即通过 Sink 推送给客户端,而不是等到轮询完成。
- 方案 A:3-5 天(需要重构 Provider 接口和 chaynsapi 实现)
- 方案 B:1-2 天(仅修改 Controller 层)
- 方案 C:1-2 天(修改 chaynsapi 轮询逻辑)
建议先实施方案 B 或 C,后续再实施方案 A。
--备注:当前上游并没有流式回复,是一次性回复
涉及文件:src/controllers/AiApi.h, src/controllers/AiApi.cc(1693 行)
AiApi 一个 Controller 包含 25+ 个端点,涵盖完全不同的业务域:
| 业务域 | 端点数 | 行数(约) |
|---|---|---|
| AI 核心 API | 5 | ~350 |
| 账号管理 | 7 | ~600 |
| 渠道管理 | 5 | ~350 |
| 错误统计 | 4 | ~200 |
| 服务状态 | 3 | ~150 |
| 日志查看 | 2 | ~130 |
- 编译效率:修改任何一个端点都需要重新编译整个 1693 行的文件
- 可读性:开发者需要在 1693 行中搜索目标函数
- 冲突风险:多人同时修改同一个文件容易产生 Git 冲突
- 违反 SRP:单一职责原则要求每个类只负责一个业务域
拆分为 5 个独立的 Controller:
src/controllers/
├── AiApiController.h/cc # POST /chaynsapi/v1/chat/completions
│ # POST /chaynsapi/v1/responses
│ # GET /chaynsapi/v1/models
│ # GET /chaynsapi/v1/responses/{id}
│ # DELETE /chaynsapi/v1/responses/{id}
│
├── AccountController.h/cc # POST /aichat/account/add
│ # POST /aichat/account/delete
│ # POST /aichat/account/update
│ # POST /aichat/account/refresh
│ # POST /aichat/account/autoregister
│ # GET /aichat/account/info
│ # GET /aichat/account/dbinfo
│
├── ChannelController.h/cc # POST /aichat/channel/add
│ # POST /aichat/channel/delete
│ # POST /aichat/channel/update
│ # POST /aichat/channel/updatestatus
│ # GET /aichat/channel/info
│
├── MetricsController.h/cc # GET /aichat/metrics/requests/series
│ # GET /aichat/metrics/errors/series
│ # GET /aichat/metrics/errors/events
│ # GET /aichat/metrics/errors/events/{id}
│ # GET /aichat/status/summary
│ # GET /aichat/status/channels
│ # GET /aichat/status/models
│
└── LogController.h/cc # GET /aichat/logs/list
# GET /aichat/logs/tail
同时提取公共辅助函数到 ControllerUtils.h:
namespace controller_utils {
// 统一错误响应构建
HttpResponsePtr makeErrorResponse(int status, const std::string& msg, const std::string& type);
// 统一 JSON 解析
std::optional<Json::Value> parseJsonBody(const HttpRequestPtr& req);
// 默认时间范围
std::pair<std::string, std::string> defaultTimeRange(int hours = 24);
}2-3 天(纯重构,不改变功能)
涉及文件:src/sessionManager/GenerationService.cpp(2109 行)
GenerationService.cpp 是整个系统的核心文件,包含以下功能模块:
| 功能模块 | 行范围 | 行数 |
|---|---|---|
| 辅助函数(匿名命名空间) | 27-347 | 320 |
| 核心编排(runGuarded/executeGuarded) | 349-626 | 277 |
| Provider 调用 | 628-653 | 25 |
| 结果事件发送(emitResultEvents) | 655-970 | 315 |
| XML 工具调用解析 | 1006-1108 | 102 |
| 强制工具调用生成 | 1110-1339 | 229 |
| 参数形状规范化 | 1341-1592 | 251 |
| 严格客户端规则 | 1594-1655 | 61 |
| 工具定义编码与注入 | 1657-2109 | 452 |
提取以下独立模块:
src/sessionManager/
├── GenerationService.h/cpp # 核心编排(~400 行)
├── ToolDefinitionEncoder.h/cpp # 工具定义编码(~500 行)
├── ForcedToolCallGenerator.h/cpp # 强制工具调用生成(~250 行)
├── ToolCallNormalizer.h/cpp # 参数形状规范化(~300 行)
├── BridgeXmlExtractor.h/cpp # XML 提取辅助函数(~200 行)
└── StrictClientRules.h/cpp # 严格客户端规则(~100 行)
每个模块提供清晰的接口:
// ToolDefinitionEncoder.h
class ToolDefinitionEncoder {
public:
struct Config {
bool useFullDefinitions = false;
bool includeDescriptions = false;
int maxDescriptionChars = 160;
};
static std::string encode(const Json::Value& tools, const Config& config);
static Config loadFromDrogonConfig();
};
// ToolCallNormalizer.h
class ToolCallNormalizer {
public:
static void normalize(
const Json::Value& toolDefs,
std::vector<generation::ToolCallDone>& toolCalls
);
};
// ForcedToolCallGenerator.h
class ForcedToolCallGenerator {
public:
static void generate(
const session_st& session,
std::vector<generation::ToolCallDone>& outToolCalls,
std::string& outTextContent
);
};2-3 天(纯提取重构)
涉及文件:src/sessionManager/Session.h:51-125
session_st 包含 20+ 个字段,横跨多个关注点:
struct session_st {
// 请求数据
std::string selectapi, selectmodel, systemprompt, requestmessage;
std::vector<ImageInfo> requestImages;
Json::Value tools, tools_raw;
std::string toolChoice;
// 响应数据
Json::Value responsemessage, api_response_data;
// 会话状态
std::string curConversationId, contextConversationId;
bool is_continuation, has_previous_response_id;
ApiType apiType;
// Provider 上下文
std::string tool_bridge_trigger, prev_provider_key;
bool supportsToolCalls;
// 客户端信息
Json::Value client_info;
// 时间信息
time_t created_time, last_active_time;
// Response API 专用
std::string response_id, lastResponseId;
// ZeroWidth 模式专用
std::string nextSessionId;
// 错误追踪
std::string request_id;
};- 作为"上帝对象"在系统中传递,任何模块都可以读写任何字段
- 难以理解哪些字段在哪些阶段有效
- 字段命名不一致(
selectapivstool_bridge_triggervscurConversationId)
第一阶段:使用嵌套结构体组织字段,保持 session_st 不变:
struct session_st {
struct RequestData {
std::string api;
std::string model;
std::string systemPrompt;
std::string message;
std::string messageRaw; // 注入 tool bridge 提示词前的原始输入
std::vector<ImageInfo> images;
Json::Value tools;
Json::Value toolsRaw;
std::string toolChoice;
} request;
struct ResponseData {
Json::Value message;
Json::Value apiData;
} response;
struct SessionState {
std::string sessionId; // 原 curConversationId
std::string contextId; // 原 contextConversationId
ApiType apiType = ApiType::ChatCompletions;
bool isContinuation = false;
bool hasPreviousResponseId = false;
std::string responseId; // Response API 专用
std::string nextSessionId; // ZeroWidth 模式
} state;
struct ProviderContext {
std::string prevProviderKey;
std::string toolBridgeTrigger;
bool supportsToolCalls = true;
} provider;
Json::Value clientInfo;
std::string requestId;
time_t createdTime = 0;
time_t lastActiveTime = 0;
Json::Value messageContext = Json::Value(Json::arrayValue);
};第二阶段:逐步将各子结构体独立出来,减少模块间的耦合。
第一阶段:3-5 天(需要修改所有引用 session_st 字段的地方)
涉及文件:src/controllers/AiApi.cc(至少 6 处)
项目中大量使用 std::thread(...).detach() 执行后台任务:
// AiApi.cc:317-333 — 账号添加后的异步处理
thread addAccountThread([accountList](){
for(auto &account:accountList) {
AccountDbManager::getInstance()->addAccount(account);
}
AccountManager::getInstance().checkUpdateAccountToken();
for(const auto &account : accountList) {
// ... 更新 accountType
}
});
addAccountThread.detach();
// AiApi.cc:447-464 — 账号删除后的异步处理
thread deleteAccountThread([accountList](){
for(auto &account:accountList) {
bool upstreamDeleted = AccountManager::getInstance().deleteUpstreamAccount(account);
AccountDbManager::getInstance()->deleteAccount(account.apiName,account.userName);
}
AccountManager::getInstance().loadAccount();
AccountManager::getInstance().checkChannelAccountCounts();
});
deleteAccountThread.detach();
// AiApi.cc:542-544 — 渠道操作后检查
std::thread([](){
AccountManager::getInstance().checkChannelAccountCounts();
}).detach();
// AiApi.cc:689-691 — 渠道更新后检查
std::thread([](){
AccountManager::getInstance().checkChannelAccountCounts();
}).detach();
// AiApi.cc:805-811 — 账号刷新
std::thread([](){
AccountManager::getInstance().checkToken();
AccountManager::getInstance().updateAllAccountTypes();
}).detach();
// AiApi.cc:849-860 — 自动注册
std::thread([apiName, count](){
for (int i = 0; i < count; ++i) {
AccountManager::getInstance().autoRegisterAccount(apiName);
if (i < count - 1) std::this_thread::sleep_for(std::chrono::seconds(5));
}
}).detach();同样在 main.cc:41-65 中也有 detach:
app().getLoop()->queueInLoop([](){
std::thread t1([]{ // 初始化线程
ChannelManager::getInstance().init();
AccountManager::getInstance().init();
ApiManager::getInstance().init();
// ...
});
t1.detach();
});-
无法优雅停机:
detach()后线程脱离管理。当main()返回或程序收到 SIGTERM 时,detached 线程的行为是未定义的(C++ 标准)。可能导致数据库写入被中断,数据损坏。 -
异常吞没:线程中的未捕获异常会直接调用
std::terminate(),导致进程崩溃,且没有任何错误日志。 -
无法控制并发度:多个管理请求可能同时触发大量后台线程,导致资源竞争。例如,连续调用
accountAdd10 次,会同时创建 10 个后台线程,都在执行checkUpdateAccountToken()。 -
无法追踪进度:调用者无法知道后台任务是否完成、是否失败。
方案 A:使用 Drogon 的事件循环(最小改动)
// 替代 thread(...).detach()
drogon::app().getLoop()->queueInLoop([accountList]() {
try {
for (auto& account : accountList) {
AccountDbManager::getInstance()->addAccount(account);
}
AccountManager::getInstance().checkUpdateAccountToken();
} catch (const std::exception& e) {
LOG_ERROR << "[后台任务] 账号添加后处理异常: " << e.what();
}
});方案 B:引入简单的任务队列(推荐)
// BackgroundTaskQueue.h
class BackgroundTaskQueue {
public:
static BackgroundTaskQueue& getInstance();
// 提交任务
void submit(std::function<void()> task, const std::string& taskName = "");
// 优雅停机
void shutdown();
// 获取队列状态
size_t pendingCount() const;
private:
std::queue<std::function<void()>> tasks_;
std::mutex mutex_;
std::condition_variable cv_;
std::vector<std::thread> workers_;
bool stopping_ = false;
};使用方式:
// AiApi.cc 中
BackgroundTaskQueue::getInstance().submit([accountList]() {
for (auto& account : accountList) {
AccountDbManager::getInstance()->addAccount(account);
}
}, "account_add_post_process");
// main.cc 中(优雅停机)
drogon::app().registerOnTerminateAdvice([]() {
BackgroundTaskQueue::getInstance().shutdown();
});方案 A:0.5 天 方案 B:1-2 天
涉及文件:src/controllers/AiApi.cc(至少 15 处)
以下模式在代码中重复出现 15+ 次:
Json::Value error;
error["error"]["message"] = "Invalid JSON in request body";
error["error"]["type"] = "invalid_request_error";
auto resp = HttpResponse::newHttpJsonResponse(error);
resp->setStatusCode(HttpStatusCode::k400BadRequest);
callback(resp);
return;以及时间格式化代码重复 5+ 次:
auto now = std::chrono::system_clock::now();
auto yesterday = now - std::chrono::hours(24);
auto formatTime = [](std::chrono::system_clock::time_point tp) -> std::string {
auto tt = std::chrono::system_clock::to_time_t(tp);
char buf[32];
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", std::gmtime(&tt));
return std::string(buf);
};创建 ControllerUtils.h:
// src/controllers/ControllerUtils.h
#pragma once
#include <drogon/HttpResponse.h>
#include <json/json.h>
#include <chrono>
namespace controller {
// 发送错误响应
inline void sendError(
std::function<void(const drogon::HttpResponsePtr&)>& callback,
drogon::HttpStatusCode status,
const std::string& message,
const std::string& type = "internal_error",
const std::string& code = ""
) {
Json::Value error;
error["error"]["message"] = message;
error["error"]["type"] = type;
if (!code.empty()) error["error"]["code"] = code;
auto resp = drogon::HttpResponse::newHttpJsonResponse(error);
resp->setStatusCode(status);
callback(resp);
}
// 解析 JSON 请求体
inline std::shared_ptr<Json::Value> parseJsonOrError(
const drogon::HttpRequestPtr& req,
std::function<void(const drogon::HttpResponsePtr&)>& callback
) {
auto jsonPtr = req->getJsonObject();
if (!jsonPtr) {
sendError(callback, drogon::k400BadRequest,
"Invalid JSON in request body", "invalid_request_error");
}
return jsonPtr;
}
// 默认时间范围
inline std::pair<std::string, std::string> defaultTimeRange(int hours = 24) {
auto now = std::chrono::system_clock::now();
auto from = now - std::chrono::hours(hours);
auto format = [](std::chrono::system_clock::time_point tp) {
auto tt = std::chrono::system_clock::to_time_t(tp);
char buf[32];
std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", std::gmtime(&tt));
return std::string(buf);
};
return {format(from), format(now)};
}
} // namespace controller0.5-1 天
涉及文件:src/controllers/AiApi.cc:289-300, src/controllers/AiApi.cc:743-755
Accountinfo_st 的 JSON 解析代码在 accountAdd 和 accountUpdate 中几乎完全相同:
// accountAdd (AiApi.cc:289-300)
accountinfo.apiName=item["apiname"].asString();
accountinfo.userName=item["username"].asString();
accountinfo.passwd=item["password"].asString();
accountinfo.authToken=item["authtoken"].empty()?"":item["authtoken"].asString();
accountinfo.userTobitId=item["usertobitid"].empty()?0:item["usertobitid"].asInt();
accountinfo.personId=item["personid"].empty()?"":item["personid"].asString();
accountinfo.useCount=item["usecount"].empty()?0:item["usecount"].asInt();
accountinfo.tokenStatus=item["tokenstatus"].empty()?false:item["tokenstatus"].asBool();
accountinfo.accountStatus=item["accountstatus"].empty()?false:item["accountstatus"].asBool();
accountinfo.accountType=item["accounttype"].empty()?"free":item["accounttype"].asString();
// accountUpdate (AiApi.cc:743-755) — 几乎完全相同类似地,accountInfo 和 accountDbInfo 中的 struct → JSON 转换也重复。
在 Accountinfo_st 和 Channelinfo_st 上添加序列化方法:
// accountManager.h
struct Accountinfo_st {
// ... 现有字段 ...
// 从 JSON 解析
static Accountinfo_st fromJson(const Json::Value& json) {
Accountinfo_st info;
info.apiName = json.get("apiname", "").asString();
info.userName = json.get("username", "").asString();
info.passwd = json.get("password", "").asString();
info.authToken = json.get("authtoken", "").asString();
info.userTobitId = json.get("usertobitid", 0).asInt();
info.personId = json.get("personid", "").asString();
info.useCount = json.get("usecount", 0).asInt();
info.tokenStatus = json.get("tokenstatus", false).asBool();
info.accountStatus = json.get("accountstatus", false).asBool();
info.accountType = json.get("accounttype", "free").asString();
return info;
}
// 转换为 JSON
Json::Value toJson(bool includeSensitive = false) const {
Json::Value json;
json["apiname"] = apiName;
json["username"] = userName;
if (includeSensitive) {
json["password"] = passwd;
json["authtoken"] = authToken;
} else {
json["password"] = "****";
json["authtoken"] = authToken.empty() ? "" : "****";
}
json["usecount"] = useCount;
json["tokenstatus"] = tokenStatus;
json["accountstatus"] = accountStatus;
json["usertobitid"] = userTobitId;
json["personid"] = personId;
json["createtime"] = createTime;
json["accounttype"] = accountType;
return json;
}
};0.5 天
涉及文件:多个
-
GenerationService.cpp:111-194:大段注释掉的旧extractXmlInputForToolCalls实现,应删除。 -
GenerationService.cpp:104-110:findFunctionCallsPos()中两行完全相同的查找:size_t p1 = s.find("<function_calls"); size_t p2 = s.find("<function_calls"); // ← 与 p1 完全相同!
看起来 p2 应该是查找全角或其他变体的标签,但实际上是复制粘贴错误。
-
Session.h:259-271:两个[[deprecated]]方法(createNewSessionOrUpdateSession,gennerateSessionstByReq)应确认无引用后删除。 -
Session.h:320-321:gennerateSessionstByResponseReq也标记为 deprecated。 -
GenerationService.cpp:2090-2108:注释掉的recordWarnStat调用块。 -
GenerationService.cpp:1731-1737:注释掉的strictToolClient系统提示截断逻辑。
逐一检查并清理,预估工作量 0.5 天。
-
命名风格混合:
selectapi(无分隔)vstool_bridge_trigger(snake_case)vscurConversationId(camelCase)requestmessagevsrequestImages
-
using namespace在头文件中:ApiManager.h:6:using namespace std;chaynsapi.h:22:using namespace std;- 这会污染所有包含该头文件的翻译单元的命名空间
-
缩进不一致:
AiApi.cc中有些代码块没有正确缩进(如 663-709 行的channelUpdate) -
中英文注释混合:日志消息混合使用中英文(如
LOG_INFO << "[生成服务] 物化完成"vsLOG_INFO << "[GenerationService] 通道...)
- 创建
.clang-format配置文件统一代码格式 - 制定命名规范(建议统一使用 camelCase 或 snake_case)
- 移除头文件中的
using namespace - 统一日志语言(建议使用英文,便于国际化搜索)
涉及文件:src/sessionManager/GenerationService.cpp:990-1004
bool GenerationService::getChannelSupportsToolCalls(const std::string& channelName) {
auto channelManager = ChannelDbManager::getInstance();
Channelinfo_st channelInfo;
if (channelManager->getChannel(channelName, channelInfo)) {
return channelInfo.supportsToolCalls;
}
return true;
}每次 AI 请求都会查一次数据库获取通道是否支持 tool calls。
- 每个 AI 请求增加一次数据库 I/O
ChannelManager已经在内存中维护了渠道列表,完全可以直接使用
bool GenerationService::getChannelSupportsToolCalls(const std::string& channelName) {
// 直接从内存中的 ChannelManager 获取
auto channelList = ChannelManager::getInstance().getChannelList();
for (const auto& ch : channelList) {
if (ch.channelName == channelName) {
return ch.supportsToolCalls;
}
}
return true; // 默认支持
}或者更高效的方式,在 ChannelManager 中添加按名称查询的方法:
// ChannelManager.h
std::optional<bool> getSupportsToolCalls(const std::string& channelName) const;0.5 天
涉及文件:src/sessionManager/GenerationService.cpp(至少 5 处)
// 每次序列化都创建新的 builder
Json::StreamWriterBuilder writer;
writer["indentation"] = "";
tc.arguments = Json::writeString(writer, args);// 使用 static 或 thread_local 避免重复创建
static thread_local Json::StreamWriterBuilder compactWriter = []() {
Json::StreamWriterBuilder w;
w["indentation"] = "";
return w;
}();
tc.arguments = Json::writeString(compactWriter, args);涉及文件:src/sessionManager/Session.h:133-134
class chatSession {
std::mutex mutex_;
std::unordered_map<std::string, session_st> session_map;
std::unordered_map<std::string, std::string> context_map;
};所有会话操作(增删改查)都通过同一个 mutex_ 串行化。
高并发时:
- 不同会话的操作也会互相阻塞
- 读操作(查询会话)和写操作(更新会话)无法并行
方案 A:读写锁
std::shared_mutex mutex_;
// 读操作
void getSession(const std::string& id, session_st& session) {
std::shared_lock lock(mutex_); // 多个读可以并行
// ...
}
// 写操作
void addSession(const std::string& id, session_st& session) {
std::unique_lock lock(mutex_); // 写操作独占
// ...
}方案 B:分段锁(更高并发度)
static constexpr size_t SHARD_COUNT = 16;
struct Shard {
std::mutex mutex;
std::unordered_map<std::string, session_st> sessions;
};
std::array<Shard, SHARD_COUNT> shards_;
Shard& getShard(const std::string& key) {
size_t hash = std::hash<std::string>{}(key);
return shards_[hash % SHARD_COUNT];
}方案 A:0.5 天 方案 B:1-2 天
emitResultEvents中text多次值拷贝materializeSession返回session_st值类型transformRequestForToolBridge中的字符串拼接
- 使用
std::move减少拷贝 - 将
session_st改为通过引用传递(部分已实现) - 使用
std::string::reserve()预分配字符串空间
涉及文件:src/controllers/AiApi.h:16-43
所有 25+ 个 API 端点都是公开的,没有任何认证机制:
// 任何人都可以:
// - 添加/删除账号(包括密码)
// - 修改渠道配置
// - 查看所有日志
// - 获取完整的账号信息(含明文密码)
ADD_METHOD_TO(AiApi::accountAdd, "/aichat/account/add", Post);
ADD_METHOD_TO(AiApi::accountDelete, "/aichat/account/delete", Post);
ADD_METHOD_TO(AiApi::logsTail, "/aichat/logs/tail", Get);- 数据泄露:任何能访问端口 5555 的人都可以获取所有账号信息(含密码)
- 服务破坏:攻击者可以删除所有账号或禁用所有渠道
- 日志泄露:日志中可能包含敏感信息
方案 A:API Key 认证中间件(推荐)
// AuthFilter.h
class AdminAuthFilter : public drogon::HttpFilter<AdminAuthFilter> {
public:
void doFilter(const drogon::HttpRequestPtr& req,
drogon::FilterCallback&& fcb,
drogon::FilterChainCallback&& fccb) override {
auto authHeader = req->getHeader("Authorization");
auto configKey = drogon::app().getCustomConfig().get("admin_api_key", "").asString();
if (authHeader == "Bearer " + configKey) {
fccb(); // 认证通过
} else {
Json::Value error;
error["error"]["message"] = "Unauthorized";
auto resp = drogon::HttpResponse::newHttpJsonResponse(error);
resp->setStatusCode(drogon::k401Unauthorized);
fcb(resp);
}
}
};
// 使用方式
ADD_METHOD_TO(AiApi::accountAdd, "/aichat/account/add", Post, "AdminAuthFilter");方案 B:独立管理端口
在配置中添加管理端口(如 5556),管理接口只绑定到该端口,通过防火墙限制访问。
方案 A:1 天 方案 B:0.5 天(但安全性依赖网络层)
备注:暂不修改
涉及文件:src/controllers/AiApi.cc:350, src/controllers/AiApi.cc:480
// AiApi.cc:350 — accountInfo 直接返回明文密码
accountitem["password"]=userName.second->passwd;
accountitem["authtoken"]=userName.second->authToken;
// AiApi.cc:480 — accountDbInfo 同样返回明文密码
accountitem["password"]=account.passwd;
accountitem["authtoken"]=account.authToken;- API 返回时对密码脱敏:
accountitem["password"] = "****";
accountitem["authtoken"] = account.authToken.empty() ? "" : account.authToken.substr(0, 8) + "...";-
数据库中考虑加密存储密码(至少使用 AES 对称加密)
-
添加
?include_sensitive=true查询参数,仅在显式请求时返回敏感信息(配合认证使用)
备注:暂不修改
涉及文件:src/main.cc:21-23, config.example.json:109
// main.cc:21-23
resp->addHeader("Access-Control-Allow-Origin", "*");
resp->addHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH");
resp->addHeader("Access-Control-Allow-Headers", "*");- 生产环境限制
Allow-Origin为具体的管理前端域名 - 限制
Allow-Headers为实际需要的头(Content-Type,Authorization) - 通过配置文件控制,开发环境保持
*
涉及文件:src/test/
只有 5 个测试文件,覆盖的模块极少:
| 测试文件 | 测试目标 |
|---|---|
test_continuity_resolver.cpp |
ContinuityResolver |
test_error_event.cpp |
ErrorEvent |
test_error_stats_config.cpp |
ErrorStatsConfig |
test_response_index.cpp |
ResponseIndex |
test_main.cc |
测试入口 |
| 模块 | 重要性 | 理由 |
|---|---|---|
GenerationService |
最高 | 核心编排层,2109 行,逻辑复杂 |
RequestAdapters |
高 | 请求解析,格式错误直接影响功能 |
XmlTagToolCallCodec |
高 | XML 解析,容易出现边界情况 |
ToolCallValidator |
高 | 工具调用校验,校验错误影响客户端 |
normalizeToolCallArguments |
高 | 参数规范化,逻辑复杂 |
ClientOutputSanitizer |
中 | 输出清洗 |
ZeroWidthEncoder |
中 | 编解码,需要验证正确性 |
ChatJsonSink / ChatSseSink |
中 | 输出格式 |
Session |
中 | 会话管理 |
第一阶段(P0):
GenerationService::emitResultEvents的单元测试RequestAdapters::buildGenerationRequestFromChat/Responses的单元测试XmlTagToolCallCodec的边界情况测试ToolCallValidator::filterInvalidToolCalls的单元测试
第二阶段(P1):
normalizeToolCallArguments的各种参数形状测试applyStrictClientRules的行为测试generateForcedToolCall的工具选择测试- Sink 实现的输出格式验证
第三阶段(P2):
- 端到端集成测试
- 并发场景测试
- 性能基准测试
第一阶段:3-5 天
涉及文件:src/main.cc:40-66
启动时直接使用配置值,无验证:
// main.cc:44-46
auto customConfig = drogon::app().getCustomConfig();
if (customConfig.isMember("session_tracking")) {
std::string mode = customConfig["session_tracking"].get("mode", "hash").asString();如果 config.json 格式错误或缺少关键配置,会在运行时才暴露问题。
// ConfigValidator.h
class ConfigValidator {
public:
struct ValidationResult {
bool valid = true;
std::vector<std::string> errors;
std::vector<std::string> warnings;
};
static ValidationResult validate(const Json::Value& config) {
ValidationResult result;
// 检查必要的配置项
if (!config.isMember("listeners") || !config["listeners"].isArray()) {
result.errors.push_back("Missing 'listeners' configuration");
result.valid = false;
}
// 检查数据库配置
if (!config.isMember("db_clients") || !config["db_clients"].isArray()) {
result.warnings.push_back("No database configured, using in-memory storage");
}
// 检查自定义配置
const auto& custom = config["custom_config"];
if (custom.isMember("session_tracking")) {
std::string mode = custom["session_tracking"].get("mode", "hash").asString();
if (mode != "hash" && mode != "zerowidth" && mode != "zero_width") {
result.errors.push_back("Invalid session_tracking.mode: " + mode);
result.valid = false;
}
}
return result;
}
};涉及文件:src/sessionManager/GenerationService.cpp(30+ 处 LOG_INFO)
// 这些都是每个请求都会执行的路径,应该是 DEBUG 而非 INFO
LOG_INFO << "[生成服务] 物化 GenerationRequest -> session_st";
LOG_INFO << "[生成服务] 执行门控, 会话密钥: " << sessionKey;
LOG_INFO << "[生成服务] 已获取执行门控, 会话: " << sessionKey;
LOG_INFO << " supportsToolCalls" << supportsToolCalls; // ← 甚至没有标签前缀
LOG_INFO << "[GenerationService] 通道 " << channelName << " supportsToolCalls: " << ...;
LOG_INFO << "[生成服务] ContinuityDecision source=...";
LOG_INFO << "[生成服务] 会话 " << (session.is_continuation ? "续接" : "新建") << ...;- 生产环境日志量巨大,每个 AI 请求产生 10+ 条 INFO 日志
- 重要的业务日志被淹没在大量调试日志中
- 日志文件快速增长,增加存储成本
| 日志内容 | 当前级别 | 建议级别 |
|---|---|---|
| 请求开始/完成 | INFO | INFO(保留) |
| 物化 session、门控获取等细节 | INFO | DEBUG |
| 工具桥接注入、XML 解析细节 | INFO | DEBUG |
| 通道能力查询结果 | INFO | DEBUG |
| 会话连续性决策详情 | INFO | DEBUG |
| Provider 错误 | ERROR | ERROR(保留) |
| 工具调用校验过滤 | WARN | WARN(保留) |
| 并发冲突拒绝 | WARN | WARN(保留) |
涉及文件:src/apipoint/chaynsapi/
目前只有 chaynsapi 一个 Provider 实现。ApiFactory 和 ApiManager 的基础设施已经支持多 Provider,但缺少通用实现。
-
实现 OpenAI 兼容 Provider:
- 直接调用任何 OpenAI 兼容 API(如 OpenAI、Azure OpenAI、Anthropic via proxy、本地 LLM)
- 支持原生 streaming
- 支持原生 tool_calls
-
多 Provider 故障转移:
- 当主 Provider 失败时,自动切换到备用 Provider
- 支持基于权重的负载均衡
-
Provider 健康检查:
- 定期检查 Provider 可用性
- 自动禁用不健康的 Provider
涉及文件:src/sessionManager/ResponseIndex.h/cpp
ResponseIndex 将 Responses API 的响应数据存储在内存中(std::unordered_map),重启后丢失。
- 服务重启后,
GET /chaynsapi/v1/responses/{id}全部 404 previous_response_id续聊在服务重启后断裂- 内存无限增长(无淘汰策略)
- 添加 LRU 淘汰策略:限制最大存储条目数(如 10000)
- 添加 TTL 自动过期:如 24 小时后自动删除
- 可选持久化:将响应数据写入数据库,重启后可恢复
class ResponseIndex {
public:
void bind(const std::string& responseId, const std::string& sessionId);
void storeResponse(const std::string& responseId, const Json::Value& response);
bool tryGetResponse(const std::string& responseId, Json::Value& out);
bool erase(const std::string& responseId);
// 新增
void setMaxEntries(size_t max); // 默认 10000
void setTtlSeconds(int ttl); // 默认 86400 (24h)
void cleanupExpired(); // 定期调用
};添加两个端点:
// GET /health — 基础存活检查
ADD_METHOD_TO(AiApi::health, "/health", Get);
void AiApi::health(const HttpRequestPtr& req, Callback&& callback) {
Json::Value resp;
resp["status"] = "ok";
resp["version"] = "1.1";
resp["uptime"] = getUptimeSeconds();
callback(HttpResponse::newHttpJsonResponse(resp));
}
// GET /ready — 就绪检查(含依赖检查)
ADD_METHOD_TO(AiApi::ready, "/ready", Get);
void AiApi::ready(const HttpRequestPtr& req, Callback&& callback) {
Json::Value resp;
resp["status"] = "ready";
resp["database"] = checkDatabaseConnection() ? "ok" : "error";
resp["providers"] = checkProvidersAvailable() ? "ok" : "error";
resp["accounts"] = AccountManager::getInstance().hasActiveAccounts() ? "ok" : "warning";
callback(HttpResponse::newHttpJsonResponse(resp));
}SessionExecutionGate 只防止同一会话的并发请求,但缺少全局限流。
使用 Drogon 的限流插件或自定义中间件:
class RateLimitFilter : public drogon::HttpFilter<RateLimitFilter> {
public:
void doFilter(const HttpRequestPtr& req,
FilterCallback&& fcb,
FilterChainCallback&& fccb) override {
std::string clientIp = req->getPeerAddr().toIp();
if (rateLimiter_.isAllowed(clientIp)) {
fccb();
} else {
Json::Value error;
error["error"]["message"] = "Too many requests";
error["error"]["type"] = "rate_limit_error";
auto resp = HttpResponse::newHttpJsonResponse(error);
resp->setStatusCode(k429TooManyRequests);
fcb(resp);
}
}
};统计口径:以
doc/development-plan.md验收勾选为准(更新日期:2026-02-08)。
| 维度 | 数量 |
|---|---|
| 已完成 | 23 |
| 待做 | 0 |
| 完成率 | 100% |
| # | 改进项 | 优先级 | 类别 | 预估工作量 | 状态 |
|---|---|---|---|---|---|
| 1 | 真正的流式输出 | P0 | 架构 | 1-5 天 | ✅ 已完成 |
| 2 | 管理接口认证 | P0 | 安全 | 0.5-1 天 | ✅ 已完成 |
| 3 | 替换裸 thread.detach | P0 | 质量 | 0.5-2 天 | ✅ 已完成 |
| 4 | 增加核心模块测试 | P0 | 维护 | 3-5 天 | ✅ 已完成 |
| 5 | Controller 拆分 | P1 | 架构 | 2-3 天 | ✅ 已完成 |
| 6 | GenerationService 拆分 | P1 | 架构 | 2-3 天 | ✅ 已完成 |
| 7 | 通道信息缓存 | P1 | 性能 | 0.5 天 | ✅ 已完成 |
| 8 | 密码脱敏 | P1 | 安全 | 0.5 天 | ✅ 已完成 |
| 9 | 错误响应去重 | P1 | 质量 | 0.5-1 天 | ✅ 已完成 |
| 10 | session_map 读写锁 | P1 | 性能 | 0.5-1 天 | ✅ 已完成 |
| 11 | 日志级别调整 | P1 | 维护 | 0.5 天 | ✅ 已完成 |
| 12 | 多 Provider 支持 | P1 | 功能 | 3-5 天 | ✅ 已完成 |
| 13 | ResponseIndex 持久化 | P1 | 功能 | 1-2 天 | ✅ 已完成 |
| 14 | JSON 字段解析去重 | P1 | 质量 | 0.5 天 | ✅ 已完成 |
| 15 | 配置验证 | P1 | 维护 | 0.5 天 | ✅ 已完成 |
| 16 | session_st 重构 | P1 | 架构 | 3-5 天 | ✅ 已完成 |
| 17 | 代码风格统一 | P2 | 质量 | 1 天 | ✅ 已完成 |
| 18 | 健康检查端点 | P2 | 功能 | 0.5 天 | ✅ 已完成 |
| 19 | 全局限流 | P2 | 安全 | 1 天 | ✅ 已完成 |
| 20 | 清理废弃代码 | P2 | 质量 | 0.5 天 | ✅ 已完成 |
| 21 | StreamWriterBuilder 复用 | P2 | 性能 | 0.5 天 | ✅ 已完成 |
| 22 | 字符串拷贝优化 | P2 | 性能 | 1 天 | ✅ 已完成 |
| 23 | CORS 限制 | P2 | 安全 | 0.5 天 | ✅ 已完成 |
当前无待做项(doc/development-plan.md 已全部勾选完成)。
- P0 必须修复:5-13 天
- P1 短期改进:13-23 天
- P2 长期优化:5-6 天
文档作者:AI Code Reviewer 审查基于版本 v1.1 的完整源码分析 建议按优先级逐步实施,每个改进项做为独立的 PR/MR 提交
以下进展基于
doc/development-plan.md的逐项核查与代码/构建验证结果。
-
前端 Admin API Key 可配置并可用
- 在
aiapi_web设置页补充 API Key 输入与持久化逻辑(保存、清空、重置联动)。 - 已与请求拦截器打通:访问
/aichat/*时自动附带Authorization: Bearer <key>。 - 关键文件:
../aiapi_web/src/components/Settings.tsx../aiapi_web/src/services/api.ts../aiapi_web/src/utils/config.ts
- 在
-
GenerationService.cpp进一步拆分至 ~400 行目标- 将事件发送与 ToolBridge 相关实现从主文件中抽离。
- 当前
src/sessionManager/GenerationService.cpp行数为 391 行。 - 新增承载实现文件:
src/sessionManager/GenerationServiceEmitAndToolBridge.cpp。 - 构建配置已同步:
src/CMakeLists.txt。
- 前端构建:在
../aiapi_web执行npm run build,通过。 - 后端构建:执行
cmake --build src/build -j4,通过。 - 测试回归:执行
./src/build/test/aiapi_test,通过(213 assertions in 65 test cases)。
doc/development-plan.md 中此前剩余 3 项已全部完成并勾选:
- 前端能正常配置和使用 API Key
- 前端
aiapi_web功能正常 -
GenerationService.cpp缩减到 ~400 行
补充说明:当前 doc/development-plan.md 已无未勾选项。