@eoasmxd/freya 0.5.0 → 0.5.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 (35) hide show
  1. package/core/dist/context.js +2 -2
  2. package/core/dist/plugin/plugin-manager.d.ts +1 -1
  3. package/core/dist/plugin/plugin-manager.js +8 -8
  4. package/core/dist/prompt/prompt-registry.d.ts +4 -4
  5. package/core/dist/prompt/prompt-registry.js +33 -14
  6. package/core/dist/skill/skill-registry.d.ts +1 -1
  7. package/core/dist/skill/skill-registry.js +5 -5
  8. package/core/dist/utils/paths.d.ts +2 -2
  9. package/core/dist/utils/paths.js +4 -3
  10. package/core/package.json +2 -2
  11. package/doc/tutorials/part5_plugins/10.1_microkernel_decoupling.md +2 -2
  12. package/freya.js +2 -2
  13. package/package.json +2 -2
  14. package/plugins/plugin-gemini/package.json +1 -1
  15. package/plugins/plugin-openai/package.json +1 -1
  16. package/plugins/plugin-telegram-channel/package.json +1 -1
  17. package/plugins/plugin-tool-fs/dist/tools.d.ts +1 -1
  18. package/plugins/plugin-tool-fs/dist/tools.js +58 -15
  19. package/plugins/plugin-tool-fs/package.json +1 -1
  20. package/plugins/plugin-tool-memory/package.json +1 -1
  21. package/plugins/plugin-tool-mysql/package.json +1 -1
  22. package/plugins/plugin-tool-web/package.json +1 -1
  23. package/plugins/plugin-wecom-channel/package.json +1 -1
  24. package/plugins/plugin-weixin-channel/package.json +1 -1
  25. package/src/packages/core/src/context.ts +2 -2
  26. package/src/packages/core/src/plugin/plugin-manager.ts +11 -11
  27. package/src/packages/core/src/prompt/prompt-registry.ts +39 -15
  28. package/src/packages/core/src/skill/skill-registry.ts +7 -7
  29. package/src/packages/core/src/utils/paths.ts +5 -4
  30. package/src/packages/sdk/src/types/context.ts +5 -5
  31. package/src/packages/ui/src/features/config/panels/PluginConfigPanel.tsx +2 -2
  32. package/src/packages/ui/src/features/config/panels/SkillConfigPanel.tsx +2 -2
  33. package/src/plugins/plugin-tool-fs/src/tools.ts +65 -15
  34. package/ui/assets/{index-CSZmaZbL.js → index-CD0PlILU.js} +1 -1
  35. package/ui/index.html +1 -1
@@ -1,39 +1,63 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { FREYA_APP, FREYA_HOME } from '../utils/paths.js';
3
+ import { FREYA_APP, FREYA_HOME, FREYA_LAUNCH } from '../utils/paths.js';
4
4
 
5
5
  export interface FreyaPrompt {
6
6
  key: string;
7
7
  content: string;
8
8
  defaultPath: string;
9
- runPath?: string;
9
+ configFileName?: string;
10
10
  }
11
11
 
12
12
  /** 提示词内存注册表,管理所有系统及插件级提示词的分类检索 */
13
13
  export class FreyaPromptRegistry {
14
14
  private prompts = new Map<string, FreyaPrompt>();
15
15
 
16
- private getRunFilePath(prompt: Omit<FreyaPrompt, 'content'>): string {
17
- return prompt.runPath || path.join(FREYA_HOME, 'config', 'prompts', path.basename(prompt.defaultPath));
16
+ private resolveProbePaths(prompt: Omit<FreyaPrompt, 'content'>): string[] {
17
+ const baseName = path.basename(prompt.defaultPath);
18
+ const rawPaths: string[] = [];
19
+
20
+ if (prompt.configFileName) {
21
+ rawPaths.push(path.join(FREYA_HOME, 'config', prompt.configFileName));
22
+ rawPaths.push(path.join(FREYA_LAUNCH, 'config', prompt.configFileName));
23
+ }
24
+
25
+ rawPaths.push(path.join(FREYA_HOME, 'config', 'prompts', baseName));
26
+ rawPaths.push(path.join(FREYA_LAUNCH, 'config', 'prompts', baseName));
27
+ rawPaths.push(prompt.defaultPath);
28
+
29
+ const candidates: string[] = [];
30
+ const seen = new Set<string>();
31
+ for (const rawPath of rawPaths) {
32
+ const normalized = path.resolve(rawPath);
33
+ if (!seen.has(normalized)) {
34
+ seen.add(normalized);
35
+ candidates.push(normalized);
36
+ }
37
+ }
38
+ return candidates;
18
39
  }
19
40
 
20
- /** 注册提示词元数据声明并执行异步双读载入 */
41
+ /** 注册提示词元数据声明并执行三层级联探针载入 */
21
42
  async register(prompt: Omit<FreyaPrompt, 'content'>): Promise<void> {
22
- const runFilePath = this.getRunFilePath(prompt);
43
+ const probePaths = this.resolveProbePaths(prompt);
23
44
  let content = '';
24
- try {
45
+
46
+ for (const filePath of probePaths) {
25
47
  try {
26
- content = await fs.readFile(runFilePath, 'utf-8');
27
- } catch {
28
- content = await fs.readFile(prompt.defaultPath, 'utf-8');
29
- }
30
- } catch {}
48
+ const text = await fs.readFile(filePath, 'utf-8');
49
+ if (text.trim().length > 0) {
50
+ content = text;
51
+ break;
52
+ }
53
+ } catch {}
54
+ }
31
55
 
32
56
  this.prompts.set(prompt.key, {
33
57
  key: prompt.key,
34
58
  content: content.trim(),
35
59
  defaultPath: prompt.defaultPath,
36
- runPath: prompt.runPath
60
+ configFileName: prompt.configFileName
37
61
  });
38
62
  }
39
63
 
@@ -58,7 +82,7 @@ export class FreyaPromptRegistry {
58
82
  return this.prompts;
59
83
  }
60
84
 
61
- /** 扫描并装载所有内核提示词,支持运行时提示词覆盖默认提示词 */
85
+ /** 扫描并装载所有内核提示词 */
62
86
  async loadKernelPrompts(): Promise<void> {
63
87
  const defaultDirPath = path.join(FREYA_APP, 'config', 'prompts');
64
88
  try {
@@ -67,7 +91,7 @@ export class FreyaPromptRegistry {
67
91
  await this.register({
68
92
  key: `core.prompt.${name}`,
69
93
  defaultPath: path.join(defaultDirPath, `core.prompt.${name}.md`),
70
- runPath: path.join(FREYA_HOME, 'config', `${name.toUpperCase()}.md`)
94
+ configFileName: `${name.toUpperCase()}.md`
71
95
  });
72
96
  }
73
97
 
@@ -1,7 +1,7 @@
1
1
  import type { FreyaContext } from '@eoasmxd/freya-sdk';
2
2
  import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
- import { FREYA_APP, FREYA_HOME, FREYA_WORKSPACE } from '../utils/paths.js';
4
+ import { FREYA_APP, FREYA_HOME, FREYA_LAUNCH } from '../utils/paths.js';
5
5
 
6
6
  export interface FreyaSkill {
7
7
  id: string;
@@ -9,7 +9,7 @@ export interface FreyaSkill {
9
9
  description: string;
10
10
  content: string;
11
11
  enabled: boolean;
12
- source: 'builtin' | 'workspace' | 'runtime';
12
+ source: 'builtin' | 'launch' | 'runtime';
13
13
  }
14
14
 
15
15
  /** 技能注册表,从 skills/ 目录加载 Markdown 格式技能并管理软开关状态 */
@@ -20,7 +20,7 @@ export class FreyaSkillRegistry {
20
20
  async loadSkills(context: FreyaContext): Promise<void> {
21
21
  this.context = context;
22
22
  const defaultSkillsDir = path.join(FREYA_APP, 'skills');
23
- const workspaceSkillsDir = path.join(FREYA_WORKSPACE, 'skills');
23
+ const launchSkillsDir = path.join(FREYA_LAUNCH, 'skills');
24
24
  const runtimeSkillsDir = path.join(FREYA_HOME, 'skills');
25
25
  const configSkillsPath = path.join(FREYA_HOME, 'config', 'skills.json');
26
26
 
@@ -29,11 +29,11 @@ export class FreyaSkillRegistry {
29
29
  await this.loadSkillsFromDirectory(defaultSkillsDir, 'builtin', context);
30
30
 
31
31
  const resolvedDefault = path.resolve(defaultSkillsDir);
32
- const resolvedWorkspace = path.resolve(workspaceSkillsDir);
32
+ const resolvedLaunch = path.resolve(launchSkillsDir);
33
33
  const resolvedRuntime = path.resolve(runtimeSkillsDir);
34
34
 
35
- if (resolvedWorkspace !== resolvedDefault && resolvedWorkspace !== resolvedRuntime) {
36
- await this.loadSkillsFromDirectory(workspaceSkillsDir, 'workspace', context);
35
+ if (resolvedLaunch !== resolvedDefault && resolvedLaunch !== resolvedRuntime) {
36
+ await this.loadSkillsFromDirectory(launchSkillsDir, 'launch', context);
37
37
  }
38
38
 
39
39
  await this.loadSkillsFromDirectory(runtimeSkillsDir, 'runtime', context);
@@ -77,7 +77,7 @@ export class FreyaSkillRegistry {
77
77
  }
78
78
 
79
79
  /** 从指定目录加载技能到内存注册表中 */
80
- private async loadSkillsFromDirectory(dirPath: string, source: 'builtin' | 'workspace' | 'runtime', context: FreyaContext): Promise<void> {
80
+ private async loadSkillsFromDirectory(dirPath: string, source: 'builtin' | 'launch' | 'runtime', context: FreyaContext): Promise<void> {
81
81
  try {
82
82
  const files = await fs.readdir(dirPath);
83
83
  for (const file of files) {
@@ -6,6 +6,7 @@ import fs from 'node:fs';
6
6
 
7
7
  const customHome = process.env.FREYA_HOME;
8
8
  const customApp = process.env.FREYA_APP;
9
+ const customLaunch = process.env.FREYA_LAUNCH;
9
10
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
10
11
 
11
12
  function resolveAppRoot(): string {
@@ -24,7 +25,7 @@ function resolveAppRoot(): string {
24
25
  if (pkg.name === '@eoasmxd/freya' || fs.existsSync(path.join(current, 'plugins'))) {
25
26
  return current;
26
27
  }
27
- } catch {}
28
+ } catch { }
28
29
  }
29
30
  const parent = path.dirname(current);
30
31
  if (parent === current) break;
@@ -43,7 +44,7 @@ export const FREYA_APP = customApp
43
44
  ? path.resolve(customApp)
44
45
  : resolveAppRoot();
45
46
 
46
- /** 宿主工程/工作区根目录(业务工程代码与定制资源区) */
47
- export const FREYA_WORKSPACE = process.env.FREYA_WORKSPACE
48
- ? path.resolve(process.env.FREYA_WORKSPACE)
47
+ /** 宿主命令启动执行根目录(默认当前工作目录 process.cwd()) */
48
+ export const FREYA_LAUNCH = customLaunch
49
+ ? path.resolve(customLaunch)
49
50
  : process.cwd();
@@ -14,15 +14,15 @@ export interface Logger {
14
14
  }
15
15
 
16
16
  export interface FreyaPaths {
17
- /** 程序物理安装根目录(只读代码与程序级静态资源区) */
17
+ /** 程序物理安装根目录(只读系统源码与内置资源区) */
18
18
  appRoot: string;
19
- /** 运行态持久化主目录(用户个性化配置与持久化数据区,默认 ~/.freya) */
19
+ /** 运行态持久化主目录(用户配置与持久化存储入口,默认 ~/.freya) */
20
20
  homeDir: string;
21
- /** 宿主工程根目录(默认为启动执行路径 process.cwd()) */
22
- workspaceRoot: string;
21
+ /** 宿主命令启动执行目录(默认启动路径 process.cwd()) */
22
+ launchDir: string;
23
23
  /** 运行时数据持久化目录(~/.freya/data) */
24
24
  dataDir: string;
25
- /** 宿主与大模型交互隔离的文件读写沙箱(~/.freya/workspace */
25
+ /** 智能体专属文件读写工作区(默认为 ~/.freya/workspace,可由配置覆盖) */
26
26
  workspaceDir: string;
27
27
  }
28
28
 
@@ -5,7 +5,7 @@ interface PluginEntry {
5
5
  name: string;
6
6
  description: string;
7
7
  enabled: boolean;
8
- source?: 'builtin' | 'workspace' | 'runtime' | 'npm';
8
+ source?: 'builtin' | 'launch' | 'runtime' | 'npm';
9
9
  }
10
10
 
11
11
  interface PluginConfigPanelProps {
@@ -70,7 +70,7 @@ export const PluginConfigPanel: React.FC<PluginConfigPanelProps> = ({ getApiUrl
70
70
  switch (source) {
71
71
  case 'builtin':
72
72
  return { label: '内置', bg: 'rgba(59, 130, 246, 0.15)', color: '#60a5fa' };
73
- case 'workspace':
73
+ case 'launch':
74
74
  return { label: '集成', bg: 'rgba(168, 85, 247, 0.15)', color: '#c084fc' };
75
75
  case 'npm':
76
76
  return { label: 'NPM', bg: 'rgba(245, 158, 11, 0.15)', color: '#fbbf24' };
@@ -5,7 +5,7 @@ interface SkillEntry {
5
5
  name: string;
6
6
  description: string;
7
7
  enabled: boolean;
8
- source: 'builtin' | 'workspace' | 'runtime';
8
+ source: 'builtin' | 'launch' | 'runtime';
9
9
  }
10
10
 
11
11
  interface SkillConfigPanelProps {
@@ -83,7 +83,7 @@ export const SkillConfigPanel: React.FC<SkillConfigPanelProps> = ({ getApiUrl })
83
83
  switch (source) {
84
84
  case 'builtin':
85
85
  return { label: '内置', bg: 'rgba(59, 130, 246, 0.15)', color: '#60a5fa' };
86
- case 'workspace':
86
+ case 'launch':
87
87
  return { label: '集成', bg: 'rgba(168, 85, 247, 0.15)', color: '#c084fc' };
88
88
  case 'runtime':
89
89
  default:
@@ -2,13 +2,56 @@ import type { FreyaContext, ToolDefinition, FreyaTool } from '@eoasmxd/freya-sdk
2
2
  import fs from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
 
5
- /** 安全工作区路径获取 (支持指定 scope 作用域) */
5
+ /** 解析作用域实际物理路径,返回 null 表示显式禁用 */
6
+ function resolveScopeBase(envValue: string | undefined, defaultPath: string, launchDir: string): string | null {
7
+ if (envValue === undefined) {
8
+ return defaultPath;
9
+ }
10
+ const trimmed = envValue.trim();
11
+ if (trimmed === '' || trimmed.toLowerCase() === 'false') {
12
+ return null;
13
+ }
14
+ return path.isAbsolute(trimmed) ? trimmed : path.resolve(launchDir, trimmed);
15
+ }
16
+
17
+ /** 获取当前激活的作用域列表与描述文本 */
18
+ function getActiveScopesInfo(): { scopes: string[]; description: string } {
19
+ const scopes = ['workspace'];
20
+ const descParts = ['默认为 "workspace" 沙箱'];
21
+
22
+ const srcVal = process.env.FREYA_FS_SRC;
23
+ const isSrcDisabled = srcVal !== undefined && (srcVal.trim() === '' || srcVal.trim().toLowerCase() === 'false');
24
+ if (!isSrcDisabled) {
25
+ scopes.push('src');
26
+ descParts.push('支持指定 "src" 源码区');
27
+ }
28
+
29
+ const docVal = process.env.FREYA_FS_DOC;
30
+ const isDocDisabled = docVal !== undefined && (docVal.trim() === '' || docVal.trim().toLowerCase() === 'false');
31
+ if (!isDocDisabled) {
32
+ scopes.push('doc');
33
+ descParts.push('指定 "doc" 文档区');
34
+ }
35
+
36
+ const description = `读取作用域(可选,${descParts.join(',')})`;
37
+ return { scopes, description };
38
+ }
39
+
40
+ /** 获取经过安全边界校验的绝对路径 */
6
41
  export function getSafePath(ctx: FreyaContext, relativePath: string, scope?: string): { targetAbs: string; baseAbs: string } {
7
- let baseAbs = ctx.paths.workspaceDir;
8
- if (scope === 'src') {
9
- baseAbs = path.join(ctx.paths.appRoot, 'src');
10
- } else if (scope === 'doc') {
11
- baseAbs = path.join(ctx.paths.appRoot, 'doc');
42
+ const targetScope = scope || 'workspace';
43
+ let baseAbs: string | null = null;
44
+
45
+ if (targetScope === 'workspace') {
46
+ baseAbs = ctx.paths.workspaceDir;
47
+ } else if (targetScope === 'src') {
48
+ baseAbs = resolveScopeBase(process.env.FREYA_FS_SRC, path.join(ctx.paths.appRoot, 'src'), ctx.paths.launchDir);
49
+ } else if (targetScope === 'doc') {
50
+ baseAbs = resolveScopeBase(process.env.FREYA_FS_DOC, path.join(ctx.paths.appRoot, 'doc'), ctx.paths.launchDir);
51
+ }
52
+
53
+ if (!baseAbs) {
54
+ throw new Error(`安全拒绝:作用域 "${targetScope}" 未开放或已被禁用。`);
12
55
  }
13
56
 
14
57
  if (relativePath && path.isAbsolute(relativePath)) {
@@ -51,6 +94,7 @@ function formatBytes(bytes: number): string {
51
94
  export class ListDirTool implements FreyaTool {
52
95
 
53
96
  getDefinition(): ToolDefinition {
97
+ const { scopes, description } = getActiveScopesInfo();
54
98
  return {
55
99
  name: 'list_dir',
56
100
  description: '列出指定目录下的文件和子文件夹列表。注意:仅允许访问相对路径,不可传绝对路径或向上越级 escape 路径。',
@@ -63,8 +107,8 @@ export class ListDirTool implements FreyaTool {
63
107
  },
64
108
  scope: {
65
109
  type: 'string',
66
- enum: ['workspace', 'src', 'doc'],
67
- description: '读取作用域(可选,默认为 "workspace" 沙箱,支持指定 "src" 源码区或 "doc" 文档区)'
110
+ enum: scopes,
111
+ description
68
112
  }
69
113
  }
70
114
  }
@@ -114,6 +158,7 @@ export class ListDirTool implements FreyaTool {
114
158
  export class ReadFileTool implements FreyaTool {
115
159
 
116
160
  getDefinition(): ToolDefinition {
161
+ const { scopes, description } = getActiveScopesInfo();
117
162
  return {
118
163
  name: 'read_file',
119
164
  description: '读取指定文件的文本内容。支持指定行号起止区间切片读取,防范大文件上下文超限。',
@@ -126,8 +171,8 @@ export class ReadFileTool implements FreyaTool {
126
171
  },
127
172
  scope: {
128
173
  type: 'string',
129
- enum: ['workspace', 'src', 'doc'],
130
- description: '读取作用域(可选,默认为 "workspace" 沙箱,支持指定 "src" 源码区或 "doc" 文档区)'
174
+ enum: scopes,
175
+ description
131
176
  },
132
177
  startLine: {
133
178
  type: 'integer',
@@ -215,7 +260,7 @@ export class WriteFileTool implements FreyaTool {
215
260
  return '❌ 参数错误:必须指定目标路径与写入内容。';
216
261
  }
217
262
  if (args.scope && args.scope !== 'workspace') {
218
- return `❌ 安全拒绝:物理源码区 (src) 与文档区 (doc) 为只读保护区,严禁写入或修改。`;
263
+ return '❌ 安全拒绝:仅允许在工作区 (workspace) 进行写入操作,其它区域均为只读保护区。';
219
264
  }
220
265
  try {
221
266
  const { targetAbs, baseAbs } = getSafePath(ctx, args.path);
@@ -263,7 +308,7 @@ export class EditFileTool implements FreyaTool {
263
308
  return '❌ 参数错误:必须指定目标路径、查找目标与替换文本。';
264
309
  }
265
310
  if (args.scope && args.scope !== 'workspace') {
266
- return `❌ 安全拒绝:物理源码区 (src) 与文档区 (doc) 为只读保护区,严禁写入或修改。`;
311
+ return '❌ 安全拒绝:仅允许在工作区 (workspace) 进行修改操作,其它区域均为只读保护区。';
267
312
  }
268
313
  try {
269
314
  const { targetAbs, baseAbs } = getSafePath(ctx, args.path);
@@ -273,12 +318,17 @@ export class EditFileTool implements FreyaTool {
273
318
  }
274
319
 
275
320
  const content = await fs.readFile(targetAbs, 'utf-8');
276
- const index = content.indexOf(args.target);
277
- if (index === -1) {
321
+ const firstIndex = content.indexOf(args.target);
322
+ if (firstIndex === -1) {
278
323
  return `❌ 修改失败:在文件 "${args.path}" 中未找到指定的 target 文本。请确保 target 在大小写、缩进和换行上与文件内完全一致。`;
279
324
  }
280
325
 
281
- const newContent = content.slice(0, index) + args.replacement + content.slice(index + args.target.length);
326
+ const secondIndex = content.indexOf(args.target, firstIndex + args.target.length);
327
+ if (secondIndex !== -1) {
328
+ return `❌ 修改拒绝:在文件 "${args.path}" 中匹配到多处相同的目标文本。请扩大 target 文本段以包含更多上下文行确保唯一性。`;
329
+ }
330
+
331
+ const newContent = content.slice(0, firstIndex) + args.replacement + content.slice(firstIndex + args.target.length);
282
332
  await fs.writeFile(targetAbs, newContent, 'utf-8');
283
333
 
284
334
  return `ℹ️ 已成功修改文件 "${args.path}" 的指定部分。`;