@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.
Files changed (50) hide show
  1. package/dist/api/index.js +69 -59
  2. package/dist/config/index.js +2 -2
  3. package/dist/index.js +85 -11
  4. package/dist/middleware/auth.js +9 -25
  5. package/dist/middleware/errorHandler.js +1 -1
  6. package/dist/routes/agent.js +30 -19
  7. package/dist/routes/auth.js +35 -26
  8. package/dist/routes/chat.js +1 -1
  9. package/dist/routes/models.js +11 -14
  10. package/dist/routes/service.js +3 -6
  11. package/dist/routes/settings.js +11 -9
  12. package/dist/routes/skillHub.js +10 -6
  13. package/dist/routes/skills.js +13 -10
  14. package/dist/routes/tasks.js +6 -3
  15. package/dist/routes/upload.js +5 -2
  16. package/dist/routes/version.js +4 -4
  17. package/dist/services/LocalTaskService.js +1 -1
  18. package/dist/services/NotificationService.js +1 -1
  19. package/dist/services/ServiceManager.js +12 -12
  20. package/dist/services/TaskSchedulerService.js +18 -9
  21. package/dist/services/TaskService.js +23 -20
  22. package/dist/services/WebSocketService.js +16 -6
  23. package/dist/services/agent/Agent.js +7 -7
  24. package/dist/services/agent/AgentManager.js +25 -17
  25. package/dist/services/agent/AgentStore.js +1 -1
  26. package/dist/services/dataService.js +60 -70
  27. package/dist/services/index.js +1 -1
  28. package/dist/services/llm/LLMClient.js +11 -9
  29. package/dist/services/memory/MemoryManager.js +177 -14
  30. package/dist/services/session/MigrationManager.js +1 -1
  31. package/dist/services/session/Session.js +70 -46
  32. package/dist/services/session/SessionManager.js +12 -5
  33. package/dist/services/session/SessionStore.js +1 -27
  34. package/dist/services/session/index.js +1 -1
  35. package/dist/services/systemPrompt.js +8 -4
  36. package/dist/services/task/PushTokenStore.js +1 -19
  37. package/dist/services/task/TaskStore.js +1 -20
  38. package/dist/services/tools/edit.js +122 -148
  39. package/dist/services/tools/exec.js +1 -1
  40. package/dist/services/tools/fetch.js +1 -1
  41. package/dist/services/tools/file.js +4 -9
  42. package/dist/services/tools/index.js +4 -3
  43. package/dist/services/tools/model.js +8 -6
  44. package/dist/services/tools/sessionsSpawn.js +54 -0
  45. package/dist/services/tools/skill.js +13 -12
  46. package/dist/services/tools/task.js +2 -4
  47. package/dist/stores/authStore.js +52 -66
  48. package/dist/stores/persistStore.js +37 -3
  49. package/package.json +8 -4
  50. 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 '@pocketclaw/shared';
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 settings = (await settingsService.get()).data;
107
- const skills = (await skillsService.list()).data;
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 '@pocketclaw/shared';
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 '@pocketclaw/shared';
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
- * 文件编辑工具 - 行范围编辑(Plan A)
2
+ * 文件编辑工具(内容匹配模式)
3
3
  *
4
4
  * 设计原则:
5
- * - LLM 提供行号范围(oldRange),工具负责读取、验证、执行
6
- * - 每个 edit 只处理一个行范围,多次编辑拆成多个 edit
7
- * - 验证机制:LLM 可提供 oldContent 供工具比对,不匹配时返回实际内容供修正
8
- * - edit 时从后往前执行,避免行号偏移
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. 指定 oldRange: { startLine, endLine } + newContent
23
- * 3. 可选提供 oldContent 让工具验证替换区间的实际内容
24
- * 4. 验证失败时工具返回实际内容,LLM 据此修正后重试
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 os from 'os';
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
- * 读取指定行范围(1-indexed)的原始内容
62
+ * 在文本中查找所有 oldContent 的起始位置
91
63
  */
92
- function getRangeContent(lines, startLine, endLine) {
93
- return lines.slice(startLine - 1, endLine).join('\n');
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 applyRangeEdit(lines, startLine, endLine, newContent) {
99
- const before = lines.slice(0, startLine - 1);
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 hasOverlappingRanges(edits) {
108
- for (let i = 0; i < edits.length; i++) {
109
- for (let j = i + 1; j < edits.length; j++) {
110
- const a = edits[i].oldRange;
111
- const b = edits[j].oldRange;
112
- if (a.startLine <= b.endLine && b.startLine <= a.endLine) {
113
- return {
114
- overlap: true,
115
- message: `编辑范围重叠:edits[${i}](第 ${a.startLine}-${a.endLine} 行)与 edits[${j}](第 ${b.startLine}-${b.endLine} 行)重叠`,
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 { overlap: false };
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
- 'LLM 先用 read 读取文件获得行号,然后指定 oldRange(起始行、结束行)和 newContent(替换后的内容)。' +
127
- '可选提供 oldContent 让工具验证实际内容是否与预期一致。' +
128
- '支持一次调用多个 edit(从后往前执行避免行号偏移)。',
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
- oldRange: {
141
- type: 'object',
142
- description: '要替换的行范围(1-indexed,包含 startLine 和 endLine)',
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
- oldContent: {
154
- type: 'string',
155
- description: '【可选】LLM 认为该范围内原有的内容。工具会比对,不一致时返回实际内容供修正',
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: ['oldRange'],
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
- const { oldRange } = edits[i];
174
- if (!oldRange || typeof oldRange.startLine !== 'number' || typeof oldRange.endLine !== 'number')
175
- return { success: false, errorMessage: `edits[${i}]: 必须提供有效的 oldRange(startLine 和 endLine)` };
176
- if (oldRange.startLine < 1)
177
- return { success: false, errorMessage: `edits[${i}]: startLine 必须 >= 1` };
178
- if (oldRange.endLine < oldRange.startLine)
179
- return { success: false, errorMessage: `edits[${i}]: endLine (${oldRange.endLine}) 必须 >= startLine (${oldRange.startLine})` };
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
- const origLines = original.split('\n').filter((_, i, arr) => !(i === arr.length - 1 && _ === ''));
212
- const totalLines = origLines.length;
213
- // ---- 验证每个 edit 的行范围 ----
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
- if (oldRange.startLine > totalLines)
217
- return {
218
- success: false,
219
- errorMessage: `edits[${i}]: startLine=${oldRange.startLine} 超出文件总行数(${totalLines})。`,
220
- };
221
- if (oldRange.endLine > totalLines)
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}]: endLine=${oldRange.endLine} 超出文件总行数(${totalLines})。`,
215
+ errorMessage: `edits[${i}]: oldContent 在文件中未找到匹配。` +
216
+ `文件当前内容片段:\n\`\`\`\n${safeContent}\n\`\`\``,
225
217
  };
226
- }
227
- // ---- 编辑范围重叠检测 ----
228
- const overlapCheck = hasOverlappingRanges(edits);
229
- if (overlapCheck.overlap) {
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}]: 提供的 oldContent 与文件第 ${oldRange.startLine}-${oldRange.endLine} 行实际内容不一致。` +
243
- `工具发现的内容:\n\`\`\`\n${safeContent}\n\`\`\``,
224
+ errorMessage: `edits[${i}]: oldContent 在文件中出现 ${positions.length} 次,请明确目标。\n${description}`,
244
225
  };
245
226
  }
246
- }
247
- // ---- 从后往前执行替换(避免行号偏移) ----
248
- let currentLines = [...origLines];
249
- const sortedEdits = [...edits].sort((a, b) => b.oldRange.endLine - a.oldRange.endLine);
250
- // 用 Map 记录 edit -> 原始索引(修复 indexOf 重复问题)
251
- const editIndexMap = new Map();
252
- edits.forEach((edit, idx) => editIndexMap.set(edit, idx));
253
- const results = [];
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: editIndexMap.get(edit) ?? -1,
260
- startLine,
261
- endLine,
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
- const modified = currentLines.join('\n');
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
- originalLines: totalLines,
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 '@pocketclaw/shared';
5
+ import { getLogger } from '@myassis/shared';
6
6
  const logger = getLogger('exec');
7
7
  const execAsync = promisify(exec);
8
8
  // 会话级工作目录状态:sessionId -> cwd
@@ -1,4 +1,4 @@
1
- import { getLogger } from '@pocketclaw/shared';
1
+ import { getLogger } from '@myassis/shared';
2
2
  const logger = getLogger('FetchTool');
3
3
  let _undiciCache = null;
4
4
  async function loadUndici() {
@@ -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 '@pocketclaw/shared';
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
- // 显示真实行号(而非 1,2,3...)让 LLM 准确知道位置
159
- const contentLines = allLines.slice(offset - 1, endLine).map((item, index) => `${offset + index} ${item}`);
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
- // 带行号前缀的显示内容,LLM 可参考行号
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 };