feat: v0.8.2 安全纵深补全 · 协议保真 · 断链修复 — 图片SSRF/根MEMORY.md保护根治 · Anthropic thinking回传+pause_turn续传 · 2523 用例全量回归 + E2E 扩充
CI / 类型检查 + Lint + 单元测试 (push) Failing after 9m45s
CI / 全量测试 (Electron ABI) (push) Failing after 6m28s
CI / 产物编译验证 (push) Successful in 11m18s

This commit is contained in:
2026-09-08 14:30:27 +08:00
parent 69776e447f
commit 4cd6e997b5
86 changed files with 4303 additions and 956 deletions
+75
View File
@@ -0,0 +1,75 @@
/**
* v0.8.2 P1-5: 敏感值脱敏单源工具测试
*
* 锁定契约:
* - 键名归一化匹配(api_key / authKey / Authorization 等形态均命中)
* - 长值保留后 4 位、短值完全掩码
* - 嵌套对象/数组递归;非敏感字段原样保留
* - 循环引用与深度上限防护(防御恶意构造的工具参数)
* - 原对象不被修改
*/
import { describe, it, expect } from 'vitest';
import { deepMaskSensitive, maskSensitiveValue } from '../mask';
describe('maskSensitiveValue', () => {
it('长值保留后 4 位', () => {
expect(maskSensitiveValue('sk-abcdefgh12345678')).toBe('***5678');
});
it('短值(≤4)完全掩码', () => {
expect(maskSensitiveValue('abc')).toBe('***');
expect(maskSensitiveValue('abcd')).toBe('***');
});
});
describe('deepMaskSensitive', () => {
it('顶层与嵌套的敏感键均被掩码', () => {
const input = {
url: 'https://api.example.com',
headers: {
Authorization: 'Bearer sk-abcdefgh12345678',
'content-type': 'application/json',
},
api_key: 'sk-abcdefgh12345678',
nested: { authToken: 'token-1234567890' },
};
const out = deepMaskSensitive(input) as typeof input;
expect(out.url).toBe('https://api.example.com');
expect(out['api_key']).toBe('***5678');
expect((out.headers as Record<string, string>).Authorization).toBe('***5678');
expect((out.headers as Record<string, string>)['content-type']).toBe('application/json');
expect((out.nested as { authToken: string }).authToken).toBe('***7890');
});
it('数组内对象同样脱敏', () => {
const out = deepMaskSensitive([{ secret: 'supersecret-value-42' }, { ok: 1 }]) as Array<
Record<string, unknown>
>;
expect(out[0].secret).toBe('***e-42');
expect(out[1].ok).toBe(1);
});
it('原对象不被修改', () => {
const input = { api_key: 'sk-abcdefgh12345678' };
deepMaskSensitive(input);
expect(input.api_key).toBe('sk-abcdefgh12345678');
});
it('循环引用与深度上限不抛错', () => {
const a: Record<string, unknown> = { name: 'a' };
a.self = a;
const out = deepMaskSensitive(a) as Record<string, unknown>;
expect(out.name).toBe('a');
expect(out.self).toBe('[circular]');
const deep: Record<string, unknown> = { v: 0 };
let cur = deep;
for (let i = 0; i < 20; i++) {
cur.next = { v: i + 1 };
cur = cur.next as Record<string, unknown>;
}
const deepOut = deepMaskSensitive(deep) as Record<string, unknown>;
expect(deepOut.v).toBe(0);
expect(JSON.stringify(deepOut)).toContain('depth-limit');
});
});
+40 -2
View File
@@ -71,7 +71,26 @@ const zh: Record<string, string> = {
'LLM 配置不完整,请检查 Provider、API Key、Base URL 和 Model 是否都已填写',
'config.error.workspaceSaveFailed': '工作空间路径保存失败:{{message}}',
// ===== 记忆维护(v0.8.1 P1-2 =====
'memory.maintain.auditTitle': 'MEMORY.md 记忆整理',
// ===== v0.8.2 P2-5 i18n 收口第三期:托盘 / 系统对话框 / 更新 / 工作空间校验 =====
'tray.menu.showWindow': '显示窗口',
'tray.menu.newSession': '新建会话',
'tray.menu.quit': '退出',
'tray.menu.status': '状态: {{status}}',
'tray.status.idle': '空闲',
'tray.status.thinking': '思考中',
'tray.status.executing': '执行中',
'tray.status.error': '异常',
'app.update.disabled': '自动更新未启用(未配置更新源或开发模式)',
'app.dialog.selectWorkspace.title': '选择工作空间目录',
'app.dialog.startupFailed.title': 'MetonaAI Desktop 启动失败',
'app.dialog.startupFailed.body':
'初始化过程中发生错误,应用即将退出。\n\n{{message}}\n\n可能原因:工作空间目录不可写、数据库文件损坏。\n可尝试在设置中切换工作空间路径后重新启动。',
'workspace.reason.empty': '路径不能为空',
'workspace.reason.notExists': '目录不存在,将在切换后自动创建',
'workspace.reason.notDirectory': '路径不是目录',
'workspace.reason.inaccessible': '无法访问路径',
'workspace.reason.inheritFailed': '继承文件失败:{{message}}',
'workspace.reason.restartRequired': '工作空间已切换,重启应用后生效',
};
const en: Record<string, string> = {
@@ -112,7 +131,26 @@ const en: Record<string, string> = {
'config.error.configIncomplete':
'LLM config incomplete. Check that Provider, API Key, Base URL and Model are all filled in.',
'config.error.workspaceSaveFailed': 'Failed to save workspace path: {{message}}',
'memory.maintain.auditTitle': 'MEMORY.md maintenance',
// ===== v0.8.2 P2-5: tray / system dialogs / update / workspace validation =====
'tray.menu.showWindow': 'Show Window',
'tray.menu.newSession': 'New Session',
'tray.menu.quit': 'Quit',
'tray.menu.status': 'Status: {{status}}',
'tray.status.idle': 'Idle',
'tray.status.thinking': 'Thinking',
'tray.status.executing': 'Executing',
'tray.status.error': 'Error',
'app.update.disabled': 'Auto-update is not enabled (no feed URL configured, or dev mode)',
'app.dialog.selectWorkspace.title': 'Select workspace directory',
'app.dialog.startupFailed.title': 'MetonaAI Desktop failed to start',
'app.dialog.startupFailed.body':
'An error occurred during initialization and the app will exit.\n\n{{message}}\n\nPossible causes: the workspace directory is not writable, or the database file is corrupted.\nTry switching the workspace path in Settings and restart.',
'workspace.reason.empty': 'Path must not be empty',
'workspace.reason.notExists': 'Directory does not exist — it will be created after switching',
'workspace.reason.notDirectory': 'Path is not a directory',
'workspace.reason.inaccessible': 'Path is not accessible',
'workspace.reason.inheritFailed': 'Failed to inherit files: {{message}}',
'workspace.reason.restartRequired': 'Workspace switched — restart the app to apply',
};
const DICTS: Record<MainLocale, Record<string, string>> = { 'zh-CN': zh, 'en-US': en };
+52
View File
@@ -0,0 +1,52 @@
/**
* v0.8.2 P1-5: 敏感值脱敏单源工具
*
* 背景:审计日志 logToolCall 原样落库完整工具 args —— 工具参数中携带的
* API Key / 鉴权头 / token(如 http_request 的 headers.authorization、
* MCP 工具的鉴权参数)以明文进入 audit_logs 表,密钥链加密(safeStorage
* 只保护配置层,不保护审计层,构成敏感信息二次扩散面。
*
* 本模块提供与配置层同源(isSensitiveConfigKey 的归一化键名匹配)的脱敏:
* - maskSensitiveValue:字符串值掩码(长值保留后 4 位,短值完全掩码)
* - deepMaskSensitive:递归遍历对象/数组,按键名匹配掩码字符串值
* (循环引用防护 + 深度上限,防御恶意构造的参数结构)
*/
import { isSensitiveConfigKey } from './secure-config';
/** 长值保留后 4 位,短值(≤4 字符)完全掩码 —— 与 ipc/shared.maskSensitive 同口径 */
export function maskSensitiveValue(value: string): string {
return value.length > 4 ? '***' + value.slice(-4) : '***';
}
const MAX_MASK_DEPTH = 6;
/**
* 深度脱敏:返回脱敏后的副本(原对象不修改)。
* 对象/数组递归;键名命中敏感模式(归一化匹配,api_key/authKey/token/secret/
* password/credential 等)时对字符串值掩码;其余值原样保留。
*/
export function deepMaskSensitive<T>(input: T, depth = 0, seen?: Set<object>): T {
if (input === null || typeof input !== 'object') return input;
if (depth >= MAX_MASK_DEPTH) return '[depth-limit]' as unknown as T;
const seenSet = seen ?? new Set<object>();
if (seenSet.has(input as object)) return '[circular]' as unknown as T;
seenSet.add(input as object);
try {
if (Array.isArray(input)) {
return input.map((item) => deepMaskSensitive(item, depth + 1, seenSet)) as unknown as T;
}
const out: Record<string, unknown> = {};
for (const [key, value] of Object.entries(input as Record<string, unknown>)) {
if (isSensitiveConfigKey(key) && typeof value === 'string' && value.length > 0) {
out[key] = maskSensitiveValue(value);
} else {
out[key] = deepMaskSensitive(value, depth + 1, seenSet);
}
}
return out as unknown as T;
} finally {
seenSet.delete(input as object);
}
}
+6 -1
View File
@@ -60,7 +60,9 @@ export function resetEncryptionUsableForTests(): void {
encryptionUsable = null;
}
/** 敏感配置 key 匹配模式(与 IPC 层审计脱敏规则保持一致) */
/** 敏感配置 key 匹配模式(与 IPC 层审计脱敏规则保持一致)
* v0.8.2 P1-5: 补 authorization / authkey / credential —— 审计 args 深度脱敏
* 复用本表,HTTP 标准鉴权头(Authorization)此前不命中导致明文入库 */
const SENSITIVE_KEY_PATTERNS = [
'apikey',
'api_key',
@@ -69,6 +71,9 @@ const SENSITIVE_KEY_PATTERNS = [
'secret',
'password',
'auth_key',
'authkey',
'authorization',
'credential',
];
/**