feat: v0.4.0 四阶段迭代 — 安全加固 + 工程基线 + 架构重构 + 双 Provider 扩展
P0 安全修复: - API Key 加密存储(safeStorage 密钥链,版本化前缀,历史明文平滑兼容) - 间接提示注入防护(SecurityScanHook 工具结果深扫描,网络工具脱敏/本地工具警示分级) - error:report IPC 断链修复(渲染进程错误上报落 electron-log + 审计) - abort 信号贯通工具层(run_command/dev-tools 子进程随会话中断终止) - run_command 沙箱加固(cd 系统目录/敏感文件读取拦截 + chcp 前缀剥离防解析退化) - .env 真实生效(dotenv 回退加载,应用内配置优先) P1 工程基础: - ESLint 9 flat config + 全部 34 条存量 warnings 清零(零容忍基线) - 测试基线 118 用例 11 文件(token/文件防护/权限/沙箱/注入/命令/引擎/注册表/审计链/摘要分层) - test:electron 双模式(ELECTRON_RUN_AS_NODE 跑 Electron ABI,SQLite 套件全执行) - SessionRecorder 多会话隔离 + 9 种 TRACE 事件补全(含最终轮 iteration_end) - Provider 故障转移(重试耗尽/不可重试一次性切换 fallback + 前端通知) - MCP 真就绪(等待全部连接完成再广播 tools:ready) - SLO/HealthChecker 真实接入(60s 巡检 + 托盘状态) - CONFIG_DEFAULTS 单一来源(消除 SEED 双源漂移) P2 架构升级: - handlers.ts 1940 行拆分为 13 个 IPC 域模块(防重入注册 + 多窗口广播) - AgentEngineManager 每会话独立引擎(LRU 30 + adapter 工厂隔离 abort 信号) - TaskOrchestrator EngineProvider 改造 + abortByParent 联动中断 SubAgent - 会话摘要分层上下文(session_summaries 滚动摘要 + 截断游标清理防因果污染) - 消息编辑重发/重新生成(truncateAfter IPC + store 动作 + UI) - Markdown 导出 / WebSearch 并行抓取(并发 3)/ 记忆 TF 缓存 / 版本构建期注入 P3 能力扩展: - OpenAI Adapter(o 系列推理模型 reasoning_effort/max_completion_tokens) - Anthropic Adapter(原生 Messages API:tool_use 块/角色合并/thinking budget/图片 base64/SSE 事件机) - 设置页/Onboarding 六 Provider 全链路接入
This commit is contained in:
@@ -27,6 +27,8 @@ export interface SessionRow {
|
||||
|
||||
export interface MessageRow {
|
||||
id: string;
|
||||
/** P2: rowid(插入序号,消息截断与分层加载的游标) */
|
||||
row_id?: number;
|
||||
session_id: string;
|
||||
role: string;
|
||||
// C-6 修复: content 允许 null — assistant 消息仅有 tool_calls 时为 null
|
||||
@@ -52,6 +54,8 @@ export interface SessionInfo {
|
||||
|
||||
export interface MessageInfo {
|
||||
id: string;
|
||||
/** P2: rowid(插入序号;会话摘要分层加载与消息截断的游标) */
|
||||
rowId?: number;
|
||||
role: string;
|
||||
// C-6 修复: content 允许 null — assistant 消息仅有 tool_calls 时为 null
|
||||
content: string | null;
|
||||
@@ -178,27 +182,81 @@ export class SessionService {
|
||||
*
|
||||
* #44 修复: 添加 limit/offset 参数支持分页,避免超长会话一次性加载导致 OOM
|
||||
* 默认不限制(limit=0),保持向后兼容;调用者可传 limit 限制返回条数
|
||||
* P2-11: 新增 afterRowid 参数——仅返回 rowid 大于该值的消息(分层上下文加载游标);
|
||||
* 排序改用 rowid(插入序号),与截断/摘要游标语义一致
|
||||
*/
|
||||
getMessages(
|
||||
sessionId: string,
|
||||
options: { limit?: number; offset?: number } = {},
|
||||
options: { limit?: number; offset?: number; afterRowid?: number } = {},
|
||||
): MessageInfo[] {
|
||||
const db = this.getDBFn();
|
||||
const { limit = 0, offset = 0 } = options;
|
||||
const { limit = 0, offset = 0, afterRowid = 0 } = options;
|
||||
|
||||
const rows = (limit > 0
|
||||
? db
|
||||
.prepare(
|
||||
'SELECT * FROM messages WHERE session_id = ? ORDER BY created_at ASC LIMIT ? OFFSET ?',
|
||||
)
|
||||
.all(sessionId, limit, offset)
|
||||
: db
|
||||
.prepare('SELECT * FROM messages WHERE session_id = ? ORDER BY created_at ASC')
|
||||
.all(sessionId)) as MessageRow[];
|
||||
let sql = 'SELECT rowid AS row_id, * FROM messages WHERE session_id = ?';
|
||||
const params: unknown[] = [sessionId];
|
||||
if (afterRowid > 0) {
|
||||
sql += ' AND rowid > ?';
|
||||
params.push(afterRowid);
|
||||
}
|
||||
sql += ' ORDER BY rowid ASC';
|
||||
if (limit > 0) {
|
||||
sql += ' LIMIT ? OFFSET ?';
|
||||
params.push(limit, offset);
|
||||
}
|
||||
|
||||
const rows = db.prepare(sql).all(...params) as MessageRow[];
|
||||
return rows.map((row) => this.toMessageInfo(row));
|
||||
}
|
||||
|
||||
/**
|
||||
* P2-11: 截断消息——删除指定消息(含/不含)之后的所有消息
|
||||
*
|
||||
* 用途:
|
||||
* - 编辑重发:删除原用户消息及其后所有消息(inclusive=true),重新发送修订版
|
||||
* - 重新生成:删除最后一条用户消息之后的所有回复(inclusive=true 于该用户消息)
|
||||
*
|
||||
* 审查修复(全量复检 #1): 同步清理 session_summaries 摘要游标。
|
||||
* 若摘要游标(summarized_until_rowid)落在被删除范围内而不清理,会导致两个缺陷:
|
||||
* 1. buildHistoryMessages 的 afterRowid 过滤返回空 tail —— 截断点之前的原文永远不加载
|
||||
* 2. 摘要内容包含已被撤销的消息("未来"内容因果污染——用户回退历史但 LLM 仍记得)
|
||||
*
|
||||
* @param sessionId 会话 ID
|
||||
* @param messageId 锚点消息 ID
|
||||
* @param inclusive true=连同锚点消息一起删除;false=仅删除其后消息
|
||||
* @returns 是否有消息被删除
|
||||
*/
|
||||
truncateMessagesAfter(sessionId: string, messageId: string, inclusive = true): boolean {
|
||||
const db = this.getDBFn();
|
||||
const op = inclusive ? '>=' : '>';
|
||||
|
||||
// 先查锚点 rowid(删除后无法再定位)
|
||||
const anchor = db
|
||||
.prepare('SELECT rowid AS rid FROM messages WHERE session_id = ? AND id = ?')
|
||||
.get(sessionId, messageId) as { rid: number } | undefined;
|
||||
if (!anchor) return false;
|
||||
|
||||
const result = db
|
||||
.prepare(`DELETE FROM messages WHERE session_id = ? AND rowid ${op} ?`)
|
||||
.run(sessionId, anchor.rid);
|
||||
|
||||
if (result.changes > 0) {
|
||||
// 同步修正会话消息计数(避免侧栏计数与实际不一致)
|
||||
db.prepare(
|
||||
`UPDATE sessions SET message_count = (SELECT COUNT(*) FROM messages WHERE session_id = ?), updated_at = ? WHERE id = ?`,
|
||||
).run(sessionId, Date.now(), sessionId);
|
||||
|
||||
// 摘要游标清理:游标覆盖到被删除范围即删摘要(下次消息量达标后由 maybeSummarize 重建)
|
||||
// - inclusive=true:锚点本身被删,游标 >= 锚点即视为被覆盖
|
||||
// - inclusive=false:锚点保留,游标 > 锚点才被覆盖(游标==锚点时摘要与现存消息仍一致)
|
||||
db.prepare(
|
||||
'DELETE FROM session_summaries WHERE session_id = ? AND summarized_until_rowid >= ?',
|
||||
).run(sessionId, anchor.rid + (inclusive ? 0 : 1));
|
||||
|
||||
log.info(`Session truncated: ${sessionId} (${result.changes} messages removed after ${messageId})`);
|
||||
}
|
||||
return result.changes > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存一条消息
|
||||
*/
|
||||
@@ -336,6 +394,7 @@ export class SessionService {
|
||||
const parsedAttachments = row.attachments ? this.safeJsonParse(row.attachments) : undefined;
|
||||
return {
|
||||
id: row.id,
|
||||
rowId: row.row_id,
|
||||
role: row.role,
|
||||
content: row.content,
|
||||
reasoningContent: row.reasoning_content ?? undefined,
|
||||
|
||||
Reference in New Issue
Block a user