feat: v0.6.0 全量审计修复 — 安全加固 + 死代码激活 + 依赖治理
基于全量源码审计(electron/ 70+ 文件、src/ 45+ 文件完整读取)的 四阶段迭代,修复 1 项安全缺陷、8 项功能缺陷、依赖与文档系统性脱节。 【P0 安全与数据正确性】 - S-1 数据导出泄露明文密钥:data:export 全量导出直接透传 configService.getAll()(敏感 key 解密返回),导出文件含明文 API Key。 新增 sanitizeExportConfig(shared.ts)逐 key 脱敏 + 5 项回归测试 - F-3 session_summaries 无级联删除:删会话后摘要永久残留。建表语句补 FOREIGN KEY ON DELETE CASCADE + 迁移 8 重建存量表(幂等检测)+ 2 项测试 - F-4 macOS activate 重建窗口后确认弹框失效:窗口创建收敛为 createMainWindow 单一入口(beforeLoad 补 setMainWindow/IPC 注册),TrayManager 补 rebindWindow,app:selectFolder 改 event.sender 动态解析窗口 【P1 死代码激活与功能补全】 - F-1 会话右键菜单挂载:ContextMenu session 分支(重命名/置顶/归档/ 导出/删除)约 200 行此前无任何触发点,Sidebar SessionItem 挂载 onContextMenu;置顶/归档 label 随状态切换显示 - F-2 归档会话不可找回:Sidebar 新增「已归档」折叠面板(恢复入口), 归档功能形成完整闭环 - F-7 Provider 白名单校验:createAdapter/buildFallbackAdapter 未知 provider 显式拒绝(此前静默落入 DeepSeekAdapter 以空配置失败) - F-8 死配置治理:接通 5 项(llm.temperature/llm.maxTokens 注入引擎、 security.promptInjectionDefense 控制 SecurityScanHook+消息检测、 logging.auditEnabled 控制 AuditLogHook、logging.traceEnabled 控制 SessionRecorder,均 fail-secure 仅显式 false 关闭);删除 4 项 无消费者配置(requireWriteConfirmation/maxFileWriteSizeKB/fontSize/ animationMode) 【P2 依赖治理与 UX 修缮】 - D-1 移除僵尸依赖 electron-store/zod/rehype-raw(源码零引用, 共裁 24 包);README 技术栈表同步删除虚假宣称 - metona-toast 升级 0.2.1 → 0.5.0(API 全兼容:107 种图标类型、 配置项超集,default/configure/use 接口不变) - F-5 工作空间手输路径实时落库中间态:改 pendingPath 草稿 + 显式 「校验」按钮(选择文件夹与手输共用 validatePath 流程) - F-6 确认弹框超时滞留:倒计时归零时主动 refreshPending(后端超时 已删条目,拉取后弹框自然消解) - D-3 ChatInput accept 移除 .pdf(分类器不识别,误导性入口); Onboarding 切换 Provider 自动填充默认 URL(与 LLMSettings 一致) 【P3 文档口径收敛】 - README:工具数 30+→28、版本 0.6.0、测试数 252、配置表补 F-8 接通项 - built-in/index.ts 计数注释 30→28 - docs/网络工具 v2 存储键名统一为点号口径(searxng.enabled) - docs/完整设计指南修正 db.handlers.ts 失效路径引用为 data.ts 【验证】 - lint 0 error / 0 warning - typecheck 双工程(node+web)0 错误 - test:electron 24 文件 252 用例全通过(+7 新增:导出脱敏 ×5、 级联删除 ×2) - electron-vite build 成功(metona-toast 0.5.0 chunk 正常) - 系统 Node 模式 npm test 225 通过 + 27 ABI skip(符合预期)
This commit is contained in:
@@ -10,7 +10,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://img.shields.io/badge/version-0.5.5-blue?style=flat-square" alt="Version" />
|
<img src="https://img.shields.io/badge/version-0.6.0-blue?style=flat-square" alt="Version" />
|
||||||
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License" />
|
<img src="https://img.shields.io/badge/license-MIT-green?style=flat-square" alt="License" />
|
||||||
<img src="https://img.shields.io/badge/Electron-35-47848F?style=flat-square&logo=electron" alt="Electron" />
|
<img src="https://img.shields.io/badge/Electron-35-47848F?style=flat-square&logo=electron" alt="Electron" />
|
||||||
<img src="https://img.shields.io/badge/React-19-61DAFB?style=flat-square&logo=react" alt="React" />
|
<img src="https://img.shields.io/badge/React-19-61DAFB?style=flat-square&logo=react" alt="React" />
|
||||||
@@ -117,10 +117,9 @@
|
|||||||
| 📝 Markdown | **react-markdown + remark-gfm** | 10 / 4 | 富文本渲染 |
|
| 📝 Markdown | **react-markdown + remark-gfm** | 10 / 4 | 富文本渲染 |
|
||||||
| 🌐 HTML 解析 | **node-html-parser** | 6 | 搜索引擎结果结构化解析 |
|
| 🌐 HTML 解析 | **node-html-parser** | 6 | 搜索引擎结果结构化解析 |
|
||||||
| 🔢 UUID | **nanoid** | 5 | 唯一 ID 生成 |
|
| 🔢 UUID | **nanoid** | 5 | 唯一 ID 生成 |
|
||||||
| ✅ 校验 | **Zod** | 3 | 运行时类型校验 |
|
|
||||||
| 💾 缓存 | **lru-cache** | 11 | 内存缓存 |
|
| 💾 缓存 | **lru-cache** | 11 | 内存缓存 |
|
||||||
| 📋 日志 | **electron-log** | 5 | 分级结构化日志 |
|
| 📋 日志 | **electron-log** | 5 | 分级结构化日志 |
|
||||||
| ⚙️ 配置 | **electron-store** | 10 | 键值对持久化配置 |
|
| ⌨️ 命令解析 | **shell-quote** | 1 | Shell 命令 token 化(防注入) |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -477,7 +476,7 @@ Metona 的 Agent 引擎采用分层架构,每层职责清晰:
|
|||||||
│ L3 工具与安全执行层 (Tools & Security) │
|
│ L3 工具与安全执行层 (Tools & Security) │
|
||||||
│ ┌────────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────────┐ │
|
│ ┌────────────┐ ┌──────────┐ ┌──────────┐ ┌─────────────────┐ │
|
||||||
│ │ ToolRegistry│ │ Sandbox │ │PolicyEng │ │ InjectionDefender│ │
|
│ │ ToolRegistry│ │ Sandbox │ │PolicyEng │ │ InjectionDefender│ │
|
||||||
│ │ 30+ 工具注册│ │ 28+ 扫描 │ │ 35+ 策略 │ │ 40+ 正则 + 语义 │ │
|
│ │ 28 工具注册 │ │ 28+ 扫描 │ │ 35+ 策略 │ │ 40+ 正则 + 语义 │ │
|
||||||
│ │ MCP 适配 │ │ 路径校验 │ │ 频率限制 │ │ 输出校验 │ │
|
│ │ MCP 适配 │ │ 路径校验 │ │ 频率限制 │ │ 输出校验 │ │
|
||||||
│ └────────────┘ └──────────┘ └──────────┘ └─────────────────┘ │
|
│ └────────────┘ └──────────┘ └──────────┘ └─────────────────┘ │
|
||||||
│ 类型: MetonaToolDef / MetonaToolCall / MetonaToolResult │
|
│ 类型: MetonaToolDef / MetonaToolCall / MetonaToolResult │
|
||||||
@@ -643,7 +642,12 @@ OLLAMA_BASE_URL=http://localhost:11434
|
|||||||
|:---|:---|:---|
|
|:---|:---|:---|
|
||||||
| `llm.provider` | (空) | LLM Provider ID(未配置时回退 .env) |
|
| `llm.provider` | (空) | LLM Provider ID(未配置时回退 .env) |
|
||||||
| `llm.model` | (空) | 模型标识符 |
|
| `llm.model` | (空) | 模型标识符 |
|
||||||
|
| `llm.temperature` | `0` | 生成温度(注入引擎请求参数) |
|
||||||
|
| `llm.maxTokens` | `63488` | 单次生成最大 token(各 Provider 按模型上限自动钳制) |
|
||||||
| `llm.multimodalEnabled` | `false` | 多模态总开关 — 未开启时即使模型支持也不能上传图片 |
|
| `llm.multimodalEnabled` | `false` | 多模态总开关 — 未开启时即使模型支持也不能上传图片 |
|
||||||
|
| `security.promptInjectionDefense` | `true` | 提示注入检测总开关(用户消息 + 工具结果扫描) |
|
||||||
|
| `logging.auditEnabled` | `true` | 工具调用审计日志开关 |
|
||||||
|
| `logging.traceEnabled` | `true` | 会话 TRACE 录制开关(JSONL 文件) |
|
||||||
| `agent.maxIterations` | `20` | ReAct 最大迭代轮次 |
|
| `agent.maxIterations` | `20` | ReAct 最大迭代轮次 |
|
||||||
| `agent.totalTimeoutMs` | `600000` | Agent 总超时 (ms) |
|
| `agent.totalTimeoutMs` | `600000` | Agent 总超时 (ms) |
|
||||||
| `agent.toolExecutionTimeoutMs` | `120000` | 单个工具执行超时 (ms) |
|
| `agent.toolExecutionTimeoutMs` | `120000` | 单个工具执行超时 (ms) |
|
||||||
@@ -867,7 +871,7 @@ npm run format # Prettier 格式化
|
|||||||
|
|
||||||
# ─── 测试 ─────────────────────────────────
|
# ─── 测试 ─────────────────────────────────
|
||||||
npm test # 运行单元测试 (Vitest, 系统 Node — audit 套件因 better-sqlite3 ABI 自动跳过)
|
npm test # 运行单元测试 (Vitest, 系统 Node — audit 套件因 better-sqlite3 ABI 自动跳过)
|
||||||
npm run test:electron # 运行全量单元测试 (Electron Node ABI, 245 用例全执行, 含 SQLite 审计链哈希 + 引擎工具链集成)
|
npm run test:electron # 运行全量单元测试 (Electron Node ABI, 252 用例全执行, 含 SQLite 审计链哈希 + 引擎工具链集成)
|
||||||
npm run test:watch # 测试监听模式
|
npm run test:watch # 测试监听模式
|
||||||
|
|
||||||
# ─── 构建 ─────────────────────────────────
|
# ─── 构建 ─────────────────────────────────
|
||||||
|
|||||||
+13
-13
@@ -56,22 +56,22 @@ SearXNG 相关配置共 12 项,各配置项的默认值与含义如下:
|
|||||||
|
|
||||||
### 1.3 持久化存储
|
### 1.3 持久化存储
|
||||||
|
|
||||||
配置通过本地数据库以键值对形式持久化。写入时逐项调用 `saveSetting(key, value)`,读取时通过 `getSetting(key, default)` 回填到运行时缓存。对应的存储键名汇总如下:
|
配置通过本地数据库以键值对形式持久化。写入时逐项调用 `saveSetting(key, value)`,读取时通过 `getSetting(key, default)` 回填到运行时缓存。对应的存储键名汇总如下(点号分层命名,与 `app_config` 表的通用键值规范一致):
|
||||||
|
|
||||||
| 存储键名 | 对应配置项 | 默认值 |
|
| 存储键名 | 对应配置项 | 默认值 |
|
||||||
|----------|-----------|--------|
|
|----------|-----------|--------|
|
||||||
| `searxng_enabled` | enabled | `false` |
|
| `searxng.enabled` | enabled | `false` |
|
||||||
| `searxng_url` | url | `''` |
|
| `searxng.url` | url | `''` |
|
||||||
| `searxng_engines` | engines | `''` |
|
| `searxng.engines` | engines | `''` |
|
||||||
| `searxng_language` | language | `'zh-CN'` |
|
| `searxng.language` | language | `'zh-CN'` |
|
||||||
| `searxng_safesearch` | safesearch | `1` |
|
| `searxng.safesearch` | safesearch | `1` |
|
||||||
| `searxng_time_range` | time_range | `''` |
|
| `searxng.time_range` | time_range | `''` |
|
||||||
| `searxng_max_results` | max_results | `0` |
|
| `searxng.max_results` | max_results | `0` |
|
||||||
| `searxng_auth_key` | auth_key | `''` |
|
| `searxng.auth_key` | auth_key | `''` |
|
||||||
| `searxng_auth_type` | auth_type | `'bearer'` |
|
| `searxng.auth_type` | auth_type | `'bearer'` |
|
||||||
| `searxng_format` | format | `'json'` |
|
| `searxng.format` | format | `'json'` |
|
||||||
| `fetch_count` | fetch_count | `0` |
|
| `searxng.fetch_count` | fetch_count | `0` |
|
||||||
| `fetch_mode` | fetch_mode | `'sequential'` |
|
| `searxng.fetch_mode` | fetch_mode | `'sequential'` |
|
||||||
|
|
||||||
### 1.4 UI 模态框
|
### 1.4 UI 模态框
|
||||||
|
|
||||||
|
|||||||
@@ -4443,7 +4443,7 @@ export function registerIPCHandlers(mainWindow: BrowserWindow): void {
|
|||||||
}
|
}
|
||||||
</code></pre>
|
</code></pre>
|
||||||
<h3>8.4 跨进程数据库访问层</h3>
|
<h3>8.4 跨进程数据库访问层</h3>
|
||||||
<pre><code class="language-typescript">// ====== electron/ipc/db.handlers.ts ======
|
<pre><code class="language-typescript">// ====== electron/ipc/data.ts(数据导出与清理域,自 handlers.ts 拆分) ======
|
||||||
|
|
||||||
import { ipcMain } from 'electron';
|
import { ipcMain } from 'electron';
|
||||||
import initSqlJs, { Database } from 'sql.js';
|
import initSqlJs, { Database } from 'sql.js';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* 内置工具导出
|
* 内置工具导出
|
||||||
*
|
*
|
||||||
* v0.3.13: 删除 todo_write 工具(零价值 + 死代码 clearSession + 与 task_manager 功能重叠)
|
* v0.3.13: 删除 todo_write 工具(零价值 + 死代码 clearSession + 与 task_manager 功能重叠)
|
||||||
* 工具总数:30 个(29 + DelegateTaskTool)
|
* 工具总数:28 个(27 + DelegateTaskTool)
|
||||||
*
|
*
|
||||||
* v0.3.3: 从 27 个工具扩展到 29 个工具
|
* v0.3.3: 从 27 个工具扩展到 29 个工具
|
||||||
* 新增:file_move, file_info
|
* 新增:file_move, file_info
|
||||||
@@ -21,7 +21,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// 文件系统工具(7 个)
|
// 文件系统工具(7 个)
|
||||||
export { ReadFileTool, WriteFileTool, ListDirectoryTool, SearchFilesTool, DeleteFileTool, FileMoveTool, FileInfoTool } from './filesystem';
|
export {
|
||||||
|
ReadFileTool,
|
||||||
|
WriteFileTool,
|
||||||
|
ListDirectoryTool,
|
||||||
|
SearchFilesTool,
|
||||||
|
DeleteFileTool,
|
||||||
|
FileMoveTool,
|
||||||
|
FileInfoTool,
|
||||||
|
} from './filesystem';
|
||||||
// v0.2.0: 精准文件编辑工具
|
// v0.2.0: 精准文件编辑工具
|
||||||
export { FileEditorTool } from './file-editor';
|
export { FileEditorTool } from './file-editor';
|
||||||
// v0.2.0: ripgrep 代码搜索工具
|
// v0.2.0: ripgrep 代码搜索工具
|
||||||
@@ -61,4 +69,3 @@ export { ThinkTool } from './think';
|
|||||||
|
|
||||||
// v0.3.1: 图片查看工具(1 个)
|
// v0.3.1: 图片查看工具(1 个)
|
||||||
export { ViewImageTool } from './view-image';
|
export { ViewImageTool } from './view-image';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
/**
|
||||||
|
* 导出脱敏回归测试(S-1)
|
||||||
|
*
|
||||||
|
* 背景:data:export 全量导出曾直接透传 configService.getAll(),
|
||||||
|
* 该方法对敏感 key 解密返回明文,导致导出文件泄露明文 API Key。
|
||||||
|
* sanitizeExportConfig 必须保证任何敏感 key 经其处理后不含明文。
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { sanitizeExportConfig, maskSensitive } from '../shared';
|
||||||
|
|
||||||
|
describe('sanitizeExportConfig — 导出配置脱敏', () => {
|
||||||
|
it('llm.apiKey / fallbackApiKey / searxng.auth_key 导出为掩码', () => {
|
||||||
|
const config = {
|
||||||
|
'llm.provider': 'deepseek',
|
||||||
|
'llm.model': 'deepseek-v4-pro',
|
||||||
|
'llm.apiKey': 'sk-very-secret-key-1234',
|
||||||
|
'llm.fallbackApiKey': 'sk-fallback-secret-9876',
|
||||||
|
'searxng.auth_key': 'bearer-token-abcdef',
|
||||||
|
'searxng.enabled': true,
|
||||||
|
};
|
||||||
|
const out = sanitizeExportConfig(config);
|
||||||
|
expect(out['llm.apiKey']).not.toContain('sk-very-secret');
|
||||||
|
expect(out['llm.apiKey']).toBe('***1234');
|
||||||
|
expect(out['llm.fallbackApiKey']).toBe('***9876');
|
||||||
|
expect(out['searxng.auth_key']).toBe('***cdef');
|
||||||
|
// 非敏感 key 原样保留
|
||||||
|
expect(out['llm.provider']).toBe('deepseek');
|
||||||
|
expect(out['searxng.enabled']).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('短敏感值(<=4 字符)完全掩码', () => {
|
||||||
|
expect(sanitizeExportConfig({ 'llm.apiKey': 'abc' })['llm.apiKey']).toBe('***');
|
||||||
|
expect(sanitizeExportConfig({ 'llm.apiKey': '' })['llm.apiKey']).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('不修改入参对象(纯函数)', () => {
|
||||||
|
const config = { 'llm.apiKey': 'sk-original-plaintext' };
|
||||||
|
const snapshot = { ...config };
|
||||||
|
sanitizeExportConfig(config);
|
||||||
|
expect(config).toEqual(snapshot);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('空对象与混合类型安全', () => {
|
||||||
|
expect(sanitizeExportConfig({})).toEqual({});
|
||||||
|
const out = sanitizeExportConfig({
|
||||||
|
'agent.maxIterations': 20,
|
||||||
|
'agent.enableThinking': true,
|
||||||
|
'ollama.numCtx': null,
|
||||||
|
'tools.run_command.enabled': false,
|
||||||
|
});
|
||||||
|
expect(out).toEqual({
|
||||||
|
'agent.maxIterations': 20,
|
||||||
|
'agent.enableThinking': true,
|
||||||
|
'ollama.numCtx': null,
|
||||||
|
'tools.run_command.enabled': false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('maskSensitive:非字符串敏感值原样返回', () => {
|
||||||
|
expect(maskSensitive('llm.apiKey', 123)).toBe(123);
|
||||||
|
expect(maskSensitive('llm.apiKey', null)).toBe(null);
|
||||||
|
});
|
||||||
|
});
|
||||||
+27
-20
@@ -460,26 +460,33 @@ export function registerAgentHandlers(ctx: IPCContext): void {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 提示注入检测(安全模块)
|
// 提示注入检测(安全模块)
|
||||||
const injectionResult = promptInjectionDefender.detect(userMessage.content);
|
// F-8 接通: security.promptInjectionDefense=false 时跳过用户消息检测
|
||||||
if (injectionResult.riskScore >= 7) {
|
// (工具结果侧的 SecurityScanHook 由 main.ts 按同一配置决定是否挂载)
|
||||||
log.warn('[PromptInjectionDefender] Blocked message:', injectionResult.findings);
|
// fail-secure: 仅显式 false 才关闭 —— 配置值异常(空串/null/类型错误)时保持防护开启
|
||||||
sendErrorEvent(
|
const injectionEnabled =
|
||||||
`Message blocked by prompt injection defense: ${injectionResult.recommendation}`,
|
configService.get<boolean>('security.promptInjectionDefense') !== false;
|
||||||
sessionId,
|
if (injectionEnabled) {
|
||||||
);
|
const injectionResult = promptInjectionDefender.detect(userMessage.content);
|
||||||
sessionRecorder.stopRecording(sessionId, {
|
if (injectionResult.riskScore >= 7) {
|
||||||
totalIterations: 0,
|
log.warn('[PromptInjectionDefender] Blocked message:', injectionResult.findings);
|
||||||
totalTokens: 0,
|
sendErrorEvent(
|
||||||
durationMs: 0,
|
`Message blocked by prompt injection defense: ${injectionResult.recommendation}`,
|
||||||
terminationReason: 'error',
|
sessionId,
|
||||||
});
|
);
|
||||||
return { success: false, error: 'Message blocked by prompt injection defense' };
|
sessionRecorder.stopRecording(sessionId, {
|
||||||
}
|
totalIterations: 0,
|
||||||
if (injectionResult.riskScore >= 4) {
|
totalTokens: 0,
|
||||||
log.warn(
|
durationMs: 0,
|
||||||
'[PromptInjectionDefender] Suspicious patterns detected:',
|
terminationReason: 'error',
|
||||||
injectionResult.findings,
|
});
|
||||||
);
|
return { success: false, error: 'Message blocked by prompt injection defense' };
|
||||||
|
}
|
||||||
|
if (injectionResult.riskScore >= 4) {
|
||||||
|
log.warn(
|
||||||
|
'[PromptInjectionDefender] Suspicious patterns detected:',
|
||||||
|
injectionResult.findings,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TRACE 层:记录上下文构建
|
// TRACE 层:记录上下文构建
|
||||||
|
|||||||
+7
-3
@@ -5,7 +5,7 @@
|
|||||||
* 审计日志链验证/查询、渲染进程错误上报(P0-3 修复断链)。
|
* 审计日志链验证/查询、渲染进程错误上报(P0-3 修复断链)。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ipcMain, shell, app, dialog } from 'electron';
|
import { ipcMain, shell, app, dialog, BrowserWindow } from 'electron';
|
||||||
import type { IPCContext } from './context';
|
import type { IPCContext } from './context';
|
||||||
import type { AuditEventType } from '../services/audit.service';
|
import type { AuditEventType } from '../services/audit.service';
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
@@ -54,8 +54,12 @@ export function registerAppHandlers(ctx: IPCContext): void {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('app:selectFolder', async (_event, defaultPath?: string) => {
|
ipcMain.handle('app:selectFolder', async (event, defaultPath?: string) => {
|
||||||
const result = await dialog.showOpenDialog(mainWindow, {
|
// F-4 补充: 从 event.sender 动态解析窗口 —— ctx.mainWindow 是注册时捕获的引用,
|
||||||
|
// macOS activate 重建窗口后为已销毁实例(dialog 挂 destroyed 窗口行为未定义)。
|
||||||
|
// fromWebContents 对任何存活窗口(含未来多窗口)都正确,解析失败回退无父对话框。
|
||||||
|
const callerWin = BrowserWindow.fromWebContents(event.sender) ?? mainWindow;
|
||||||
|
const result = await dialog.showOpenDialog(callerWin, {
|
||||||
properties: ['openDirectory', 'createDirectory'],
|
properties: ['openDirectory', 'createDirectory'],
|
||||||
defaultPath: defaultPath ?? app.getPath('home'),
|
defaultPath: defaultPath ?? app.getPath('home'),
|
||||||
title: '选择工作空间目录',
|
title: '选择工作空间目录',
|
||||||
|
|||||||
+56
-14
@@ -5,6 +5,7 @@
|
|||||||
import { ipcMain } from 'electron';
|
import { ipcMain } from 'electron';
|
||||||
import type { IPCContext } from './context';
|
import type { IPCContext } from './context';
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
|
import { sanitizeExportConfig } from './shared';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 导出限流常量:单会话导出的最大消息条数。
|
* 导出限流常量:单会话导出的最大消息条数。
|
||||||
@@ -33,7 +34,10 @@ const sanitizeExportMessage = (msg: Record<string, unknown>): Record<string, unk
|
|||||||
void _omit;
|
void _omit;
|
||||||
return {
|
return {
|
||||||
...msg,
|
...msg,
|
||||||
toolResult: { ...toolResult, result: { ...rest, _displayNote: '[image base64 omitted in export]' } },
|
toolResult: {
|
||||||
|
...toolResult,
|
||||||
|
result: { ...rest, _displayNote: '[image base64 omitted in export]' },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,15 +65,26 @@ export function registerDataHandlers(ctx: IPCContext): void {
|
|||||||
if (sessionId) {
|
if (sessionId) {
|
||||||
// 导出单个会话:剥离超大 base64 后返回
|
// 导出单个会话:剥离超大 base64 后返回
|
||||||
const messages = sessionService.getMessages(sessionId);
|
const messages = sessionService.getMessages(sessionId);
|
||||||
const sanitized = messages.map((m) => sanitizeExportMessage(m as unknown as Record<string, unknown>));
|
const sanitized = messages.map((m) =>
|
||||||
|
sanitizeExportMessage(m as unknown as Record<string, unknown>),
|
||||||
|
);
|
||||||
return { success: true, data: sanitized };
|
return { success: true, data: sanitized };
|
||||||
}
|
}
|
||||||
// 导出所有会话:剥离 dataUrl + 每会话限条数,防渲染进程 Blob 序列化 OOM
|
// 导出所有会话:剥离 dataUrl + 每会话限条数,防渲染进程 Blob 序列化 OOM
|
||||||
|
// S-1 修复:config 走 sanitizeExportConfig 脱敏 —— getAll() 对敏感 key
|
||||||
|
// 解密返回明文,直接透传会把明文 API Key 写入用户下载的导出文件
|
||||||
const sessions = sessionService.list();
|
const sessions = sessionService.list();
|
||||||
const allData: Record<string, unknown> = { sessions: [], config: configService.getAll() };
|
const allData: Record<string, unknown> = {
|
||||||
|
sessions: [],
|
||||||
|
config: sanitizeExportConfig(configService.getAll()),
|
||||||
|
};
|
||||||
for (const session of sessions) {
|
for (const session of sessions) {
|
||||||
const rawMessages = sessionService.getMessages(session.id, { limit: MAX_EXPORT_MESSAGES_PER_SESSION });
|
const rawMessages = sessionService.getMessages(session.id, {
|
||||||
const sanitizedMessages = rawMessages.map((m) => sanitizeExportMessage(m as unknown as Record<string, unknown>));
|
limit: MAX_EXPORT_MESSAGES_PER_SESSION,
|
||||||
|
});
|
||||||
|
const sanitizedMessages = rawMessages.map((m) =>
|
||||||
|
sanitizeExportMessage(m as unknown as Record<string, unknown>),
|
||||||
|
);
|
||||||
(allData.sessions as Array<Record<string, unknown>>).push({
|
(allData.sessions as Array<Record<string, unknown>>).push({
|
||||||
...session,
|
...session,
|
||||||
messages: sanitizedMessages,
|
messages: sanitizedMessages,
|
||||||
@@ -93,10 +108,16 @@ export function registerDataHandlers(ctx: IPCContext): void {
|
|||||||
db.exec('DELETE FROM messages');
|
db.exec('DELETE FROM messages');
|
||||||
db.exec('DELETE FROM sessions');
|
db.exec('DELETE FROM sessions');
|
||||||
db.exec('COMMIT');
|
db.exec('COMMIT');
|
||||||
log.info(`[DATA] All sessions cleared: ${sessCount.c} sessions, ${msgCount.c} messages deleted`);
|
log.info(
|
||||||
|
`[DATA] All sessions cleared: ${sessCount.c} sessions, ${msgCount.c} messages deleted`,
|
||||||
|
);
|
||||||
return { success: true, deletedSessions: sessCount.c, deletedMessages: msgCount.c };
|
return { success: true, deletedSessions: sessCount.c, deletedMessages: msgCount.c };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
try { db.exec('ROLLBACK'); } catch { /* 忽略回滚错误 */ }
|
try {
|
||||||
|
db.exec('ROLLBACK');
|
||||||
|
} catch {
|
||||||
|
/* 忽略回滚错误 */
|
||||||
|
}
|
||||||
log.error('[DATA] clearSessions failed:', (error as Error).message);
|
log.error('[DATA] clearSessions failed:', (error as Error).message);
|
||||||
return { success: false, error: (error as Error).message };
|
return { success: false, error: (error as Error).message };
|
||||||
}
|
}
|
||||||
@@ -108,19 +129,36 @@ export function registerDataHandlers(ctx: IPCContext): void {
|
|||||||
const db = sessionService.getDB();
|
const db = sessionService.getDB();
|
||||||
try {
|
try {
|
||||||
// M-41 修复: 三个 DELETE 操作用事务包裹,防止部分失败导致三类记忆数据不一致
|
// M-41 修复: 三个 DELETE 操作用事务包裹,防止部分失败导致三类记忆数据不一致
|
||||||
const epiCount = db.prepare('SELECT COUNT(*) as c FROM episodic_memories').get() as { c: number };
|
const epiCount = db.prepare('SELECT COUNT(*) as c FROM episodic_memories').get() as {
|
||||||
const semCount = db.prepare('SELECT COUNT(*) as c FROM semantic_memories').get() as { c: number };
|
c: number;
|
||||||
const workCount = db.prepare('SELECT COUNT(*) as c FROM working_memories').get() as { c: number };
|
};
|
||||||
|
const semCount = db.prepare('SELECT COUNT(*) as c FROM semantic_memories').get() as {
|
||||||
|
c: number;
|
||||||
|
};
|
||||||
|
const workCount = db.prepare('SELECT COUNT(*) as c FROM working_memories').get() as {
|
||||||
|
c: number;
|
||||||
|
};
|
||||||
db.exec('BEGIN');
|
db.exec('BEGIN');
|
||||||
db.exec('DELETE FROM episodic_memories');
|
db.exec('DELETE FROM episodic_memories');
|
||||||
db.exec('DELETE FROM semantic_memories');
|
db.exec('DELETE FROM semantic_memories');
|
||||||
db.exec('DELETE FROM working_memories');
|
db.exec('DELETE FROM working_memories');
|
||||||
db.exec('COMMIT');
|
db.exec('COMMIT');
|
||||||
log.info(`[DATA] All memories cleared: ${epiCount.c} episodic, ${semCount.c} semantic, ${workCount.c} working memories deleted`);
|
log.info(
|
||||||
return { success: true, deletedEpisodic: epiCount.c, deletedSemantic: semCount.c, deletedWorking: workCount.c };
|
`[DATA] All memories cleared: ${epiCount.c} episodic, ${semCount.c} semantic, ${workCount.c} working memories deleted`,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
deletedEpisodic: epiCount.c,
|
||||||
|
deletedSemantic: semCount.c,
|
||||||
|
deletedWorking: workCount.c,
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// M-41 修复: 失败时回滚事务,确保数据一致性
|
// M-41 修复: 失败时回滚事务,确保数据一致性
|
||||||
try { db.exec('ROLLBACK'); } catch { /* 忽略回滚错误 */ }
|
try {
|
||||||
|
db.exec('ROLLBACK');
|
||||||
|
} catch {
|
||||||
|
/* 忽略回滚错误 */
|
||||||
|
}
|
||||||
log.error('[DATA] clearMemories failed:', (error as Error).message);
|
log.error('[DATA] clearMemories failed:', (error as Error).message);
|
||||||
return { success: false, error: (error as Error).message };
|
return { success: false, error: (error as Error).message };
|
||||||
}
|
}
|
||||||
@@ -143,7 +181,11 @@ export function registerDataHandlers(ctx: IPCContext): void {
|
|||||||
log.info('[DATA] Audit logs cleared');
|
log.info('[DATA] Audit logs cleared');
|
||||||
return { success: true };
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
try { db.exec('ROLLBACK'); } catch { /* 忽略回滚错误 */ }
|
try {
|
||||||
|
db.exec('ROLLBACK');
|
||||||
|
} catch {
|
||||||
|
/* 忽略回滚错误 */
|
||||||
|
}
|
||||||
return { success: false, error: (error as Error).message };
|
return { success: false, error: (error as Error).message };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+61
-11
@@ -13,11 +13,20 @@ import { isSensitiveConfigKey } from '../utils/secure-config';
|
|||||||
|
|
||||||
/** LLM 相关配置 key(变更时触发热重载 Adapter) */
|
/** LLM 相关配置 key(变更时触发热重载 Adapter) */
|
||||||
export const LLM_CONFIG_KEYS = [
|
export const LLM_CONFIG_KEYS = [
|
||||||
'llm.provider', 'llm.model', 'llm.apiKey', 'llm.baseURL',
|
'llm.provider',
|
||||||
'llm.fallbackProvider', 'llm.fallbackModel', 'llm.fallbackApiKey', 'llm.fallbackBaseURL',
|
'llm.model',
|
||||||
|
'llm.apiKey',
|
||||||
|
'llm.baseURL',
|
||||||
|
'llm.fallbackProvider',
|
||||||
|
'llm.fallbackModel',
|
||||||
|
'llm.fallbackApiKey',
|
||||||
|
'llm.fallbackBaseURL',
|
||||||
'ollama.numCtx',
|
'ollama.numCtx',
|
||||||
'deepseek.contextWindow', 'agnes.contextWindow', 'mimo.contextWindow',
|
'deepseek.contextWindow',
|
||||||
'openai.contextWindow', 'anthropic.contextWindow',
|
'agnes.contextWindow',
|
||||||
|
'mimo.contextWindow',
|
||||||
|
'openai.contextWindow',
|
||||||
|
'anthropic.contextWindow',
|
||||||
];
|
];
|
||||||
|
|
||||||
/** 敏感配置值脱敏(审计日志用:长值保留后 4 位,短值完全掩码) */
|
/** 敏感配置值脱敏(审计日志用:长值保留后 4 位,短值完全掩码) */
|
||||||
@@ -28,20 +37,43 @@ export function maskSensitive(key: string, value: unknown): unknown {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出配置脱敏(S-1 修复):对 configService.getAll() 的结果逐 key 脱敏。
|
||||||
|
*
|
||||||
|
* 背景:getAll() 对敏感 key 解密后返回明文(ConfigService 的读取契约),
|
||||||
|
* data:export 直接透传会把明文 API Key / 认证密钥写入用户下载的 JSON 文件,
|
||||||
|
* 绕过 safeStorage 密钥链加密。此函数确保任何导出路径不泄露明文密钥。
|
||||||
|
*
|
||||||
|
* @param config configService.getAll() 的完整配置快照
|
||||||
|
* @returns 脱敏后的副本(敏感值替换为掩码,原对象不修改)
|
||||||
|
*/
|
||||||
|
export function sanitizeExportConfig(config: Record<string, unknown>): Record<string, unknown> {
|
||||||
|
const out: Record<string, unknown> = {};
|
||||||
|
for (const [key, value] of Object.entries(config)) {
|
||||||
|
out[key] = maskSensitive(key, value);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provider 切换时清空 API key(C-1 修复,供 set/setBatch 共用)
|
* Provider 切换时清空 API key(C-1 修复,供 set/setBatch 共用)
|
||||||
*
|
*
|
||||||
* 必须在写入 entries 之前执行:若前端把 llm.apiKey 放在 llm.provider 之前,
|
* 必须在写入 entries 之前执行:若前端把 llm.apiKey 放在 llm.provider 之前,
|
||||||
* 先 set apiKey 再处理 provider 会把用户刚填的 key 清空。
|
* 先 set apiKey 再处理 provider 会把用户刚填的 key 清空。
|
||||||
*/
|
*/
|
||||||
export function clearApiKeyOnProviderChange(ctx: IPCContext, entries: Array<{ key: string; value: unknown }>): void {
|
export function clearApiKeyOnProviderChange(
|
||||||
|
ctx: IPCContext,
|
||||||
|
entries: Array<{ key: string; value: unknown }>,
|
||||||
|
): void {
|
||||||
const providerEntry = entries.find((e) => e.key === 'llm.provider');
|
const providerEntry = entries.find((e) => e.key === 'llm.provider');
|
||||||
if (!providerEntry) return;
|
if (!providerEntry) return;
|
||||||
const oldProvider = ctx.configService.get<string>('llm.provider') ?? '';
|
const oldProvider = ctx.configService.get<string>('llm.provider') ?? '';
|
||||||
const newProvider = (providerEntry.value as string) ?? '';
|
const newProvider = (providerEntry.value as string) ?? '';
|
||||||
if (oldProvider && newProvider && oldProvider !== newProvider) {
|
if (oldProvider && newProvider && oldProvider !== newProvider) {
|
||||||
ctx.configService.set('llm.apiKey', '');
|
ctx.configService.set('llm.apiKey', '');
|
||||||
log.info(`[CONFIG] Provider changed (${oldProvider} → ${newProvider}), API key cleared to prevent incompatible key usage`);
|
log.info(
|
||||||
|
`[CONFIG] Provider changed (${oldProvider} → ${newProvider}), API key cleared to prevent incompatible key usage`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,8 +94,12 @@ export function applyEngineConfigKey(ctx: IPCContext, key: string, value: unknow
|
|||||||
orchestrator.updateDefaultConfig({ thinkingEnabled: value as boolean });
|
orchestrator.updateDefaultConfig({ thinkingEnabled: value as boolean });
|
||||||
break;
|
break;
|
||||||
case 'agent.thinkingEffort':
|
case 'agent.thinkingEffort':
|
||||||
agentEngineManager.updateConfigAll({ thinkingEffort: value as 'low' | 'medium' | 'high' | 'max' });
|
agentEngineManager.updateConfigAll({
|
||||||
orchestrator.updateDefaultConfig({ thinkingEffort: value as 'low' | 'medium' | 'high' | 'max' });
|
thinkingEffort: value as 'low' | 'medium' | 'high' | 'max',
|
||||||
|
});
|
||||||
|
orchestrator.updateDefaultConfig({
|
||||||
|
thinkingEffort: value as 'low' | 'medium' | 'high' | 'max',
|
||||||
|
});
|
||||||
break;
|
break;
|
||||||
case 'agent.toolExecutionTimeoutMs':
|
case 'agent.toolExecutionTimeoutMs':
|
||||||
agentEngineManager.updateConfigAll({ toolExecutionTimeoutMs: value as number });
|
agentEngineManager.updateConfigAll({ toolExecutionTimeoutMs: value as number });
|
||||||
@@ -121,8 +157,20 @@ export async function applyConfigSideEffects(
|
|||||||
const logLevelEntry = entries.find((e) => e.key === 'logging.level');
|
const logLevelEntry = entries.find((e) => e.key === 'logging.level');
|
||||||
if (logLevelEntry && typeof logLevelEntry.value === 'string') {
|
if (logLevelEntry && typeof logLevelEntry.value === 'string') {
|
||||||
const { transports } = await import('electron-log');
|
const { transports } = await import('electron-log');
|
||||||
transports.file.level = logLevelEntry.value as 'error' | 'warn' | 'info' | 'debug' | 'verbose' | 'silly';
|
transports.file.level = logLevelEntry.value as
|
||||||
transports.console.level = logLevelEntry.value as 'error' | 'warn' | 'info' | 'debug' | 'verbose' | 'silly';
|
| 'error'
|
||||||
|
| 'warn'
|
||||||
|
| 'info'
|
||||||
|
| 'debug'
|
||||||
|
| 'verbose'
|
||||||
|
| 'silly';
|
||||||
|
transports.console.level = logLevelEntry.value as
|
||||||
|
| 'error'
|
||||||
|
| 'warn'
|
||||||
|
| 'info'
|
||||||
|
| 'debug'
|
||||||
|
| 'verbose'
|
||||||
|
| 'silly';
|
||||||
log.info(`[CONFIG] Log level updated to ${logLevelEntry.value}`);
|
log.info(`[CONFIG] Log level updated to ${logLevelEntry.value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,7 +180,9 @@ export async function applyConfigSideEffects(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 5. 工作空间路径写入独立文件(下次启动生效)
|
// 5. 工作空间路径写入独立文件(下次启动生效)
|
||||||
const workspaceEntry = entries.find((e) => e.key === 'workspace.path' && typeof e.value === 'string');
|
const workspaceEntry = entries.find(
|
||||||
|
(e) => e.key === 'workspace.path' && typeof e.value === 'string',
|
||||||
|
);
|
||||||
if (workspaceEntry) {
|
if (workspaceEntry) {
|
||||||
try {
|
try {
|
||||||
const { writeWorkspacePathToFile } = await import('../main');
|
const { writeWorkspacePathToFile } = await import('../main');
|
||||||
|
|||||||
+87
-33
@@ -137,6 +137,14 @@ const ENV_FALLBACK: Record<string, { apiKey?: string; baseURL?: string }> = {
|
|||||||
anthropic: { apiKey: 'ANTHROPIC_API_KEY', baseURL: 'ANTHROPIC_BASE_URL' },
|
anthropic: { apiKey: 'ANTHROPIC_API_KEY', baseURL: 'ANTHROPIC_BASE_URL' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* F-7 修复: 已知 Provider 白名单
|
||||||
|
* 此前 createAdapter 的 switch default 静默落入 DeepSeekAdapter —— provider 配置
|
||||||
|
* 拼写错误(如 "deepsek")不报错,而是以空配置静默失败。显式校验后未知值
|
||||||
|
* 走"配置不完整"路径,用户可在设置中看到明确提示。
|
||||||
|
*/
|
||||||
|
const KNOWN_PROVIDERS = new Set(['deepseek', 'agnes', 'mimo', 'ollama', 'openai', 'anthropic']);
|
||||||
|
|
||||||
async function initialize(): Promise<void> {
|
async function initialize(): Promise<void> {
|
||||||
log.info('MetonaAI Desktop starting...');
|
log.info('MetonaAI Desktop starting...');
|
||||||
electronApp.setAppUserModelId('com.metona.ai-desktop');
|
electronApp.setAppUserModelId('com.metona.ai-desktop');
|
||||||
@@ -222,6 +230,14 @@ async function initialize(): Promise<void> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// F-7 修复: 未知 Provider 显式拒绝(此前 switch default 静默落入 DeepSeekAdapter)
|
||||||
|
if (!KNOWN_PROVIDERS.has(provider)) {
|
||||||
|
log.warn(
|
||||||
|
`Unknown LLM provider "${provider}". Supported: ${[...KNOWN_PROVIDERS].join(', ')}.`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (!apiKey && provider !== 'ollama') {
|
if (!apiKey && provider !== 'ollama') {
|
||||||
log.warn(`API key is required for provider "${provider}". Please set it in Settings.`);
|
log.warn(`API key is required for provider "${provider}". Please set it in Settings.`);
|
||||||
return null;
|
return null;
|
||||||
@@ -262,6 +278,15 @@ async function initialize(): Promise<void> {
|
|||||||
// ===== 步骤 5: 工作空间文件 + System Prompt =====
|
// ===== 步骤 5: 工作空间文件 + System Prompt =====
|
||||||
const contextBuilder = new ContextBuilder();
|
const contextBuilder = new ContextBuilder();
|
||||||
|
|
||||||
|
// ===== F-8 接通: 此前为死配置的安全/日志开关真实消费 =====
|
||||||
|
// fail-secure: 仅显式 false 才关闭 —— 配置值异常(空串/null/类型错误)时保持默认开启
|
||||||
|
const promptInjectionEnabled =
|
||||||
|
configService.get<boolean>('security.promptInjectionDefense') !== false;
|
||||||
|
const auditEnabled = configService.get<boolean>('logging.auditEnabled') !== false;
|
||||||
|
const traceEnabled = configService.get<boolean>('logging.traceEnabled') !== false;
|
||||||
|
// SessionRecorder 录制总开关(false 时不写 TRACE JSONL 文件)
|
||||||
|
sessionRecorder.setEnabled(traceEnabled);
|
||||||
|
|
||||||
// ===== v0.2.0: 安全模块(必须在工具注册之前) =====
|
// ===== v0.2.0: 安全模块(必须在工具注册之前) =====
|
||||||
const policyEngine = new PolicyEngine();
|
const policyEngine = new PolicyEngine();
|
||||||
const sandboxManager = new SandboxManager({
|
const sandboxManager = new SandboxManager({
|
||||||
@@ -330,16 +355,24 @@ async function initialize(): Promise<void> {
|
|||||||
const mcpManager = new MCPManager(() => db, toolRegistry);
|
const mcpManager = new MCPManager(() => db, toolRegistry);
|
||||||
|
|
||||||
// ===== Hooks(P0-2: SecurityScanHook 前置,对工具结果做间接注入防护) =====
|
// ===== Hooks(P0-2: SecurityScanHook 前置,对工具结果做间接注入防护) =====
|
||||||
|
// F-8 接通: SecurityScanHook / AuditLogHook 按 security.promptInjectionDefense
|
||||||
|
// 与 logging.auditEnabled 配置构建(此前两配置为死配置,hook 无条件挂载)
|
||||||
const preToolHooks = [
|
const preToolHooks = [
|
||||||
new PermissionCheckHook(policyEngine),
|
new PermissionCheckHook(policyEngine),
|
||||||
new RateLimitHook(20),
|
new RateLimitHook(20),
|
||||||
confirmationHook,
|
confirmationHook,
|
||||||
];
|
];
|
||||||
const postToolHooks = [
|
const postToolHooks = [
|
||||||
new SecurityScanHook(promptDefender),
|
...(promptInjectionEnabled ? [new SecurityScanHook(promptDefender)] : []),
|
||||||
new AuditLogHook(auditService),
|
...(auditEnabled ? [new AuditLogHook(auditService)] : []),
|
||||||
new MemoryTriggerHook(memoryManager),
|
new MemoryTriggerHook(memoryManager),
|
||||||
];
|
];
|
||||||
|
if (!promptInjectionEnabled) {
|
||||||
|
log.warn('[Security] promptInjectionDefense disabled by config — injection scanning off');
|
||||||
|
}
|
||||||
|
if (!auditEnabled) {
|
||||||
|
log.warn('[Logging] auditEnabled disabled by config — tool audit logging off');
|
||||||
|
}
|
||||||
|
|
||||||
// ===== P2-10: Agent Engine Manager(每会话独立引擎,替代全局单引擎) =====
|
// ===== P2-10: Agent Engine Manager(每会话独立引擎,替代全局单引擎) =====
|
||||||
const buildAdapter = (): IMetonaProviderAdapter => createAdapter() ?? FALLBACK_ADAPTER;
|
const buildAdapter = (): IMetonaProviderAdapter => createAdapter() ?? FALLBACK_ADAPTER;
|
||||||
@@ -360,6 +393,9 @@ async function initialize(): Promise<void> {
|
|||||||
| 'max'
|
| 'max'
|
||||||
| null) ?? 'high',
|
| null) ?? 'high',
|
||||||
toolExecutionTimeoutMs: configService.get<number>('agent.toolExecutionTimeoutMs') ?? 120_000,
|
toolExecutionTimeoutMs: configService.get<number>('agent.toolExecutionTimeoutMs') ?? 120_000,
|
||||||
|
// F-8 接通: llm.temperature / llm.maxTokens 此前为死配置(引擎硬编码 0.0/63488)
|
||||||
|
temperature: configService.get<number>('llm.temperature') ?? 0.0,
|
||||||
|
maxTokens: configService.get<number>('llm.maxTokens') ?? 63488,
|
||||||
},
|
},
|
||||||
toolRegistry,
|
toolRegistry,
|
||||||
preToolHooks,
|
preToolHooks,
|
||||||
@@ -383,6 +419,13 @@ async function initialize(): Promise<void> {
|
|||||||
'';
|
'';
|
||||||
if (!baseURL) return null;
|
if (!baseURL) return null;
|
||||||
if (!apiKey && provider !== 'ollama') return null;
|
if (!apiKey && provider !== 'ollama') return null;
|
||||||
|
// F-7 修复: 故障转移 Provider 同样做白名单校验(未知值禁用故障转移而非静默降级)
|
||||||
|
if (!KNOWN_PROVIDERS.has(provider)) {
|
||||||
|
log.warn(
|
||||||
|
`[Fallback] Unknown provider "${provider}". Supported: ${[...KNOWN_PROVIDERS].join(', ')}. Failover disabled.`,
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const contextWindow =
|
const contextWindow =
|
||||||
provider !== 'ollama'
|
provider !== 'ollama'
|
||||||
? (configService.get<number>(`${provider}.contextWindow`) ?? undefined)
|
? (configService.get<number>(`${provider}.contextWindow`) ?? undefined)
|
||||||
@@ -573,36 +616,44 @@ async function initialize(): Promise<void> {
|
|||||||
|
|
||||||
// ===== 窗口管理 =====
|
// ===== 窗口管理 =====
|
||||||
windowManager = new WindowManager();
|
windowManager = new WindowManager();
|
||||||
const mainWindow = windowManager.createWindow({
|
// 局部 const 捕获:闭包内 TS 无法对模块级可空变量做流收窄
|
||||||
id: 'main',
|
const wm = windowManager;
|
||||||
workspacePath: workspaceInfo.path,
|
// F-4 修复: 窗口创建收敛为单一入口 —— macOS activate / 托盘重建窗口此前不走
|
||||||
title: 'MetonaAI Desktop',
|
// beforeLoad,导致 confirmationHook 仍指向已销毁的旧窗口,所有需确认工具被
|
||||||
// P2-11 修复: 在 loadURL 之前注册 IPC handler,消除渲染进程加载与 IPC 注册的时序窗口
|
// "no main window available" 永久阻断。此闭包保证任何重建路径都完成
|
||||||
beforeLoad: (win) => {
|
// setMainWindow + IPC 注册(registerAllIPCHandlers 有防重入标志,重复调用安全)。
|
||||||
confirmationHook.setMainWindow(win);
|
const createMainWindow = (): BrowserWindow =>
|
||||||
registerAllIPCHandlers({
|
wm.createWindow({
|
||||||
mainWindow: win,
|
id: 'main',
|
||||||
sessionService,
|
workspacePath: workspaceInfo.path,
|
||||||
configService,
|
title: 'MetonaAI Desktop',
|
||||||
workspaceService,
|
// P2-11 修复: 在 loadURL 之前注册 IPC handler,消除渲染进程加载与 IPC 注册的时序窗口
|
||||||
contextBuilder,
|
beforeLoad: (win) => {
|
||||||
agentEngineManager,
|
confirmationHook.setMainWindow(win);
|
||||||
toolRegistry,
|
registerAllIPCHandlers({
|
||||||
auditService,
|
mainWindow: win,
|
||||||
sessionRecorder,
|
sessionService,
|
||||||
memoryManager,
|
configService,
|
||||||
mcpManager,
|
workspaceService,
|
||||||
reloadAdapter,
|
contextBuilder,
|
||||||
promptInjectionDefender: promptDefender,
|
agentEngineManager,
|
||||||
outputValidator,
|
toolRegistry,
|
||||||
confirmationHook,
|
auditService,
|
||||||
memoryConsolidator,
|
sessionRecorder,
|
||||||
orchestrator,
|
memoryManager,
|
||||||
sessionSummaryService,
|
mcpManager,
|
||||||
toolsReadyRef,
|
reloadAdapter,
|
||||||
});
|
promptInjectionDefender: promptDefender,
|
||||||
},
|
outputValidator,
|
||||||
});
|
confirmationHook,
|
||||||
|
memoryConsolidator,
|
||||||
|
orchestrator,
|
||||||
|
sessionSummaryService,
|
||||||
|
toolsReadyRef,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const mainWindow = createMainWindow();
|
||||||
|
|
||||||
// ===== 系统托盘 =====
|
// ===== 系统托盘 =====
|
||||||
const resourcesPath = join(__dirname, '../../assets');
|
const resourcesPath = join(__dirname, '../../assets');
|
||||||
@@ -683,7 +734,10 @@ async function initialize(): Promise<void> {
|
|||||||
|
|
||||||
app.on('activate', () => {
|
app.on('activate', () => {
|
||||||
if (windowManager && windowManager.count === 0) {
|
if (windowManager && windowManager.count === 0) {
|
||||||
windowManager.createWindow({ id: 'main', workspacePath: workspaceInfo.path });
|
// F-4 修复: 重建窗口走统一入口(beforeLoad 补 confirmationHook/IPC),
|
||||||
|
// 并把托盘引用重新绑定到新窗口(否则托盘点击指向已销毁窗口)
|
||||||
|
const win = createMainWindow();
|
||||||
|
trayManager?.rebindWindow(win);
|
||||||
} else {
|
} else {
|
||||||
windowManager?.focusWindow();
|
windowManager?.focusWindow();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -255,3 +255,78 @@ describe.skipIf(!dbAvailable)('SessionSummary 分层上下文 × 截断交互',
|
|||||||
expect(history.every((m: any) => !m.images?.length)).toBe(true);
|
expect(history.every((m: any) => !m.images?.length)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* F-3 回归:session_summaries 级联删除
|
||||||
|
*
|
||||||
|
* 缺陷:该表此前无 FOREIGN KEY ON DELETE CASCADE —— 删除会话后摘要残留。
|
||||||
|
* 迁移 8 重建表补级联;此测试验证目标 schema 的行为契约:
|
||||||
|
* DELETE sessions 必须级联清除对应摘要(真实库由 DatabaseService 开启 foreign_keys pragma)。
|
||||||
|
*/
|
||||||
|
describe.skipIf(!dbAvailable)('session_summaries 级联删除(F-3)', () => {
|
||||||
|
let db: any;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
db = new Database(':memory:');
|
||||||
|
// 与 DatabaseService.initialize 一致:开启外键约束(级联依赖此开关)
|
||||||
|
db.pragma('foreign_keys = ON');
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE sessions (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
title TEXT DEFAULT '新会话',
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL,
|
||||||
|
message_count INTEGER DEFAULT 0,
|
||||||
|
pinned INTEGER DEFAULT 0,
|
||||||
|
archived INTEGER DEFAULT 0,
|
||||||
|
metadata TEXT DEFAULT '{}'
|
||||||
|
);
|
||||||
|
CREATE TABLE session_summaries (
|
||||||
|
session_id TEXT PRIMARY KEY,
|
||||||
|
summary TEXT NOT NULL,
|
||||||
|
summarized_until_rowid INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
|
||||||
|
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
db?.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('外键声明存在且为 CASCADE(迁移 8 后的目标 schema)', () => {
|
||||||
|
const fks = db.prepare('PRAGMA foreign_key_list(session_summaries)').all() as Array<{
|
||||||
|
table: string;
|
||||||
|
on_delete: string;
|
||||||
|
}>;
|
||||||
|
expect(fks.some((fk) => fk.table === 'sessions' && fk.on_delete === 'CASCADE')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('删除会话后摘要级联清除(不再残留孤儿数据)', () => {
|
||||||
|
const now = Date.now();
|
||||||
|
db.prepare(`INSERT INTO sessions (id, created_at, updated_at) VALUES ('s_cascade', ?, ?)`).run(
|
||||||
|
now,
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
db.prepare(
|
||||||
|
`INSERT INTO session_summaries (session_id, summary, summarized_until_rowid, updated_at)
|
||||||
|
VALUES ('s_cascade', '将被级联删除的摘要', 99, ?)`,
|
||||||
|
).run(now);
|
||||||
|
db.prepare(`INSERT INTO sessions (id, created_at, updated_at) VALUES ('s_keep', ?, ?)`).run(
|
||||||
|
now,
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
db.prepare(
|
||||||
|
`INSERT INTO session_summaries (session_id, summary, summarized_until_rowid, updated_at)
|
||||||
|
VALUES ('s_keep', '保留的摘要', 50, ?)`,
|
||||||
|
).run(now);
|
||||||
|
|
||||||
|
db.prepare(`DELETE FROM sessions WHERE id = 's_cascade'`).run();
|
||||||
|
|
||||||
|
const remaining = db.prepare(`SELECT session_id FROM session_summaries`).all() as Array<{
|
||||||
|
session_id: string;
|
||||||
|
}>;
|
||||||
|
expect(remaining.map((r) => r.session_id)).toEqual(['s_keep']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export const CONFIG_DEFAULTS: ConfigDefaultEntry[] = [
|
|||||||
{ key: 'llm.model', value: '', category: 'llm' },
|
{ key: 'llm.model', value: '', category: 'llm' },
|
||||||
{ key: 'llm.apiKey', value: '', category: 'llm' },
|
{ key: 'llm.apiKey', value: '', category: 'llm' },
|
||||||
{ key: 'llm.baseURL', value: '', category: 'llm' },
|
{ key: 'llm.baseURL', value: '', category: 'llm' },
|
||||||
|
// F-8 接通: temperature/maxTokens 此前为死配置(引擎硬编码),现由 main.ts 注入引擎
|
||||||
{ key: 'llm.temperature', value: 0, category: 'llm' },
|
{ key: 'llm.temperature', value: 0, category: 'llm' },
|
||||||
{ key: 'llm.maxTokens', value: 63488, category: 'llm' },
|
{ key: 'llm.maxTokens', value: 63488, category: 'llm' },
|
||||||
// v0.5.4: 多模态总开关 — 即使模型支持多模态,未开启也不能上传图片(默认关闭,
|
// v0.5.4: 多模态总开关 — 即使模型支持多模态,未开启也不能上传图片(默认关闭,
|
||||||
@@ -63,16 +64,18 @@ export const CONFIG_DEFAULTS: ConfigDefaultEntry[] = [
|
|||||||
{ key: 'agent.toolExecutionTimeoutMs', value: 120000, category: 'agent' },
|
{ key: 'agent.toolExecutionTimeoutMs', value: 120000, category: 'agent' },
|
||||||
|
|
||||||
// 安全配置
|
// 安全配置
|
||||||
{ key: 'security.requireWriteConfirmation', value: true, category: 'security' },
|
// F-8 清理: 移除死配置 security.requireWriteConfirmation / security.maxFileWriteSizeKB
|
||||||
{ key: 'security.maxFileWriteSizeKB', value: 1024, category: 'security' },
|
// (无任何消费者 —— 确认策略由工具定义的 requiresPermission/riskLevel 驱动,
|
||||||
|
// 文件大小上限由 file-guard.ts 的 MAX_FILE_SIZE_BYTES 常量控制)
|
||||||
|
// F-8 接通: promptInjectionDefense 由 main.ts(SecurityScanHook)与 ipc/agent.ts(用户消息检测)消费
|
||||||
{ key: 'security.promptInjectionDefense', value: true, category: 'security' },
|
{ key: 'security.promptInjectionDefense', value: true, category: 'security' },
|
||||||
|
|
||||||
// UI 配置
|
// UI 配置
|
||||||
|
// F-8 清理: 移除死配置 ui.fontSize / ui.animationMode(无消费者;主题走 localStorage)
|
||||||
{ key: 'ui.theme', value: 'auto', category: 'ui' },
|
{ key: 'ui.theme', value: 'auto', category: 'ui' },
|
||||||
{ key: 'ui.animationMode', value: 'auto', category: 'ui' },
|
|
||||||
{ key: 'ui.fontSize', value: 'medium', category: 'ui' },
|
|
||||||
|
|
||||||
// 日志配置
|
// 日志配置
|
||||||
|
// F-8 接通: auditEnabled/traceEnabled 由 main.ts 构建 hooks 与 SessionRecorder 时消费
|
||||||
{ key: 'logging.level', value: 'info', category: 'logging' },
|
{ key: 'logging.level', value: 'info', category: 'logging' },
|
||||||
{ key: 'logging.auditEnabled', value: true, category: 'logging' },
|
{ key: 'logging.auditEnabled', value: true, category: 'logging' },
|
||||||
{ key: 'logging.traceEnabled', value: true, category: 'logging' },
|
{ key: 'logging.traceEnabled', value: true, category: 'logging' },
|
||||||
@@ -286,11 +289,14 @@ export class DatabaseService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
-- ===== P2: 会话摘要表(分层上下文——超长会话早期消息压缩为摘要,LLM 只加载摘要 + 近期原文) =====
|
-- ===== P2: 会话摘要表(分层上下文——超长会话早期消息压缩为摘要,LLM 只加载摘要 + 近期原文) =====
|
||||||
|
-- F-3 修复: 补 FOREIGN KEY ON DELETE CASCADE —— 此前无级联,删除会话后摘要残留,
|
||||||
|
-- 随使用无限累积(messages/tasks 均有级联,唯独此表遗漏)
|
||||||
CREATE TABLE IF NOT EXISTS session_summaries (
|
CREATE TABLE IF NOT EXISTS session_summaries (
|
||||||
session_id TEXT PRIMARY KEY,
|
session_id TEXT PRIMARY KEY,
|
||||||
summary TEXT NOT NULL,
|
summary TEXT NOT NULL,
|
||||||
summarized_until_rowid INTEGER NOT NULL,
|
summarized_until_rowid INTEGER NOT NULL,
|
||||||
updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000)
|
updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
|
||||||
|
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
||||||
);
|
);
|
||||||
|
|
||||||
-- ===== 索引 =====
|
-- ===== 索引 =====
|
||||||
@@ -463,6 +469,49 @@ export class DatabaseService {
|
|||||||
// 新写入的消息仍通过触发器正常进入索引
|
// 新写入的消息仍通过触发器正常进入索引
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// F-3 迁移 8: 重建 session_summaries 表,补 FOREIGN KEY ON DELETE CASCADE
|
||||||
|
// 此前该表无级联删除 —— 删除会话(sessions:delete / data:clearSessions)后
|
||||||
|
// 摘要记录永久残留,随使用无限累积。SQLite 不支持 ALTER ADD CONSTRAINT,需重建表。
|
||||||
|
// 幂等:PRAGMA foreign_key_list 检测已有级联则跳过。
|
||||||
|
try {
|
||||||
|
const fkRows = db.prepare('PRAGMA foreign_key_list(session_summaries)').all() as Array<{
|
||||||
|
table: string;
|
||||||
|
on_delete: string;
|
||||||
|
}>;
|
||||||
|
const hasCascade = fkRows.some(
|
||||||
|
(fk) => fk.table === 'sessions' && fk.on_delete === 'CASCADE',
|
||||||
|
);
|
||||||
|
if (!hasCascade) {
|
||||||
|
log.info('[DB] Migration: rebuilding session_summaries table to add ON DELETE CASCADE');
|
||||||
|
const rebuildSummaries = db.transaction(() => {
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS session_summaries_new (
|
||||||
|
session_id TEXT PRIMARY KEY,
|
||||||
|
summary TEXT NOT NULL,
|
||||||
|
summarized_until_rowid INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL DEFAULT (unixepoch() * 1000),
|
||||||
|
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO session_summaries_new (session_id, summary, summarized_until_rowid, updated_at)
|
||||||
|
SELECT session_id, summary, summarized_until_rowid, updated_at
|
||||||
|
FROM session_summaries;
|
||||||
|
|
||||||
|
DROP TABLE session_summaries;
|
||||||
|
ALTER TABLE session_summaries_new RENAME TO session_summaries;
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
rebuildSummaries();
|
||||||
|
log.info(
|
||||||
|
'[DB] Migration: session_summaries table rebuilt successfully (ON DELETE CASCADE added)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const msg = toErrorMessage(error);
|
||||||
|
log.warn(`[DB] Migration 8 (session_summaries CASCADE) skipped: ${msg}`);
|
||||||
|
// 非致命 — 级联缺失仅导致摘要残留(不影响会话读写),但建议用户检查工作空间数据库
|
||||||
|
}
|
||||||
|
|
||||||
// C-6 修复 迁移 5: 重建 messages 表,将 content 列从 NOT NULL 改为允许 NULL
|
// C-6 修复 迁移 5: 重建 messages 表,将 content 列从 NOT NULL 改为允许 NULL
|
||||||
// @see project_memory.md — Assistant messages with tool_calls must set content to null
|
// @see project_memory.md — Assistant messages with tool_calls must set content to null
|
||||||
// SQLite 不支持 ALTER COLUMN,需要重建表
|
// SQLite 不支持 ALTER COLUMN,需要重建表
|
||||||
|
|||||||
@@ -57,8 +57,22 @@ export class SessionRecorder {
|
|||||||
/** P1-6: 每会话独立状态(支持并发会话录制) */
|
/** P1-6: 每会话独立状态(支持并发会话录制) */
|
||||||
private sessions = new Map<string, SessionRecordState>();
|
private sessions = new Map<string, SessionRecordState>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* F-8 接通: 录制总开关(logging.traceEnabled,默认 true)
|
||||||
|
* 关闭时 writeEvent 丢弃所有事件(状态 Map 仍维护以保持接口兼容)
|
||||||
|
*/
|
||||||
|
private enabled = true;
|
||||||
|
|
||||||
constructor(private workspacePath: string) {}
|
constructor(private workspacePath: string) {}
|
||||||
|
|
||||||
|
/** F-8: 设置录制开关(main.ts 启动时按 logging.traceEnabled 注入) */
|
||||||
|
setEnabled(enabled: boolean): void {
|
||||||
|
this.enabled = enabled;
|
||||||
|
if (!enabled) {
|
||||||
|
log.info('[SessionRecorder] Trace recording disabled by config (logging.traceEnabled=false)');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 获取指定会话的录制状态(不存在返回 null) */
|
/** 获取指定会话的录制状态(不存在返回 null) */
|
||||||
private state(sessionId: string): SessionRecordState | null {
|
private state(sessionId: string): SessionRecordState | null {
|
||||||
return this.sessions.get(sessionId) ?? null;
|
return this.sessions.get(sessionId) ?? null;
|
||||||
@@ -262,6 +276,8 @@ export class SessionRecorder {
|
|||||||
private writeEvent(sessionId: string, data: Record<string, unknown>): void {
|
private writeEvent(sessionId: string, data: Record<string, unknown>): void {
|
||||||
const state = this.state(sessionId);
|
const state = this.state(sessionId);
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
|
// F-8 接通: logging.traceEnabled=false 时丢弃事件(不写文件)
|
||||||
|
if (!this.enabled) return;
|
||||||
|
|
||||||
const event: TraceEvent = {
|
const event: TraceEvent = {
|
||||||
seq: state.seq++,
|
seq: state.seq++,
|
||||||
|
|||||||
@@ -74,6 +74,23 @@ export class TrayManager {
|
|||||||
TrayManager.isQuitting = true;
|
TrayManager.isQuitting = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* F-4 修复: 重新绑定主窗口引用(macOS activate / 托盘重建窗口后调用)
|
||||||
|
*
|
||||||
|
* 旧窗口销毁后 this.mainWindow 指向已 destroyed 实例,托盘点击(显示/隐藏)
|
||||||
|
* 与"关闭到托盘"行为全部失效。此方法更新引用并为新窗口重新挂载 close 拦截
|
||||||
|
* (原监听随旧窗口销毁自动释放,新窗口需要同样的 hide-to-tray 行为)。
|
||||||
|
*/
|
||||||
|
rebindWindow(mainWindow: BrowserWindow): void {
|
||||||
|
this.mainWindow = mainWindow;
|
||||||
|
mainWindow.on('close', (event) => {
|
||||||
|
if (!TrayManager.isQuitting) {
|
||||||
|
event.preventDefault();
|
||||||
|
mainWindow.hide();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新托盘状态
|
* 更新托盘状态
|
||||||
*/
|
*/
|
||||||
|
|||||||
Generated
+15
-337
@@ -1,17 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "metona-ai-desktop",
|
"name": "metona-ai-desktop",
|
||||||
"version": "0.5.3",
|
"version": "0.5.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "metona-ai-desktop",
|
"name": "metona-ai-desktop",
|
||||||
"version": "0.5.3",
|
"version": "0.5.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@metona-team/metona-toast": "^0.2.1",
|
"@metona-team/metona-toast": "^0.5.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||||
"@mui/icons-material": "^9.1.1",
|
"@mui/icons-material": "^9.1.1",
|
||||||
"@mui/material": "^9.1.2",
|
"@mui/material": "^9.1.2",
|
||||||
@@ -19,7 +19,6 @@
|
|||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"electron-log": "^5.3.3",
|
"electron-log": "^5.3.3",
|
||||||
"electron-store": "^10.0.1",
|
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
"lru-cache": "^11.1.0",
|
"lru-cache": "^11.1.0",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
@@ -29,10 +28,8 @@
|
|||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-virtuoso": "^4.18.12",
|
"react-virtuoso": "^4.18.12",
|
||||||
"rehype-highlight": "^7.0.2",
|
"rehype-highlight": "^7.0.2",
|
||||||
"rehype-raw": "^7.0.0",
|
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"shell-quote": "^1.10.0",
|
"shell-quote": "^1.10.0",
|
||||||
"zod": "^3.25.67",
|
|
||||||
"zustand": "^5.0.5"
|
"zustand": "^5.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -1884,10 +1881,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@metona-team/metona-toast": {
|
"node_modules/@metona-team/metona-toast": {
|
||||||
"version": "0.2.1",
|
"version": "0.5.0",
|
||||||
"resolved": "https://git.metona.cn/api/packages/MetonaTeam/npm/%40metona-team%2Fmetona-toast/-/0.2.1/metona-toast-0.2.1.tgz",
|
"resolved": "https://git.metona.cn/api/packages/MetonaTeam/npm/%40metona-team%2Fmetona-toast/-/0.5.0/metona-toast-0.5.0.tgz",
|
||||||
"integrity": "sha512-JPK+83T6pULRbMxZ67Baxsdnggl0aGCteY8A6mLg1ePK6dLfl2WBtxH0x4Q9CL/+Llbh5ozCVKPKcUurTRjAbw==",
|
"integrity": "sha512-JcvFoaMaB+TML/QYhD5g5hWbJms2oX9exUqyGR8JOhoEksJlpezncphphvfBu/kcTWR52ZTndXddLarYWAYtug==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=17.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@modelcontextprotocol/sdk": {
|
"node_modules/@modelcontextprotocol/sdk": {
|
||||||
"version": "1.29.0",
|
"version": "1.29.0",
|
||||||
@@ -3991,16 +3996,6 @@
|
|||||||
"node": ">= 4.0.0"
|
"node": ">= 4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/atomically": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/atomically/-/atomically-2.1.1.tgz",
|
|
||||||
"integrity": "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"stubborn-fs": "^2.0.0",
|
|
||||||
"when-exit": "^2.1.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/autoprefixer": {
|
"node_modules/autoprefixer": {
|
||||||
"version": "10.5.2",
|
"version": "10.5.2",
|
||||||
"resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.5.2.tgz",
|
"resolved": "https://registry.npmmirror.com/autoprefixer/-/autoprefixer-10.5.2.tgz",
|
||||||
@@ -4720,53 +4715,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/conf": {
|
|
||||||
"version": "14.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/conf/-/conf-14.0.0.tgz",
|
|
||||||
"integrity": "sha512-L6BuueHTRuJHQvQVc6YXYZRtN5vJUtOdCTLn0tRYYV5azfbAFcPghB5zEE40mVrV6w7slMTqUfkDomutIK14fw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ajv": "^8.17.1",
|
|
||||||
"ajv-formats": "^3.0.1",
|
|
||||||
"atomically": "^2.0.3",
|
|
||||||
"debounce-fn": "^6.0.0",
|
|
||||||
"dot-prop": "^9.0.0",
|
|
||||||
"env-paths": "^3.0.0",
|
|
||||||
"json-schema-typed": "^8.0.1",
|
|
||||||
"semver": "^7.7.2",
|
|
||||||
"uint8array-extras": "^1.4.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/conf/node_modules/env-paths": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/env-paths/-/env-paths-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/conf/node_modules/semver": {
|
|
||||||
"version": "7.8.5",
|
|
||||||
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz",
|
|
||||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
|
||||||
"license": "ISC",
|
|
||||||
"bin": {
|
|
||||||
"semver": "bin/semver.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/content-disposition": {
|
"node_modules/content-disposition": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-1.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/content-disposition/-/content-disposition-1.1.0.tgz",
|
||||||
@@ -4961,21 +4909,6 @@
|
|||||||
"url": "https://github.com/sponsors/kossnocorp"
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/debounce-fn": {
|
|
||||||
"version": "6.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/debounce-fn/-/debounce-fn-6.0.0.tgz",
|
|
||||||
"integrity": "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"mimic-function": "^5.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
|
||||||
@@ -5335,21 +5268,6 @@
|
|||||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/dot-prop": {
|
|
||||||
"version": "9.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/dot-prop/-/dot-prop-9.0.0.tgz",
|
|
||||||
"integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"type-fest": "^4.18.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "17.4.2",
|
"version": "17.4.2",
|
||||||
"resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-17.4.2.tgz",
|
"resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-17.4.2.tgz",
|
||||||
@@ -5614,22 +5532,6 @@
|
|||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-store": {
|
|
||||||
"version": "10.1.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/electron-store/-/electron-store-10.1.0.tgz",
|
|
||||||
"integrity": "sha512-oL8bRy7pVCLpwhmXy05Rh/L6O93+k9t6dqSw0+MckIc3OmCTZm6Mp04Q4f/J0rtu84Ky6ywkR8ivtGOmrq+16w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"conf": "^14.0.0",
|
|
||||||
"type-fest": "^4.41.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.379",
|
"version": "1.5.379",
|
||||||
"resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.379.tgz",
|
"resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.379.tgz",
|
||||||
@@ -5744,18 +5646,6 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/entities": {
|
|
||||||
"version": "6.0.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz",
|
|
||||||
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
|
||||||
"license": "BSD-2-Clause",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.12"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/env-paths": {
|
"node_modules/env-paths": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmmirror.com/env-paths/-/env-paths-2.2.1.tgz",
|
"resolved": "https://registry.npmmirror.com/env-paths/-/env-paths-2.2.1.tgz",
|
||||||
@@ -6942,26 +6832,6 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hast-util-from-parse5": {
|
|
||||||
"version": "8.0.3",
|
|
||||||
"resolved": "https://registry.npmmirror.com/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz",
|
|
||||||
"integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/hast": "^3.0.0",
|
|
||||||
"@types/unist": "^3.0.0",
|
|
||||||
"devlop": "^1.0.0",
|
|
||||||
"hastscript": "^9.0.0",
|
|
||||||
"property-information": "^7.0.0",
|
|
||||||
"vfile": "^6.0.0",
|
|
||||||
"vfile-location": "^5.0.0",
|
|
||||||
"web-namespaces": "^2.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/hast-util-is-element": {
|
"node_modules/hast-util-is-element": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmmirror.com/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz",
|
"resolved": "https://registry.npmmirror.com/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz",
|
||||||
@@ -6975,44 +6845,6 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hast-util-parse-selector": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/hast": "^3.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/hast-util-raw": {
|
|
||||||
"version": "9.1.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/hast-util-raw/-/hast-util-raw-9.1.0.tgz",
|
|
||||||
"integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/hast": "^3.0.0",
|
|
||||||
"@types/unist": "^3.0.0",
|
|
||||||
"@ungap/structured-clone": "^1.0.0",
|
|
||||||
"hast-util-from-parse5": "^8.0.0",
|
|
||||||
"hast-util-to-parse5": "^8.0.0",
|
|
||||||
"html-void-elements": "^3.0.0",
|
|
||||||
"mdast-util-to-hast": "^13.0.0",
|
|
||||||
"parse5": "^7.0.0",
|
|
||||||
"unist-util-position": "^5.0.0",
|
|
||||||
"unist-util-visit": "^5.0.0",
|
|
||||||
"vfile": "^6.0.0",
|
|
||||||
"web-namespaces": "^2.0.0",
|
|
||||||
"zwitch": "^2.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/hast-util-to-jsx-runtime": {
|
"node_modules/hast-util-to-jsx-runtime": {
|
||||||
"version": "2.3.6",
|
"version": "2.3.6",
|
||||||
"resolved": "https://registry.npmmirror.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
|
"resolved": "https://registry.npmmirror.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
|
||||||
@@ -7040,25 +6872,6 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hast-util-to-parse5": {
|
|
||||||
"version": "8.0.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/hast-util-to-parse5/-/hast-util-to-parse5-8.0.1.tgz",
|
|
||||||
"integrity": "sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/hast": "^3.0.0",
|
|
||||||
"comma-separated-tokens": "^2.0.0",
|
|
||||||
"devlop": "^1.0.0",
|
|
||||||
"property-information": "^7.0.0",
|
|
||||||
"space-separated-tokens": "^2.0.0",
|
|
||||||
"web-namespaces": "^2.0.0",
|
|
||||||
"zwitch": "^2.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/hast-util-to-text": {
|
"node_modules/hast-util-to-text": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz",
|
||||||
@@ -7088,23 +6901,6 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/hastscript": {
|
|
||||||
"version": "9.0.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/hastscript/-/hastscript-9.0.1.tgz",
|
|
||||||
"integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/hast": "^3.0.0",
|
|
||||||
"comma-separated-tokens": "^2.0.0",
|
|
||||||
"hast-util-parse-selector": "^4.0.0",
|
|
||||||
"property-information": "^7.0.0",
|
|
||||||
"space-separated-tokens": "^2.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/he": {
|
"node_modules/he": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz",
|
"resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz",
|
||||||
@@ -7190,16 +6986,6 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/html-void-elements": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/html-void-elements/-/html-void-elements-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/http-cache-semantics": {
|
"node_modules/http-cache-semantics": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmmirror.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
"resolved": "https://registry.npmmirror.com/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz",
|
||||||
@@ -9082,18 +8868,6 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mimic-function": {
|
|
||||||
"version": "5.0.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/mimic-function/-/mimic-function-5.0.1.tgz",
|
|
||||||
"integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/mimic-response": {
|
"node_modules/mimic-response": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/mimic-response/-/mimic-response-1.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/mimic-response/-/mimic-response-1.0.1.tgz",
|
||||||
@@ -9567,18 +9341,6 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/parse5": {
|
|
||||||
"version": "7.3.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/parse5/-/parse5-7.3.0.tgz",
|
|
||||||
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"entities": "^6.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/parseurl": {
|
"node_modules/parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmmirror.com/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
@@ -10278,21 +10040,6 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rehype-raw": {
|
|
||||||
"version": "7.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/rehype-raw/-/rehype-raw-7.0.0.tgz",
|
|
||||||
"integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/hast": "^3.0.0",
|
|
||||||
"hast-util-raw": "^9.0.0",
|
|
||||||
"vfile": "^6.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/remark-gfm": {
|
"node_modules/remark-gfm": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmmirror.com/remark-gfm/-/remark-gfm-4.0.1.tgz",
|
"resolved": "https://registry.npmmirror.com/remark-gfm/-/remark-gfm-4.0.1.tgz",
|
||||||
@@ -11055,21 +10802,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/stubborn-fs": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/stubborn-fs/-/stubborn-fs-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"stubborn-utils": "^1.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/stubborn-utils": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmmirror.com/stubborn-utils/-/stubborn-utils-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/style-to-js": {
|
"node_modules/style-to-js": {
|
||||||
"version": "1.1.21",
|
"version": "1.1.21",
|
||||||
"resolved": "https://registry.npmmirror.com/style-to-js/-/style-to-js-1.1.21.tgz",
|
"resolved": "https://registry.npmmirror.com/style-to-js/-/style-to-js-1.1.21.tgz",
|
||||||
@@ -11453,18 +11185,6 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/type-fest": {
|
|
||||||
"version": "4.41.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-4.41.0.tgz",
|
|
||||||
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
|
|
||||||
"license": "(MIT OR CC0-1.0)",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/type-is": {
|
"node_modules/type-is": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/type-is/-/type-is-2.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/type-is/-/type-is-2.1.0.tgz",
|
||||||
@@ -11534,18 +11254,6 @@
|
|||||||
"typescript": ">=4.8.4 <6.1.0"
|
"typescript": ">=4.8.4 <6.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/uint8array-extras": {
|
|
||||||
"version": "1.5.0",
|
|
||||||
"resolved": "https://registry.npmmirror.com/uint8array-extras/-/uint8array-extras-1.5.0.tgz",
|
|
||||||
"integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/undici": {
|
"node_modules/undici": {
|
||||||
"version": "6.27.0",
|
"version": "6.27.0",
|
||||||
"resolved": "https://registry.npmmirror.com/undici/-/undici-6.27.0.tgz",
|
"resolved": "https://registry.npmmirror.com/undici/-/undici-6.27.0.tgz",
|
||||||
@@ -11812,20 +11520,6 @@
|
|||||||
"url": "https://opencollective.com/unified"
|
"url": "https://opencollective.com/unified"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vfile-location": {
|
|
||||||
"version": "5.0.3",
|
|
||||||
"resolved": "https://registry.npmmirror.com/vfile-location/-/vfile-location-5.0.3.tgz",
|
|
||||||
"integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/unist": "^3.0.0",
|
|
||||||
"vfile": "^6.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/unified"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/vfile-message": {
|
"node_modules/vfile-message": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmmirror.com/vfile-message/-/vfile-message-4.0.3.tgz",
|
"resolved": "https://registry.npmmirror.com/vfile-message/-/vfile-message-4.0.3.tgz",
|
||||||
@@ -12011,16 +11705,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/web-namespaces": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmmirror.com/web-namespaces/-/web-namespaces-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"funding": {
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/webcrypto-core": {
|
"node_modules/webcrypto-core": {
|
||||||
"version": "1.9.2",
|
"version": "1.9.2",
|
||||||
"resolved": "https://registry.npmmirror.com/webcrypto-core/-/webcrypto-core-1.9.2.tgz",
|
"resolved": "https://registry.npmmirror.com/webcrypto-core/-/webcrypto-core-1.9.2.tgz",
|
||||||
@@ -12035,12 +11719,6 @@
|
|||||||
"tslib": "^2.8.1"
|
"tslib": "^2.8.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/when-exit": {
|
|
||||||
"version": "2.1.5",
|
|
||||||
"resolved": "https://registry.npmmirror.com/when-exit/-/when-exit-2.1.5.tgz",
|
|
||||||
"integrity": "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
|
||||||
|
|||||||
+2
-5
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "metona-ai-desktop",
|
"name": "metona-ai-desktop",
|
||||||
"version": "0.5.5",
|
"version": "0.6.0",
|
||||||
"description": "MetonaAI Desktop — 生产级通用 AI Agent 智能体桌面应用",
|
"description": "MetonaAI Desktop — 生产级通用 AI Agent 智能体桌面应用",
|
||||||
"main": "dist-electron/main/main.js",
|
"main": "dist-electron/main/main.js",
|
||||||
"author": "Metona Team",
|
"author": "Metona Team",
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.1",
|
"@emotion/styled": "^11.14.1",
|
||||||
"@metona-team/metona-toast": "^0.2.1",
|
"@metona-team/metona-toast": "^0.5.0",
|
||||||
"@modelcontextprotocol/sdk": "^1.12.1",
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
||||||
"@mui/icons-material": "^9.1.1",
|
"@mui/icons-material": "^9.1.1",
|
||||||
"@mui/material": "^9.1.2",
|
"@mui/material": "^9.1.2",
|
||||||
@@ -47,7 +47,6 @@
|
|||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"electron-log": "^5.3.3",
|
"electron-log": "^5.3.3",
|
||||||
"electron-store": "^10.0.1",
|
|
||||||
"fuse.js": "^7.1.0",
|
"fuse.js": "^7.1.0",
|
||||||
"lru-cache": "^11.1.0",
|
"lru-cache": "^11.1.0",
|
||||||
"nanoid": "^5.1.5",
|
"nanoid": "^5.1.5",
|
||||||
@@ -57,10 +56,8 @@
|
|||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
"react-virtuoso": "^4.18.12",
|
"react-virtuoso": "^4.18.12",
|
||||||
"rehype-highlight": "^7.0.2",
|
"rehype-highlight": "^7.0.2",
|
||||||
"rehype-raw": "^7.0.0",
|
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"shell-quote": "^1.10.0",
|
"shell-quote": "^1.10.0",
|
||||||
"zod": "^3.25.67",
|
|
||||||
"zustand": "^5.0.5"
|
"zustand": "^5.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -199,6 +199,9 @@ export function ConfirmationDialog(): React.JSX.Element | null {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 倒计时:每 200ms 更新剩余时间(取所有请求中最早过期的)
|
// 倒计时:每 200ms 更新剩余时间(取所有请求中最早过期的)
|
||||||
|
// F-6 修复: 归零时主动拉取 pending —— 后端超时已 resolve(false) 并删除条目,
|
||||||
|
// 但此前不通知前端,弹框滞留在"已超时"且禁止关闭。主动拉取后列表为空,
|
||||||
|
// 弹框自然消解;若后端 timer 尚未触发(前端轮询略早),拉回的 pending 保留原请求。
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (requests.length === 0) return;
|
if (requests.length === 0) return;
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
@@ -215,12 +218,13 @@ export function ConfirmationDialog(): React.JSX.Element | null {
|
|||||||
if (remaining <= 0) {
|
if (remaining <= 0) {
|
||||||
setRemainingMs(0);
|
setRemainingMs(0);
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
|
void refreshPending();
|
||||||
} else {
|
} else {
|
||||||
setRemainingMs(remaining);
|
setRemainingMs(remaining);
|
||||||
}
|
}
|
||||||
}, 200);
|
}, 200);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [requests]);
|
}, [requests, refreshPending]);
|
||||||
|
|
||||||
// 按工具名分组(同工具多次调用折叠为一组)
|
// 按工具名分组(同工具多次调用折叠为一组)
|
||||||
const grouped: GroupedRequests[] = useMemo(() => {
|
const grouped: GroupedRequests[] = useMemo(() => {
|
||||||
|
|||||||
@@ -367,7 +367,9 @@ export function createContextMenuItems(type: ContextMenuType, data?: unknown): C
|
|||||||
{
|
{
|
||||||
id: 'pin',
|
id: 'pin',
|
||||||
icon: Pin,
|
icon: Pin,
|
||||||
label: '置顶',
|
label: useSessionStore.getState().sessions.find((x) => x.id === sid)?.pinned
|
||||||
|
? '取消置顶'
|
||||||
|
: '置顶',
|
||||||
action: async () => {
|
action: async () => {
|
||||||
if (!sid) return;
|
if (!sid) return;
|
||||||
const s = useSessionStore.getState().sessions.find((x) => x.id === sid);
|
const s = useSessionStore.getState().sessions.find((x) => x.id === sid);
|
||||||
@@ -387,10 +389,13 @@ export function createContextMenuItems(type: ContextMenuType, data?: unknown): C
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
// 修复: archive 之前完全未调用 IPC,UI 改了但 DB 没改,重启后状态丢失
|
// 修复: archive 之前完全未调用 IPC,UI 改了但 DB 没改,重启后状态丢失
|
||||||
|
// F-2: 归档后可在侧栏「已归档」折叠面板中找回
|
||||||
{
|
{
|
||||||
id: 'archive',
|
id: 'archive',
|
||||||
icon: Archive,
|
icon: Archive,
|
||||||
label: '归档',
|
label: useSessionStore.getState().sessions.find((x) => x.id === sid)?.archived
|
||||||
|
? '取消归档'
|
||||||
|
: '归档',
|
||||||
action: async () => {
|
action: async () => {
|
||||||
if (!sid) return;
|
if (!sid) return;
|
||||||
const s = useSessionStore.getState().sessions.find((x) => x.id === sid);
|
const s = useSessionStore.getState().sessions.find((x) => x.id === sid);
|
||||||
|
|||||||
@@ -505,8 +505,8 @@ export function ChatInput(): React.JSX.Element {
|
|||||||
multiple
|
multiple
|
||||||
accept={
|
accept={
|
||||||
supportsImages
|
supportsImages
|
||||||
? 'image/*,.txt,.md,.json,.csv,.ts,.tsx,.js,.jsx,.py,.rb,.go,.rs,.java,.c,.cpp,.h,.css,.html,.xml,.yaml,.yml,.toml,.ini,.sh,.sql,.log,.pdf'
|
? 'image/*,.txt,.md,.json,.csv,.ts,.tsx,.js,.jsx,.py,.rb,.go,.rs,.java,.c,.cpp,.h,.css,.html,.xml,.yaml,.yml,.toml,.ini,.sh,.sql,.log'
|
||||||
: '.txt,.md,.json,.csv,.ts,.tsx,.js,.jsx,.py,.rb,.go,.rs,.java,.c,.cpp,.h,.css,.html,.xml,.yaml,.yml,.toml,.ini,.sh,.sql,.log,.pdf'
|
: '.txt,.md,.json,.csv,.ts,.tsx,.js,.jsx,.py,.rb,.go,.rs,.java,.c,.cpp,.h,.css,.html,.xml,.yaml,.yml,.toml,.ini,.sh,.sql,.log'
|
||||||
}
|
}
|
||||||
style={{ display: 'none' }}
|
style={{ display: 'none' }}
|
||||||
onChange={handleFileChange}
|
onChange={handleFileChange}
|
||||||
|
|||||||
@@ -33,11 +33,13 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Trash2,
|
Trash2,
|
||||||
|
Archive,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useSessionStore, type Session } from '@renderer/stores/session-store';
|
import { useSessionStore, type Session } from '@renderer/stores/session-store';
|
||||||
import { useAgentStore } from '@renderer/stores/agent-store';
|
import { useAgentStore } from '@renderer/stores/agent-store';
|
||||||
import { formatRelativeTime } from '@renderer/lib/formatters';
|
import { formatRelativeTime } from '@renderer/lib/formatters';
|
||||||
import { LAYOUT } from '@renderer/lib/constants';
|
import { LAYOUT } from '@renderer/lib/constants';
|
||||||
|
import { ContextMenu, createContextMenuItems } from '@renderer/components/ContextMenu';
|
||||||
|
|
||||||
export function Sidebar(): React.JSX.Element {
|
export function Sidebar(): React.JSX.Element {
|
||||||
const sessions = useSessionStore((s) => s.sessions);
|
const sessions = useSessionStore((s) => s.sessions);
|
||||||
@@ -60,29 +62,27 @@ export function Sidebar(): React.JSX.Element {
|
|||||||
.list()
|
.list()
|
||||||
.then((list) => {
|
.then((list) => {
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
useSessionStore
|
useSessionStore.getState().setSessions(
|
||||||
.getState()
|
(
|
||||||
.setSessions(
|
list as Array<{
|
||||||
(
|
id: string;
|
||||||
list as Array<{
|
title: string;
|
||||||
id: string;
|
createdAt: number;
|
||||||
title: string;
|
updatedAt: number;
|
||||||
createdAt: number;
|
messageCount: number;
|
||||||
updatedAt: number;
|
pinned: boolean;
|
||||||
messageCount: number;
|
archived: boolean;
|
||||||
pinned: boolean;
|
}>
|
||||||
archived: boolean;
|
).map((s) => ({
|
||||||
}>
|
id: s.id,
|
||||||
).map((s) => ({
|
title: s.title,
|
||||||
id: s.id,
|
createdAt: s.createdAt,
|
||||||
title: s.title,
|
updatedAt: s.updatedAt,
|
||||||
createdAt: s.createdAt,
|
messageCount: s.messageCount,
|
||||||
updatedAt: s.updatedAt,
|
pinned: s.pinned,
|
||||||
messageCount: s.messageCount,
|
archived: s.archived,
|
||||||
pinned: s.pinned,
|
})),
|
||||||
archived: s.archived,
|
);
|
||||||
})),
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
// M-11 修复: 记录错误而非静默吞掉,便于诊断
|
// M-11 修复: 记录错误而非静默吞掉,便于诊断
|
||||||
@@ -155,17 +155,15 @@ export function Sidebar(): React.JSX.Element {
|
|||||||
pinned: boolean;
|
pinned: boolean;
|
||||||
archived: boolean;
|
archived: boolean;
|
||||||
};
|
};
|
||||||
useSessionStore
|
useSessionStore.getState().addSession({
|
||||||
.getState()
|
id: r.id,
|
||||||
.addSession({
|
title: r.title,
|
||||||
id: r.id,
|
createdAt: r.createdAt,
|
||||||
title: r.title,
|
updatedAt: r.updatedAt,
|
||||||
createdAt: r.createdAt,
|
messageCount: r.messageCount,
|
||||||
updatedAt: r.updatedAt,
|
pinned: r.pinned,
|
||||||
messageCount: r.messageCount,
|
archived: r.archived,
|
||||||
pinned: r.pinned,
|
});
|
||||||
archived: r.archived,
|
|
||||||
});
|
|
||||||
setCurrentSession(r.id);
|
setCurrentSession(r.id);
|
||||||
loadSessionMessages(r.id);
|
loadSessionMessages(r.id);
|
||||||
return;
|
return;
|
||||||
@@ -265,12 +263,116 @@ export function Sidebar(): React.JSX.Element {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Divider sx={{ mt: 'auto', mb: 1 }} />
|
<Divider sx={{ mt: 'auto', mb: 1 }} />
|
||||||
|
{/* F-2 修复: 归档会话视图 —— 此前归档入口激活后(会话右键菜单),
|
||||||
|
归档会话从列表永久消失且无恢复路径(数据在 DB 但 UI 不可达) */}
|
||||||
|
<ArchivedSessionsPanel />
|
||||||
<ToolManagerPanel />
|
<ToolManagerPanel />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* F-2: 已归档会话折叠面板 — 提供「恢复到列表」入口
|
||||||
|
*
|
||||||
|
* 归档会话不参与主列表渲染(filteredSessions 过滤 archived),
|
||||||
|
* 此面板是唯一的找回路径:恢复(archive IPC 反向调用)或删除。
|
||||||
|
*/
|
||||||
|
function ArchivedSessionsPanel(): React.JSX.Element | null {
|
||||||
|
const sessions = useSessionStore((s) => s.sessions);
|
||||||
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
const archived = sessions.filter((s) => s.archived);
|
||||||
|
|
||||||
|
if (archived.length === 0) return null;
|
||||||
|
|
||||||
|
const handleUnarchive = async (sessionId: string) => {
|
||||||
|
try {
|
||||||
|
const r = await window.metona?.sessions?.archive(sessionId, false);
|
||||||
|
if (r?.success) {
|
||||||
|
useSessionStore.getState().archiveSession(sessionId, false);
|
||||||
|
} else {
|
||||||
|
import('@metona-team/metona-toast')
|
||||||
|
.then((mod) => mod.default.error(r?.error ?? '恢复失败'))
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Sidebar] Unarchive failed:', err);
|
||||||
|
import('@metona-team/metona-toast')
|
||||||
|
.then((mod) => mod.default.error('恢复失败'))
|
||||||
|
.catch(() => {});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ mb: 1 }}>
|
||||||
|
<ListItemButton
|
||||||
|
onClick={() => setExpanded(!expanded)}
|
||||||
|
dense
|
||||||
|
sx={{ borderRadius: 1, px: 1.5, py: 0.75 }}
|
||||||
|
>
|
||||||
|
<ListItemIcon sx={{ minWidth: 24 }}>
|
||||||
|
{expanded ? <ChevronDown size={12} /> : <ChevronRight size={12} />}
|
||||||
|
<Archive size={12} style={{ marginLeft: 4 }} />
|
||||||
|
</ListItemIcon>
|
||||||
|
<ListItemText
|
||||||
|
primary={
|
||||||
|
<Typography variant="body2" sx={{ fontSize: 12 }}>
|
||||||
|
已归档
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Typography variant="caption" sx={{ color: 'text.disabled', fontSize: 10 }}>
|
||||||
|
{archived.length} 个
|
||||||
|
</Typography>
|
||||||
|
</ListItemButton>
|
||||||
|
<Collapse in={expanded}>
|
||||||
|
<List
|
||||||
|
dense
|
||||||
|
disablePadding
|
||||||
|
sx={{
|
||||||
|
pl: 2,
|
||||||
|
maxHeight: 180,
|
||||||
|
overflowY: 'auto',
|
||||||
|
pr: 0.5,
|
||||||
|
'&::-webkit-scrollbar': { width: 6 },
|
||||||
|
'&::-webkit-scrollbar-thumb': { bgcolor: 'divider', borderRadius: 3 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{archived.map((s) => (
|
||||||
|
<ListItemButton key={s.id} dense sx={{ py: 0.25, px: 1, borderRadius: 1 }}>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 0,
|
||||||
|
fontSize: 11,
|
||||||
|
color: 'text.disabled',
|
||||||
|
overflow: 'hidden',
|
||||||
|
textOverflow: 'ellipsis',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
}}
|
||||||
|
title={s.title}
|
||||||
|
>
|
||||||
|
{s.title}
|
||||||
|
</Typography>
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
sx={{ fontSize: 10, minWidth: 40, flexShrink: 0 }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
void handleUnarchive(s.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
恢复
|
||||||
|
</Button>
|
||||||
|
</ListItemButton>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Collapse>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function SessionItem({
|
function SessionItem({
|
||||||
session,
|
session,
|
||||||
isActive,
|
isActive,
|
||||||
@@ -285,6 +387,9 @@ function SessionItem({
|
|||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
|
||||||
|
// F-1 修复: 挂载会话右键菜单(重命名/置顶/归档/导出/删除)
|
||||||
|
// 此前 ContextMenu 的 session 分支约 200 行无任何触发点(死代码)
|
||||||
|
const [contextMenu, setContextMenu] = useState<{ x: number; y: number } | null>(null);
|
||||||
|
|
||||||
const handleDelete = (e: React.MouseEvent) => {
|
const handleDelete = (e: React.MouseEvent) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -337,6 +442,10 @@ function SessionItem({
|
|||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
selected={isActive}
|
selected={isActive}
|
||||||
dense
|
dense
|
||||||
|
onContextMenu={(e: React.MouseEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setContextMenu({ x: e.clientX, y: e.clientY });
|
||||||
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
borderRadius: 1.5,
|
borderRadius: 1.5,
|
||||||
mb: 0.25,
|
mb: 0.25,
|
||||||
@@ -447,6 +556,16 @@ function SessionItem({
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
{/* F-1: 会话右键菜单(重命名/置顶/归档/导出/删除),此前无触发点 */}
|
||||||
|
{contextMenu && (
|
||||||
|
<ContextMenu
|
||||||
|
x={contextMenu.x}
|
||||||
|
y={contextMenu.y}
|
||||||
|
items={createContextMenuItems('session', { sessionId: session.id })}
|
||||||
|
onClose={() => setContextMenu(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
import { ArrowRight, ArrowLeft, CheckCircle, Eye, EyeOff } from 'lucide-react';
|
import { ArrowRight, ArrowLeft, CheckCircle, Eye, EyeOff } from 'lucide-react';
|
||||||
import { useUIStore } from '@renderer/stores/ui-store';
|
import { useUIStore } from '@renderer/stores/ui-store';
|
||||||
import { useAgentStore } from '@renderer/stores/agent-store';
|
import { useAgentStore } from '@renderer/stores/agent-store';
|
||||||
|
import { PROVIDER_URLS } from '@renderer/components/settings/useConfig';
|
||||||
|
|
||||||
const STEPS = ['欢迎', '配置 LLM', '自定义 Agent', '工作空间', '开始使用'];
|
const STEPS = ['欢迎', '配置 LLM', '自定义 Agent', '工作空间', '开始使用'];
|
||||||
|
|
||||||
@@ -245,6 +246,13 @@ export function OnboardingWizard(): React.JSX.Element | null {
|
|||||||
setProvider(v);
|
setProvider(v);
|
||||||
// 联动默认上下文窗口(与 SettingsModal 默认值一致)
|
// 联动默认上下文窗口(与 SettingsModal 默认值一致)
|
||||||
setContextWindow(DEFAULT_CTX[v] ?? null);
|
setContextWindow(DEFAULT_CTX[v] ?? null);
|
||||||
|
// D-3 修复: 自动填充默认 URL(与 LLMSettings 行为一致 —
|
||||||
|
// 仅在 URL 为空或仍是某 Provider 的默认值时覆盖,用户自定义 URL 不动)
|
||||||
|
const currentUrl = baseURL.trim();
|
||||||
|
const isDefaultUrl = Object.values(PROVIDER_URLS).includes(currentUrl);
|
||||||
|
if (isDefaultUrl || !currentUrl) {
|
||||||
|
setBaseURL(PROVIDER_URLS[v] ?? '');
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MenuItem value="deepseek">DeepSeek</MenuItem>
|
<MenuItem value="deepseek">DeepSeek</MenuItem>
|
||||||
|
|||||||
@@ -36,23 +36,33 @@ export function WorkspaceSettings() {
|
|||||||
|
|
||||||
const currentPath = workspacePath;
|
const currentPath = workspacePath;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验路径并进入"待切换"状态(选择文件夹与手输路径共用)
|
||||||
|
* F-5 修复: 手输路径此前经 useConfig 实时落库 —— 每敲一个字符就持久化
|
||||||
|
* 残缺路径并写 workspace-config.json,中途退出后下次启动加载半截路径。
|
||||||
|
* 现在输入只更新 pendingPath 草稿,点「校验」后才进入切换流程。
|
||||||
|
*/
|
||||||
|
const validatePath = async (path: string) => {
|
||||||
|
if (!path.trim()) return;
|
||||||
|
setPendingPath(path.trim());
|
||||||
|
setChecking(true);
|
||||||
|
setCheckResult(null);
|
||||||
|
try {
|
||||||
|
const result = await window.metona.workspace.check(path.trim());
|
||||||
|
setCheckResult(result);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[WorkspaceSettings]', err);
|
||||||
|
setCheckResult({ valid: false, reason: (err as Error).message });
|
||||||
|
} finally {
|
||||||
|
setChecking(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleSelect = async () => {
|
const handleSelect = async () => {
|
||||||
if (!window.metona?.app?.selectFolder) return;
|
if (!window.metona?.app?.selectFolder) return;
|
||||||
const r = await window.metona.app.selectFolder(currentPath || undefined);
|
const r = await window.metona.app.selectFolder(currentPath || undefined);
|
||||||
if (!r.canceled && r.path) {
|
if (!r.canceled && r.path) {
|
||||||
// 立即校验新路径
|
await validatePath(r.path);
|
||||||
setPendingPath(r.path);
|
|
||||||
setChecking(true);
|
|
||||||
setCheckResult(null);
|
|
||||||
try {
|
|
||||||
const result = await window.metona.workspace.check(r.path);
|
|
||||||
setCheckResult(result);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[WorkspaceSettings]', err);
|
|
||||||
setCheckResult({ valid: false, reason: (err as Error).message });
|
|
||||||
} finally {
|
|
||||||
setChecking(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -168,13 +178,22 @@ export function WorkspaceSettings() {
|
|||||||
工作空间是 Metona 的组织核心,包含 SOUL.md、MEMORY.md 两个必需文件。
|
工作空间是 Metona 的组织核心,包含 SOUL.md、MEMORY.md 两个必需文件。
|
||||||
</Typography>
|
</Typography>
|
||||||
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
|
<Stack direction="row" spacing={1} sx={{ alignItems: 'center' }}>
|
||||||
|
{/* F-5: 手输路径只更新草稿(pendingPath),不实时落库 */}
|
||||||
<TextField
|
<TextField
|
||||||
size="small"
|
size="small"
|
||||||
value={workspacePath}
|
value={pendingPath ?? workspacePath}
|
||||||
onChange={(e) => setWorkspacePath(e.target.value)}
|
onChange={(e) => setPendingPath(e.target.value)}
|
||||||
placeholder="~/MetonaWorkspaces/default/"
|
placeholder="~/MetonaWorkspaces/default/"
|
||||||
sx={{ flex: 1 }}
|
sx={{ flex: 1 }}
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
onClick={() => validatePath(pendingPath ?? workspacePath)}
|
||||||
|
disabled={checking || !(pendingPath ?? workspacePath).trim()}
|
||||||
|
>
|
||||||
|
{checking ? '校验中...' : '校验'}
|
||||||
|
</Button>
|
||||||
<Button variant="outlined" size="small" onClick={handleSelect}>
|
<Button variant="outlined" size="small" onClick={handleSelect}>
|
||||||
选择文件夹
|
选择文件夹
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user