@myassis/gateway 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/index.js +69 -59
- package/dist/config/index.js +2 -2
- package/dist/index.js +85 -11
- package/dist/middleware/auth.js +9 -25
- package/dist/middleware/errorHandler.js +1 -1
- package/dist/routes/agent.js +30 -19
- package/dist/routes/auth.js +35 -26
- package/dist/routes/chat.js +1 -1
- package/dist/routes/models.js +11 -14
- package/dist/routes/service.js +3 -6
- package/dist/routes/settings.js +11 -9
- package/dist/routes/skillHub.js +10 -6
- package/dist/routes/skills.js +13 -10
- package/dist/routes/tasks.js +6 -3
- package/dist/routes/upload.js +5 -2
- package/dist/routes/version.js +4 -4
- package/dist/services/LocalTaskService.js +1 -1
- package/dist/services/NotificationService.js +1 -1
- package/dist/services/ServiceManager.js +12 -12
- package/dist/services/TaskSchedulerService.js +18 -9
- package/dist/services/TaskService.js +23 -20
- package/dist/services/WebSocketService.js +16 -6
- package/dist/services/agent/Agent.js +7 -7
- package/dist/services/agent/AgentManager.js +25 -17
- package/dist/services/agent/AgentStore.js +1 -1
- package/dist/services/dataService.js +60 -70
- package/dist/services/index.js +1 -1
- package/dist/services/llm/LLMClient.js +11 -9
- package/dist/services/memory/MemoryManager.js +177 -14
- package/dist/services/session/MigrationManager.js +1 -1
- package/dist/services/session/Session.js +70 -46
- package/dist/services/session/SessionManager.js +12 -5
- package/dist/services/session/SessionStore.js +1 -27
- package/dist/services/session/index.js +1 -1
- package/dist/services/systemPrompt.js +8 -4
- package/dist/services/task/PushTokenStore.js +1 -19
- package/dist/services/task/TaskStore.js +1 -20
- package/dist/services/tools/edit.js +122 -148
- package/dist/services/tools/exec.js +1 -1
- package/dist/services/tools/fetch.js +1 -1
- package/dist/services/tools/file.js +4 -9
- package/dist/services/tools/index.js +4 -3
- package/dist/services/tools/model.js +8 -6
- package/dist/services/tools/sessionsSpawn.js +54 -0
- package/dist/services/tools/skill.js +13 -12
- package/dist/services/tools/task.js +2 -4
- package/dist/stores/authStore.js +52 -66
- package/dist/stores/persistStore.js +37 -3
- package/package.json +8 -4
- package/scripts/postbuild.js +39 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { authStore } from '@/stores';
|
|
1
2
|
import os from 'os';
|
|
2
3
|
import { settingsService, skillsService } from './dataService';
|
|
3
|
-
import { getLogger } from '@
|
|
4
|
+
import { getLogger } from '@myassis/shared';
|
|
4
5
|
const logger = getLogger('SystemPrompt');
|
|
5
6
|
// 预设的对话风格模板
|
|
6
7
|
export const CHAT_STYLE_TEMPLATES = [
|
|
@@ -103,8 +104,9 @@ function getInstalledSkillsPrompt(skills) {
|
|
|
103
104
|
*/
|
|
104
105
|
export async function getSystemPromptAsync(agent, customPrompt) {
|
|
105
106
|
const osInfo = getOSInfo();
|
|
106
|
-
const
|
|
107
|
-
const
|
|
107
|
+
const token = authStore.get(agent.userId).accessToken;
|
|
108
|
+
const settings = (await settingsService.get(token)).data;
|
|
109
|
+
const skills = (await skillsService.list(token)).data;
|
|
108
110
|
const skillPrompts = getInstalledSkillsPrompt(skills);
|
|
109
111
|
const chineseLocalName = LANGUAGES.filter(x => x.code === settings.language)[0]?.chineseName ?? '简体中文';
|
|
110
112
|
const date = new Date();
|
|
@@ -112,6 +114,7 @@ export async function getSystemPromptAsync(agent, customPrompt) {
|
|
|
112
114
|
【当前运行环境】:${osInfo}。
|
|
113
115
|
【当前时间】:${date.toLocaleString(settings.language)},weekday:${date.getDay()}。
|
|
114
116
|
【语言】你的回复语言严格遵循用户设备系统语言:${chineseLocalName},无论用户使用任何语言提问,你都只使用【${chineseLocalName}】进行完整回复,不要混用其他语言。
|
|
117
|
+
【尽力原则】充分分析用户需求,尽力满足,不要偷懒,不要得过且过,要诚实,不能胡编乱造。
|
|
115
118
|
【工具调用】
|
|
116
119
|
1.逐步思考,分步骤调用工具,工具调用前尽量返回content,明确当前工具调用的目的。
|
|
117
120
|
2.工具调用的参数全部采用JSON语法,调用前先验证JSON语法正确后,再回复,切记未验证JSON语法就直接回复。
|
|
@@ -128,7 +131,7 @@ export async function getSystemPromptAsync(agent, customPrompt) {
|
|
|
128
131
|
【解决问题思路】
|
|
129
132
|
1.首先分析用户需求,根据需求罗列出实施计划。如果对需求不够明确,需要把计划罗列完后,让用户确认后再执行。
|
|
130
133
|
2.按照计划分步执行
|
|
131
|
-
3
|
|
134
|
+
3.所有步骤完成后,验证结果,比如编写程序,则需要执行类型检查
|
|
132
135
|
4.验证完全通过,总结完成情况,告诉用户
|
|
133
136
|
【代码规范】
|
|
134
137
|
## 1 面向对象代码设计
|
|
@@ -141,6 +144,7 @@ export async function getSystemPromptAsync(agent, customPrompt) {
|
|
|
141
144
|
## 3 修改已经存在的代码文件
|
|
142
145
|
1)修改Bug,找到Bug后精确替换,不要调整架构
|
|
143
146
|
2)修改业务功能,删除旧的业务功能,重新添加新的,不要在旧的上面做修改,直接删除旧功能,然后添加新功能
|
|
147
|
+
【代码提交】务必类型检查通过
|
|
144
148
|
${skillPrompts}`;
|
|
145
149
|
// 如果有自定义提示词,追加
|
|
146
150
|
if (customPrompt) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* PushTokenStore - 用户设备推送 Token 存储
|
|
3
3
|
* 使用 SQLite 管理用户的多设备推送 Token
|
|
4
4
|
*/
|
|
5
|
-
import { getLogger } from '@
|
|
5
|
+
import { getLogger } from '@myassis/shared';
|
|
6
6
|
const logger = getLogger('PushTokenStore');
|
|
7
7
|
import { sessionStore } from '../session/SessionStore';
|
|
8
8
|
export class PushTokenStore {
|
|
@@ -25,26 +25,8 @@ export class PushTokenStore {
|
|
|
25
25
|
UNIQUE(user_id, token)
|
|
26
26
|
)
|
|
27
27
|
`);
|
|
28
|
-
// 确保 device_id 和 device_name 列存在(向后兼容)
|
|
29
|
-
this.ensureColumns();
|
|
30
28
|
logger.info('Tables initialized');
|
|
31
29
|
}
|
|
32
|
-
ensureColumns() {
|
|
33
|
-
const tableInfo = this.db.prepare("PRAGMA table_info(push_tokens)").all();
|
|
34
|
-
const columnNames = tableInfo.map((col) => col.name);
|
|
35
|
-
if (!columnNames.includes('device_id')) {
|
|
36
|
-
this.db.exec("ALTER TABLE push_tokens ADD COLUMN device_id TEXT");
|
|
37
|
-
logger.info('Added column: device_id');
|
|
38
|
-
}
|
|
39
|
-
if (!columnNames.includes('device_name')) {
|
|
40
|
-
this.db.exec("ALTER TABLE push_tokens ADD COLUMN device_name TEXT");
|
|
41
|
-
logger.info('Added column: device_name');
|
|
42
|
-
}
|
|
43
|
-
if (!columnNames.includes('provider')) {
|
|
44
|
-
this.db.exec("ALTER TABLE push_tokens ADD COLUMN provider TEXT DEFAULT 'expo'");
|
|
45
|
-
logger.info('Added column: provider');
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
30
|
/**
|
|
49
31
|
* 添加或更新 Push Token
|
|
50
32
|
*/
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* TaskStore - Gateway 任务数据访问层
|
|
3
3
|
* 操作 SQLite 数据库,与 AgentStore/SessionStore 共享同一个数据库实例
|
|
4
4
|
*/
|
|
5
|
-
import { getLogger } from '@
|
|
5
|
+
import { getLogger } from '@myassis/shared';
|
|
6
6
|
import { sessionStore } from '../session/SessionStore';
|
|
7
7
|
const logger = getLogger('TaskStore');
|
|
8
8
|
function rowToTaskData(row) {
|
|
@@ -59,27 +59,8 @@ export class TaskStore {
|
|
|
59
59
|
updated_at INTEGER NOT NULL
|
|
60
60
|
)
|
|
61
61
|
`);
|
|
62
|
-
// 确保必要列存在(向后兼容)
|
|
63
|
-
this.ensureColumns();
|
|
64
62
|
logger.info('Tables initialized');
|
|
65
63
|
}
|
|
66
|
-
ensureColumns() {
|
|
67
|
-
const tableInfo = this.db.prepare("PRAGMA table_info(tasks)").all();
|
|
68
|
-
const columnNames = tableInfo.map((col) => col.name);
|
|
69
|
-
const requiredColumns = [
|
|
70
|
-
{ name: 'platform_apply', sql: 'ALTER TABLE tasks ADD COLUMN platform_apply TEXT' },
|
|
71
|
-
{ name: 'push_token', sql: 'ALTER TABLE tasks ADD COLUMN push_token TEXT' },
|
|
72
|
-
{ name: 'recurrence_rule', sql: 'ALTER TABLE tasks ADD COLUMN recurrence_rule TEXT' },
|
|
73
|
-
{ name: 'interval_value', sql: 'ALTER TABLE tasks ADD COLUMN interval_value INTEGER' },
|
|
74
|
-
{ name: 'interval_unit', sql: 'ALTER TABLE tasks ADD COLUMN interval_unit TEXT' },
|
|
75
|
-
];
|
|
76
|
-
for (const col of requiredColumns) {
|
|
77
|
-
if (!columnNames.includes(col.name)) {
|
|
78
|
-
this.db.exec(col.sql);
|
|
79
|
-
logger.info(`Added column: ${col.name}`);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
64
|
// ========== Task Operations ==========
|
|
84
65
|
insert(task) {
|
|
85
66
|
this.db.prepare(`
|
|
@@ -1,71 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* 文件编辑工具(内容匹配模式)
|
|
3
3
|
*
|
|
4
4
|
* 设计原则:
|
|
5
|
-
* - LLM
|
|
6
|
-
* -
|
|
7
|
-
* -
|
|
8
|
-
* -
|
|
5
|
+
* - LLM 提供 oldContent(精确字符串)+ newContent,工具在文件中搜索 oldContent 并替换
|
|
6
|
+
* - 不依赖行号,避免 LLM 数错行
|
|
7
|
+
* - oldContent 为必填项,缺失则拒绝执行
|
|
8
|
+
* - 若 oldContent 在文件中出现多次,默认只替换第一个匹配;设置 replaceAll: true 可替换全部
|
|
9
|
+
* - 若未设置 replaceAll 且出现多次匹配,返回所有匹配位置(含上下文)供 LLM 修正
|
|
10
|
+
* - 可选提供 oldRange 作为辅助校验:若提供,工具验证该范围内内容是否与 oldContent 一致
|
|
9
11
|
*
|
|
10
12
|
* 安全机制:
|
|
11
|
-
* - 路径白名单校验,禁止路径穿越
|
|
12
13
|
* - 文件不存在时拒绝操作(不允许静默创建)
|
|
13
14
|
* - 目录不存在时拒绝操作(不允许递归创建目录)
|
|
14
15
|
* - 文件大小限制(默认 10MB)
|
|
15
16
|
* - 编辑操作数量限制(默认 20 个)
|
|
16
|
-
* - 编辑范围重叠检测
|
|
17
17
|
* - 敏感内容脱敏(限制错误返回中的内容长度)
|
|
18
18
|
* - 异步 I/O,不阻塞事件循环
|
|
19
19
|
*
|
|
20
20
|
* LLM 用法:
|
|
21
|
-
* 1. 先用 read
|
|
22
|
-
* 2. 指定
|
|
23
|
-
* 3.
|
|
24
|
-
* 4.
|
|
21
|
+
* 1. 先用 read 读取文件内容
|
|
22
|
+
* 2. 指定 oldContent(精确字符串,必填)+ newContent(替换后的内容)
|
|
23
|
+
* 3. 可选:replaceAll: true 替换所有匹配;oldRange 辅助校验特定位置
|
|
24
|
+
* 4. 匹配失败时工具返回实际内容片段,LLM 据此修正后重试
|
|
25
25
|
*/
|
|
26
26
|
import fs from 'fs/promises';
|
|
27
27
|
import path from 'path';
|
|
28
|
-
import
|
|
29
|
-
import { getLogger } from '@pocketclaw/shared';
|
|
28
|
+
import { getLogger } from '@myassis/shared';
|
|
30
29
|
const logger = getLogger('edit');
|
|
31
|
-
// ----------
|
|
32
|
-
/** 允许访问的根目录白名单 */
|
|
33
|
-
const ALLOWED_ROOTS = [
|
|
34
|
-
os.homedir(),
|
|
35
|
-
os.tmpdir(),
|
|
36
|
-
process.env.WORKSPACE_DIR || '',
|
|
37
|
-
process.env.GATEWAY_DIR || '',
|
|
38
|
-
].filter(Boolean).map(p => path.resolve(p));
|
|
30
|
+
// ---------- 常量 ----------
|
|
39
31
|
/** 最大文件大小(10MB) */
|
|
40
32
|
const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
41
33
|
/** 单次最大编辑操作数 */
|
|
42
34
|
const MAX_EDITS = 20;
|
|
43
35
|
/** 错误信息中返回的最大内容长度 */
|
|
44
36
|
const MAX_ERROR_CONTENT_LENGTH = 500;
|
|
45
|
-
|
|
37
|
+
/** 匹配位置上下文展示行数 */
|
|
38
|
+
const CONTEXT_LINES = 3;
|
|
39
|
+
// ---------- 辅助函数 ----------
|
|
46
40
|
/**
|
|
47
|
-
*
|
|
48
|
-
*/
|
|
49
|
-
function isPathAllowed(filePath) {
|
|
50
|
-
// 标准化路径,解析 .. 等相对路径组件
|
|
51
|
-
const resolved = path.resolve(filePath);
|
|
52
|
-
const normalized = path.normalize(filePath);
|
|
53
|
-
// 检测路径穿越:标准化后仍包含 .. 表示非法穿越
|
|
54
|
-
if (normalized.includes('..')) {
|
|
55
|
-
return { allowed: false, reason: `路径包含穿越组件(..): ${filePath}` };
|
|
56
|
-
}
|
|
57
|
-
// 校验路径是否在允许的根目录下
|
|
58
|
-
const isUnderAllowedRoot = ALLOWED_ROOTS.some(root => resolved.startsWith(root + path.sep) || resolved === root);
|
|
59
|
-
// if (!isUnderAllowedRoot) {
|
|
60
|
-
// return {
|
|
61
|
-
// allowed: false,
|
|
62
|
-
// reason: `路径不在允许范围内: ${resolved}`,
|
|
63
|
-
// };
|
|
64
|
-
// }
|
|
65
|
-
return { allowed: true };
|
|
66
|
-
}
|
|
67
|
-
/**
|
|
68
|
-
* 检测敏感文件路径
|
|
41
|
+
* 检测敏感文件路径(脱敏用,不阻止操作)
|
|
69
42
|
*/
|
|
70
43
|
function isSensitivePath(filePath) {
|
|
71
44
|
const lower = filePath.toLowerCase();
|
|
@@ -85,47 +58,54 @@ function sanitizeContent(content, filePath) {
|
|
|
85
58
|
}
|
|
86
59
|
return content;
|
|
87
60
|
}
|
|
88
|
-
// ---------- 核心逻辑 ----------
|
|
89
61
|
/**
|
|
90
|
-
*
|
|
62
|
+
* 在文本中查找所有 oldContent 的起始位置
|
|
91
63
|
*/
|
|
92
|
-
function
|
|
93
|
-
|
|
64
|
+
function findAllPositions(content, search) {
|
|
65
|
+
const positions = [];
|
|
66
|
+
let pos = 0;
|
|
67
|
+
if (search.length === 0)
|
|
68
|
+
return positions;
|
|
69
|
+
while ((pos = content.indexOf(search, pos)) !== -1) {
|
|
70
|
+
positions.push(pos);
|
|
71
|
+
pos += Math.max(search.length, 1);
|
|
72
|
+
}
|
|
73
|
+
return positions;
|
|
94
74
|
}
|
|
95
75
|
/**
|
|
96
|
-
*
|
|
76
|
+
* 将字符偏移量转换为行号(1-indexed)
|
|
97
77
|
*/
|
|
98
|
-
function
|
|
99
|
-
|
|
100
|
-
const after = lines.slice(endLine);
|
|
101
|
-
const newLines = newContent ? newContent.split('\n') : [];
|
|
102
|
-
return [...before, ...newLines, ...after];
|
|
78
|
+
function offsetToLine(content, offset) {
|
|
79
|
+
return content.slice(0, offset).split('\n').length;
|
|
103
80
|
}
|
|
104
81
|
/**
|
|
105
|
-
*
|
|
82
|
+
* 生成匹配位置的可读描述(含上下文字符)
|
|
106
83
|
*/
|
|
107
|
-
function
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
}
|
|
84
|
+
function describeMatches(content, positions, search, filePath) {
|
|
85
|
+
const lines = content.split('\n');
|
|
86
|
+
const parts = [];
|
|
87
|
+
for (const pos of positions) {
|
|
88
|
+
const lineNo = offsetToLine(content, pos);
|
|
89
|
+
const start = Math.max(0, lineNo - CONTEXT_LINES - 1);
|
|
90
|
+
const end = Math.min(lines.length, lineNo + CONTEXT_LINES);
|
|
91
|
+
const context = lines.slice(start, end).map((l, i) => `${start + i + 1} | ${l}`).join('\n');
|
|
92
|
+
parts.push(`匹配位置(行号约 ${lineNo}):\n${context}`);
|
|
119
93
|
}
|
|
120
|
-
return {
|
|
94
|
+
return `oldContent 在文件中出现 ${positions.length} 次,请明确要替换的目标。` +
|
|
95
|
+
`可提供 oldRange(起始行号)辅助定位,或设置 replaceAll: true 替换全部。\n\n` +
|
|
96
|
+
parts.join('\n\n');
|
|
121
97
|
}
|
|
122
98
|
// ---------- 工具定义 ----------
|
|
123
99
|
const editTool = {
|
|
124
100
|
name: 'edit',
|
|
125
|
-
description: '
|
|
126
|
-
'
|
|
127
|
-
'
|
|
128
|
-
'
|
|
101
|
+
description: '⚠️ oldContent 为必填项,未提供将拒绝执行。\n\n' +
|
|
102
|
+
'文件编辑工具(内容匹配模式)。\n' +
|
|
103
|
+
'LLM 先用 read 读取文件内容,然后指定 oldContent(精确字符串,必填)和 newContent(替换后的内容)。\n' +
|
|
104
|
+
'工具在文件中搜索 oldContent 并替换,不依赖行号。\n' +
|
|
105
|
+
'若 oldContent 出现多次,默认只替换第一个匹配;设置 replaceAll: true 可替换全部。\n' +
|
|
106
|
+
'若未设置 replaceAll 且出现多次匹配,工具返回所有匹配位置供 LLM 选择。\n' +
|
|
107
|
+
'可选提供 oldRange(起始行号)辅助校验特定位置。\n' +
|
|
108
|
+
'支持一次调用多个 edit。',
|
|
129
109
|
parameters: {
|
|
130
110
|
type: 'object',
|
|
131
111
|
properties: {
|
|
@@ -135,29 +115,32 @@ const editTool = {
|
|
|
135
115
|
description: '编辑操作列表',
|
|
136
116
|
items: {
|
|
137
117
|
type: 'object',
|
|
138
|
-
description: '
|
|
118
|
+
description: '单个内容匹配编辑操作',
|
|
139
119
|
properties: {
|
|
140
|
-
|
|
141
|
-
type: '
|
|
142
|
-
description: '
|
|
143
|
-
properties: {
|
|
144
|
-
startLine: { type: 'number', description: '起始行号(1-indexed)' },
|
|
145
|
-
endLine: { type: 'number', description: '结束行号(1-indexed,必须 >= startLine)' },
|
|
146
|
-
},
|
|
147
|
-
required: ['startLine', 'endLine'],
|
|
120
|
+
oldContent: {
|
|
121
|
+
type: 'string',
|
|
122
|
+
description: '【必填】要被替换的原有内容(精确字符串匹配,区分大小写)',
|
|
148
123
|
},
|
|
149
124
|
newContent: {
|
|
150
125
|
type: 'string',
|
|
151
|
-
description: '
|
|
126
|
+
description: '【必填】替换后的新内容',
|
|
152
127
|
},
|
|
153
|
-
|
|
154
|
-
type: '
|
|
155
|
-
description: '
|
|
128
|
+
replaceAll: {
|
|
129
|
+
type: 'boolean',
|
|
130
|
+
description: '是否替换所有匹配(默认 false,只替换第一个)。当 oldContent 在文件中出现多次时使用',
|
|
131
|
+
},
|
|
132
|
+
oldRange: {
|
|
133
|
+
type: 'object',
|
|
134
|
+
description: '【可选】辅助校验:oldContent 所在的大致行范围,用于验证匹配位置',
|
|
135
|
+
properties: {
|
|
136
|
+
startLine: { type: 'number', description: '起始行号(1-indexed,可选)' },
|
|
137
|
+
endLine: { type: 'number', description: '结束行号(1-indexed,可选)' },
|
|
138
|
+
},
|
|
156
139
|
},
|
|
157
140
|
},
|
|
158
|
-
required: ['
|
|
141
|
+
required: ['oldContent', 'newContent'],
|
|
159
142
|
},
|
|
160
|
-
}
|
|
143
|
+
},
|
|
161
144
|
},
|
|
162
145
|
required: ['path', 'edits'],
|
|
163
146
|
},
|
|
@@ -170,18 +153,15 @@ const editTool = {
|
|
|
170
153
|
if (!Array.isArray(edits) || edits.length === 0)
|
|
171
154
|
return { success: false, errorMessage: 'edits 必须为非空数组' };
|
|
172
155
|
for (let i = 0; i < edits.length; i++) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
if (
|
|
177
|
-
return { success: false, errorMessage: `edits[${i}]:
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
const pathCheck = isPathAllowed(filePath);
|
|
183
|
-
if (!pathCheck.allowed) {
|
|
184
|
-
return { success: false, errorMessage: pathCheck.reason };
|
|
156
|
+
if (typeof edits[i].oldContent !== 'string') {
|
|
157
|
+
return { success: false, errorMessage: `edits[${i}]: oldContent 为必填项,且必须为字符串` };
|
|
158
|
+
}
|
|
159
|
+
if (typeof edits[i].newContent !== 'string') {
|
|
160
|
+
return { success: false, errorMessage: `edits[${i}]: newContent 为必填项,且必须为字符串` };
|
|
161
|
+
}
|
|
162
|
+
if (edits[i].oldContent.length === 0) {
|
|
163
|
+
return { success: false, errorMessage: `edits[${i}]: oldContent 不能为空字符串` };
|
|
164
|
+
}
|
|
185
165
|
}
|
|
186
166
|
// ---- 文件存在性 & 大小校验 ----
|
|
187
167
|
let original;
|
|
@@ -208,61 +188,58 @@ const editTool = {
|
|
|
208
188
|
errorMessage: `单次最多 ${MAX_EDITS} 个编辑操作,当前 ${edits.length} 个`,
|
|
209
189
|
};
|
|
210
190
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
191
|
+
// ---- 执行每个 edit ----
|
|
192
|
+
let current = original;
|
|
193
|
+
const results = [];
|
|
214
194
|
for (let i = 0; i < edits.length; i++) {
|
|
215
|
-
const { oldRange } = edits[i];
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
195
|
+
const { oldContent, newContent, replaceAll, oldRange } = edits[i];
|
|
196
|
+
// ---- 若提供了 oldRange,做辅助校验 ----
|
|
197
|
+
if (oldRange && oldRange.startLine >= 1 && oldRange.endLine >= oldRange.startLine) {
|
|
198
|
+
const lines = current.split('\n');
|
|
199
|
+
const rangeContent = lines.slice(oldRange.startLine - 1, oldRange.endLine).join('\n');
|
|
200
|
+
if (!rangeContent.includes(oldContent)) {
|
|
201
|
+
const safeContent = sanitizeContent(rangeContent, filePath);
|
|
202
|
+
return {
|
|
203
|
+
success: false,
|
|
204
|
+
errorMessage: `edits[${i}]: oldRange 指定的行范围内容中未找到 oldContent。` +
|
|
205
|
+
`该范围实际内容:\n\`\`\`\n${safeContent}\n\`\`\``,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
// ---- 查找所有匹配位置 ----
|
|
210
|
+
const positions = findAllPositions(current, oldContent);
|
|
211
|
+
if (positions.length === 0) {
|
|
212
|
+
const safeContent = sanitizeContent(current.slice(0, 2000), filePath);
|
|
222
213
|
return {
|
|
223
214
|
success: false,
|
|
224
|
-
errorMessage: `edits[${i}]:
|
|
215
|
+
errorMessage: `edits[${i}]: oldContent 在文件中未找到匹配。` +
|
|
216
|
+
`文件当前内容片段:\n\`\`\`\n${safeContent}\n\`\`\``,
|
|
225
217
|
};
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
return { success: false, errorMessage: overlapCheck.message };
|
|
231
|
-
}
|
|
232
|
-
// ---- 验证 oldContent(如果有) ----
|
|
233
|
-
for (let i = 0; i < edits.length; i++) {
|
|
234
|
-
const { oldRange, oldContent } = edits[i];
|
|
235
|
-
if (!oldContent)
|
|
236
|
-
continue;
|
|
237
|
-
const actualContent = getRangeContent(origLines, oldRange.startLine, oldRange.endLine);
|
|
238
|
-
if (actualContent !== oldContent) {
|
|
239
|
-
const safeContent = sanitizeContent(actualContent, filePath);
|
|
218
|
+
}
|
|
219
|
+
// ---- 多次匹配且未设置 replaceAll → 返回匹配位置 ----
|
|
220
|
+
if (positions.length > 1 && !replaceAll) {
|
|
221
|
+
const description = describeMatches(current, positions, oldContent, filePath);
|
|
240
222
|
return {
|
|
241
223
|
success: false,
|
|
242
|
-
errorMessage: `edits[${i}]:
|
|
243
|
-
`工具发现的内容:\n\`\`\`\n${safeContent}\n\`\`\``,
|
|
224
|
+
errorMessage: `edits[${i}]: oldContent 在文件中出现 ${positions.length} 次,请明确目标。\n${description}`,
|
|
244
225
|
};
|
|
245
226
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
for (const edit of sortedEdits) {
|
|
255
|
-
const { oldRange, newContent = '' } = edit;
|
|
256
|
-
const { startLine, endLine } = oldRange;
|
|
257
|
-
currentLines = applyRangeEdit(currentLines, startLine, endLine, newContent);
|
|
227
|
+
// ---- 执行替换 ----
|
|
228
|
+
if (replaceAll) {
|
|
229
|
+
current = current.split(oldContent).join(newContent);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
const pos = positions[0];
|
|
233
|
+
current = current.slice(0, pos) + newContent + current.slice(pos + oldContent.length);
|
|
234
|
+
}
|
|
258
235
|
results.push({
|
|
259
|
-
editIndex:
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
newLines: newContent ? newContent.split('\n').length : 0,
|
|
236
|
+
editIndex: i,
|
|
237
|
+
matches: positions.length,
|
|
238
|
+
replaced: replaceAll ? positions.length : 1,
|
|
263
239
|
});
|
|
264
240
|
}
|
|
265
|
-
|
|
241
|
+
// ---- 恢复行尾格式 ----
|
|
242
|
+
let modified = current;
|
|
266
243
|
// ---- 内容未变检测 ----
|
|
267
244
|
if (modified === original) {
|
|
268
245
|
return {
|
|
@@ -291,8 +268,7 @@ const editTool = {
|
|
|
291
268
|
editCount: edits.length,
|
|
292
269
|
originalSize: original.length,
|
|
293
270
|
modifiedSize: modified.length,
|
|
294
|
-
|
|
295
|
-
modifiedLines: currentLines.length,
|
|
271
|
+
results,
|
|
296
272
|
});
|
|
297
273
|
return {
|
|
298
274
|
success: true,
|
|
@@ -300,11 +276,9 @@ const editTool = {
|
|
|
300
276
|
path: filePath,
|
|
301
277
|
editCount: edits.length,
|
|
302
278
|
stats: {
|
|
303
|
-
originalLines: totalLines,
|
|
304
|
-
modifiedLines: currentLines.length,
|
|
305
|
-
linesChanged: currentLines.length - totalLines,
|
|
306
279
|
originalSize: original.length,
|
|
307
280
|
modifiedSize: modified.length,
|
|
281
|
+
details: results,
|
|
308
282
|
},
|
|
309
283
|
}),
|
|
310
284
|
};
|
|
@@ -2,7 +2,7 @@ import { exec } from 'child_process';
|
|
|
2
2
|
import { promisify } from 'util';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import os from 'os';
|
|
5
|
-
import { getLogger } from '@
|
|
5
|
+
import { getLogger } from '@myassis/shared';
|
|
6
6
|
const logger = getLogger('exec');
|
|
7
7
|
const execAsync = promisify(exec);
|
|
8
8
|
// 会话级工作目录状态:sessionId -> cwd
|
|
@@ -7,7 +7,7 @@ import path from 'path';
|
|
|
7
7
|
import os from 'os';
|
|
8
8
|
import { exec } from 'child_process';
|
|
9
9
|
import { promisify } from 'util';
|
|
10
|
-
import { getLogger } from '@
|
|
10
|
+
import { getLogger } from '@myassis/shared';
|
|
11
11
|
const logger = getLogger('FileTool');
|
|
12
12
|
const execAsync = promisify(exec);
|
|
13
13
|
const DEFAULT_EXCLUDE_DIRS = [
|
|
@@ -155,19 +155,14 @@ export const fileTool = {
|
|
|
155
155
|
if (offset > totalLines) {
|
|
156
156
|
return { success: true, output: JSON.stringify({ totalLines, message: `offset (${offset}) 超出文件总行数 (${totalLines}),文件已读完` }) };
|
|
157
157
|
}
|
|
158
|
-
//
|
|
159
|
-
const
|
|
160
|
-
// 原始内容(无行号前缀),供 LLM 用作 edit 的 oldContent 比对
|
|
161
|
-
const rawContent = allLines.slice(offset - 1, endLine).join('\n');
|
|
158
|
+
// 直接返回纯内容,供 LLM 做 edit oldContent 比对
|
|
159
|
+
const content = allLines.slice(offset - 1, endLine).join('\n');
|
|
162
160
|
return {
|
|
163
161
|
success: true,
|
|
164
162
|
output: JSON.stringify({
|
|
165
163
|
totalLines,
|
|
166
164
|
readRange: { startLine: offset, endLine },
|
|
167
|
-
|
|
168
|
-
content: contentLines.join('\n'),
|
|
169
|
-
// 原始内容(无行号),供 oldContent 比对使用
|
|
170
|
-
rawContent,
|
|
165
|
+
content,
|
|
171
166
|
}),
|
|
172
167
|
};
|
|
173
168
|
}
|
|
@@ -15,10 +15,11 @@ import { taskTool } from './task';
|
|
|
15
15
|
import { modelTool } from './model';
|
|
16
16
|
import { editTool } from './edit';
|
|
17
17
|
import { webFetchTool } from './webFetch';
|
|
18
|
+
import { sessionsSpawnTool } from './sessionsSpawn';
|
|
18
19
|
export const tools = [
|
|
19
20
|
searchTool, calculatorTool, screenshotTool, keyboardTool, mouseTool,
|
|
20
21
|
skillTool, execTool, taskTool, modelTool, fetchTool,
|
|
21
|
-
editTool, webFetchTool, fileTool
|
|
22
|
+
editTool, webFetchTool, fileTool, sessionsSpawnTool
|
|
22
23
|
];
|
|
23
24
|
export function getToolByName(name) {
|
|
24
25
|
return tools.find(tool => tool.name === name);
|
|
@@ -26,11 +27,11 @@ export function getToolByName(name) {
|
|
|
26
27
|
/**
|
|
27
28
|
* 执行工具调用,返回 ToolResult(AI 可见格式)
|
|
28
29
|
*/
|
|
29
|
-
export async function executeTool(name, args, sessionId, messageId) {
|
|
30
|
+
export async function executeTool(name, args, sessionId, messageId, userId) {
|
|
30
31
|
const tool = getToolByName(name);
|
|
31
32
|
if (!tool)
|
|
32
33
|
throw new Error(`Tool not found: ${name}`);
|
|
33
|
-
return await tool.handler(args, sessionId, messageId);
|
|
34
|
+
return await tool.handler(args, sessionId, messageId, userId);
|
|
34
35
|
}
|
|
35
36
|
export function getToolDefinitions() {
|
|
36
37
|
return tools.map(tool => ({
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { modelsService } from '../dataService';
|
|
2
|
+
import { authStore } from '@/stores';
|
|
2
3
|
export const modelTool = {
|
|
3
4
|
name: 'model',
|
|
4
5
|
description: '模型管理工具',
|
|
@@ -21,12 +22,13 @@ export const modelTool = {
|
|
|
21
22
|
},
|
|
22
23
|
required: ['action'],
|
|
23
24
|
},
|
|
24
|
-
handler: async (args) => {
|
|
25
|
+
handler: async (args, sessionId, messageId, userId) => {
|
|
25
26
|
try {
|
|
26
27
|
const { action, modelId, modelName, modelIdName, baseUrl, apiKey, provider = 'doubao', isPrimary = false, supportsToolCall = true } = args;
|
|
28
|
+
const token = authStore.get(userId).accessToken;
|
|
27
29
|
switch (action) {
|
|
28
30
|
case 'list': {
|
|
29
|
-
const response = await modelsService.list();
|
|
31
|
+
const response = await modelsService.list(token);
|
|
30
32
|
if (!response.success)
|
|
31
33
|
return { success: false, errorMessage: response.error || '获取列表失败' };
|
|
32
34
|
const models = response.data || [];
|
|
@@ -35,7 +37,7 @@ export const modelTool = {
|
|
|
35
37
|
case 'add': {
|
|
36
38
|
if (!modelName || !baseUrl || !modelIdName)
|
|
37
39
|
return { success: false, errorMessage: '添加模型需要提供模型名称、模型 ID 和 API 地址' };
|
|
38
|
-
const response = await modelsService.create({ modelName, modelId: modelIdName, baseUrl, apiKey, provider, isPrimary, supportsToolCall });
|
|
40
|
+
const response = await modelsService.create({ modelName, modelId: modelIdName, baseUrl, apiKey, provider, isPrimary, supportsToolCall }, token);
|
|
39
41
|
if (!response.success)
|
|
40
42
|
return { success: false, errorMessage: response.error || '添加失败' };
|
|
41
43
|
return { success: true, output: JSON.stringify({ model: response.data }) };
|
|
@@ -54,7 +56,7 @@ export const modelTool = {
|
|
|
54
56
|
updateData.isPrimary = isPrimary;
|
|
55
57
|
if (supportsToolCall !== undefined)
|
|
56
58
|
updateData.supportsToolCall = supportsToolCall;
|
|
57
|
-
const response = await modelsService.update(modelId, updateData);
|
|
59
|
+
const response = await modelsService.update(modelId, updateData, token);
|
|
58
60
|
if (!response.success)
|
|
59
61
|
return { success: false, errorMessage: response.error || '更新失败' };
|
|
60
62
|
return { success: true, output: JSON.stringify({ model: response.data }) };
|
|
@@ -62,7 +64,7 @@ export const modelTool = {
|
|
|
62
64
|
case 'delete': {
|
|
63
65
|
if (!modelId)
|
|
64
66
|
return { success: false, errorMessage: '删除模型需要提供模型 ID' };
|
|
65
|
-
const response = await modelsService.delete(modelId);
|
|
67
|
+
const response = await modelsService.delete(modelId, token);
|
|
66
68
|
if (!response.success)
|
|
67
69
|
return { success: false, errorMessage: response.error || '删除失败' };
|
|
68
70
|
return { success: true };
|
|
@@ -70,7 +72,7 @@ export const modelTool = {
|
|
|
70
72
|
case 'setPrimary': {
|
|
71
73
|
if (!modelId)
|
|
72
74
|
return { success: false, errorMessage: '设为默认需要提供模型 ID' };
|
|
73
|
-
const response = await modelsService.setPrimary(modelId);
|
|
75
|
+
const response = await modelsService.setPrimary(modelId, token);
|
|
74
76
|
if (!response.success)
|
|
75
77
|
return { success: false, errorMessage: response.error || '设置失败' };
|
|
76
78
|
return { success: true };
|