feat: v0.9.1 — 图片压缩、文件token预算截断、并行上传、文档同步
- 图片上传: Canvas前端压缩(1280px等比缩放+JPEG), 300KB以下跳过 - 文件上传: Token预算感知截断(30% numCtx), 26种语言注释剥离 - 性能: handleImagePaths/handleTextFilePaths 并行化(Promise.all) - sub-agent.ts: 修复3个TS编译错误(tool_name类型, success重复) - 版本号: 全项目统一至0.9.1(10处) - 文档: BUILD.md/DEVELOPMENT.md 同步实际架构, 移除过期分支引用 - README: 组件列表修正(log-panel.ts→prompt-modal.ts) - 帮助面板: 更新图片/文件上传描述
This commit is contained in:
@@ -14,7 +14,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="https://img.shields.io/badge/version-v0.9.0-E8734A?style=flat-square" alt="version">
|
<img src="https://img.shields.io/badge/version-v0.9.1-E8734A?style=flat-square" alt="version">
|
||||||
<img src="https://img.shields.io/badge/electron-33+-47848F?style=flat-square&logo=electron" alt="electron">
|
<img src="https://img.shields.io/badge/electron-33+-47848F?style=flat-square&logo=electron" alt="electron">
|
||||||
<img src="https://img.shields.io/badge/typescript-5.7+-3178C6?style=flat-square&logo=typescript" alt="typescript">
|
<img src="https://img.shields.io/badge/typescript-5.7+-3178C6?style=flat-square&logo=typescript" alt="typescript">
|
||||||
<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">
|
||||||
@@ -177,13 +177,13 @@ main.ts (入口)
|
|||||||
├── chat-area.ts # 聊天消息区域
|
├── chat-area.ts # 聊天消息区域
|
||||||
├── input-area.ts # 输入框 + 文件上传
|
├── input-area.ts # 输入框 + 文件上传
|
||||||
├── workspace-panel.ts # 终端 + 文件浏览器
|
├── workspace-panel.ts # 终端 + 文件浏览器
|
||||||
├── log-panel.ts # 结构化日志面板
|
|
||||||
├── settings-modal.ts # 设置面板
|
├── settings-modal.ts # 设置面板
|
||||||
├── history-modal.ts # 会话历史
|
├── history-modal.ts # 会话历史
|
||||||
├── memory-modal.ts # 记忆管理
|
├── memory-modal.ts # 记忆管理
|
||||||
├── tools-modal.ts # 工具列表
|
├── tools-modal.ts # 工具列表
|
||||||
├── token-dashboard.ts # Token 消耗仪表盘
|
├── token-dashboard.ts # Token 消耗仪表盘
|
||||||
├── tool-confirm-modal.ts # 工具执行确认
|
├── tool-confirm-modal.ts # 工具执行确认
|
||||||
|
├── prompt-modal.ts # 系统提示词查看
|
||||||
├── toast.ts # Toast 通知
|
├── toast.ts # Toast 通知
|
||||||
└── lightbox.ts # 图片灯箱
|
└── lightbox.ts # 图片灯箱
|
||||||
```
|
```
|
||||||
@@ -231,7 +231,7 @@ npm start
|
|||||||
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist
|
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist
|
||||||
```
|
```
|
||||||
|
|
||||||
产出:`release/Metona Ollama Setup v0.9.0.exe`
|
产出:`release/Metona Ollama Setup v0.9.1.exe`
|
||||||
|
|
||||||
## 🛠️ 常用命令
|
## 🛠️ 常用命令
|
||||||
|
|
||||||
@@ -400,13 +400,13 @@ main.ts (Entry)
|
|||||||
├── chat-area.ts # Chat message area
|
├── chat-area.ts # Chat message area
|
||||||
├── input-area.ts # Input box + file upload
|
├── input-area.ts # Input box + file upload
|
||||||
├── workspace-panel.ts # Terminal + file browser
|
├── workspace-panel.ts # Terminal + file browser
|
||||||
├── log-panel.ts # Structured log panel
|
|
||||||
├── settings-modal.ts # Settings panel
|
├── settings-modal.ts # Settings panel
|
||||||
├── history-modal.ts # Session history
|
├── history-modal.ts # Session history
|
||||||
├── memory-modal.ts # Memory management
|
├── memory-modal.ts # Memory management
|
||||||
├── tools-modal.ts # Tool list
|
├── tools-modal.ts # Tool list
|
||||||
├── token-dashboard.ts # Token consumption dashboard
|
├── token-dashboard.ts # Token consumption dashboard
|
||||||
├── tool-confirm-modal.ts # Tool execution confirmation
|
├── tool-confirm-modal.ts # Tool execution confirmation
|
||||||
|
├── prompt-modal.ts # System prompt viewer
|
||||||
├── toast.ts # Toast notifications
|
├── toast.ts # Toast notifications
|
||||||
└── lightbox.ts # Image lightbox
|
└── lightbox.ts # Image lightbox
|
||||||
```
|
```
|
||||||
@@ -454,7 +454,7 @@ npm start
|
|||||||
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist
|
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm run dist
|
||||||
```
|
```
|
||||||
|
|
||||||
Output: `release/Metona Ollama Setup v0.9.0.exe`
|
Output: `release/Metona Ollama Setup v0.9.1.exe`
|
||||||
|
|
||||||
## 🛠️ Common Commands
|
## 🛠️ Common Commands
|
||||||
|
|
||||||
|
|||||||
+7
-8
@@ -14,7 +14,6 @@
|
|||||||
# 1. 克隆并安装依赖
|
# 1. 克隆并安装依赖
|
||||||
git clone https://gitee.com/thzxx/metona-ollama-desktop.git
|
git clone https://gitee.com/thzxx/metona-ollama-desktop.git
|
||||||
cd metona-ollama-desktop
|
cd metona-ollama-desktop
|
||||||
git checkout desktop-v1.1.0
|
|
||||||
npm config set registry https://registry.npmmirror.com
|
npm config set registry https://registry.npmmirror.com
|
||||||
npm install
|
npm install
|
||||||
|
|
||||||
@@ -92,9 +91,9 @@ npm run dist # NSIS 安装包
|
|||||||
|
|
||||||
| 文件 | 说明 |
|
| 文件 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `release/Metona Ollama Setup v1.1.0.exe` | NSIS 安装包(可选目录、创建快捷方式) |
|
| `release/Metona Ollama Setup v0.9.1.exe` | NSIS 安装包(可选目录、创建快捷方式) |
|
||||||
|
|
||||||
> v3.2.0 起不再提供绿色便携版,仅保留 NSIS 安装包。
|
> 仅提供 NSIS 安装包。
|
||||||
|
|
||||||
## 发布到 Gitee
|
## 发布到 Gitee
|
||||||
|
|
||||||
@@ -102,18 +101,18 @@ npm run dist # NSIS 安装包
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
TOKEN="your_access_token"
|
TOKEN="your_access_token"
|
||||||
curl -X POST "https://gitee.com/api/v5/repos/thzxx/metona-ollama/releases?access_token=$TOKEN" \
|
curl -X POST "https://gitee.com/api/v5/repos/thzxx/metona-ollama-desktop/releases?access_token=$TOKEN" \
|
||||||
-H "Content-Type: application/json" \
|
-H "Content-Type: application/json" \
|
||||||
-d '{"tag_name":"v3.2.0","name":"v3.2.0","body":"Release notes","target_commitish":"refactor/modular-architecture"}'
|
-d '{"tag_name":"v0.9.1","name":"v0.9.1","body":"Release notes","target_commitish":"master"}'
|
||||||
```
|
```
|
||||||
|
|
||||||
### 上传附件
|
### 上传附件
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
TOKEN="your_access_token"
|
TOKEN="your_access_token"
|
||||||
RELEASE_ID="600564"
|
RELEASE_ID="<release_id>"
|
||||||
curl -X POST "https://gitee.com/api/v5/repos/thzxx/metona-ollama/releases/$RELEASE_ID/attach_files?access_token=$TOKEN" \
|
curl -X POST "https://gitee.com/api/v5/repos/thzxx/metona-ollama-desktop/releases/$RELEASE_ID/attach_files?access_token=$TOKEN" \
|
||||||
-H "Content-Type: multipart/form-data" -F "file=@release/Metona Ollama Setup 3.2.0.exe"
|
-H "Content-Type: multipart/form-data" -F "file=@release/Metona Ollama Setup v0.9.1.exe"
|
||||||
```
|
```
|
||||||
|
|
||||||
## 故障排查
|
## 故障排查
|
||||||
|
|||||||
+11
-23
@@ -1,6 +1,6 @@
|
|||||||
# Metona Ollama Desktop — 开发规范
|
# Metona Ollama Desktop — 开发规范
|
||||||
|
|
||||||
> 版本: v1.1.0 | 更新: 2026-04-24 | 维护: 项目团队
|
> 版本: v0.9.1 | 更新: 2026-06-05 | 维护: 项目团队
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -60,9 +60,7 @@ src/
|
|||||||
│ │ ├── context-manager.ts # 上下文窗口管理
|
│ │ ├── context-manager.ts # 上下文窗口管理
|
||||||
│ │ ├── skill-manager.ts # 技能自动生成(Level 0)
|
│ │ ├── skill-manager.ts # 技能自动生成(Level 0)
|
||||||
│ │ ├── sub-agent.ts # 子代理委派
|
│ │ ├── sub-agent.ts # 子代理委派
|
||||||
│ │ ├── cron-manager.ts # 定时任务
|
|
||||||
│ │ ├── mcp-client.ts # MCP 渲染端客户端
|
│ │ ├── mcp-client.ts # MCP 渲染端客户端
|
||||||
│ │ ├── document-processor.ts # 文档分块
|
|
||||||
│ │ ├── log-service.ts # 结构化日志
|
│ │ ├── log-service.ts # 结构化日志
|
||||||
│ │ └── crypto.ts # AES-256-GCM 加密
|
│ │ └── crypto.ts # AES-256-GCM 加密
|
||||||
│ ├── utils/
|
│ ├── utils/
|
||||||
@@ -154,10 +152,10 @@ import {
|
|||||||
|
|
||||||
## 五、架构规范
|
## 五、架构规范
|
||||||
|
|
||||||
### 5.1 四大子系统
|
### 5.1 五大子系统
|
||||||
|
|
||||||
```
|
```
|
||||||
① Agent 系统 → agent-engine.ts + tool-registry.ts(38 工具 + MCP 动态)
|
① Agent 系统 → agent-engine.ts + tool-registry.ts(38 内置工具 + MCP 动态)
|
||||||
② 记忆系统 → memory-manager.ts + vector-memory.ts
|
② 记忆系统 → memory-manager.ts + vector-memory.ts
|
||||||
③ 向量存储 → vector-store.ts(IVF 索引)
|
③ 向量存储 → vector-store.ts(IVF 索引)
|
||||||
④ 工作空间 → workspace.ts (主进程) + workspace-panel.ts (渲染进程)
|
④ 工作空间 → workspace.ts (主进程) + workspace-panel.ts (渲染进程)
|
||||||
@@ -194,7 +192,7 @@ import {
|
|||||||
### 5.4 联网搜索联动
|
### 5.4 联网搜索联动
|
||||||
|
|
||||||
- web_search 返回搜索结果(标题、URL、摘要),默认 15 条
|
- web_search 返回搜索结果(标题、URL、摘要),默认 15 条
|
||||||
- TOOL_USAGE_GUIDE 要求模型在 web_search 后必须选择相关 URL 调用 web_fetch 抓取详情
|
- 模型在 web_search 后应选择相关 URL 调用 web_fetch 抓取详情
|
||||||
- web_fetch 默认返回完整内容(`max_chars=0`),不截断
|
- web_fetch 默认返回完整内容(`max_chars=0`),不截断
|
||||||
|
|
||||||
### 5.5 工作空间与 AI 集成
|
### 5.5 工作空间与 AI 集成
|
||||||
@@ -258,23 +256,17 @@ npm run dist # 构建 Windows 安装包
|
|||||||
| `package.json` | `"version": "X.Y.Z"` |
|
| `package.json` | `"version": "X.Y.Z"` |
|
||||||
| `package-lock.json` | 顶层 version 字段(2 处) |
|
| `package-lock.json` | 顶层 version 字段(2 处) |
|
||||||
| `src/renderer/index.html` | `<span class="app-version">vX.Y.Z</span>` |
|
| `src/renderer/index.html` | `<span class="app-version">vX.Y.Z</span>` |
|
||||||
| `README.md` | 版本徽章、下载文件名、构建分支引用 |
|
| `README.md` | 版本徽章、下载文件名 |
|
||||||
| `docs/BUILD.md` | 构建分支引用、新增构建日志条目 |
|
| `src/main/menu.ts` | 关于对话框版本号 |
|
||||||
| `docs/CHANGELOG.md` | 版本分支结构链、新增版本说明条目 |
|
| `docs/BUILD.md` | 构建产物文件名、Gitee release tag |
|
||||||
| `docs/DEVELOPMENT.md` | 文件头部版本号 |
|
| `docs/DEVELOPMENT.md` | 文件头部版本号 |
|
||||||
|
|
||||||
### 更新日志
|
|
||||||
|
|
||||||
- **所有更新日志只写在 `docs/CHANGELOG.md`**
|
|
||||||
- `README.md` 不写历史版本记录,只保留一行链接指向 `docs/CHANGELOG.md`
|
|
||||||
|
|
||||||
### 7.2 发布流程
|
### 7.2 发布流程
|
||||||
|
|
||||||
1. 更新版本号(同步上述文件)
|
1. 更新版本号(同步上述文件)
|
||||||
2. 更新 `docs/CHANGELOG.md`
|
2. 构建并测试:`npm start`
|
||||||
3. 构建并测试:`npm start`
|
3. 构建安装包:`npm run dist`
|
||||||
4. 构建安装包:`npm run dist`
|
4. 推送到 master
|
||||||
5. 推送到 develop → 合并到 master → 创建版本分支
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -282,11 +274,7 @@ npm run dist # 构建 Windows 安装包
|
|||||||
|
|
||||||
### 分支模型
|
### 分支模型
|
||||||
|
|
||||||
- `master` — 稳定发布版本
|
- `master` — 主分支
|
||||||
- `develop` — 开发主分支
|
|
||||||
- `metona-ollama-desktop-vX.Y.Z` — 版本发布分支
|
|
||||||
- `feature/*` — 功能开发
|
|
||||||
- `fix/*` — Bug 修复
|
|
||||||
|
|
||||||
### Commit 规范
|
### Commit 规范
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "metona-ollama-desktop",
|
"name": "metona-ollama-desktop",
|
||||||
"version": "1.1.0",
|
"version": "0.9.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "metona-ollama-desktop",
|
"name": "metona-ollama-desktop",
|
||||||
"version": "1.1.0",
|
"version": "0.9.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"sql.js": "^1.11.0"
|
"sql.js": "^1.11.0"
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "metona-ollama-desktop",
|
"name": "metona-ollama-desktop",
|
||||||
"version": "0.9.0",
|
"version": "0.9.1",
|
||||||
"description": "Metona Ollama - TypeScript + Electron 桌面 AI 聊天客户端",
|
"description": "Metona Ollama - TypeScript + Electron 桌面 AI 聊天客户端",
|
||||||
"main": "dist/main/main.js",
|
"main": "dist/main/main.js",
|
||||||
"author": "thzxx",
|
"author": "thzxx",
|
||||||
@@ -48,7 +48,7 @@
|
|||||||
"requestedExecutionLevel": "asInvoker"
|
"requestedExecutionLevel": "asInvoker"
|
||||||
},
|
},
|
||||||
"nsis": {
|
"nsis": {
|
||||||
"artifactName": "Metona Ollama Setup v0.9.0.${ext}",
|
"artifactName": "Metona Ollama Setup v0.9.1.${ext}",
|
||||||
"oneClick": false,
|
"oneClick": false,
|
||||||
"allowToChangeInstallationDirectory": true,
|
"allowToChangeInstallationDirectory": true,
|
||||||
"createDesktopShortcut": true,
|
"createDesktopShortcut": true,
|
||||||
|
|||||||
+1
-1
@@ -101,7 +101,7 @@ export function createMenu(): void {
|
|||||||
dialog.showMessageBox(mainWindow!, {
|
dialog.showMessageBox(mainWindow!, {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
title: '关于 Metona Ollama',
|
title: '关于 Metona Ollama',
|
||||||
message: 'Metona Ollama Desktop v0.9.0',
|
message: 'Metona Ollama Desktop v0.9.1',
|
||||||
detail: 'TypeScript + Electron Ollama AI 聊天客户端\n\nhttps://gitee.com/thzxx/metona-ollama',
|
detail: 'TypeScript + Electron Ollama AI 聊天客户端\n\nhttps://gitee.com/thzxx/metona-ollama',
|
||||||
icon: getIconPath()
|
icon: getIconPath()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -133,28 +133,90 @@ function autoResizeTextarea(): void {
|
|||||||
chatInputEl.style.height = Math.min(chatInputEl.scrollHeight, 120) + 'px';
|
chatInputEl.style.height = Math.min(chatInputEl.scrollHeight, 120) + 'px';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── 图片压缩管线 ──
|
||||||
|
const IMG_MAX_WIDTH = 1280;
|
||||||
|
const IMG_MAX_HEIGHT = 1280;
|
||||||
|
const IMG_QUALITY = 0.8;
|
||||||
|
const IMG_MAX_SIZE_BEFORE_COMPRESS = 300 * 1024; // 300KB以下不压缩
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Canvas 前端压缩图片:等比缩放 + JPEG 转换
|
||||||
|
* base64 → Image → Canvas 缩放绘制 → toBlob → 压缩后 base64
|
||||||
|
*/
|
||||||
|
function compressImage(base64: string): Promise<{ compressed: string; origBytes: number; newBytes: number }> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// 先计算原始 base64 的大小(字节数 ≈ base64长度 * 0.75)
|
||||||
|
const origBytes = Math.ceil(base64.length * 0.75);
|
||||||
|
// 小图跳过压缩
|
||||||
|
if (origBytes <= IMG_MAX_SIZE_BEFORE_COMPRESS) {
|
||||||
|
resolve({ compressed: base64, origBytes, newBytes: origBytes });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
let { width, height } = img;
|
||||||
|
// 等比缩放
|
||||||
|
if (width > IMG_MAX_WIDTH || height > IMG_MAX_HEIGHT) {
|
||||||
|
const ratio = Math.min(IMG_MAX_WIDTH / width, IMG_MAX_HEIGHT / height);
|
||||||
|
width = Math.round(width * ratio);
|
||||||
|
height = Math.round(height * ratio);
|
||||||
|
}
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
const ctx = canvas.getContext('2d')!;
|
||||||
|
ctx.drawImage(img, 0, 0, width, height);
|
||||||
|
canvas.toBlob((blob) => {
|
||||||
|
if (!blob) { resolve({ compressed: base64, origBytes, newBytes: origBytes }); return; }
|
||||||
|
const reader = new FileReader();
|
||||||
|
let newBytes = 0;
|
||||||
|
reader.onloadend = () => {
|
||||||
|
const compressed = (reader.result as string).split(',')[1] || base64;
|
||||||
|
newBytes = blob.size;
|
||||||
|
resolve({ compressed, origBytes, newBytes });
|
||||||
|
};
|
||||||
|
reader.onerror = () => { resolve({ compressed: base64, origBytes, newBytes: origBytes }); };
|
||||||
|
reader.readAsDataURL(blob);
|
||||||
|
}, 'image/jpeg', IMG_QUALITY);
|
||||||
|
};
|
||||||
|
img.onerror = () => resolve({ compressed: base64, origBytes, newBytes: origBytes });
|
||||||
|
img.src = `data:image/png;base64,${base64}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function handleImagePaths(paths: string[]): Promise<void> {
|
async function handleImagePaths(paths: string[]): Promise<void> {
|
||||||
const bridge = getBridge();
|
const bridge = getBridge();
|
||||||
const maxSize = 20 * 1024 * 1024;
|
const maxSize = 20 * 1024 * 1024;
|
||||||
for (const filePath of paths) {
|
|
||||||
|
// 并行读取所有图片
|
||||||
|
const results = await Promise.all(paths.map(async (filePath) => {
|
||||||
const name = filePath.split(/[/\\]/).pop() || filePath;
|
const name = filePath.split(/[/\\]/).pop() || filePath;
|
||||||
try {
|
try {
|
||||||
const result = await bridge!.fs.readFileBase64(filePath);
|
const result = await bridge!.fs.readFileBase64(filePath);
|
||||||
if (!result.success) {
|
if (!result.success) return { name, error: result.error };
|
||||||
appendSystemMessage(`❌ 读取 ${name} 失败: ${result.error}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const size = result.size ?? 0;
|
const size = result.size ?? 0;
|
||||||
if (size > maxSize) {
|
if (size > maxSize) return { name, error: `超过 20MB 限制` };
|
||||||
appendSystemMessage(`⚠️ 图片 ${name} 超过 20MB 限制`);
|
return { name, base64: result.content!, size };
|
||||||
|
} catch (err) {
|
||||||
|
return { name, error: (err as Error).message };
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (const r of results) {
|
||||||
|
if ('error' in r && r.error) {
|
||||||
|
appendSystemMessage(`❌ 读取 ${r.name} 失败: ${r.error}`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const base64 = result.content!;
|
if ('base64' in r) {
|
||||||
pendingImages.push({ name, base64 });
|
const imgResult = r as { name: string; base64: string; size: number };
|
||||||
logInfo(`图片已添加: ${name}`, formatSize(size));
|
const { compressed, origBytes, newBytes } = await compressImage(imgResult.base64);
|
||||||
} catch (err) {
|
pendingImages.push({ name: imgResult.name, base64: compressed });
|
||||||
logError('图片读取失败', (err as Error).message);
|
if (origBytes !== newBytes) {
|
||||||
appendSystemMessage(`❌ 读取 ${name} 失败`);
|
logInfo(`图片已压缩: ${r.name}`, `${formatSize(origBytes)} → ${formatSize(newBytes)} (${Math.round((1 - newBytes / origBytes) * 100)}%)`);
|
||||||
|
} else {
|
||||||
|
logInfo(`图片已添加: ${r.name}`, formatSize(origBytes));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
renderImagePreviews();
|
renderImagePreviews();
|
||||||
@@ -177,36 +239,40 @@ function renderImagePreviews(): void {
|
|||||||
|
|
||||||
async function handleTextFilePaths(paths: string[]): Promise<void> {
|
async function handleTextFilePaths(paths: string[]): Promise<void> {
|
||||||
const bridge = getBridge();
|
const bridge = getBridge();
|
||||||
for (const filePath of paths) {
|
|
||||||
|
// 并行读取所有文件
|
||||||
|
const results = await Promise.all(paths.map(async (filePath) => {
|
||||||
const name = filePath.split(/[/\\]/).pop() || filePath;
|
const name = filePath.split(/[/\\]/).pop() || filePath;
|
||||||
if (!isTextFile(name)) {
|
if (!isTextFile(name)) return { name, error: `不是支持的文本/代码文件` };
|
||||||
appendSystemMessage(`⚠️ ${name} 不是支持的文本/代码文件`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const result = await bridge!.fs.readFile(filePath);
|
const result = await bridge!.fs.readFile(filePath);
|
||||||
if (!result.success) {
|
if (!result.success) return { name, error: result.error };
|
||||||
appendSystemMessage(`❌ 读取 ${name} 失败: ${result.error}`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const content = result.content as string;
|
const content = result.content as string;
|
||||||
const size = new TextEncoder().encode(content).length;
|
const size = new TextEncoder().encode(content).length;
|
||||||
if (size > MAX_FILE_SIZE) {
|
if (size > MAX_FILE_SIZE) return { name, error: `超过 500KB 限制(${formatSize(size)})` };
|
||||||
appendSystemMessage(`⚠️ ${name} 超过 500KB 限制(${formatSize(size)})`);
|
if (pendingFiles.some(f => f.name === name)) return { name, skipped: true };
|
||||||
continue;
|
return { name, content, language: detectLanguage(name), size };
|
||||||
}
|
|
||||||
if (pendingFiles.some(f => f.name === name)) {
|
|
||||||
appendSystemMessage(`ℹ️ ${name} 已添加`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
pendingFiles.push({ name, content, language: detectLanguage(name), size });
|
|
||||||
logInfo(`文件已添加: ${name}`, formatSize(size));
|
|
||||||
renderFilePreviews();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logError('文件读取失败', (err as Error).message);
|
return { name, error: (err as Error).message };
|
||||||
appendSystemMessage(`❌ 读取 ${name} 失败`);
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
for (const r of results) {
|
||||||
|
if ('skipped' in r) {
|
||||||
|
appendSystemMessage(`ℹ️ ${r.name} 已添加`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ('error' in r && r.error) {
|
||||||
|
appendSystemMessage(`❌ 读取 ${r.name} 失败: ${r.error}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ('content' in r) {
|
||||||
|
const fileResult = r as { name: string; content: string; language: string; size: number };
|
||||||
|
pendingFiles.push({ name: fileResult.name, content: fileResult.content, language: fileResult.language, size: fileResult.size });
|
||||||
|
logInfo(`文件已添加: ${fileResult.name}`, formatSize(fileResult.size));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
renderFilePreviews();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFilePreviews(): void {
|
function renderFilePreviews(): void {
|
||||||
@@ -283,7 +349,7 @@ async function handleRetry(): Promise<void> {
|
|||||||
.map(m => {
|
.map(m => {
|
||||||
let content = m.content || '';
|
let content = m.content || '';
|
||||||
if (m._fileContents?.length) {
|
if (m._fileContents?.length) {
|
||||||
const fileParts = m._fileContents.map(f => `📄 文件:\n\`\`\`${f.language}\n${f.content}\n\`\`\``);
|
const fileParts = buildFileContentParts(m._fileContents);
|
||||||
content = content ? content + '\n\n---\n' + fileParts.join('\n\n---\n') : fileParts.join('\n\n---\n');
|
content = content ? content + '\n\n---\n' + fileParts.join('\n\n---\n') : fileParts.join('\n\n---\n');
|
||||||
}
|
}
|
||||||
return { role: m.role, content, ...(m.images?.length && { images: m.images }) };
|
return { role: m.role, content, ...(m.images?.length && { images: m.images }) };
|
||||||
@@ -539,14 +605,117 @@ function stopGeneration(): void {
|
|||||||
if (abortController) abortController.abort();
|
if (abortController) abortController.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Token 预算比例:文件内容最多占用上下文窗口的比例 */
|
||||||
|
const FILE_TOKEN_BUDGET_RATIO = 0.3;
|
||||||
|
|
||||||
|
// ── 注释剥离 ──
|
||||||
|
|
||||||
|
interface CommentRule { single: RegExp | null; multi: [RegExp, string] | null }
|
||||||
|
|
||||||
|
const COMMENT_RULES: Record<string, CommentRule> = {
|
||||||
|
javascript: { single: /\/\/.*$/gm, multi: [/\/\*[\s\S]*?\*\//g, ''] },
|
||||||
|
typescript: { single: /\/\/.*$/gm, multi: [/\/\*[\s\S]*?\*\//g, ''] },
|
||||||
|
tsx: { single: /\/\/.*$/gm, multi: [/\/\*[\s\S]*?\*\//g, ''] },
|
||||||
|
jsx: { single: /\/\/.*$/gm, multi: [/\/\*[\s\S]*?\*\//g, ''] },
|
||||||
|
java: { single: /\/\/.*$/gm, multi: [/\/\*[\s\S]*?\*\//g, ''] },
|
||||||
|
c: { single: /\/\/.*$/gm, multi: [/\/\*[\s\S]*?\*\//g, ''] },
|
||||||
|
cpp: { single: /\/\/.*$/gm, multi: [/\/\*[\s\S]*?\*\//g, ''] },
|
||||||
|
csharp: { single: /\/\/.*$/gm, multi: [/\/\*[\s\S]*?\*\//g, ''] },
|
||||||
|
go: { single: /\/\/.*$/gm, multi: [/\/\*[\s\S]*?\*\//g, ''] },
|
||||||
|
rust: { single: /\/\/.*$/gm, multi: [/\/\*[\s\S]*?\*\//g, ''] },
|
||||||
|
swift: { single: /\/\/.*$/gm, multi: [/\/\*[\s\S]*?\*\//g, ''] },
|
||||||
|
kotlin: { single: /\/\/.*$/gm, multi: [/\/\*[\s\S]*?\*\//g, ''] },
|
||||||
|
scala: { single: /\/\/.*$/gm, multi: [/\/\*[\s\S]*?\*\//g, ''] },
|
||||||
|
css: { single: null, multi: [/\/\*[\s\S]*?\*\//g, ''] },
|
||||||
|
python: { single: /#.*$/gm, multi: [/('''[\s\S]*?'''|\"\"\"[\s\S]*?\"\"\")/g, ''] },
|
||||||
|
ruby: { single: /#.*$/gm, multi: null },
|
||||||
|
perl: { single: /#.*$/gm, multi: null },
|
||||||
|
shell: { single: /#.*$/gm, multi: null },
|
||||||
|
bash: { single: /#.*$/gm, multi: null },
|
||||||
|
r: { single: /#.*$/gm, multi: null },
|
||||||
|
yaml: { single: /#.*$/gm, multi: null },
|
||||||
|
sql: { single: /--.*$/gm, multi: [/\/\*[\s\S]*?\*\//g, ''] },
|
||||||
|
lua: { single: /--.*$/gm, multi: [/--\[\[[\s\S]*?\]\]/g, ''] },
|
||||||
|
haskell: { single: /--.*$/gm, multi: [/\{-[\s\S]*?-\}/g, ''] },
|
||||||
|
html: { single: null, multi: [/<!--[\s\S]*?-->/g, ''] },
|
||||||
|
xml: { single: null, multi: [/<!--[\s\S]*?-->/g, ''] },
|
||||||
|
markdown: { single: null, multi: [/<!--[\s\S]*?-->/g, ''] },
|
||||||
|
};
|
||||||
|
|
||||||
|
function stripComments(content: string, language: string): string {
|
||||||
|
const rule = COMMENT_RULES[language];
|
||||||
|
if (!rule) return content;
|
||||||
|
|
||||||
|
let result = content;
|
||||||
|
if (rule.single) result = result.replace(rule.single, '');
|
||||||
|
if (rule.multi) result = result.replace(rule.multi[0], rule.multi[1]);
|
||||||
|
|
||||||
|
// 压缩连续空白行:>2个连续换行 → 2个换行
|
||||||
|
result = result.replace(/\n{3,}/g, '\n\n');
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建文件内容 Parts,带 token 预算感知
|
||||||
|
* - 小文件直接嵌入,大文件按预算截断
|
||||||
|
* - 代码文件自动剥离注释
|
||||||
|
*/
|
||||||
|
function buildFileContentParts(fileContents: Array<{ language: string; content: string }>): string[] {
|
||||||
|
const numCtx = state.get<number>(KEYS.NUM_CTX, 24576);
|
||||||
|
const fileBudget = Math.floor(numCtx * FILE_TOKEN_BUDGET_RATIO);
|
||||||
|
let usedTokens = 0;
|
||||||
|
const maxSingleFileTokens = Math.floor(fileBudget * 0.5); // 单文件不超过预算的50%
|
||||||
|
|
||||||
|
return fileContents.map((f, i) => {
|
||||||
|
const lang = f.language || '';
|
||||||
|
const rawTokens = estimateTokens(f.content);
|
||||||
|
const total = fileContents.length;
|
||||||
|
|
||||||
|
// 文件足够小,直接全量(不剥离注释,保持完整性)
|
||||||
|
if (rawTokens <= 500 && usedTokens + rawTokens <= fileBudget) {
|
||||||
|
usedTokens += rawTokens;
|
||||||
|
return `📄 文件 ${i + 1}/${total}:\n\n\`\`\`${lang}\n${f.content}\n\`\`\``;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 大文件:剥离注释 + token 预算截断
|
||||||
|
const remaining = fileBudget - usedTokens;
|
||||||
|
if (remaining <= 0) {
|
||||||
|
return `⚠️ 文件 ${i + 1}/${total} 因 token 预算耗尽被跳过 (${f.content.length} 字符)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = stripComments(f.content, lang);
|
||||||
|
let truncated = false;
|
||||||
|
|
||||||
|
// 如果剥离注释后仍然超预算,截断到剩余预算
|
||||||
|
const strippedTokens = estimateTokens(text);
|
||||||
|
if (strippedTokens > maxSingleFileTokens) {
|
||||||
|
// 按比例截取字符:预算 / 估算tokens * 长度
|
||||||
|
const ratio = Math.min(1, maxSingleFileTokens / strippedTokens);
|
||||||
|
const cutAt = Math.floor(text.length * ratio);
|
||||||
|
text = text.slice(0, cutAt);
|
||||||
|
truncated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalTokens = estimateTokens(text);
|
||||||
|
if (usedTokens + finalTokens > fileBudget) {
|
||||||
|
const ratio = Math.min(1, remaining / finalTokens);
|
||||||
|
const cutAt = Math.floor(text.length * ratio);
|
||||||
|
text = text.slice(0, cutAt);
|
||||||
|
truncated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
usedTokens += estimateTokens(text);
|
||||||
|
const label = strippedTokens !== rawTokens ? ' (已剥离注释)' : '';
|
||||||
|
const truncLabel = truncated ? ' [已截断]' : '';
|
||||||
|
return `📄 文件 ${i + 1}/${total}${label}${truncLabel}:\n\n\`\`\`${lang}\n${text}\n\`\`\``;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function buildApiMessages(messages: ChatMessage[]): Array<{ role: string; content: string; images?: string[] }> {
|
function buildApiMessages(messages: ChatMessage[]): Array<{ role: string; content: string; images?: string[] }> {
|
||||||
return messages.map(m => {
|
return messages.map(m => {
|
||||||
let content = m.content || '';
|
let content = m.content || '';
|
||||||
if (m._fileContents && m._fileContents.length > 0) {
|
if (m._fileContents && m._fileContents.length > 0) {
|
||||||
const fileParts = m._fileContents.map(f => {
|
const fileParts = buildFileContentParts(m._fileContents);
|
||||||
const lang = f.language || '';
|
|
||||||
return `📄 以下是一个文件内容:\n\n\`\`\`${lang}\n${f.content}\n\`\`\``;
|
|
||||||
});
|
|
||||||
if (content) {
|
if (content) {
|
||||||
content += '\n\n---\n' + fileParts.join('\n\n---\n');
|
content += '\n\n---\n' + fileParts.join('\n\n---\n');
|
||||||
} else {
|
} else {
|
||||||
@@ -1015,7 +1184,7 @@ async function sendMessageWithAgentLoop(text: string, currentSession: ChatSessio
|
|||||||
.map(m => {
|
.map(m => {
|
||||||
let content = m.content || '';
|
let content = m.content || '';
|
||||||
if (m._fileContents && m._fileContents.length > 0) {
|
if (m._fileContents && m._fileContents.length > 0) {
|
||||||
const fileParts = m._fileContents.map(f => `📄 文件:\n\`\`\`${f.language}\n${f.content}\n\`\`\``);
|
const fileParts = buildFileContentParts(m._fileContents);
|
||||||
content = content ? content + '\n\n---\n' + fileParts.join('\n\n---\n') : fileParts.join('\n\n---\n');
|
content = content ? content + '\n\n---\n' + fileParts.join('\n\n---\n') : fileParts.join('\n\n---\n');
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<span class="logo">🦙</span>
|
<span class="logo">🦙</span>
|
||||||
<span class="app-title">Metona Ollama</span>
|
<span class="app-title">Metona Ollama</span>
|
||||||
<span class="app-version">v0.9.0</span>
|
<span class="app-version">v0.9.1</span>
|
||||||
<button class="icon-btn help-btn" id="btnHelp" title="使用帮助">
|
<button class="icon-btn help-btn" id="btnHelp" title="使用帮助">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
|
<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
|
||||||
@@ -393,7 +393,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="help-section"><h4>🚀 快速开始</h4><ol><li>确保 Ollama 已启动(默认 <code>http://127.0.0.1:11434</code>,可在设置中修改)</li><li>顶部模型栏选择一个模型,右侧会显示模型能力徽章(🧠 Think / 👁️ Vision / 🔧 Tools)</li><li>输入消息,按 <kbd>Enter</kbd> 发送,<kbd>Shift+Enter</kbd> 换行</li></ol></div>
|
<div class="help-section"><h4>🚀 快速开始</h4><ol><li>确保 Ollama 已启动(默认 <code>http://127.0.0.1:11434</code>,可在设置中修改)</li><li>顶部模型栏选择一个模型,右侧会显示模型能力徽章(🧠 Think / 👁️ Vision / 🔧 Tools)</li><li>输入消息,按 <kbd>Enter</kbd> 发送,<kbd>Shift+Enter</kbd> 换行</li></ol></div>
|
||||||
<div class="help-section"><h4>💬 聊天功能</h4><ul><li><strong>流式回复</strong> — 实时打字效果,随时点 ■ 停止</li><li><strong>Think 推理</strong> — 下方 Think 按钮切换,让模型展示深度思考过程(需模型支持,如 Qwen3)</li><li><strong>上下文长度自动检测</strong> — 切换模型时自动从模型元数据获取实际支持的上下文长度(如 Qwen3.6 27B = 262,144 tokens),无需手动配置</li><li><strong>多模态</strong> — 上传图片,模型需支持 Vision 能力</li><li><strong>文件分析</strong> — 支持 50+ 种文本/代码格式,单文件 ≤500KB,内容以代码块发送给模型</li><li><strong>AI 回复顶部</strong> — 每条 AI 回复上方展示 📋 系统提示词折叠卡片,可点击查看实际发送给模型的完整上下文</li></ul></div>
|
<div class="help-section"><h4>💬 聊天功能</h4><ul><li><strong>流式回复</strong> — 实时打字效果,随时点 ■ 停止</li><li><strong>Think 推理</strong> — 下方 Think 按钮切换,让模型展示深度思考过程(需模型支持,如 Qwen3)</li><li><strong>上下文长度自动检测</strong> — 切换模型时自动从模型元数据获取实际支持的上下文长度(如 Qwen3.6 27B = 262,144 tokens),无需手动配置</li><li><strong>多模态</strong> — 上传图片,模型需支持 Vision 能力。图片自动压缩至合适分辨率,节省上下文</li><li><strong>文件分析</strong> — 支持 50+ 种文本/代码格式,单文件 ≤500KB,自动剥离注释并按上下文预算智能截断</li><li><strong>AI 回复顶部</strong> — 每条 AI 回复上方展示 📋 系统提示词折叠卡片,可点击查看实际发送给模型的完整上下文</li></ul></div>
|
||||||
<div class="help-section"><h4>🔧 Tool Calling(AI 自主操作)</h4><ul><li>设置中开启后,AI 可以在对话中<strong>自主调用本地工具</strong>,像一个本地 Agent</li><li><strong>38 个工具</strong>,分为 8 类:<ul><li><strong>文件系统</strong>(16 个):read_file / write_file / list_directory / search_files / create_directory / delete_file / move_file / copy_file / append_file / edit_file / get_file_info / tree / download_file / diff_files / replace_in_files / read_multiple_files</li><li><strong>命令执行</strong>(1 个):run_command(实时流式输出,支持自动/需确认/禁用三种模式)</li><li><strong>联网搜索</strong>(2 个):web_search(联网搜索,Bing + 百度 + Google 三引擎)/ web_fetch(网页抓取)</li><li><strong>Git</strong>(1 个):git(status / log / diff / add / commit / push / pull / branch / checkout / stash / reset / merge / clone / init / remote / tag,共 16 个子操作)</li><li><strong>浏览器控制</strong>(8 个):browser_open / browser_screenshot / browser_evaluate / browser_extract / browser_click / browser_type / browser_scroll / browser_close</li><li><strong>压缩</strong>(1 个):compress(创建/解压 zip/tar.gz)</li><li><strong>记忆 & 会话 & Skill</strong>(8 个):memory_search / memory_add / memory_replace / memory_remove / session_list / session_read / skill_list / skill_view</li><li><strong>子代理</strong>(1 个):spawn_task</li></ul></li><li>仅 <code>run_command</code>(命令执行)支持三模式切换:自动/需确认/禁用</li><li>危险命令(<code>rm -rf</code>、<code>mkfs</code>、反弹 shell 等)和系统路径(<code>/etc</code>、<code>~/.ssh</code> 等)被自动拦截</li><li>工具调用以<strong>可视化卡片</strong>展示状态(pending → running → success/error),在工作空间🔧工具页签中显示</li><li>默认最大 <strong>85 轮</strong>工具调用循环,可在设置中调整,也可随时中断</li><li>无超时限制,宁可等待也不中断,由用户控制生命周期</li><li>独立工具自动并行执行,有依赖关系的工具串行执行</li><li>⚠️ 需要模型支持 Tool Calling(推荐 Qwen3、Llama 3.1+、Mistral)</li></ul></div>
|
<div class="help-section"><h4>🔧 Tool Calling(AI 自主操作)</h4><ul><li>设置中开启后,AI 可以在对话中<strong>自主调用本地工具</strong>,像一个本地 Agent</li><li><strong>38 个工具</strong>,分为 8 类:<ul><li><strong>文件系统</strong>(16 个):read_file / write_file / list_directory / search_files / create_directory / delete_file / move_file / copy_file / append_file / edit_file / get_file_info / tree / download_file / diff_files / replace_in_files / read_multiple_files</li><li><strong>命令执行</strong>(1 个):run_command(实时流式输出,支持自动/需确认/禁用三种模式)</li><li><strong>联网搜索</strong>(2 个):web_search(联网搜索,Bing + 百度 + Google 三引擎)/ web_fetch(网页抓取)</li><li><strong>Git</strong>(1 个):git(status / log / diff / add / commit / push / pull / branch / checkout / stash / reset / merge / clone / init / remote / tag,共 16 个子操作)</li><li><strong>浏览器控制</strong>(8 个):browser_open / browser_screenshot / browser_evaluate / browser_extract / browser_click / browser_type / browser_scroll / browser_close</li><li><strong>压缩</strong>(1 个):compress(创建/解压 zip/tar.gz)</li><li><strong>记忆 & 会话 & Skill</strong>(8 个):memory_search / memory_add / memory_replace / memory_remove / session_list / session_read / skill_list / skill_view</li><li><strong>子代理</strong>(1 个):spawn_task</li></ul></li><li>仅 <code>run_command</code>(命令执行)支持三模式切换:自动/需确认/禁用</li><li>危险命令(<code>rm -rf</code>、<code>mkfs</code>、反弹 shell 等)和系统路径(<code>/etc</code>、<code>~/.ssh</code> 等)被自动拦截</li><li>工具调用以<strong>可视化卡片</strong>展示状态(pending → running → success/error),在工作空间🔧工具页签中显示</li><li>默认最大 <strong>85 轮</strong>工具调用循环,可在设置中调整,也可随时中断</li><li>无超时限制,宁可等待也不中断,由用户控制生命周期</li><li>独立工具自动并行执行,有依赖关系的工具串行执行</li><li>⚠️ 需要模型支持 Tool Calling(推荐 Qwen3、Llama 3.1+、Mistral)</li></ul></div>
|
||||||
<div class="help-section"><h4>🧠 Agent 记忆系统</h4><ul><li>设置中开启后,AI 从对话中<strong>自动提取关键信息</strong>(事实/偏好/规则),跨会话持续积累</li><li>对话满 <strong>6 条消息</strong>后自动触发记忆提取</li><li>新对话时自动检索相关记忆注入上下文,让 AI "记住"你</li><li>点击顶部 🧠 按钮打开记忆面板:搜索、筛选、编辑、删除</li><li><strong>记忆容量上限 500 条</strong>,超限时自动清理低价值条目(规则类型受保护)</li><li><strong>向量语义搜索</strong>:在设置中选择嵌入模型后,支持语义级别记忆检索(更精准)</li><li>未选择嵌入模型时,使用关键词匹配检索记忆</li><li>AI 可通过 <code>memory_search</code> / <code>memory_add</code> / <code>memory_replace</code> / <code>memory_remove</code> 工具主动管理记忆</li><li>AI 可通过 <code>session_list</code> / <code>session_read</code> 查阅历史会话</li><li>记忆写入前自动安全扫描(prompt injection / 敏感信息 / 不可见字符检测)</li><li>记忆存储在本地 SQLite,不会上传到任何服务器</li></ul></div>
|
<div class="help-section"><h4>🧠 Agent 记忆系统</h4><ul><li>设置中开启后,AI 从对话中<strong>自动提取关键信息</strong>(事实/偏好/规则),跨会话持续积累</li><li>对话满 <strong>6 条消息</strong>后自动触发记忆提取</li><li>新对话时自动检索相关记忆注入上下文,让 AI "记住"你</li><li>点击顶部 🧠 按钮打开记忆面板:搜索、筛选、编辑、删除</li><li><strong>记忆容量上限 500 条</strong>,超限时自动清理低价值条目(规则类型受保护)</li><li><strong>向量语义搜索</strong>:在设置中选择嵌入模型后,支持语义级别记忆检索(更精准)</li><li>未选择嵌入模型时,使用关键词匹配检索记忆</li><li>AI 可通过 <code>memory_search</code> / <code>memory_add</code> / <code>memory_replace</code> / <code>memory_remove</code> 工具主动管理记忆</li><li>AI 可通过 <code>session_list</code> / <code>session_read</code> 查阅历史会话</li><li>记忆写入前自动安全扫描(prompt injection / 敏感信息 / 不可见字符检测)</li><li>记忆存储在本地 SQLite,不会上传到任何服务器</li></ul></div>
|
||||||
<div class="help-section"><h4>🔌 MCP(Model Context Protocol)</h4><ul><li>支持连接外部 MCP Server,动态扩展工具能力</li><li>设置面板可添加/启用/禁用/删除 MCP 服务器</li><li>MCP 工具以 <code>mcp_{server}_{tool}</code> 前缀注册,与内置工具统一调度</li><li>启动时自动连接已启用的 MCP 服务器</li></ul></div>
|
<div class="help-section"><h4>🔌 MCP(Model Context Protocol)</h4><ul><li>支持连接外部 MCP Server,动态扩展工具能力</li><li>设置面板可添加/启用/禁用/删除 MCP 服务器</li><li>MCP 工具以 <code>mcp_{server}_{tool}</code> 前缀注册,与内置工具统一调度</li><li>启动时自动连接已启用的 MCP 服务器</li></ul></div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/**
|
/**
|
||||||
* SubAgent - 子代理委派系统 (v0.9.0 增强版)
|
* SubAgent - 子代理委派系统 (v0.9.1 增强版)
|
||||||
* 子代理拥有受限工具集(只读),可独立完成调研/搜索/分析类任务
|
* 子代理拥有受限工具集(只读),可独立完成调研/搜索/分析类任务
|
||||||
* 主 Agent 通过 spawn_task 工具并行委派多个子代理
|
* 主 Agent 通过 spawn_task 工具并行委派多个子代理
|
||||||
*/
|
*/
|
||||||
@@ -62,7 +62,7 @@ export async function executeSubAgent(
|
|||||||
const systemPrompt = `你是一个子任务执行 Agent,拥有只读工具权限。请高效完成指定任务,给出结果报告。
|
const systemPrompt = `你是一个子任务执行 Agent,拥有只读工具权限。请高效完成指定任务,给出结果报告。
|
||||||
${context ? `\n附加上下文:\n${context}` : ''}`;
|
${context ? `\n附加上下文:\n${context}` : ''}`;
|
||||||
|
|
||||||
const messages: Array<{ role: string; content: string; tool_calls?: ToolCall[] }> = [
|
const messages: Array<{ role: string; content: string; tool_calls?: ToolCall[]; tool_name?: string }> = [
|
||||||
{ role: 'system', content: systemPrompt },
|
{ role: 'system', content: systemPrompt },
|
||||||
{ role: 'user', content: task }
|
{ role: 'user', content: task }
|
||||||
];
|
];
|
||||||
@@ -147,6 +147,6 @@ function formatResult(name: string, r: ToolResult): string {
|
|||||||
case 'web_search': return JSON.stringify({ success: true, query: r.query, total: r.total, results: (r.results as any[])?.slice(0, 3) });
|
case 'web_search': return JSON.stringify({ success: true, query: r.query, total: r.total, results: (r.results as any[])?.slice(0, 3) });
|
||||||
case 'web_fetch': return JSON.stringify({ success: true, url: r.url, content: String(r.content || '').slice(0, 3000) });
|
case 'web_fetch': return JSON.stringify({ success: true, url: r.url, content: String(r.content || '').slice(0, 3000) });
|
||||||
case 'read_file': return JSON.stringify({ success: true, path: r.path, content: String(r.content || '').slice(0, 3000) });
|
case 'read_file': return JSON.stringify({ success: true, path: r.path, content: String(r.content || '').slice(0, 3000) });
|
||||||
default: return JSON.stringify({ success: true, ...r });
|
default: return JSON.stringify(r);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user