foliko 1.1.89 → 1.1.91
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/package.json +1 -1
- package/plugins/default-plugins.js +131 -19
- package/plugins/feishu-plugin.js +1 -1
- package/plugins/file-system-plugin.js +8 -7
- package/plugins/install-plugin.js +11 -6
- package/plugins/memory-plugin.js +4 -4
- package/plugins/plugin-manager-plugin.js +7 -5
- package/plugins/qq-plugin.js +9 -6
- package/plugins/rules-plugin.js +1 -1
- package/plugins/session-plugin.js +32 -23
- package/plugins/shell-executor-plugin.js +4 -1
- package/plugins/storage-plugin.js +1 -1
- package/plugins/subagent-plugin.js +1 -1
- package/plugins/telegram-plugin.js +2 -2
- package/plugins/weixin-plugin.js +9 -6
- package/src/capabilities/skill-manager.js +3 -2
- package/src/capabilities/workflow-engine.js +2 -1
- package/src/core/agent-chat.js +2 -1
- package/src/core/agent.js +1 -1
- package/src/core/constants.js +4 -0
- package/src/core/framework.js +241 -2
- package/src/core/plugin-base.js +11 -0
- package/src/core/plugin-manager.js +18 -2
package/package.json
CHANGED
|
@@ -14,7 +14,13 @@ const bootstrapLog = logger.child('Bootstrap')
|
|
|
14
14
|
* 加载 .foliko 目录下的配置
|
|
15
15
|
* 返回配置对象,可用于手动加载插件
|
|
16
16
|
*/
|
|
17
|
-
function loadAgentConfig(agentDir = '.foliko') {
|
|
17
|
+
function loadAgentConfig(framework, agentDir = '.foliko') {
|
|
18
|
+
// 向后兼容:旧调用 loadAgentConfig('.foliko') 时把 framework 当成 agentDir
|
|
19
|
+
if (typeof framework === 'string') {
|
|
20
|
+
agentDir = framework;
|
|
21
|
+
framework = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
const config = {
|
|
19
25
|
agentDir,
|
|
20
26
|
ai: {},
|
|
@@ -24,13 +30,9 @@ function loadAgentConfig(agentDir = '.foliko') {
|
|
|
24
30
|
agentsDir: null, // 子Agent配置目录
|
|
25
31
|
pluginLinks: {} // 外部插件链接 { "插件名": "外部路径" }
|
|
26
32
|
}
|
|
27
|
-
const
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
if (!fs.existsSync(resolvedDir)) {
|
|
31
|
-
//log.info(` .foliko directory not found: ${resolvedDir}`)
|
|
32
|
-
return config
|
|
33
|
-
}
|
|
33
|
+
const cwd = framework?.getCwd?.() ?? process.cwd()
|
|
34
|
+
const cmdDir = path.resolve(cwd)
|
|
35
|
+
const resolvedDir = path.resolve(cwd, agentDir)
|
|
34
36
|
|
|
35
37
|
//log.info(` Loading config from: ${resolvedDir}`)
|
|
36
38
|
|
|
@@ -146,10 +148,10 @@ function loadAgentConfig(agentDir = '.foliko') {
|
|
|
146
148
|
|
|
147
149
|
// 添加 .foliko/skills 目录(不存在则创建)
|
|
148
150
|
const cmdskillsDir = path.join(cmdDir, 'skills')
|
|
149
|
-
if (fs.existsSync(resolvedDir) && !fs.existsSync(cmdskillsDir)) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
151
|
+
// if (fs.existsSync(resolvedDir) && !fs.existsSync(cmdskillsDir)) {
|
|
152
|
+
// fs.mkdirSync(cmdskillsDir, { recursive: true })
|
|
153
|
+
// //log.info(` Created skills directory: ${cmdskillsDir}`)
|
|
154
|
+
// }
|
|
153
155
|
if (fs.existsSync(cmdskillsDir)) {
|
|
154
156
|
config.skillsDirs.push(cmdskillsDir)
|
|
155
157
|
}
|
|
@@ -167,6 +169,114 @@ function loadAgentConfig(agentDir = '.foliko') {
|
|
|
167
169
|
// config.skillsDirs.push(defaultSkillsDir)
|
|
168
170
|
// }
|
|
169
171
|
|
|
172
|
+
// ============ HOME DIR (~/.foliko/) SCAN ============
|
|
173
|
+
// Home 是独立配置源:
|
|
174
|
+
// - 单文件配置:项目不存在时 home 作为回退
|
|
175
|
+
// - 多文件资源(skills/agents/plugins):两个目录都扫描
|
|
176
|
+
const homeDir = framework?.getHomeAgentDir?.()
|
|
177
|
+
const isHomeSameAsProject = homeDir && path.resolve(homeDir) === resolvedDir
|
|
178
|
+
if (homeDir && fs.existsSync(homeDir) && !isHomeSameAsProject) {
|
|
179
|
+
// 单文件配置回退
|
|
180
|
+
const homeConfig = path.join(homeDir, 'config')
|
|
181
|
+
if (!fs.existsSync(path.join(resolvedDir, 'config')) && fs.existsSync(homeConfig)) {
|
|
182
|
+
try {
|
|
183
|
+
const lines = fs.readFileSync(homeConfig, 'utf-8').split('\n')
|
|
184
|
+
for (const line of lines) {
|
|
185
|
+
const trimmed = line.trim()
|
|
186
|
+
if (!trimmed || trimmed.startsWith('#')) continue
|
|
187
|
+
const colonIndex = trimmed.indexOf(':')
|
|
188
|
+
if (colonIndex === -1) continue
|
|
189
|
+
const key = trimmed.substring(0, colonIndex).trim()
|
|
190
|
+
const value = trimmed.substring(colonIndex + 1).trim()
|
|
191
|
+
if (key === 'ai_key' || key === 'api_key') {
|
|
192
|
+
if (!config.ai.apiKey) config.ai.apiKey = value
|
|
193
|
+
} else if (key === 'ai_model' || key === 'model') {
|
|
194
|
+
if (!config.ai.model) config.ai.model = value
|
|
195
|
+
} else if (key === 'ai_provider' || key === 'provider') {
|
|
196
|
+
if (!config.ai.provider) config.ai.provider = value
|
|
197
|
+
} else if (key === 'ai_base_url' || key === 'baseURL') {
|
|
198
|
+
if (!config.ai.baseURL) config.ai.baseURL = value
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} catch (err) {
|
|
202
|
+
log.error(' Failed to load home config:', err.message)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const homeAi = path.join(homeDir, 'ai.json')
|
|
207
|
+
if (!fs.existsSync(path.join(resolvedDir, 'ai.json')) && fs.existsSync(homeAi)) {
|
|
208
|
+
try {
|
|
209
|
+
const homeObj = JSON.parse(fs.readFileSync(homeAi, 'utf-8'))
|
|
210
|
+
config.ai = { ...homeObj, ...config.ai }
|
|
211
|
+
} catch (err) {
|
|
212
|
+
log.error(' Failed to load home ai.json:', err.message)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const homePluginsFile = path.join(homeDir, 'plugins.json')
|
|
217
|
+
if (!fs.existsSync(path.join(resolvedDir, 'plugins.json')) && fs.existsSync(homePluginsFile)) {
|
|
218
|
+
try {
|
|
219
|
+
const pc = JSON.parse(fs.readFileSync(homePluginsFile, 'utf-8'))
|
|
220
|
+
if (pc.telegram && !config.telegram) config.telegram = pc.telegram
|
|
221
|
+
if (pc.weixin && !config.weixin) config.weixin = pc.weixin
|
|
222
|
+
if (pc.email && !config.email) config.email = pc.email
|
|
223
|
+
config.pluginLinks = { ...(pc.pluginLinks || {}), ...config.pluginLinks }
|
|
224
|
+
} catch (err) {
|
|
225
|
+
log.error(' Failed to load home plugins.json:', err.message)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const homeWeixin = path.join(homeDir, 'weixin.json')
|
|
230
|
+
if (!fs.existsSync(path.join(resolvedDir, 'weixin.json')) && fs.existsSync(homeWeixin)) {
|
|
231
|
+
try {
|
|
232
|
+
const w = JSON.parse(fs.readFileSync(homeWeixin, 'utf-8'))
|
|
233
|
+
config.weixin = { ...w, ...config.weixin }
|
|
234
|
+
} catch (err) {
|
|
235
|
+
log.error(' Failed to load home weixin.json:', err.message)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const homeMcp = path.join(homeDir, 'mcp_config.json')
|
|
240
|
+
if (!fs.existsSync(path.join(resolvedDir, 'mcp_config.json')) && fs.existsSync(homeMcp)) {
|
|
241
|
+
try {
|
|
242
|
+
const m = JSON.parse(fs.readFileSync(homeMcp, 'utf-8'))
|
|
243
|
+
config.mcpServers = { ...(m.mcpServers || {}), ...config.mcpServers }
|
|
244
|
+
} catch (err) {
|
|
245
|
+
log.error(' Failed to load home mcp_config.json:', err.message)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// agents 目录:home 仅在项目无 agentsDir 时生效
|
|
250
|
+
const homeAgentsDir = path.join(homeDir, 'agents')
|
|
251
|
+
if (!config.agentsDir && fs.existsSync(homeAgentsDir)) {
|
|
252
|
+
config.agentsDir = homeAgentsDir
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// skills 目录:项目 .foliko/skills 之后插入 home
|
|
256
|
+
const homeSkillsDir = path.join(homeDir, 'skills')
|
|
257
|
+
if (!fs.existsSync(homeSkillsDir)) {
|
|
258
|
+
try { fs.mkdirSync(homeSkillsDir, { recursive: true }) } catch (err) { /* ignore */ }
|
|
259
|
+
}
|
|
260
|
+
if (fs.existsSync(homeSkillsDir)) {
|
|
261
|
+
const projectFolikoSkillsIdx = config.skillsDirs.findIndex(
|
|
262
|
+
d => d === path.join(resolvedDir, 'skills')
|
|
263
|
+
)
|
|
264
|
+
const insertAt = projectFolikoSkillsIdx >= 0
|
|
265
|
+
? projectFolikoSkillsIdx + 1
|
|
266
|
+
: config.skillsDirs.length
|
|
267
|
+
config.skillsDirs.splice(insertAt, 0, homeSkillsDir)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// home plugins:扫描并加入 pluginLinks
|
|
271
|
+
const homePluginsDir = path.join(homeDir, 'plugins')
|
|
272
|
+
if (fs.existsSync(homePluginsDir)) {
|
|
273
|
+
for (const name of scanPluginNames(homePluginsDir)) {
|
|
274
|
+
const resolvedPlugin = resolvePluginPath(homePluginsDir, name)
|
|
275
|
+
if (resolvedPlugin) config.pluginLinks[name] = resolvedPlugin.path
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
170
280
|
return config
|
|
171
281
|
}
|
|
172
282
|
|
|
@@ -194,7 +304,7 @@ class DefaultPlugins extends Plugin {
|
|
|
194
304
|
|
|
195
305
|
start(framework) {
|
|
196
306
|
// 加载配置
|
|
197
|
-
this._config = loadAgentConfig(this._agentDir)
|
|
307
|
+
this._config = loadAgentConfig(this._framework, this._agentDir)
|
|
198
308
|
return this
|
|
199
309
|
}
|
|
200
310
|
|
|
@@ -202,12 +312,12 @@ class DefaultPlugins extends Plugin {
|
|
|
202
312
|
* 获取加载的配置
|
|
203
313
|
*/
|
|
204
314
|
getConfig() {
|
|
205
|
-
return this._config || loadAgentConfig(this._agentDir)
|
|
315
|
+
return this._config || loadAgentConfig(this._framework, this._agentDir)
|
|
206
316
|
}
|
|
207
317
|
|
|
208
318
|
reload(framework) {
|
|
209
319
|
this._framework = framework
|
|
210
|
-
this._config = loadAgentConfig(this._agentDir)
|
|
320
|
+
this._config = loadAgentConfig(this._framework, this._agentDir)
|
|
211
321
|
}
|
|
212
322
|
|
|
213
323
|
uninstall(framework) {
|
|
@@ -510,15 +620,17 @@ function scanPluginNames(pluginsDir) {
|
|
|
510
620
|
async function loadCustomPlugins(framework, agentConfig) {
|
|
511
621
|
const { Plugin } = require('../src/core/plugin-base')
|
|
512
622
|
|
|
623
|
+
const cwd = framework?.getCwd?.() ?? process.cwd()
|
|
624
|
+
|
|
513
625
|
// 加载两个目录下的自定义插件:
|
|
514
626
|
// 1. .foliko/plugins/ - 用户自定义插件(强制启用)
|
|
515
627
|
// 2. plugins/ - 项目内置插件(自动加载)
|
|
516
628
|
const dirs = [
|
|
517
|
-
{ dir: path.resolve(
|
|
518
|
-
{ dir: path.resolve(
|
|
519
|
-
{ dir: path.resolve(__dirname, '..', 'plugins'), forceEnabled: false }
|
|
629
|
+
{ dir: path.resolve(cwd, '.foliko', 'plugins'), forceEnabled: true },
|
|
630
|
+
{ dir: path.resolve(cwd, 'plugins'), forceEnabled: false },
|
|
631
|
+
{ dir: path.resolve(__dirname, '..', 'plugins'), forceEnabled: false }
|
|
520
632
|
// 项目下的 plugins 目录(兼容旧版本)
|
|
521
|
-
|
|
633
|
+
|
|
522
634
|
]
|
|
523
635
|
|
|
524
636
|
for (const { dir, forceEnabled } of dirs) {
|
package/plugins/feishu-plugin.js
CHANGED
|
@@ -262,7 +262,7 @@ class FeishuPlugin extends Plugin {
|
|
|
262
262
|
const agent = this._framework.createSessionAgent(`feishu_${openId}`, {
|
|
263
263
|
systemPrompt: this.systemPrompt,
|
|
264
264
|
sharedPrompt: `工作目录: {{WORK_DIR}}`,
|
|
265
|
-
metadata: { WORK_DIR: process.cwd() }
|
|
265
|
+
metadata: { WORK_DIR: this._framework?.getCwd?.() ?? process.cwd() }
|
|
266
266
|
})
|
|
267
267
|
this._sessionAgents.set(openId, agent)
|
|
268
268
|
|
|
@@ -26,7 +26,7 @@ class FileSystemPlugin extends Plugin {
|
|
|
26
26
|
// 路径安全验证:防止路径穿越攻击
|
|
27
27
|
const validatePath = (filePath, allowOutsideCwd = false) => {
|
|
28
28
|
const resolved = path.resolve(filePath)
|
|
29
|
-
const cwd = process.cwd()
|
|
29
|
+
const cwd = framework?.getCwd?.() ?? process.cwd()
|
|
30
30
|
|
|
31
31
|
// 允许绝对路径且在允许列表中的路径(如果有的话)
|
|
32
32
|
if (allowOutsideCwd) {
|
|
@@ -51,6 +51,7 @@ class FileSystemPlugin extends Plugin {
|
|
|
51
51
|
recursive: z.boolean().optional().describe('是否递归')
|
|
52
52
|
}),
|
|
53
53
|
execute: async (args, framework) => {
|
|
54
|
+
const cwd = framework?.getCwd?.() ?? process.cwd()
|
|
54
55
|
const dirPath = args.path || args.dirPath || '.'
|
|
55
56
|
const recursive = args.recursive || false
|
|
56
57
|
try {
|
|
@@ -60,7 +61,7 @@ class FileSystemPlugin extends Plugin {
|
|
|
60
61
|
const entries = fs.readdirSync(currentPath, { withFileTypes: true })
|
|
61
62
|
for (const entry of entries) {
|
|
62
63
|
const fullPath = path.join(currentPath, entry.name)
|
|
63
|
-
const relativePath = path.relative(
|
|
64
|
+
const relativePath = path.relative(cwd, fullPath)
|
|
64
65
|
if (relativePath.includes('node_modules') || relativePath.includes('.git')) {
|
|
65
66
|
continue
|
|
66
67
|
}
|
|
@@ -476,7 +477,7 @@ edits 数组可以包含多个不重叠的替换操作,每个操作通过 oldT
|
|
|
476
477
|
execute: async (args, framework) => {
|
|
477
478
|
const { path: filePath, edits } = args;
|
|
478
479
|
const editDiff = require('../src/utils/edit-diff');
|
|
479
|
-
const result = await editDiff.applyEditsToFile(filePath, edits, process.cwd());
|
|
480
|
+
const result = await editDiff.applyEditsToFile(filePath, edits, framework?.getCwd?.() ?? process.cwd());
|
|
480
481
|
return result;
|
|
481
482
|
}
|
|
482
483
|
};
|
|
@@ -509,7 +510,7 @@ edits 数组可以包含多个不重叠的替换操作,每个操作通过 oldT
|
|
|
509
510
|
}),
|
|
510
511
|
execute: async (args, framework) => {
|
|
511
512
|
const pattern = args.pattern
|
|
512
|
-
const dirPath = args.path || process.cwd()
|
|
513
|
+
const dirPath = args.path || (framework?.getCwd?.() ?? process.cwd())
|
|
513
514
|
const targetFile = args.file
|
|
514
515
|
const fileType = args.fileType
|
|
515
516
|
const maxResults = args.maxResults || 100
|
|
@@ -625,7 +626,7 @@ edits 数组可以包含多个不重叠的替换操作,每个操作通过 oldT
|
|
|
625
626
|
if (results.length >= maxResults) break
|
|
626
627
|
|
|
627
628
|
const fullPath = path.join(currentPath, entry.name)
|
|
628
|
-
const relativePath = path.relative(
|
|
629
|
+
const relativePath = path.relative(cwd, fullPath)
|
|
629
630
|
|
|
630
631
|
// 检查是否在排除目录中
|
|
631
632
|
const shouldExclude = excludeDirs.some(exclude =>
|
|
@@ -696,7 +697,7 @@ edits 数组可以包含多个不重叠的替换操作,每个操作通过 oldT
|
|
|
696
697
|
}),
|
|
697
698
|
execute: async (args, framework) => {
|
|
698
699
|
const command = args.cmd || args.command || args.run
|
|
699
|
-
const cwd = args.cwd || process.cwd()
|
|
700
|
+
const cwd = args.cwd || (framework?.getCwd?.() ?? process.cwd())
|
|
700
701
|
const timeout = Math.min(args.timeout || 30000, 120000) // 最多 2 分钟
|
|
701
702
|
|
|
702
703
|
// 验证命令:检查危险的 shell 模式
|
|
@@ -762,7 +763,7 @@ edits 数组可以包含多个不重叠的替换操作,每个操作通过 oldT
|
|
|
762
763
|
}),
|
|
763
764
|
execute: async (args, framework) => {
|
|
764
765
|
const command = args.cmd || args.command
|
|
765
|
-
const cwd = args.cwd || process.cwd()
|
|
766
|
+
const cwd = args.cwd || (framework?.getCwd?.() ?? process.cwd())
|
|
766
767
|
const timeout = Math.min(args.timeout || 30000, 120000)
|
|
767
768
|
|
|
768
769
|
if (!command) {
|
|
@@ -30,6 +30,8 @@ class InstallPlugin extends Plugin {
|
|
|
30
30
|
|
|
31
31
|
start(framework) {
|
|
32
32
|
// 确保 node_modules 目录存在
|
|
33
|
+
const cwd = this._framework?.getCwd?.() ?? process.cwd()
|
|
34
|
+
this._agentDir = path.resolve(cwd, this._agentDir)
|
|
33
35
|
this._nodeModulesDir = path.join(this._agentDir, 'node_modules')
|
|
34
36
|
if (!fs.existsSync(this._nodeModulesDir)) {
|
|
35
37
|
fs.mkdirSync(this._nodeModulesDir, { recursive: true })
|
|
@@ -46,10 +48,11 @@ class InstallPlugin extends Plugin {
|
|
|
46
48
|
}),
|
|
47
49
|
execute: async (args) => {
|
|
48
50
|
const { package: packageName, path: targetPath, file: packageJsonPath } = args
|
|
51
|
+
const cwd = this._framework?.getCwd?.() ?? process.cwd()
|
|
49
52
|
|
|
50
53
|
// 优先处理 file 参数(从 package.json 安装)
|
|
51
54
|
if (packageJsonPath) {
|
|
52
|
-
const resolvedPath = path.resolve(
|
|
55
|
+
const resolvedPath = path.resolve(cwd, packageJsonPath)
|
|
53
56
|
return this._installFromPackageJson(resolvedPath, targetPath)
|
|
54
57
|
}
|
|
55
58
|
|
|
@@ -60,7 +63,7 @@ class InstallPlugin extends Plugin {
|
|
|
60
63
|
|
|
61
64
|
// 只有 path,安装该目录的 package.json
|
|
62
65
|
if (targetPath) {
|
|
63
|
-
const resolvedPath = path.resolve(
|
|
66
|
+
const resolvedPath = path.resolve(cwd, targetPath)
|
|
64
67
|
const pkgJson = path.join(resolvedPath, 'package.json')
|
|
65
68
|
if (fs.existsSync(pkgJson)) {
|
|
66
69
|
return this._installFromPackageJson(pkgJson, targetPath)
|
|
@@ -68,7 +71,7 @@ class InstallPlugin extends Plugin {
|
|
|
68
71
|
return { success: false, error: `package.json not found at ${pkgJson}` }
|
|
69
72
|
}
|
|
70
73
|
|
|
71
|
-
//
|
|
74
|
+
// 都不提供,报错
|
|
72
75
|
return { success: false, error: 'Must provide package name or package.json path' }
|
|
73
76
|
}
|
|
74
77
|
})
|
|
@@ -83,8 +86,9 @@ class InstallPlugin extends Plugin {
|
|
|
83
86
|
*/
|
|
84
87
|
_installPackage(packageName, targetPath = null) {
|
|
85
88
|
try {
|
|
89
|
+
const cwd = this._framework?.getCwd?.() ?? process.cwd()
|
|
86
90
|
const installPath = targetPath
|
|
87
|
-
? path.resolve(
|
|
91
|
+
? path.resolve(cwd, targetPath)
|
|
88
92
|
: this._agentDir
|
|
89
93
|
|
|
90
94
|
//log.info(` Installing ${packageName} to ${installPath}...`)
|
|
@@ -121,10 +125,11 @@ class InstallPlugin extends Plugin {
|
|
|
121
125
|
*/
|
|
122
126
|
_installFromPackageJson(packageJsonPath, targetPath = null) {
|
|
123
127
|
try {
|
|
124
|
-
const
|
|
128
|
+
const cwd = this._framework?.getCwd?.() ?? process.cwd()
|
|
129
|
+
const resolvedPkgPath = path.resolve(cwd, packageJsonPath)
|
|
125
130
|
const pkgDir = path.dirname(resolvedPkgPath)
|
|
126
131
|
const installPath = targetPath
|
|
127
|
-
? path.resolve(
|
|
132
|
+
? path.resolve(cwd, targetPath)
|
|
128
133
|
: pkgDir
|
|
129
134
|
|
|
130
135
|
//log.info(` Installing dependencies from ${resolvedPkgPath} to ${installPath}...`)
|
package/plugins/memory-plugin.js
CHANGED
|
@@ -210,9 +210,9 @@ function _extractJSON(text) {
|
|
|
210
210
|
* MemoryStore - 双层存储(内存 + 文件)
|
|
211
211
|
*/
|
|
212
212
|
class MemoryStore extends EventEmitter {
|
|
213
|
-
constructor(baseDir) {
|
|
213
|
+
constructor(baseDir, cwd) {
|
|
214
214
|
super()
|
|
215
|
-
this._baseDir = path.resolve(process.cwd(), baseDir)
|
|
215
|
+
this._baseDir = path.resolve(cwd ?? process.cwd(), baseDir)
|
|
216
216
|
this._memory = new Map() // memoryId -> memory object
|
|
217
217
|
this._indexByType = new Map() // type -> Set of memoryIds
|
|
218
218
|
this._indexByProject = new Map() // project -> Set of memoryIds
|
|
@@ -573,7 +573,7 @@ class MemoryPlugin extends Plugin {
|
|
|
573
573
|
|
|
574
574
|
start(framework) {
|
|
575
575
|
// 初始化存储
|
|
576
|
-
this._store = new MemoryStore(this.config.memoryDir)
|
|
576
|
+
this._store = new MemoryStore(this.config.memoryDir, this._framework?.getCwd?.())
|
|
577
577
|
|
|
578
578
|
// 创建复用的记忆提取子 Agent
|
|
579
579
|
this._memoryAgent = framework.createSubAgent({
|
|
@@ -1276,7 +1276,7 @@ ${prompt}`
|
|
|
1276
1276
|
reload(framework) {
|
|
1277
1277
|
this._framework = framework
|
|
1278
1278
|
// 重新初始化存储
|
|
1279
|
-
this._store = new MemoryStore(this.config.memoryDir)
|
|
1279
|
+
this._store = new MemoryStore(this.config.memoryDir, this._framework?.getCwd?.())
|
|
1280
1280
|
}
|
|
1281
1281
|
|
|
1282
1282
|
uninstall(framework) {
|
|
@@ -212,8 +212,9 @@ class PluginManagerPlugin extends Plugin {
|
|
|
212
212
|
* 发布插件到远程仓库
|
|
213
213
|
*/
|
|
214
214
|
async _publishPlugin(pluginName, repo) {
|
|
215
|
-
const
|
|
216
|
-
const
|
|
215
|
+
const cwd = this._framework?.getCwd?.() ?? process.cwd();
|
|
216
|
+
const pluginsDir = path.resolve(cwd, '.foliko', 'plugins');
|
|
217
|
+
const localPluginsDir = path.resolve(cwd, 'plugins');
|
|
217
218
|
|
|
218
219
|
// 确定插件目录
|
|
219
220
|
let actualPluginsDir = pluginsDir;
|
|
@@ -251,7 +252,7 @@ class PluginManagerPlugin extends Plugin {
|
|
|
251
252
|
|
|
252
253
|
let isNewRepo = false;
|
|
253
254
|
try {
|
|
254
|
-
gitCommand(`clone ${repo} "${tmpDir}" --depth 1`, process.cwd());
|
|
255
|
+
gitCommand(`clone ${repo} "${tmpDir}" --depth 1`, this._framework?.getCwd?.() ?? process.cwd());
|
|
255
256
|
} catch (err) {
|
|
256
257
|
//log.info('Initializing new repository...');
|
|
257
258
|
fs.mkdirSync(tmpDir, { recursive: true });
|
|
@@ -331,7 +332,8 @@ class PluginManagerPlugin extends Plugin {
|
|
|
331
332
|
* 从远程仓库安装插件
|
|
332
333
|
*/
|
|
333
334
|
async _installPlugin(pluginName, repo) {
|
|
334
|
-
const
|
|
335
|
+
const cwd = this._framework?.getCwd?.() ?? process.cwd();
|
|
336
|
+
const localPluginsDir = path.resolve(cwd, '.foliko', 'plugins');
|
|
335
337
|
const tmpDir = path.join(require('os').tmpdir(), `foliko-plugin-install-${Date.now()}`);
|
|
336
338
|
|
|
337
339
|
try {
|
|
@@ -339,7 +341,7 @@ class PluginManagerPlugin extends Plugin {
|
|
|
339
341
|
|
|
340
342
|
// 克隆仓库
|
|
341
343
|
fs.mkdirSync(tmpDir, { recursive: true });
|
|
342
|
-
gitCommand(`clone ${repo} "${tmpDir}" --depth 1`,
|
|
344
|
+
gitCommand(`clone ${repo} "${tmpDir}" --depth 1`, cwd);
|
|
343
345
|
|
|
344
346
|
// 查找插件
|
|
345
347
|
const pluginDir = path.join(tmpDir, pluginName);
|
package/plugins/qq-plugin.js
CHANGED
|
@@ -49,11 +49,7 @@ class QQPlugin extends Plugin {
|
|
|
49
49
|
this.agent = null
|
|
50
50
|
this.sessionId = null
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
this.downloader = new FileDownloader({
|
|
54
|
-
retries: 3,
|
|
55
|
-
baseDir: saveDir
|
|
56
|
-
})
|
|
52
|
+
this.downloader = null
|
|
57
53
|
|
|
58
54
|
this.systemPrompt = `你是一个 QQ 助手。
|
|
59
55
|
|
|
@@ -70,6 +66,13 @@ class QQPlugin extends Plugin {
|
|
|
70
66
|
|
|
71
67
|
install(framework) {
|
|
72
68
|
this._framework = framework
|
|
69
|
+
if (!this.downloader) {
|
|
70
|
+
const saveDir = path.join(this._framework?.getCwd?.() ?? process.cwd(), '.foliko', 'data', this.name)
|
|
71
|
+
this.downloader = new FileDownloader({
|
|
72
|
+
retries: 3,
|
|
73
|
+
baseDir: saveDir
|
|
74
|
+
})
|
|
75
|
+
}
|
|
73
76
|
this._registerTools()
|
|
74
77
|
return this
|
|
75
78
|
}
|
|
@@ -527,7 +530,7 @@ class QQPlugin extends Plugin {
|
|
|
527
530
|
name: `qq_${identifier}`,
|
|
528
531
|
systemPrompt: this.systemPrompt,
|
|
529
532
|
sharedPrompt: `工作目录: {{WORK_DIR}}`,
|
|
530
|
-
metadata: { WORK_DIR: process.cwd(), platform: 'qq', identifier }
|
|
533
|
+
metadata: { WORK_DIR: this._framework?.getCwd?.() ?? process.cwd(), platform: 'qq', identifier }
|
|
531
534
|
})
|
|
532
535
|
|
|
533
536
|
this._sessionAgents.set(identifier, agent)
|
package/plugins/rules-plugin.js
CHANGED
|
@@ -168,7 +168,7 @@ class RulesPlugin extends Plugin {
|
|
|
168
168
|
* 从目录加载规则文件
|
|
169
169
|
*/
|
|
170
170
|
loadFromDirectory(rulesDir) {
|
|
171
|
-
const resolvedDir = path.resolve(process.cwd(), rulesDir)
|
|
171
|
+
const resolvedDir = path.resolve(this._framework?.getCwd?.() ?? process.cwd(), rulesDir)
|
|
172
172
|
|
|
173
173
|
if (!fs.existsSync(resolvedDir)) {
|
|
174
174
|
//log.info(` Rules directory not found: ${resolvedDir}`)
|
|
@@ -16,7 +16,7 @@ const { buildSessionContext, EntryTypes } = require('../src/core/session-entry')
|
|
|
16
16
|
* Get default session directory for a cwd
|
|
17
17
|
*/
|
|
18
18
|
function getDefaultSessionDir(cwd) {
|
|
19
|
-
const agentDir = path.join(
|
|
19
|
+
const agentDir = path.join(cwd, '.foliko');
|
|
20
20
|
const sessionDir = path.join(agentDir, 'sessions');
|
|
21
21
|
return sessionDir;
|
|
22
22
|
}
|
|
@@ -54,10 +54,18 @@ class SessionPlugin extends Plugin {
|
|
|
54
54
|
return this;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* 获取当前工作目录(framework.getCwd() 优先, fallback 到 process.cwd())
|
|
59
|
+
*/
|
|
60
|
+
_defaultCwd() {
|
|
61
|
+
return this._framework?.getCwd?.() ?? process.cwd();
|
|
62
|
+
}
|
|
63
|
+
|
|
57
64
|
/**
|
|
58
65
|
* Get or create a SessionManager for a session
|
|
59
66
|
*/
|
|
60
|
-
_getSessionManager(sessionId, cwd
|
|
67
|
+
_getSessionManager(sessionId, cwd) {
|
|
68
|
+
cwd = cwd ?? this._defaultCwd();
|
|
61
69
|
if (this._sessionManagers.has(sessionId)) {
|
|
62
70
|
return this._sessionManagers.get(sessionId);
|
|
63
71
|
}
|
|
@@ -72,7 +80,7 @@ class SessionPlugin extends Plugin {
|
|
|
72
80
|
/**
|
|
73
81
|
* List all sessions for current cwd
|
|
74
82
|
*/
|
|
75
|
-
async listSessions(cwd =
|
|
83
|
+
async listSessions(cwd = this._defaultCwd()) {
|
|
76
84
|
const sessionDir = getDefaultSessionDir(cwd);
|
|
77
85
|
const sessions = [];
|
|
78
86
|
|
|
@@ -115,7 +123,7 @@ class SessionPlugin extends Plugin {
|
|
|
115
123
|
cwd: z.string().optional().describe('工作目录')
|
|
116
124
|
}),
|
|
117
125
|
execute: async (args, fw) => {
|
|
118
|
-
const cwd = args.cwd ||
|
|
126
|
+
const cwd = args.cwd || this._defaultCwd();
|
|
119
127
|
const sessionDir = getDefaultSessionDir(cwd);
|
|
120
128
|
const manager = new SessionManager(cwd, sessionDir, undefined, true);
|
|
121
129
|
const sessionId = manager.getSessionId();
|
|
@@ -140,7 +148,7 @@ class SessionPlugin extends Plugin {
|
|
|
140
148
|
cwd: z.string().optional().describe('工作目录')
|
|
141
149
|
}),
|
|
142
150
|
execute: async (args) => {
|
|
143
|
-
const cwd = args.cwd ||
|
|
151
|
+
const cwd = args.cwd || this._defaultCwd();
|
|
144
152
|
const manager = this._getSessionManager(args.sessionId, cwd);
|
|
145
153
|
const entries = manager.getEntries();
|
|
146
154
|
|
|
@@ -163,7 +171,7 @@ class SessionPlugin extends Plugin {
|
|
|
163
171
|
cwd: z.string().optional().describe('工作目录')
|
|
164
172
|
}),
|
|
165
173
|
execute: async (args) => {
|
|
166
|
-
const cwd = args.cwd ||
|
|
174
|
+
const cwd = args.cwd || this._defaultCwd();
|
|
167
175
|
const sessions = await this.listSessions(cwd);
|
|
168
176
|
return {
|
|
169
177
|
success: true,
|
|
@@ -181,7 +189,7 @@ class SessionPlugin extends Plugin {
|
|
|
181
189
|
cwd: z.string().optional().describe('工作目录')
|
|
182
190
|
}),
|
|
183
191
|
execute: async (args) => {
|
|
184
|
-
const cwd = args.cwd ||
|
|
192
|
+
const cwd = args.cwd || this._defaultCwd();
|
|
185
193
|
const manager = this._getSessionManager(args.sessionId, cwd);
|
|
186
194
|
|
|
187
195
|
if (manager.getSessionFile() && require('fs').existsSync(manager.getSessionFile())) {
|
|
@@ -203,7 +211,7 @@ class SessionPlugin extends Plugin {
|
|
|
203
211
|
limit: z.number().optional().describe('返回消息数量限制')
|
|
204
212
|
}),
|
|
205
213
|
execute: async (args) => {
|
|
206
|
-
const cwd = args.cwd ||
|
|
214
|
+
const cwd = args.cwd || this._defaultCwd();
|
|
207
215
|
const manager = this._getSessionManager(args.sessionId, cwd);
|
|
208
216
|
const context = manager.buildSessionContext();
|
|
209
217
|
let messages = context.messages;
|
|
@@ -233,7 +241,7 @@ class SessionPlugin extends Plugin {
|
|
|
233
241
|
cwd: z.string().optional().describe('工作目录')
|
|
234
242
|
}),
|
|
235
243
|
execute: async (args) => {
|
|
236
|
-
const cwd = args.cwd ||
|
|
244
|
+
const cwd = args.cwd || this._defaultCwd();
|
|
237
245
|
const manager = this._getSessionManager(args.sessionId, cwd);
|
|
238
246
|
|
|
239
247
|
try {
|
|
@@ -262,7 +270,7 @@ class SessionPlugin extends Plugin {
|
|
|
262
270
|
cwd: z.string().optional().describe('工作目录')
|
|
263
271
|
}),
|
|
264
272
|
execute: async (args) => {
|
|
265
|
-
const cwd = args.cwd ||
|
|
273
|
+
const cwd = args.cwd || this._defaultCwd();
|
|
266
274
|
const manager = this._getSessionManager(args.sessionId, cwd);
|
|
267
275
|
|
|
268
276
|
try {
|
|
@@ -307,7 +315,7 @@ class SessionPlugin extends Plugin {
|
|
|
307
315
|
/**
|
|
308
316
|
* Add message to session
|
|
309
317
|
*/
|
|
310
|
-
addMessage(sessionId, message, cwd =
|
|
318
|
+
addMessage(sessionId, message, cwd = this._defaultCwd()) {
|
|
311
319
|
const manager = this._getSessionManager(sessionId, cwd);
|
|
312
320
|
const entryId = manager.appendMessage(message);
|
|
313
321
|
|
|
@@ -322,7 +330,7 @@ class SessionPlugin extends Plugin {
|
|
|
322
330
|
/**
|
|
323
331
|
* Get session context for LLM
|
|
324
332
|
*/
|
|
325
|
-
getSessionContext(sessionId, cwd =
|
|
333
|
+
getSessionContext(sessionId, cwd = this._defaultCwd()) {
|
|
326
334
|
const manager = this._getSessionManager(sessionId, cwd);
|
|
327
335
|
return manager.buildSessionContext();
|
|
328
336
|
}
|
|
@@ -330,14 +338,14 @@ class SessionPlugin extends Plugin {
|
|
|
330
338
|
/**
|
|
331
339
|
* Get or create session (alias for getSessionContext)
|
|
332
340
|
*/
|
|
333
|
-
getOrCreateSession(sessionId, options = {}, cwd =
|
|
341
|
+
getOrCreateSession(sessionId, options = {}, cwd = this._defaultCwd()) {
|
|
334
342
|
return this.getSessionContext(sessionId, cwd);
|
|
335
343
|
}
|
|
336
344
|
|
|
337
345
|
/**
|
|
338
346
|
* Get specific entry
|
|
339
347
|
*/
|
|
340
|
-
getEntry(sessionId, entryId, cwd =
|
|
348
|
+
getEntry(sessionId, entryId, cwd = this._defaultCwd()) {
|
|
341
349
|
const manager = this._getSessionManager(sessionId, cwd);
|
|
342
350
|
return manager.getEntry(entryId);
|
|
343
351
|
}
|
|
@@ -345,7 +353,7 @@ class SessionPlugin extends Plugin {
|
|
|
345
353
|
/**
|
|
346
354
|
* Get branch (path from root to leaf)
|
|
347
355
|
*/
|
|
348
|
-
getBranch(sessionId, fromId, cwd =
|
|
356
|
+
getBranch(sessionId, fromId, cwd = this._defaultCwd()) {
|
|
349
357
|
const manager = this._getSessionManager(sessionId, cwd);
|
|
350
358
|
return manager.getBranch(fromId);
|
|
351
359
|
}
|
|
@@ -353,7 +361,7 @@ class SessionPlugin extends Plugin {
|
|
|
353
361
|
/**
|
|
354
362
|
* Append compaction entry
|
|
355
363
|
*/
|
|
356
|
-
appendCompaction(sessionId, summary, firstKeptEntryId, tokensBefore, details, cwd =
|
|
364
|
+
appendCompaction(sessionId, summary, firstKeptEntryId, tokensBefore, details, cwd = this._defaultCwd()) {
|
|
357
365
|
const manager = this._getSessionManager(sessionId, cwd);
|
|
358
366
|
return manager.appendCompaction(summary, firstKeptEntryId, tokensBefore, details, false);
|
|
359
367
|
}
|
|
@@ -361,7 +369,7 @@ class SessionPlugin extends Plugin {
|
|
|
361
369
|
/**
|
|
362
370
|
* Append thinking level change
|
|
363
371
|
*/
|
|
364
|
-
appendThinkingLevelChange(sessionId, thinkingLevel, cwd =
|
|
372
|
+
appendThinkingLevelChange(sessionId, thinkingLevel, cwd = this._defaultCwd()) {
|
|
365
373
|
const manager = this._getSessionManager(sessionId, cwd);
|
|
366
374
|
return manager.appendThinkingLevelChange(thinkingLevel);
|
|
367
375
|
}
|
|
@@ -369,7 +377,7 @@ class SessionPlugin extends Plugin {
|
|
|
369
377
|
/**
|
|
370
378
|
* Append model change
|
|
371
379
|
*/
|
|
372
|
-
appendModelChange(sessionId, provider, modelId, cwd =
|
|
380
|
+
appendModelChange(sessionId, provider, modelId, cwd = this._defaultCwd()) {
|
|
373
381
|
const manager = this._getSessionManager(sessionId, cwd);
|
|
374
382
|
return manager.appendModelChange(provider, modelId);
|
|
375
383
|
}
|
|
@@ -377,7 +385,7 @@ class SessionPlugin extends Plugin {
|
|
|
377
385
|
/**
|
|
378
386
|
* Set session label
|
|
379
387
|
*/
|
|
380
|
-
setLabel(sessionId, entryId, label, cwd =
|
|
388
|
+
setLabel(sessionId, entryId, label, cwd = this._defaultCwd()) {
|
|
381
389
|
const manager = this._getSessionManager(sessionId, cwd);
|
|
382
390
|
return manager.appendLabelChange(entryId, label);
|
|
383
391
|
}
|
|
@@ -385,7 +393,7 @@ class SessionPlugin extends Plugin {
|
|
|
385
393
|
/**
|
|
386
394
|
* Get session info (compatible with external callers)
|
|
387
395
|
*/
|
|
388
|
-
getSession(sessionId, cwd =
|
|
396
|
+
getSession(sessionId, cwd = this._defaultCwd()) {
|
|
389
397
|
const manager = this._getSessionManager(sessionId, cwd);
|
|
390
398
|
const entries = manager.getEntries();
|
|
391
399
|
return {
|
|
@@ -414,8 +422,9 @@ class SessionPlugin extends Plugin {
|
|
|
414
422
|
async _doCleanup() {
|
|
415
423
|
try {
|
|
416
424
|
const fs = require('fs');
|
|
417
|
-
const
|
|
418
|
-
|
|
425
|
+
const cwd = this._defaultCwd();
|
|
426
|
+
const sessionDir = getDefaultSessionDir(cwd);
|
|
427
|
+
|
|
419
428
|
if (!fs.existsSync(sessionDir)) {
|
|
420
429
|
return;
|
|
421
430
|
}
|
|
@@ -426,7 +435,7 @@ class SessionPlugin extends Plugin {
|
|
|
426
435
|
for (const file of files) {
|
|
427
436
|
const filePath = path.join(sessionDir, file);
|
|
428
437
|
try {
|
|
429
|
-
const manager = SessionManager.open(filePath, sessionDir,
|
|
438
|
+
const manager = SessionManager.open(filePath, sessionDir, cwd);
|
|
430
439
|
const entries = manager.getEntries();
|
|
431
440
|
|
|
432
441
|
// Clean up if exceeds maxHistoryLength
|
|
@@ -19,7 +19,7 @@ class ShellExecutorPlugin extends Plugin {
|
|
|
19
19
|
|
|
20
20
|
this.config = {
|
|
21
21
|
timeout: config.timeout || 60000,
|
|
22
|
-
workingDir: config.workingDir
|
|
22
|
+
workingDir: config.workingDir
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
this._framework = null
|
|
@@ -27,6 +27,9 @@ class ShellExecutorPlugin extends Plugin {
|
|
|
27
27
|
|
|
28
28
|
install(framework) {
|
|
29
29
|
this._framework = framework
|
|
30
|
+
if (!this.config.workingDir) {
|
|
31
|
+
this.config.workingDir = framework?.getCwd?.() ?? process.cwd()
|
|
32
|
+
}
|
|
30
33
|
return this
|
|
31
34
|
}
|
|
32
35
|
|
|
@@ -44,7 +44,7 @@ class StoragePlugin extends Plugin {
|
|
|
44
44
|
_initStorage() {
|
|
45
45
|
if (this._storageManager) return;
|
|
46
46
|
|
|
47
|
-
const baseDir = path.resolve(process.cwd(), this.config.path);
|
|
47
|
+
const baseDir = path.resolve(this._framework?.getCwd?.() ?? process.cwd(), this.config.path);
|
|
48
48
|
const filePath = path.join(baseDir, `${this.config.namespace}.jsonl`);
|
|
49
49
|
|
|
50
50
|
// 传递 compaction 配置
|
|
@@ -587,7 +587,7 @@ class SubAgentManagerPlugin extends Plugin {
|
|
|
587
587
|
*/
|
|
588
588
|
_loadAgentsFromDir() {
|
|
589
589
|
// 默认使用 .foliko/agents 目录
|
|
590
|
-
const agentsDir = this.config.agentsDir || path.join(process.cwd(), '.foliko', 'agents')
|
|
590
|
+
const agentsDir = this.config.agentsDir || path.join(this._framework?.getCwd?.() ?? process.cwd(), '.foliko', 'agents')
|
|
591
591
|
//log.info(' _loadAgentsFromDir called, agentsDir:', agentsDir)
|
|
592
592
|
if (!fs.existsSync(agentsDir)) {
|
|
593
593
|
//log.info(' agentsDir not found or does not exist')
|
|
@@ -254,7 +254,7 @@ class TelegramPlugin extends Plugin {
|
|
|
254
254
|
const agent = this._framework.createSessionAgent(`telegram_${chatId}`, {
|
|
255
255
|
systemPrompt: this.systemPrompt,
|
|
256
256
|
sharedPrompt: `工作目录: {{WORK_DIR}}`,
|
|
257
|
-
metadata: { WORK_DIR: process.cwd() }
|
|
257
|
+
metadata: { WORK_DIR: this._framework?.getCwd?.() ?? process.cwd() }
|
|
258
258
|
})
|
|
259
259
|
this._sessionAgents.set(chatId, agent)
|
|
260
260
|
|
|
@@ -477,7 +477,7 @@ class TelegramPlugin extends Plugin {
|
|
|
477
477
|
async _downloadFile(fileId, ext, subDir) {
|
|
478
478
|
const path = require('path')
|
|
479
479
|
const fs = require('fs')
|
|
480
|
-
const saveDir = path.join(process.cwd(), '.foliko', 'data', subDir)
|
|
480
|
+
const saveDir = path.join(this._framework?.getCwd?.() ?? process.cwd(), '.foliko', 'data', subDir)
|
|
481
481
|
if (!fs.existsSync(saveDir)) fs.mkdirSync(saveDir, { recursive: true })
|
|
482
482
|
|
|
483
483
|
const fileName = `${Date.now()}_${Math.random().toString(36).substring(7)}.${ext}`
|
package/plugins/weixin-plugin.js
CHANGED
|
@@ -63,17 +63,20 @@ class WeixinPlugin extends Plugin {
|
|
|
63
63
|
this._initialized = false
|
|
64
64
|
// 预创建的 sessionScope 监听器(避免每次消息都创建/销毁)
|
|
65
65
|
this._sessionScopes = new Map()
|
|
66
|
-
|
|
67
|
-
this.downloader=new FileDownloader({
|
|
68
|
-
retries: 3,
|
|
69
|
-
baseDir:saveDir
|
|
70
|
-
})
|
|
66
|
+
this.downloader = null
|
|
71
67
|
this.agent=null
|
|
72
68
|
this.sessionId=null
|
|
73
69
|
}
|
|
74
70
|
|
|
75
71
|
install(framework) {
|
|
76
72
|
this._framework = framework
|
|
73
|
+
if (!this.downloader) {
|
|
74
|
+
const saveDir = path.join(this._framework?.getCwd?.() ?? process.cwd(), '.foliko', 'data', this.name)
|
|
75
|
+
this.downloader = new FileDownloader({
|
|
76
|
+
retries: 3,
|
|
77
|
+
baseDir: saveDir
|
|
78
|
+
})
|
|
79
|
+
}
|
|
77
80
|
// 注册微信发送工具
|
|
78
81
|
this._registerTools()
|
|
79
82
|
return this
|
|
@@ -473,7 +476,7 @@ class WeixinPlugin extends Plugin {
|
|
|
473
476
|
name: `weixin_${userId}`,
|
|
474
477
|
systemPrompt: this.systemPrompt,
|
|
475
478
|
sharedPrompt: `工作目录: {{WORK_DIR}}`,
|
|
476
|
-
metadata: { WORK_DIR: process.cwd() }
|
|
479
|
+
metadata: { WORK_DIR: this._framework?.getCwd?.() ?? process.cwd() }
|
|
477
480
|
})
|
|
478
481
|
|
|
479
482
|
this._sessionAgents.set(userId, agent)
|
|
@@ -247,7 +247,7 @@ class Skill {
|
|
|
247
247
|
sessionId: framework?._currentSessionId || 'unknown',
|
|
248
248
|
agent: null,
|
|
249
249
|
framework: framework,
|
|
250
|
-
cwd: process.cwd(), // 工作目录,用于路径解析
|
|
250
|
+
cwd: framework?.getCwd?.() ?? process.cwd(), // 工作目录,用于路径解析
|
|
251
251
|
};
|
|
252
252
|
// 支持 argumentParser 先解析参数
|
|
253
253
|
let parsedArgs = toolArgs.command || '';
|
|
@@ -520,8 +520,9 @@ class SkillManagerPlugin extends Plugin {
|
|
|
520
520
|
let totalLoaded = 0;
|
|
521
521
|
|
|
522
522
|
// 1. 扫描 skillsDirs
|
|
523
|
+
const baseCwd = this._framework?.getCwd?.() ?? process.cwd();
|
|
523
524
|
for (const skillsDir of this._skillsDirs) {
|
|
524
|
-
const resolvedDir = path.resolve(
|
|
525
|
+
const resolvedDir = path.resolve(baseCwd, skillsDir);
|
|
525
526
|
|
|
526
527
|
if (!fs.existsSync(resolvedDir)) {
|
|
527
528
|
continue;
|
|
@@ -881,7 +881,8 @@ class WorkflowPlugin extends Plugin {
|
|
|
881
881
|
* 加载目录中的工作流定义
|
|
882
882
|
*/
|
|
883
883
|
_loadWorkflows() {
|
|
884
|
-
const
|
|
884
|
+
const cwd = this._framework?.getCwd?.() ?? process.cwd();
|
|
885
|
+
const dir = path.resolve(cwd, this._workflowsDir);
|
|
885
886
|
if (!fs.existsSync(dir)) {
|
|
886
887
|
fs.mkdirSync(dir, { recursive: true });
|
|
887
888
|
// log.info(` Created workflows directory: ${dir}`);
|
package/src/core/agent-chat.js
CHANGED
|
@@ -475,7 +475,7 @@ class AgentChatHandler extends EventEmitter {
|
|
|
475
475
|
if (!this._aiClient) {
|
|
476
476
|
throw new Error('AI client not configured.');
|
|
477
477
|
}
|
|
478
|
-
|
|
478
|
+
this._validateToolCalls(messages);
|
|
479
479
|
const systemPrompt = framework.getSystemPrompt();
|
|
480
480
|
//await fs.promises.writeFile(`.${sessionId}_systemPrompt.md`, systemPrompt); // 调试用:保存系统提示词
|
|
481
481
|
const tools = this._getAITools(aiTool);
|
|
@@ -650,6 +650,7 @@ class AgentChatHandler extends EventEmitter {
|
|
|
650
650
|
if (!this._aiClient) {
|
|
651
651
|
throw new Error('AI client not configured.');
|
|
652
652
|
}
|
|
653
|
+
this._validateToolCalls(messages);
|
|
653
654
|
const systemPrompt = framework.getSystemPrompt();
|
|
654
655
|
const tools = this._getAITools(aiTool);
|
|
655
656
|
|
package/src/core/agent.js
CHANGED
package/src/core/constants.js
CHANGED
|
@@ -122,6 +122,9 @@ const SYSTEM_PLUGINS = new Set([
|
|
|
122
122
|
/** Agent 配置目录名 */
|
|
123
123
|
const AGENT_DIR = '.foliko';
|
|
124
124
|
|
|
125
|
+
/** 用户主目录下 Agent 配置目录名 */
|
|
126
|
+
const HOME_AGENT_DIR_NAME = '.foliko';
|
|
127
|
+
|
|
125
128
|
// ============================================================================
|
|
126
129
|
// Session 存储
|
|
127
130
|
// ============================================================================
|
|
@@ -186,6 +189,7 @@ module.exports = {
|
|
|
186
189
|
DEFAULT_PLUGIN_PRIORITY,
|
|
187
190
|
SYSTEM_PLUGINS,
|
|
188
191
|
AGENT_DIR,
|
|
192
|
+
HOME_AGENT_DIR_NAME,
|
|
189
193
|
|
|
190
194
|
// Storage
|
|
191
195
|
DEFAULT_SESSION_STORAGE_TYPE,
|
package/src/core/framework.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
7
8
|
const { AsyncLocalStorage } = require('async_hooks');
|
|
8
9
|
const { EventEmitter } = require('../utils/event-emitter');
|
|
9
10
|
const { PluginManager } = require('./plugin-manager');
|
|
@@ -15,6 +16,7 @@ const { ContextManager } = require('./context-manager');
|
|
|
15
16
|
const { SessionManager } = require('./session-manager');
|
|
16
17
|
const { CoordinatorManager } = require('./coordinator-manager');
|
|
17
18
|
const { Logger, LOG_LEVELS } = require('../utils/logger');
|
|
19
|
+
const { AGENT_DIR, HOME_AGENT_DIR_NAME, SYSTEM_PLUGINS } = require('./constants');
|
|
18
20
|
// 创建一个连接到 Framework 的 logger
|
|
19
21
|
function createFrameworkLogger(framework) {
|
|
20
22
|
const logger = new Logger({ namespace: 'foliko' });
|
|
@@ -92,6 +94,10 @@ class Framework extends EventEmitter {
|
|
|
92
94
|
/**
|
|
93
95
|
* @param {Object} config - 配置
|
|
94
96
|
* @param {boolean} [config.debug=false] - 调试模式
|
|
97
|
+
* @param {boolean} [config.silent=false] - 是否静默(不输出日志)
|
|
98
|
+
* @param {string} [config.cwd] - 初始工作目录(默认 process.cwd(),可后续用 setCwd 切换)
|
|
99
|
+
* @param {string} [config.agentDir='.foliko'] - 项目级 Agent 目录名
|
|
100
|
+
* @param {string} [config.homeAgentDir] - 用户级 Agent 目录(默认 ~/.foliko,可被 FOLIKO_HOME 环境变量覆盖)
|
|
95
101
|
*/
|
|
96
102
|
constructor(config = {}) {
|
|
97
103
|
super();
|
|
@@ -130,6 +136,15 @@ class Framework extends EventEmitter {
|
|
|
130
136
|
baseDir: '.foliko/sessions',
|
|
131
137
|
};
|
|
132
138
|
|
|
139
|
+
// 工作目录与 Agent 目录
|
|
140
|
+
this._cwd = config.cwd || process.cwd();
|
|
141
|
+
this._agentDir = config.agentDir || AGENT_DIR;
|
|
142
|
+
this._homeAgentDir =
|
|
143
|
+
config.homeAgentDir ||
|
|
144
|
+
process.env.FOLIKO_HOME ||
|
|
145
|
+
path.join(os.homedir(), HOME_AGENT_DIR_NAME);
|
|
146
|
+
this._changingCwd = false;
|
|
147
|
+
|
|
133
148
|
// 注册 logger 到 framework
|
|
134
149
|
this.logger = createFrameworkLogger(this);
|
|
135
150
|
|
|
@@ -525,7 +540,7 @@ class Framework extends EventEmitter {
|
|
|
525
540
|
}
|
|
526
541
|
|
|
527
542
|
// 获取工作目录
|
|
528
|
-
const cwd = options.cwd ||
|
|
543
|
+
const cwd = options.cwd || this._cwd;
|
|
529
544
|
const sessionDir = path.join(cwd, '.foliko', 'sessions');
|
|
530
545
|
|
|
531
546
|
// 获取或创建 session file path
|
|
@@ -567,7 +582,7 @@ class Framework extends EventEmitter {
|
|
|
567
582
|
return manager;
|
|
568
583
|
}
|
|
569
584
|
|
|
570
|
-
const cwd = options.cwd ||
|
|
585
|
+
const cwd = options.cwd || this._cwd;
|
|
571
586
|
const sessionDir = path.join(cwd, '.foliko', 'sessions');
|
|
572
587
|
const sessionFile = path.join(sessionDir, `${sessionId}.jsonl`);
|
|
573
588
|
|
|
@@ -673,6 +688,230 @@ class Framework extends EventEmitter {
|
|
|
673
688
|
return this;
|
|
674
689
|
}
|
|
675
690
|
|
|
691
|
+
// ============================================================================
|
|
692
|
+
// 工作目录与 Agent 目录
|
|
693
|
+
// ============================================================================
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* 获取当前工作目录
|
|
697
|
+
* @returns {string}
|
|
698
|
+
*/
|
|
699
|
+
getCwd() {
|
|
700
|
+
return this._cwd;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* 获取项目级 Agent 目录(绝对路径)
|
|
705
|
+
* @returns {string}
|
|
706
|
+
*/
|
|
707
|
+
getAgentDir() {
|
|
708
|
+
return path.resolve(this._cwd, this._agentDir);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* 获取用户级 Agent 目录(绝对路径)
|
|
713
|
+
* @returns {string}
|
|
714
|
+
*/
|
|
715
|
+
getHomeAgentDir() {
|
|
716
|
+
return this._homeAgentDir;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* 解析项目级 Agent 目录下的相对路径
|
|
721
|
+
* @param {...string} parts
|
|
722
|
+
* @returns {string}
|
|
723
|
+
*/
|
|
724
|
+
resolveAgentPath(...parts) {
|
|
725
|
+
return path.join(this.getAgentDir(), ...parts);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* 解析用户级 Agent 目录下的相对路径
|
|
730
|
+
* @param {...string} parts
|
|
731
|
+
* @returns {string}
|
|
732
|
+
*/
|
|
733
|
+
resolveHomeAgentPath(...parts) {
|
|
734
|
+
return path.join(this.getHomeAgentDir(), ...parts);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* 动态切换工作目录
|
|
739
|
+
*
|
|
740
|
+
* 切换后会自动:
|
|
741
|
+
* 1. 更新 PluginManager 状态文件位置
|
|
742
|
+
* 2. 销毁已存在的 Session 上下文(旧路径失效)
|
|
743
|
+
* 3. 通知所有已加载插件(onCwdChanged 钩子)并触发 reload
|
|
744
|
+
* 4. 发送 cwd:changed 事件
|
|
745
|
+
*
|
|
746
|
+
* @param {string} newCwd - 新工作目录(相对或绝对路径)
|
|
747
|
+
* @param {Object} [options]
|
|
748
|
+
* @param {boolean} [options.reload=true] - 是否自动重载已加载插件
|
|
749
|
+
* @param {string} [options.agentDir] - 顺带覆盖项目 agent 目录名
|
|
750
|
+
* @param {string} [options.homeAgentDir] - 顺带覆盖用户 agent 目录路径
|
|
751
|
+
* @returns {Promise<this>}
|
|
752
|
+
*/
|
|
753
|
+
async setCwd(newCwd, options = {}) {
|
|
754
|
+
if (this._changingCwd) {
|
|
755
|
+
throw new Error('setCwd re-entered during cwd change');
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const oldCwd = this._cwd;
|
|
759
|
+
const resolved = path.resolve(newCwd);
|
|
760
|
+
if (resolved === oldCwd) {
|
|
761
|
+
return this;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
this._changingCwd = true;
|
|
765
|
+
try {
|
|
766
|
+
this._cwd = resolved;
|
|
767
|
+
if (options.agentDir) this._agentDir = options.agentDir;
|
|
768
|
+
if (options.homeAgentDir) this._homeAgentDir = options.homeAgentDir;
|
|
769
|
+
|
|
770
|
+
// 1. 更新 PluginManager 状态文件位置
|
|
771
|
+
this.pluginManager._setStateFile(resolved);
|
|
772
|
+
|
|
773
|
+
// 2. 销毁已存在的 Session 上下文(旧路径失效)
|
|
774
|
+
const count = this._sessionContexts.size;
|
|
775
|
+
for (const sid of Array.from(this._sessionContexts.keys())) {
|
|
776
|
+
this.destroySessionContext(sid);
|
|
777
|
+
}
|
|
778
|
+
this.emit('session:contexts-invalidated', { oldCwd, newCwd: resolved, count });
|
|
779
|
+
|
|
780
|
+
// 3. 通知已加载插件 + 触发 reload
|
|
781
|
+
if (options.reload !== false) {
|
|
782
|
+
for (const entry of this.pluginManager.getAll()) {
|
|
783
|
+
const inst = entry.instance;
|
|
784
|
+
try {
|
|
785
|
+
if (typeof inst?.onCwdChanged === 'function') {
|
|
786
|
+
await inst.onCwdChanged(oldCwd, resolved, this);
|
|
787
|
+
}
|
|
788
|
+
await this.pluginManager.reload(entry.name);
|
|
789
|
+
} catch (err) {
|
|
790
|
+
this.logger.warn(`Plugin ${entry.name} failed during cwd change: ${err.message}`);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// 4. 发送 cwd:changed 事件
|
|
796
|
+
this.emit('cwd:changed', { oldCwd, newCwd: resolved, framework: this });
|
|
797
|
+
} finally {
|
|
798
|
+
this._changingCwd = false;
|
|
799
|
+
}
|
|
800
|
+
return this;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// ============================================================================
|
|
804
|
+
// 项目重扫 — 重新扫描当前 cwd 下的插件与技能
|
|
805
|
+
// ============================================================================
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* 重新扫描当前 cwd 下的插件与技能目录
|
|
809
|
+
*
|
|
810
|
+
* 工作流程:
|
|
811
|
+
* 1. 卸载 keepLoaded 列表外的所有已加载插件
|
|
812
|
+
* 2. 重新读取 DefaultPlugins 配置(新的 skillsDirs / agentsDir / pluginLinks)
|
|
813
|
+
* 3. 重新跑 bootstrapDefaults 加载所有默认插件(核心 + 自定义)
|
|
814
|
+
* 4. 同步 SkillManager._skillsDirs 并触发技能重扫
|
|
815
|
+
* 5. 发出 rescan:complete 事件
|
|
816
|
+
*
|
|
817
|
+
* 典型用法: 配合 setCwd 切换到新项目
|
|
818
|
+
* await framework.setCwd('D:/new-project');
|
|
819
|
+
* await framework.rescanProject();
|
|
820
|
+
*
|
|
821
|
+
* @param {Object} [options]
|
|
822
|
+
* @param {string[]} [options.keepLoaded] - 保留的插件名列表(默认保留系统插件)
|
|
823
|
+
* @param {boolean} [options.reloadKept=false] - 是否对 keepLoaded 中的插件也执行 reload
|
|
824
|
+
* @param {boolean} [options.reloadSkillManager=true] - 是否重扫技能
|
|
825
|
+
* @returns {Promise<{unloaded: string[], loaded: string[], keepLoaded: string[]}>}
|
|
826
|
+
*/
|
|
827
|
+
async rescanProject(options = {}) {
|
|
828
|
+
const { bootstrapDefaults, DefaultPlugins, loadAgentConfig } = require('../../plugins/default-plugins');
|
|
829
|
+
const keepList = options.keepLoaded || Array.from(SYSTEM_PLUGINS);
|
|
830
|
+
const keepSet = new Set(keepList);
|
|
831
|
+
const unloadList = [];
|
|
832
|
+
const loadList = [];
|
|
833
|
+
|
|
834
|
+
// 1. 卸载 keepSet 之外的插件
|
|
835
|
+
for (const entry of Array.from(this.pluginManager.getAll())) {
|
|
836
|
+
if (keepSet.has(entry.name)) continue;
|
|
837
|
+
const name = entry.name;
|
|
838
|
+
try {
|
|
839
|
+
await this.pluginManager.unload(name);
|
|
840
|
+
// 同步从注册表移除,避免 bootstrapDefaults 的 has() 跳过
|
|
841
|
+
this.pluginManager._plugins.delete(name);
|
|
842
|
+
unloadList.push(name);
|
|
843
|
+
} catch (err) {
|
|
844
|
+
this.logger.warn(`rescanProject: failed to unload ${name}: ${err.message}`);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// 2. 确保 defaults 插件已加载,重新读取配置
|
|
849
|
+
let defaultsPlugin = this.pluginManager.get('defaults');
|
|
850
|
+
if (!defaultsPlugin) {
|
|
851
|
+
const dp = new DefaultPlugins({ agentDir: this._agentDir });
|
|
852
|
+
try {
|
|
853
|
+
await this.loadPlugin(dp);
|
|
854
|
+
defaultsPlugin = dp;
|
|
855
|
+
} catch (err) {
|
|
856
|
+
this.logger.warn(`rescanProject: failed to load defaults: ${err.message}`);
|
|
857
|
+
}
|
|
858
|
+
} else {
|
|
859
|
+
try {
|
|
860
|
+
await defaultsPlugin.reload(this);
|
|
861
|
+
} catch (err) {
|
|
862
|
+
this.logger.warn(`rescanProject: failed to reload defaults: ${err.message}`);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
const agentConfig = defaultsPlugin
|
|
866
|
+
? defaultsPlugin.getConfig() || loadAgentConfig(this, this._agentDir)
|
|
867
|
+
: loadAgentConfig(this, this._agentDir);
|
|
868
|
+
|
|
869
|
+
// 3. 跑 bootstrapDefaults 加载默认插件(核心 + 自定义)
|
|
870
|
+
const beforeNames = new Set(this.pluginManager.getAll().map((p) => p.name));
|
|
871
|
+
try {
|
|
872
|
+
await bootstrapDefaults(this, { _config: agentConfig, _skipConfigLoad: true });
|
|
873
|
+
} catch (err) {
|
|
874
|
+
this.logger.warn(`rescanProject: bootstrapDefaults failed: ${err.message}`);
|
|
875
|
+
}
|
|
876
|
+
for (const name of this.pluginManager.getAll().map((p) => p.name)) {
|
|
877
|
+
if (!beforeNames.has(name)) loadList.push(name);
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// 4. 同步并重扫技能
|
|
881
|
+
if (options.reloadSkillManager !== false) {
|
|
882
|
+
const skillManager = this.pluginManager.get('skill-manager');
|
|
883
|
+
if (skillManager) {
|
|
884
|
+
if (Array.isArray(agentConfig.skillsDirs)) {
|
|
885
|
+
skillManager._skillsDirs = agentConfig.skillsDirs;
|
|
886
|
+
}
|
|
887
|
+
try {
|
|
888
|
+
await skillManager.reload(this);
|
|
889
|
+
} catch (err) {
|
|
890
|
+
this.logger.warn(`rescanProject: skill-manager reload failed: ${err.message}`);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// 5. (可选) 对保留的插件也执行 reload
|
|
896
|
+
if (options.reloadKept) {
|
|
897
|
+
for (const name of keepList) {
|
|
898
|
+
if (!this.pluginManager.has(name)) continue;
|
|
899
|
+
try {
|
|
900
|
+
await this.pluginManager.reload(name);
|
|
901
|
+
} catch (err) {
|
|
902
|
+
this.logger.warn(`rescanProject: reload kept ${name} failed: ${err.message}`);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const result = { unloaded: unloadList, loaded: loadList, keepLoaded: keepList };
|
|
908
|
+
this.emit('rescan:complete', result);
|
|
909
|
+
this.logger.info(
|
|
910
|
+
`rescanProject: unloaded=${unloadList.length} loaded=${loadList.length} kept=${keepList.length}`
|
|
911
|
+
);
|
|
912
|
+
return result;
|
|
913
|
+
}
|
|
914
|
+
|
|
676
915
|
/**
|
|
677
916
|
* 启动 Session 自动清理
|
|
678
917
|
* @param {number} [intervalMs=60000] - 检查间隔
|
package/src/core/plugin-base.js
CHANGED
|
@@ -244,6 +244,17 @@ class Plugin {
|
|
|
244
244
|
this._autoRegisterSubAgents();
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
/**
|
|
248
|
+
* 工作目录变更钩子 — 框架级 cwd 切换时由 Framework.setCwd 调用
|
|
249
|
+
* 子类可重写以重新加载 cwd 相关的资源(路径、文件句柄等)
|
|
250
|
+
* @param {string} oldCwd
|
|
251
|
+
* @param {string} newCwd
|
|
252
|
+
* @param {Framework} framework
|
|
253
|
+
*/
|
|
254
|
+
onCwdChanged(oldCwd, newCwd, framework) {
|
|
255
|
+
// 默认 no-op
|
|
256
|
+
}
|
|
257
|
+
|
|
247
258
|
/**
|
|
248
259
|
* 卸载插件 - 清理资源
|
|
249
260
|
* @param {Framework} framework - 框架实例
|
|
@@ -22,7 +22,12 @@ class PluginManager {
|
|
|
22
22
|
this.framework = framework;
|
|
23
23
|
this._plugins = new Map();
|
|
24
24
|
this._loading = false;
|
|
25
|
-
this._stateFile = path.join(
|
|
25
|
+
this._stateFile = path.join(
|
|
26
|
+
framework?.getCwd?.() ?? process.cwd(),
|
|
27
|
+
'.foliko',
|
|
28
|
+
'data',
|
|
29
|
+
'plugins-state.json'
|
|
30
|
+
);
|
|
26
31
|
this._knownPlugins = new Set(); // 已知插件列表(包括未加载的)
|
|
27
32
|
|
|
28
33
|
// 创建子日志器
|
|
@@ -111,6 +116,16 @@ class PluginManager {
|
|
|
111
116
|
return this._stateFile;
|
|
112
117
|
}
|
|
113
118
|
|
|
119
|
+
/**
|
|
120
|
+
* 更新状态文件路径(Framework.setCwd 时调用)
|
|
121
|
+
* @param {string} cwd - 新的工作目录
|
|
122
|
+
* @private
|
|
123
|
+
*/
|
|
124
|
+
_setStateFile(cwd) {
|
|
125
|
+
this._stateFile = path.join(cwd, '.foliko', 'data', 'plugins-state.json');
|
|
126
|
+
this._stateCache = null;
|
|
127
|
+
}
|
|
128
|
+
|
|
114
129
|
/**
|
|
115
130
|
* 保存插件状态到文件
|
|
116
131
|
* 注意:AI 插件配置不保存(从环境变量和命令行获取)
|
|
@@ -442,7 +457,8 @@ class PluginManager {
|
|
|
442
457
|
* @private
|
|
443
458
|
*/
|
|
444
459
|
async _discoverCustomPlugins() {
|
|
445
|
-
const
|
|
460
|
+
const cwd = this.framework?.getCwd?.() ?? process.cwd();
|
|
461
|
+
const pluginsDir = path.resolve(cwd, '.foliko', 'plugins');
|
|
446
462
|
if (!fs.existsSync(pluginsDir)) {
|
|
447
463
|
return;
|
|
448
464
|
}
|