feat: v0.8.2 安全纵深补全 · 协议保真 · 断链修复 — 图片SSRF/根MEMORY.md保护根治 · Anthropic thinking回传+pause_turn续传 · 2523 用例全量回归 + E2E 扩充
This commit is contained in:
+72
-32
@@ -44,6 +44,18 @@ function buildTimezoneLabel(): string {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* v0.8.2 P2-4: 会话元数据实时刷新广播(专用 session:updated 通道)。
|
||||
* 消费方:Sidebar(title / messageCount / updatedAt 实时刷新)。
|
||||
* 语义清理:标题生成不再伪装成 config:changed(合成 key session.title.<id>)。
|
||||
*/
|
||||
function broadcastSessionUpdated(
|
||||
sessionId: string,
|
||||
patch: { title?: string; messageCount?: number; updatedAt?: number },
|
||||
): void {
|
||||
broadcast('session:updated', { sessionId, ...patch });
|
||||
}
|
||||
|
||||
/** 单会话的 text_delta 节流状态 */
|
||||
interface ThrottleState {
|
||||
buffer: string;
|
||||
@@ -509,10 +521,46 @@ export function registerAgentHandlers(ctx: IPCContext): void {
|
||||
let engineUserMessage: MetonaMessage;
|
||||
|
||||
try {
|
||||
// 提示注入检测(安全模块)
|
||||
// F-8 接通: security.promptInjectionDefense=false 时跳过用户消息检测
|
||||
// (工具结果侧的 SecurityScanHook 由 main.ts 按同一配置决定是否挂载)
|
||||
// fail-secure: 仅显式 false 才关闭 —— 配置值异常(空串/null/类型错误)时保持防护开启
|
||||
//
|
||||
// v0.8.2 P1-4 根治: 检测提前到用户消息落库**之前** —— 旧顺序先 saveMessage
|
||||
// 后 detect,riskScore≥7 阻断时恶意内容已持久化进会话历史(被阻断的消息
|
||||
// 仍可在历史中读到、参与后续上下文)。检测只依赖消息内容本身,无需
|
||||
// systemPrompt,前置无任何依赖障碍。
|
||||
const injectionEnabled =
|
||||
configService.get<boolean>('security.promptInjectionDefense') !== false;
|
||||
if (injectionEnabled) {
|
||||
const injectionResult = promptInjectionDefender.detect(userMessage.content);
|
||||
if (injectionResult.riskScore >= 7) {
|
||||
log.warn('[PromptInjectionDefender] Blocked message:', injectionResult.findings);
|
||||
sendErrorEvent(
|
||||
`Message blocked by prompt injection defense: ${injectionResult.recommendation}`,
|
||||
sessionId,
|
||||
);
|
||||
await sessionRecorder.stopRecording(sessionId, {
|
||||
totalIterations: 0,
|
||||
totalTokens: 0,
|
||||
durationMs: 0,
|
||||
terminationReason: 'error',
|
||||
});
|
||||
return { success: false, error: 'Message blocked by prompt injection defense' };
|
||||
}
|
||||
if (injectionResult.riskScore >= 4) {
|
||||
log.warn(
|
||||
'[PromptInjectionDefender] Suspicious patterns detected:',
|
||||
injectionResult.findings,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存用户消息到数据库
|
||||
// v0.7.4 回归修复: 透传前端消息 id(ChatMessage.id 由 genMsgId 生成)——
|
||||
// 否则 DB 用 msg_<nanoid> 生成不同 id,用户对刚发送消息"仅保存"
|
||||
// (updateMessageContent 按 id 匹配)会 0 行更新失败。
|
||||
// v0.8.2 P1-4: 位于注入检测之后 —— 被阻断的内容不进入会话历史
|
||||
sessionService.saveMessage({
|
||||
sessionId,
|
||||
role: 'user',
|
||||
@@ -521,6 +569,15 @@ export function registerAgentHandlers(ctx: IPCContext): void {
|
||||
id: (userMessage as MetonaMessage & { id?: string }).id,
|
||||
});
|
||||
|
||||
// v0.8.2 P2-4: 用户消息落库后即时广播(Sidebar 的条数/时间同步刷新)
|
||||
{
|
||||
const updatedSession = sessionService.getSession(sessionId);
|
||||
broadcastSessionUpdated(sessionId, {
|
||||
messageCount: updatedSession?.messageCount,
|
||||
updatedAt: updatedSession?.updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
// P2-11: 分层加载历史——存在滚动摘要时只加载 [摘要 + 近期原文]
|
||||
history = sessionSummaryService.buildHistoryMessages(sessionId).slice(0, -1);
|
||||
|
||||
@@ -600,39 +657,9 @@ export function registerAgentHandlers(ctx: IPCContext): void {
|
||||
return { success: false, error: prepErr };
|
||||
}
|
||||
|
||||
// 提示注入检测在 try 内执行(需 systemPrompt 已构建,与引擎运行同域)
|
||||
// 提示注入检测已前移至数据准备 try 块的开头(v0.8.2 P1-4:先检测后落库)
|
||||
|
||||
try {
|
||||
// 提示注入检测(安全模块)
|
||||
// F-8 接通: security.promptInjectionDefense=false 时跳过用户消息检测
|
||||
// (工具结果侧的 SecurityScanHook 由 main.ts 按同一配置决定是否挂载)
|
||||
// fail-secure: 仅显式 false 才关闭 —— 配置值异常(空串/null/类型错误)时保持防护开启
|
||||
const injectionEnabled =
|
||||
configService.get<boolean>('security.promptInjectionDefense') !== false;
|
||||
if (injectionEnabled) {
|
||||
const injectionResult = promptInjectionDefender.detect(userMessage.content);
|
||||
if (injectionResult.riskScore >= 7) {
|
||||
log.warn('[PromptInjectionDefender] Blocked message:', injectionResult.findings);
|
||||
sendErrorEvent(
|
||||
`Message blocked by prompt injection defense: ${injectionResult.recommendation}`,
|
||||
sessionId,
|
||||
);
|
||||
await sessionRecorder.stopRecording(sessionId, {
|
||||
totalIterations: 0,
|
||||
totalTokens: 0,
|
||||
durationMs: 0,
|
||||
terminationReason: 'error',
|
||||
});
|
||||
return { success: false, error: 'Message blocked by prompt injection defense' };
|
||||
}
|
||||
if (injectionResult.riskScore >= 4) {
|
||||
log.warn(
|
||||
'[PromptInjectionDefender] Suspicious patterns detected:',
|
||||
injectionResult.findings,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TRACE 层:记录上下文构建
|
||||
sessionRecorder.recordContextBuilt(sessionId, {
|
||||
tokenCount: estimateMessagesTokens(history),
|
||||
@@ -813,13 +840,15 @@ export function registerAgentHandlers(ctx: IPCContext): void {
|
||||
}
|
||||
|
||||
// v0.7.3 P4-1: 首个完成的 run 之后生成精炼会话标题(每会话幂等,失败静默)
|
||||
// v0.8.2 P2-4: 标题广播改走专用 session:updated 事件 —— 此前伪装成
|
||||
// config:changed(合成 key session.title.<id>),语义混用、渲染层需特判。
|
||||
if (output.terminationReason === 'completed') {
|
||||
titleGenerator
|
||||
.maybeGenerateTitle(sessionId, userMessage.content, output.finalAnswer)
|
||||
.then((title) => {
|
||||
if (title) {
|
||||
// 广播重命名结果,前端 Sidebar 实时刷新标题
|
||||
broadcast('config:changed', { key: `session.title.${sessionId}`, value: title });
|
||||
broadcastSessionUpdated(sessionId, { title });
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -827,6 +856,17 @@ export function registerAgentHandlers(ctx: IPCContext): void {
|
||||
});
|
||||
}
|
||||
|
||||
// v0.8.2 P2-4: 会话元数据实时刷新 —— 此前 Sidebar 仅挂载时 list() 一次,
|
||||
// 流式过程中 messageCount/updatedAt 停留在旧值直到重启。run 收尾统一
|
||||
// 广播 session:updated(含 DB 最新 messageCount/updatedAt)。
|
||||
{
|
||||
const updatedSession = sessionService.getSession(sessionId);
|
||||
broadcastSessionUpdated(sessionId, {
|
||||
messageCount: updatedSession?.messageCount,
|
||||
updatedAt: updatedSession?.updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
// TOOL 层:记录会话结束 / TRACE 层:停止录制
|
||||
auditService.logSessionEnd({
|
||||
sessionId,
|
||||
|
||||
Reference in New Issue
Block a user