foliko 1.1.92 → 2.0.0

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 (212) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/CLAUDE.md +56 -30
  3. package/REFACTORING_PLAN.md +645 -0
  4. package/docs/architecture.md +131 -0
  5. package/docs/migration.md +57 -0
  6. package/docs/public-api.md +138 -0
  7. package/docs/usage.md +385 -0
  8. package/examples/ambient-example.js +20 -137
  9. package/examples/basic.js +21 -48
  10. package/examples/bootstrap.js +16 -74
  11. package/examples/mcp-example.js +6 -29
  12. package/examples/skill-example.js +6 -19
  13. package/examples/workflow.js +8 -56
  14. package/package.json +8 -4
  15. package/plugins/README.md +49 -0
  16. package/plugins/{ambient-agent → ambient}/EventWatcher.js +1 -1
  17. package/plugins/{ambient-agent → ambient}/ExplorerLoop.js +3 -3
  18. package/plugins/{ambient-agent → ambient}/GoalManager.js +2 -2
  19. package/plugins/ambient/README.md +14 -0
  20. package/plugins/{ambient-agent → ambient}/Reflector.js +1 -1
  21. package/plugins/{ambient-agent → ambient}/StateStore.js +1 -1
  22. package/plugins/{ambient-agent → ambient}/index.js +2 -2
  23. package/plugins/{ai-plugin.js → core/ai/index.js} +14 -30
  24. package/plugins/{audit-plugin.js → core/audit/index.js} +3 -30
  25. package/plugins/{coordinator-plugin.js → core/coordinator/index.js} +3 -35
  26. package/plugins/core/default/bootstrap.js +202 -0
  27. package/plugins/core/default/config.js +220 -0
  28. package/plugins/core/default/index.js +58 -0
  29. package/plugins/core/mcp/index.js +1 -0
  30. package/plugins/{python-plugin-loader.js → core/python-loader/index.js} +7 -187
  31. package/plugins/{rules-plugin.js → core/rules/index.js} +121 -64
  32. package/plugins/{scheduler-plugin.js → core/scheduler/index.js} +12 -114
  33. package/plugins/{session-plugin.js → core/session/index.js} +9 -73
  34. package/{src/capabilities/skill-manager.js → plugins/core/skill-manager/index.js} +64 -18
  35. package/plugins/{storage-plugin.js → core/storage/index.js} +5 -29
  36. package/plugins/{subagent-plugin.js → core/sub-agent/index.js} +10 -171
  37. package/plugins/{think-plugin.js → core/think/index.js} +24 -91
  38. package/{src/capabilities/workflow-engine.js → plugins/core/workflow/index.js} +87 -85
  39. package/plugins/default-plugins.js +6 -720
  40. package/plugins/{data-splitter-plugin.js → executors/data-splitter/index.js} +9 -83
  41. package/plugins/{extension-executor-plugin.js → executors/extension/index.js} +13 -97
  42. package/plugins/{python-executor-plugin.js → executors/python/index.js} +6 -31
  43. package/plugins/{shell-executor-plugin.js → executors/shell/index.js} +2 -5
  44. package/plugins/install/README.md +9 -0
  45. package/plugins/{install-plugin.js → install/index.js} +3 -3
  46. package/plugins/{file-system-plugin.js → io/file-system/index.js} +34 -236
  47. package/plugins/{web-plugin.js → io/web/index.js} +11 -113
  48. package/plugins/memory/README.md +13 -0
  49. package/plugins/{memory-plugin.js → memory/index.js} +4 -18
  50. package/plugins/messaging/email/README.md +19 -0
  51. package/plugins/{email → messaging/email}/index.js +2 -2
  52. package/plugins/{feishu-plugin.js → messaging/feishu/index.js} +3 -3
  53. package/plugins/{qq-plugin.js → messaging/qq/index.js} +5 -16
  54. package/plugins/{telegram-plugin.js → messaging/telegram/index.js} +3 -3
  55. package/plugins/{weixin-plugin.js → messaging/weixin/index.js} +15 -15
  56. package/plugins/{plugin-manager-plugin.js → plugin-manager/index.js} +36 -180
  57. package/plugins/{tools-plugin.js → tools/index.js} +68 -116
  58. package/plugins/trading/README.md +15 -0
  59. package/plugins/{gate-trading.js → trading/index.js} +8 -8
  60. package/{examples → sandbox}/test-concurrent-chat.js +2 -2
  61. package/{examples → sandbox}/test-long-chat.js +2 -2
  62. package/{examples → sandbox}/test-session-chat.js +2 -2
  63. package/{examples → sandbox}/test-web-plugin.js +1 -1
  64. package/{examples → sandbox}/test-weixin-feishu.js +2 -2
  65. package/src/agent/base.js +56 -0
  66. package/src/{core/agent-chat.js → agent/chat.js} +11 -11
  67. package/src/{core/coordinator-manager.js → agent/coordinator.js} +3 -3
  68. package/src/agent/index.js +111 -0
  69. package/src/agent/main.js +337 -0
  70. package/src/agent/prompt.js +78 -0
  71. package/src/agent/sub.js +198 -0
  72. package/src/agent/worker.js +104 -0
  73. package/{cli/bin/foliko.js → src/cli/bin.js} +1 -1
  74. package/{cli/src → src/cli}/commands/chat.js +25 -21
  75. package/{cli/src → src/cli}/index.js +1 -0
  76. package/{cli/src → src/cli}/ui/chat-ui-old.js +40 -178
  77. package/{cli/src → src/cli}/ui/chat-ui.js +3 -3
  78. package/{cli/src → src/cli}/ui/components/footer-bar.js +1 -1
  79. package/src/{core → common}/constants.js +3 -0
  80. package/src/common/errors.js +402 -0
  81. package/src/{utils → common}/logger.js +33 -0
  82. package/src/{utils/chat-queue.js → common/queue.js} +2 -2
  83. package/src/config/plugin-config.js +50 -0
  84. package/src/context/agent.js +32 -0
  85. package/src/context/compaction-prompts.js +170 -0
  86. package/src/context/compaction-utils.js +191 -0
  87. package/src/context/compressor.js +413 -0
  88. package/src/context/index.js +9 -0
  89. package/src/{core/context-manager.js → context/manager.js} +1 -1
  90. package/src/context/request.js +50 -0
  91. package/src/context/session.js +33 -0
  92. package/src/context/storage.js +30 -0
  93. package/src/executors/mcp-client.js +153 -0
  94. package/src/executors/mcp-desc.js +236 -0
  95. package/src/executors/mcp-executor.js +91 -956
  96. package/src/{core → framework}/command-registry.js +1 -1
  97. package/src/framework/framework.js +300 -0
  98. package/src/framework/index.js +18 -0
  99. package/src/framework/lifecycle.js +203 -0
  100. package/src/framework/loader.js +78 -0
  101. package/src/framework/registry.js +86 -0
  102. package/src/{core/ui-extension-context.js → framework/ui-extension.js} +1 -1
  103. package/src/index.js +130 -15
  104. package/src/llm/index.js +26 -0
  105. package/src/llm/provider.js +212 -0
  106. package/src/llm/registry.js +11 -0
  107. package/src/{core/token-counter.js → llm/tokens.js} +4 -37
  108. package/src/{core/plugin-base.js → plugin/base.js} +10 -136
  109. package/src/plugin/index.js +14 -0
  110. package/src/plugin/loader.js +101 -0
  111. package/src/plugin/manager.js +261 -0
  112. package/src/{core → session}/branch-summary-auto.js +2 -2
  113. package/src/{core/chat-session.js → session/chat.js} +2 -2
  114. package/src/session/index.js +7 -0
  115. package/src/{core/session-manager.js → session/session.js} +2 -2
  116. package/src/session/ttl.js +92 -0
  117. package/src/{core/jsonl-storage.js → storage/jsonl.js} +1 -1
  118. package/src/tool/executor.js +85 -0
  119. package/src/tool/index.js +15 -0
  120. package/src/tool/registry.js +143 -0
  121. package/src/{core/tool-router.js → tool/router.js} +17 -124
  122. package/src/tool/schema.js +108 -0
  123. package/src/utils/data-splitter.js +1 -1
  124. package/src/utils/download.js +1 -1
  125. package/src/utils/index.js +6 -6
  126. package/src/utils/message-validator.js +1 -1
  127. package/tests/core/context-storage.test.js +46 -0
  128. package/tests/core/llm.test.js +54 -0
  129. package/tests/core/plugin.test.js +42 -0
  130. package/tests/core/tool.test.js +60 -0
  131. package/tests/setup.js +10 -0
  132. package/tests/smoke.test.js +58 -0
  133. package/vitest.config.js +9 -0
  134. package/cli/src/daemon.js +0 -149
  135. package/docs/CONTEXT_DESIGN.md +0 -1596
  136. package/docs/ai-sdk-optimization.md +0 -655
  137. package/docs/features.md +0 -120
  138. package/docs/qq-bot.md +0 -976
  139. package/docs/quick-reference.md +0 -160
  140. package/docs/user-manual.md +0 -1391
  141. package/images/geometric_shapes.jpg +0 -0
  142. package/images/sunset_mountain_lake.jpg +0 -0
  143. package/skills/poster-guide/SKILL.md +0 -792
  144. package/src/capabilities/index.js +0 -11
  145. package/src/core/agent.js +0 -808
  146. package/src/core/context-compressor.js +0 -959
  147. package/src/core/enhanced-context-compressor.js +0 -210
  148. package/src/core/framework.js +0 -1422
  149. package/src/core/index.js +0 -30
  150. package/src/core/plugin-manager.js +0 -961
  151. package/src/core/provider-registry.js +0 -159
  152. package/src/core/provider.js +0 -156
  153. package/src/core/request-context.js +0 -98
  154. package/src/core/subagent.js +0 -442
  155. package/src/core/system-prompt-builder.js +0 -120
  156. package/src/core/tool-executor.js +0 -202
  157. package/src/core/tool-registry.js +0 -517
  158. package/src/core/worker-agent.js +0 -192
  159. package/src/executors/executor-base.js +0 -58
  160. package/src/utils/error-boundary.js +0 -363
  161. package/src/utils/error.js +0 -374
  162. package/system.md +0 -1645
  163. package/website_v2/README.md +0 -57
  164. package/website_v2/SPEC.md +0 -1
  165. package/website_v2/docs/api.html +0 -128
  166. package/website_v2/docs/configuration.html +0 -147
  167. package/website_v2/docs/plugin-development.html +0 -129
  168. package/website_v2/docs/project-structure.html +0 -89
  169. package/website_v2/docs/skill-development.html +0 -85
  170. package/website_v2/index.html +0 -489
  171. package/website_v2/scripts/main.js +0 -93
  172. package/website_v2/styles/animations.css +0 -8
  173. package/website_v2/styles/docs.css +0 -83
  174. package/website_v2/styles/main.css +0 -417
  175. package/xhs_auth.json +0 -268
  176. package//346/265/267/346/212/245/346/217/222/344/273/266.md +0 -621
  177. /package/plugins/{ambient-agent → ambient}/constants.js +0 -0
  178. /package/plugins/{email → messaging/email}/constants.js +0 -0
  179. /package/plugins/{email → messaging/email}/handlers.js +0 -0
  180. /package/plugins/{email → messaging/email}/monitor.js +0 -0
  181. /package/plugins/{email → messaging/email}/parser.js +0 -0
  182. /package/plugins/{email → messaging/email}/reply.js +0 -0
  183. /package/plugins/{email → messaging/email}/utils.js +0 -0
  184. /package/{examples → sandbox}/test-chat.js +0 -0
  185. /package/{examples → sandbox}/test-mcp.js +0 -0
  186. /package/{examples → sandbox}/test-reload.js +0 -0
  187. /package/{examples → sandbox}/test-telegram.js +0 -0
  188. /package/{examples → sandbox}/test-tg-bot.js +0 -0
  189. /package/{examples → sandbox}/test-tg-simple.js +0 -0
  190. /package/{examples → sandbox}/test-tg.js +0 -0
  191. /package/{examples → sandbox}/test-think.js +0 -0
  192. /package/src/{core/sub-agent-config.js → agent/sub-config.js} +0 -0
  193. /package/{cli/src → src/cli}/commands/daemon.js +0 -0
  194. /package/{cli/src → src/cli}/commands/list.js +0 -0
  195. /package/{cli/src → src/cli}/commands/plugin.js +0 -0
  196. /package/{cli/src → src/cli}/ui/components/agent-mention-provider.js +0 -0
  197. /package/{cli/src → src/cli}/ui/components/chained-autocomplete-provider.js +0 -0
  198. /package/{cli/src → src/cli}/ui/components/message-bubble.js +0 -0
  199. /package/{cli/src → src/cli}/ui/components/status-bar.js +0 -0
  200. /package/{cli/src → src/cli}/utils/ansi.js +0 -0
  201. /package/{cli/src → src/cli}/utils/config.js +0 -0
  202. /package/{cli/src → src/cli}/utils/markdown.js +0 -0
  203. /package/{cli/src → src/cli}/utils/plugin-config.js +0 -0
  204. /package/{cli/src → src/cli}/utils/render-diff.js +0 -0
  205. /package/src/{utils/circuit-breaker.js → common/circuit.js} +0 -0
  206. /package/src/{utils/edit-diff.js → common/diff.js} +0 -0
  207. /package/src/{utils/event-emitter.js → common/events.js} +0 -0
  208. /package/src/{utils → common}/id.js +0 -0
  209. /package/src/{utils → common}/retry.js +0 -0
  210. /package/src/{core/notification-manager.js → notification/manager.js} +0 -0
  211. /package/src/{core/session-entry.js → session/entry.js} +0 -0
  212. /package/src/{core/storage-manager.js → storage/manager.js} +0 -0
@@ -5,52 +5,12 @@
5
5
 
6
6
  const fs = require('fs')
7
7
  const path = require('path')
8
- const { Plugin } = require('../src/core/plugin-base')
9
- const { logger } = require('../src/utils/logger')
8
+ const { Plugin } = require('../../../src/plugin/base')
9
+ const { logger } = require('../../../src/common/logger')
10
10
  const log = logger.child('PythonPluginLoader')
11
11
  const { z } = require('zod')
12
12
  const { zodSchemaToMarkdown } = require('@chnak/zod-to-markdown')
13
-
14
- // 将 JSON Schema 转换为 Zod Schema
15
- function jsonSchemaToZod(jsonSchema) {
16
- if (!jsonSchema || jsonSchema.type !== 'object') {
17
- return z.object({})
18
- }
19
-
20
- const properties = jsonSchema.properties || {}
21
- const required = jsonSchema.required || []
22
-
23
- const shape = {}
24
- for (const [key, prop] of Object.entries(properties)) {
25
- let zodType
26
- switch (prop.type) {
27
- case 'string':
28
- zodType = z.string()
29
- break
30
- case 'number':
31
- zodType = z.number()
32
- break
33
- case 'boolean':
34
- zodType = z.boolean()
35
- break
36
- case 'array':
37
- zodType = z.array(z.any())
38
- break
39
- case 'object':
40
- zodType = jsonSchemaToZod(prop)
41
- break
42
- default:
43
- zodType = z.any()
44
- }
45
- // 如果不是 required 字段,则标记为 optional
46
- if (!required.includes(key)) {
47
- zodType = zodType.optional()
48
- }
49
- shape[key] = zodType
50
- }
51
-
52
- return z.object(shape)
53
- }
13
+ const { jsonSchemaToZod } = require('../../../src/tool/schema')
54
14
 
55
15
  class PythonPluginLoader extends Plugin {
56
16
  constructor(config = {}) {
@@ -62,7 +22,7 @@ class PythonPluginLoader extends Plugin {
62
22
  this._agentDir = config.agentDir || '.foliko' // 默认从 .foliko 目录加载 Python 插件
63
23
  this._pythonPlugins = new Map()
64
24
  this._framework = null
65
- this.system = true
25
+ this.system = true
66
26
  }
67
27
 
68
28
  install(framework) {
@@ -80,20 +40,6 @@ class PythonPluginLoader extends Plugin {
80
40
  // 加载所有 Python 插件(会注册工具到 ExtensionExecutor)
81
41
  this._loadPythonPlugins()
82
42
 
83
- // 监听 agent 创建事件,附加 Python 插件信息到系统提示词
84
- // framework.on('agent:created', (agent) => {
85
- // this._refreshAgentPythonPluginsPrompt(agent)
86
- // })
87
-
88
- // 等待框架就绪后,刷新所有已有 agent 的 Python 插件提示词
89
- // if (framework._ready) {
90
- // this._refreshAllAgentsPythonPluginsPrompt(framework)
91
- // } else {
92
- // framework.once('framework:ready', () => {
93
- // this._refreshAllAgentsPythonPluginsPrompt(framework)
94
- // })
95
- // }
96
-
97
43
  return this
98
44
  }
99
45
 
@@ -121,7 +67,7 @@ class PythonPluginLoader extends Plugin {
121
67
  if (Object.keys(params).length > 0) {
122
68
  try {
123
69
  const jsonSchema = this._paramsToJsonSchema(params)
124
- const zodSchema = this._jsonSchemaToZod(jsonSchema)
70
+ const zodSchema = jsonSchemaToZod(jsonSchema)
125
71
  const schemaMd = zodSchemaToMarkdown(zodSchema)
126
72
  if (schemaMd) {
127
73
  desc += `**参数:**\n\n${schemaMd}\n`
@@ -173,133 +119,7 @@ class PythonPluginLoader extends Plugin {
173
119
  * 将 JSON Schema 转换为 Zod schema
174
120
  */
175
121
  _jsonSchemaToZod(jsonSchema) {
176
- if (!jsonSchema || !jsonSchema.properties) {
177
- return z.object({})
178
- }
179
-
180
- try {
181
- const shape = {}
182
- const properties = jsonSchema.properties
183
- const required = jsonSchema.required || []
184
-
185
- for (const [key, prop] of Object.entries(properties)) {
186
- shape[key] = this._jsonSchemaPropToZod(prop, required.includes(key))
187
- }
188
-
189
- return z.object(shape)
190
- } catch (e) {
191
- return z.object({})
192
- }
193
- }
194
-
195
- /**
196
- * 将 JSON Schema 属性转换为 Zod 类型(支持完整嵌套)
197
- */
198
- _jsonSchemaPropToZod(prop, isRequired) {
199
- // 1. 处理 enum(枚举值)
200
- if (prop.enum) {
201
- let zodType = z.string().enum(prop.enum)
202
- return isRequired ? zodType : zodType.optional()
203
- }
204
-
205
- const type = prop.type || 'string'
206
-
207
- // 2. 简单类型
208
- switch (type) {
209
- case 'string':
210
- return isRequired ? z.string() : z.string().optional()
211
- case 'number':
212
- case 'integer':
213
- return isRequired ? z.number() : z.number().optional()
214
- case 'boolean':
215
- return isRequired ? z.boolean() : z.boolean().optional()
216
- default:
217
- break
218
- }
219
-
220
- // 3. 数组类型
221
- if (type === 'array') {
222
- let itemsZod = z.any()
223
- if (prop.items) {
224
- // 支持 oneOf(联合类型数组)
225
- if (prop.items.oneOf) {
226
- const unionTypes = prop.items.oneOf.map(item => this._jsonSchemaPropToZod(item, true))
227
- if (unionTypes.length > 0) {
228
- itemsZod = z.union(unionTypes)
229
- }
230
- } else {
231
- itemsZod = this._jsonSchemaPropToZod(prop.items, true)
232
- }
233
- }
234
- return isRequired ? z.array(itemsZod) : z.array(itemsZod).optional()
235
- }
236
-
237
- // 4. 对象类型
238
- if (type === 'object') {
239
- if (prop.properties && Object.keys(prop.properties).length > 0) {
240
- const nested = {}
241
- for (const [k, v] of Object.entries(prop.properties)) {
242
- nested[k] = this._jsonSchemaPropToZod(v, prop.required?.includes(k) || false)
243
- }
244
- const objZod = z.object(nested)
245
- return isRequired ? objZod : objZod.optional()
246
- }
247
- // 无 properties 的 object,视为 record
248
- const recordZod = z.record(z.any())
249
- return isRequired ? recordZod : recordZod.optional()
250
- }
251
-
252
- // 5. oneOf(联合类型)
253
- if (prop.oneOf) {
254
- const unionTypes = prop.oneOf.map(item => this._jsonSchemaPropToZod(item, true))
255
- if (unionTypes.length > 0) {
256
- const unionZod = z.union(unionTypes)
257
- return isRequired ? unionZod : unionZod.optional()
258
- }
259
- }
260
-
261
- // 6. Fallback
262
- return isRequired ? z.any() : z.any().optional()
263
- }
264
-
265
- /**
266
- * 刷新单个 agent 的 Python 插件提示词
267
- */
268
- _refreshAgentPythonPluginsPrompt(agent) {
269
- const existingPrompt = agent._originalPrompt || ''
270
- if (existingPrompt.includes('【Python 插件】')) {
271
- return
272
- }
273
-
274
- const pyDesc = this._buildPythonPluginsDescription()
275
- if (!pyDesc) return
276
-
277
- // 将 Python 插件描述追加到系统提示词
278
- //agent.setSystemPrompt(existingPrompt + '\n\n' + pyDesc)
279
- }
280
-
281
- /**
282
- * 刷新所有 agent 的 Python 插件提示词
283
- */
284
- _refreshAllAgentsPythonPluginsPrompt(framework) {
285
- const visited = new Set()
286
-
287
- const traverse = (agent) => {
288
- if (!agent || visited.has(agent)) return
289
- visited.add(agent)
290
- this._refreshAgentPythonPluginsPrompt(agent)
291
-
292
- // 递归处理子 agent
293
- const subAgents = agent.getSubAgents?.() || agent._subAgents || new Map()
294
- for (const [name, subAgentInfo] of subAgents) {
295
- traverse(subAgentInfo.agent)
296
- }
297
- }
298
-
299
- const agents = framework._agents || []
300
- for (const agent of agents) {
301
- traverse(agent)
302
- }
122
+ return jsonSchemaToZod(jsonSchema)
303
123
  }
304
124
 
305
125
  /**
@@ -496,7 +316,7 @@ class PythonPluginLoader extends Plugin {
496
316
  const inner = normalized.slice(1, -1)
497
317
  const properties = {}
498
318
  const required = []
499
-
319
+
500
320
  const parts = this._splitByComma(inner)
501
321
  for (const part of parts) {
502
322
  const colonIdx = part.indexOf(':')
@@ -3,26 +3,21 @@
3
3
  * 控制工具调用权限、内容过滤、触发动作
4
4
  */
5
5
 
6
- const { Plugin } = require('../src/core/plugin-base')
7
- const { logger } = require('../src/utils/logger')
6
+ const { Plugin } = require('../../../src/plugin/base')
7
+ const { logger } = require('../../../src/common/logger')
8
8
  const log = logger.child('RulesPlugin')
9
9
  const { z } = require('zod')
10
10
  const fs = require('fs')
11
11
  const path = require('path')
12
+ const os = require('os')
12
13
 
13
- /**
14
- * 内置规则类型
15
- */
16
14
  const RuleType = {
17
- ALLOW: 'allow', // 允许
18
- DENY: 'deny', // 拒绝
19
- TRANSFORM: 'transform', // 转换
20
- RATE_LIMIT: 'rate_limit' // 限流
15
+ ALLOW: 'allow',
16
+ DENY: 'deny',
17
+ TRANSFORM: 'transform',
18
+ RATE_LIMIT: 'rate_limit'
21
19
  }
22
20
 
23
- /**
24
- * 规则评估结果
25
- */
26
21
  class RuleResult {
27
22
  constructor(allowed, rule = null, transform = null) {
28
23
  this.allowed = allowed
@@ -38,13 +33,14 @@ class RulesPlugin extends Plugin {
38
33
  this.version = '1.0.0'
39
34
  this.description = '规则引擎插件,用于控制工具调用权限、内容过滤、触发动作'
40
35
  this.priority = 25
41
- this.system = true
42
-
36
+ this.system = true
37
+
43
38
  this.config = {
44
39
  rulesDir: config.rulesDir || '.foliko/rules',
45
40
  autoLoad: config.autoLoad !== false
46
41
  }
47
42
 
43
+ this._homeRulesDir = path.join(os.homedir(), '.foliko', 'rules')
48
44
  this._framework = null
49
45
  this._rules = []
50
46
  this._ruleStats = {
@@ -61,12 +57,11 @@ class RulesPlugin extends Plugin {
61
57
  }
62
58
 
63
59
  start(framework) {
64
- // 自动加载规则文件
65
- if (this.config.autoLoad && this.config.rulesDir) {
60
+ if (this.config.autoLoad && this.config.rulesDir && !this._loaded) {
61
+ this._loaded = true;
66
62
  this.loadFromDirectory(this.config.rulesDir)
67
63
  }
68
64
 
69
- // 注册规则管理工具
70
65
  framework.registerTool({
71
66
  name: 'rules_list',
72
67
  description: '列出所有规则',
@@ -164,63 +159,131 @@ class RulesPlugin extends Plugin {
164
159
  return this
165
160
  }
166
161
 
167
- /**
168
- * 从目录加载规则文件
169
- */
170
162
  loadFromDirectory(rulesDir) {
171
- const resolvedDir = path.resolve(this._framework?.getCwd?.() ?? process.cwd(), rulesDir)
172
-
173
- if (!fs.existsSync(resolvedDir)) {
174
- //log.info(` Rules directory not found: ${resolvedDir}`)
175
- return
163
+ const cwd = this._framework?.getCwd?.() ?? process.cwd()
164
+ const dirs = [
165
+ this._homeRulesDir, // ~/.foliko/rules(优先)
166
+ path.resolve(cwd, rulesDir || this.config.rulesDir), // 项目级
167
+ ]
168
+
169
+ for (const resolvedDir of dirs) {
170
+ if (!fs.existsSync(resolvedDir)) continue
171
+
172
+ try {
173
+ const files = fs.readdirSync(resolvedDir).filter(f => f.endsWith('.json') || f.endsWith('.md'))
174
+
175
+ for (const file of files) {
176
+ const filePath = path.join(resolvedDir, file)
177
+ const content = fs.readFileSync(filePath, 'utf-8')
178
+
179
+ if (file.endsWith('.json')) {
180
+ const rules = JSON.parse(content)
181
+ for (const rule of rules) {
182
+ if (rule.pattern) rule.pattern = new RegExp(rule.pattern)
183
+ rule.enabled = rule.enabled !== false
184
+ this._rules.push(rule)
185
+ }
186
+ } else if (file.endsWith('.md')) {
187
+ // 解析 YAML frontmatter
188
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
189
+ if (!match) continue;
190
+ const fm = this._parseFrontmatter(match[1]);
191
+ const rule = {
192
+ name: fm.name || fm.id || path.basename(file, '.md'),
193
+ type: fm.type || 'deny',
194
+ target: fm.target || (fm.trigger?.conditions?.[0]?.tool_name) || '*',
195
+ pattern: fm.condition ? new RegExp(fm.condition) : null,
196
+ description: fm.description || '',
197
+ action: fm.action || {},
198
+ enabled: fm.enabled !== false,
199
+ };
200
+ this._rules.push(rule);
201
+ }
202
+ }
203
+ } catch (err) {
204
+ log.error(` Failed to load rules from ${resolvedDir}: ${err.message}`)
205
+ }
176
206
  }
207
+ }
177
208
 
178
- try {
179
- const files = fs.readdirSync(resolvedDir).filter(f => f.endsWith('.json'))
209
+ _parseFrontmatter(yaml) {
210
+ const result = {};
211
+ const lines = yaml.split('\n');
212
+ let currentKey = null;
213
+ let currentIndent = 0;
214
+ const stack = [];
180
215
 
181
- for (const file of files) {
182
- const filePath = path.join(resolvedDir, file)
183
- const content = fs.readFileSync(filePath, 'utf-8')
184
- const rules = JSON.parse(content)
216
+ for (const line of lines) {
217
+ const trimmed = line.trim();
218
+ if (!trimmed || trimmed.startsWith('#')) continue;
185
219
 
186
- for (const rule of rules) {
187
- if (rule.pattern) {
188
- rule.pattern = new RegExp(rule.pattern)
189
- }
190
- rule.enabled = rule.enabled !== false
191
- this._rules.push(rule)
220
+ const indent = line.length - line.trimStart().length;
221
+ const colonIndex = trimmed.indexOf(':');
222
+
223
+ if (colonIndex > 0 && !trimmed.startsWith('-')) {
224
+ const key = trimmed.substring(0, colonIndex).trim();
225
+ let value = trimmed.substring(colonIndex + 1).trim();
226
+
227
+ // 处理数组项后面的属性 (conditions 列表项)
228
+ if (trimmed.startsWith('- ') && colonIndex > 1) {
229
+ continue;
230
+ }
231
+
232
+ if (value === '' || value.startsWith('#')) {
233
+ currentKey = key;
234
+ currentIndent = indent;
235
+ if (!result[key]) result[key] = Array.isArray(result[key]) ? result[key] : {};
236
+ stack.push({ key, indent });
237
+ continue;
192
238
  }
193
239
 
194
- //log.info(` Loaded ${rules.length} rules from ${file}`)
240
+ value = value.replace(/^["']|["']$/g, '');
241
+ if (value === 'true') value = true;
242
+ else if (value === 'false') value = false;
243
+
244
+ if (currentKey && indent > currentIndent) {
245
+ const parent = result[currentKey];
246
+ if (typeof parent === 'object' && !Array.isArray(parent)) {
247
+ parent[key] = value;
248
+ }
249
+ } else {
250
+ result[key] = value;
251
+ currentKey = null;
252
+ }
253
+ } else if (trimmed.startsWith('- ')) {
254
+ const item = trimmed.substring(2).trim();
255
+ if (currentKey) {
256
+ if (!Array.isArray(result[currentKey])) result[currentKey] = [];
257
+ const subColon = item.indexOf(':');
258
+ if (subColon > 0) {
259
+ const subKey = item.substring(0, subColon).trim();
260
+ let subVal = item.substring(subColon + 1).trim().replace(/^["']|["']$/g, '');
261
+ const obj = {};
262
+ obj[subKey] = subVal;
263
+ result[currentKey].push(obj);
264
+ } else {
265
+ result[currentKey].push(item);
266
+ }
267
+ }
195
268
  }
196
- } catch (err) {
197
- log.error(` Failed to load rules: ${err.message}`)
198
269
  }
270
+
271
+ return result;
199
272
  }
200
273
 
201
- /**
202
- * 评估目标是否符合规则
203
- * @param {string} target - 目标 (工具名, *, @event-type)
204
- * @param {Object} input - 输入参数
205
- * @returns {RuleResult}
206
- */
207
274
  evaluate(target, input = {}) {
208
275
  this._ruleStats.total++
209
276
 
210
- // 按优先级排序规则 (先加载的在前)
211
277
  for (const rule of this._rules) {
212
278
  if (!rule.enabled) continue
213
279
 
214
- // 匹配目标
215
280
  if (rule.target !== '*' && rule.target !== target) continue
216
281
 
217
- // 匹配模式 (如果有)
218
282
  if (rule.pattern) {
219
283
  const targetStr = typeof input === 'string' ? input : JSON.stringify(input)
220
284
  if (!rule.pattern.test(targetStr)) continue
221
285
  }
222
286
 
223
- // 评估规则
224
287
  if (rule.type === RuleType.DENY) {
225
288
  this._ruleStats.denied++
226
289
  return new RuleResult(false, rule)
@@ -237,35 +300,22 @@ class RulesPlugin extends Plugin {
237
300
  }
238
301
  }
239
302
 
240
- // 没有匹配规则,默认允许
241
303
  this._ruleStats.allowed++
242
304
  return new RuleResult(true)
243
305
  }
244
306
 
245
- /**
246
- * 检查工具调用是否允许
247
- */
248
307
  checkToolCall(toolName, args) {
249
308
  return this.evaluate(toolName, args)
250
309
  }
251
310
 
252
- /**
253
- * 检查消息是否允许
254
- */
255
311
  checkMessage(message, context) {
256
312
  return this.evaluate('@message', { message, context })
257
313
  }
258
314
 
259
- /**
260
- * 添加规则
261
- */
262
315
  addRule(rule) {
263
316
  this._rules.push(rule)
264
317
  }
265
318
 
266
- /**
267
- * 移除规则
268
- */
269
319
  removeRule(name) {
270
320
  const index = this._rules.findIndex(r => r.name === name)
271
321
  if (index !== -1) {
@@ -279,12 +329,19 @@ class RulesPlugin extends Plugin {
279
329
  this._framework = framework
280
330
  this._rules = []
281
331
  this._ruleStats = { total: 0, allowed: 0, denied: 0, transformed: 0 }
332
+ this._loaded = false;
282
333
 
283
334
  if (this.config.autoLoad && this.config.rulesDir) {
284
335
  this.loadFromDirectory(this.config.rulesDir)
285
336
  }
286
337
  }
287
338
 
339
+ async onCwdChanged(oldCwd, newCwd, framework) {
340
+ this._rules = []
341
+ this._ruleStats = { total: 0, allowed: 0, denied: 0, transformed: 0 }
342
+ this._loaded = false
343
+ }
344
+
288
345
  uninstall(framework) {
289
346
  this._rules = []
290
347
  this._ruleStats = { total: 0, allowed: 0, denied: 0, transformed: 0 }
@@ -292,4 +349,4 @@ class RulesPlugin extends Plugin {
292
349
  }
293
350
  }
294
351
 
295
- module.exports = RulesPlugin
352
+ module.exports = RulesPlugin