@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.
- package/core/dist/context.js +2 -2
- package/core/dist/plugin/plugin-manager.d.ts +1 -1
- package/core/dist/plugin/plugin-manager.js +8 -8
- package/core/dist/prompt/prompt-registry.d.ts +4 -4
- package/core/dist/prompt/prompt-registry.js +33 -14
- package/core/dist/skill/skill-registry.d.ts +1 -1
- package/core/dist/skill/skill-registry.js +5 -5
- package/core/dist/utils/paths.d.ts +2 -2
- package/core/dist/utils/paths.js +4 -3
- package/core/package.json +2 -2
- package/doc/tutorials/part5_plugins/10.1_microkernel_decoupling.md +2 -2
- package/freya.js +2 -2
- package/package.json +2 -2
- package/plugins/plugin-gemini/package.json +1 -1
- package/plugins/plugin-openai/package.json +1 -1
- package/plugins/plugin-telegram-channel/package.json +1 -1
- package/plugins/plugin-tool-fs/dist/tools.d.ts +1 -1
- package/plugins/plugin-tool-fs/dist/tools.js +58 -15
- package/plugins/plugin-tool-fs/package.json +1 -1
- package/plugins/plugin-tool-memory/package.json +1 -1
- package/plugins/plugin-tool-mysql/package.json +1 -1
- package/plugins/plugin-tool-web/package.json +1 -1
- package/plugins/plugin-wecom-channel/package.json +1 -1
- package/plugins/plugin-weixin-channel/package.json +1 -1
- package/src/packages/core/src/context.ts +2 -2
- package/src/packages/core/src/plugin/plugin-manager.ts +11 -11
- package/src/packages/core/src/prompt/prompt-registry.ts +39 -15
- package/src/packages/core/src/skill/skill-registry.ts +7 -7
- package/src/packages/core/src/utils/paths.ts +5 -4
- package/src/packages/sdk/src/types/context.ts +5 -5
- package/src/packages/ui/src/features/config/panels/PluginConfigPanel.tsx +2 -2
- package/src/packages/ui/src/features/config/panels/SkillConfigPanel.tsx +2 -2
- package/src/plugins/plugin-tool-fs/src/tools.ts +65 -15
- package/ui/assets/{index-CSZmaZbL.js → index-CD0PlILU.js} +1 -1
- 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
|
-
|
|
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
|
|
17
|
-
|
|
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
|
|
43
|
+
const probePaths = this.resolveProbePaths(prompt);
|
|
23
44
|
let content = '';
|
|
24
|
-
|
|
45
|
+
|
|
46
|
+
for (const filePath of probePaths) {
|
|
25
47
|
try {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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' | '
|
|
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
|
|
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
|
|
32
|
+
const resolvedLaunch = path.resolve(launchSkillsDir);
|
|
33
33
|
const resolvedRuntime = path.resolve(runtimeSkillsDir);
|
|
34
34
|
|
|
35
|
-
if (
|
|
36
|
-
await this.loadSkillsFromDirectory(
|
|
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' | '
|
|
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
|
|
48
|
-
? path.resolve(
|
|
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
|
-
/**
|
|
19
|
+
/** 运行态持久化主目录(用户配置与持久化存储入口,默认 ~/.freya) */
|
|
20
20
|
homeDir: string;
|
|
21
|
-
/**
|
|
22
|
-
|
|
21
|
+
/** 宿主命令启动执行目录(默认启动路径 process.cwd()) */
|
|
22
|
+
launchDir: string;
|
|
23
23
|
/** 运行时数据持久化目录(~/.freya/data) */
|
|
24
24
|
dataDir: string;
|
|
25
|
-
/**
|
|
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' | '
|
|
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 '
|
|
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' | '
|
|
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 '
|
|
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
|
-
/**
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
baseAbs =
|
|
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:
|
|
67
|
-
description
|
|
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:
|
|
130
|
-
description
|
|
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
|
|
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
|
|
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
|
|
277
|
-
if (
|
|
321
|
+
const firstIndex = content.indexOf(args.target);
|
|
322
|
+
if (firstIndex === -1) {
|
|
278
323
|
return `❌ 修改失败:在文件 "${args.path}" 中未找到指定的 target 文本。请确保 target 在大小写、缩进和换行上与文件内完全一致。`;
|
|
279
324
|
}
|
|
280
325
|
|
|
281
|
-
const
|
|
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}" 的指定部分。`;
|