foliko 1.1.75 → 1.1.76
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/.claude/settings.local.json +5 -1
- package/.dockerignore +45 -45
- package/.env.example +56 -56
- package/cli/src/ui/chat-ui.js +56 -126
- package/cli/src/ui/components/agent-mention-provider.js +175 -0
- package/cli/src/ui/components/chained-autocomplete-provider.js +64 -0
- package/cli/src/ui/{footer-bar.js → components/footer-bar.js} +2 -2
- package/docker-compose.yml +33 -33
- package/docs/features.md +120 -120
- package/docs/quick-reference.md +160 -160
- package/package.json +1 -1
- package/plugins/ai-plugin.js +3 -3
- package/plugins/ambient-agent/ExplorerLoop.js +17 -17
- package/plugins/ambient-agent/index.js +2 -2
- package/plugins/data-splitter-plugin.js +9 -9
- package/plugins/default-plugins.js +7 -8
- package/plugins/extension-executor-plugin.js +2 -2
- package/plugins/feishu-plugin.js +5 -5
- package/plugins/install-plugin.js +4 -4
- package/plugins/memory-plugin.js +1 -1
- package/plugins/plugin-manager-plugin.js +9 -8
- package/plugins/python-plugin-loader.js +2 -2
- package/plugins/qq-plugin.js +4 -4
- package/plugins/rules-plugin.js +2 -2
- package/plugins/scheduler-plugin.js +13 -13
- package/plugins/session-plugin.js +2 -2
- package/plugins/subagent-plugin.js +1 -1
- package/plugins/telegram-plugin.js +6 -6
- package/plugins/think-plugin.js +4 -4
- package/plugins/tools-plugin.js +1 -1
- package/plugins/web-plugin.js +16 -16
- package/skills/find-skills/SKILL.md +133 -133
- package/skills/foliko-dev/AGENTS.md +236 -236
- package/skills/mcp-usage/SKILL.md +200 -200
- package/skills/subagent-guide/SKILL.md +237 -237
- package/skills/workflow-guide/SKILL.md +646 -646
- package/src/capabilities/skill-manager.js +5 -5
- package/src/capabilities/workflow-engine.js +5 -5
- package/src/core/agent-chat.js +8 -8
- package/src/core/agent.js +80 -5
- package/src/core/branch-summary-auto.js +4 -4
- package/src/core/chat-session.js +1 -0
- package/src/core/session-entry.js +14 -6
- package/src/core/session-manager.js +15 -3
- package/src/executors/mcp-executor.js +21 -25
- package/src/utils/data-splitter.js +3 -3
- package/src/utils/message-validator.js +1 -1
- package/website_v2/styles/animations.css +7 -7
- /package/cli/src/ui/{message-bubble.js → components/message-bubble.js} +0 -0
- /package/cli/src/ui/{status-bar.js → components/status-bar.js} +0 -0
|
@@ -230,7 +230,11 @@
|
|
|
230
230
|
"Bash(grep -r \"logger\\\\.\" D:/date/20260516/pi/packages --include=\"*.ts\" ! -path \"*/node_modules/*\" ! -path \"*/test/*\")",
|
|
231
231
|
"Bash(grep *)",
|
|
232
232
|
"Bash(node -e \"require\\('./plugins/audit-plugin.js'\\); console.log\\('audit-plugin loaded'\\)\")",
|
|
233
|
-
"Bash(node -e \"const { createAI, DEFAULT_PROVIDERS } = require\\('./src/core/provider'\\); console.log\\('Providers:', Object.keys\\(DEFAULT_PROVIDERS\\)\\);\")"
|
|
233
|
+
"Bash(node -e \"const { createAI, DEFAULT_PROVIDERS } = require\\('./src/core/provider'\\); console.log\\('Providers:', Object.keys\\(DEFAULT_PROVIDERS\\)\\);\")",
|
|
234
|
+
"Bash(npx designmd-mcp *)",
|
|
235
|
+
"Bash(node -e \"const { spawn } = require\\('child_process'\\); const p = spawn\\('npx', ['designmd-mcp'], { stdio: 'pipe' }\\); let out = ''; p.stdout.on\\('data', \\(d\\) => { out += d.toString\\(\\); if\\(out.length > 1000\\) { console.log\\('OUTPUT_TOO_LONG:' + out.length\\); process.exit\\(0\\); } }\\); p.stderr.on\\('data', \\(d\\) => console.log\\('STDERR:' + d.toString\\(\\)\\)\\); setTimeout\\(\\(\\) => { console.log\\('FINAL_OUTPUT_LENGTH:' + out.length\\); process.exit\\(0\\); }, 3000\\);\")",
|
|
236
|
+
"Bash(where npx *)",
|
|
237
|
+
"Bash(node *)"
|
|
234
238
|
]
|
|
235
239
|
}
|
|
236
240
|
}
|
package/.dockerignore
CHANGED
|
@@ -1,45 +1,45 @@
|
|
|
1
|
-
# 依赖
|
|
2
|
-
node_modules
|
|
3
|
-
.foliko/node_modules
|
|
4
|
-
|
|
5
|
-
# 日志
|
|
6
|
-
*.log
|
|
7
|
-
npm-debug.log*
|
|
8
|
-
|
|
9
|
-
# 环境变量(包含敏感信息)
|
|
10
|
-
.env
|
|
11
|
-
.env.local
|
|
12
|
-
|
|
13
|
-
# Git
|
|
14
|
-
.git
|
|
15
|
-
.gitignore
|
|
16
|
-
|
|
17
|
-
# IDE
|
|
18
|
-
.idea
|
|
19
|
-
.vscode
|
|
20
|
-
*.swp
|
|
21
|
-
*.swo
|
|
22
|
-
|
|
23
|
-
# 文档(可选保留)
|
|
24
|
-
*.md
|
|
25
|
-
!README.md
|
|
26
|
-
|
|
27
|
-
# 测试
|
|
28
|
-
test
|
|
29
|
-
tests
|
|
30
|
-
coverage
|
|
31
|
-
|
|
32
|
-
# 临时文件
|
|
33
|
-
tmp
|
|
34
|
-
temp
|
|
35
|
-
*.tmp
|
|
36
|
-
|
|
37
|
-
# Docker 相关(避免递归)
|
|
38
|
-
Dockerfile*
|
|
39
|
-
docker-compose*
|
|
40
|
-
.dockerignore
|
|
41
|
-
|
|
42
|
-
# CI/CD
|
|
43
|
-
.github
|
|
44
|
-
.gitlab-ci.yml
|
|
45
|
-
.travis.yml
|
|
1
|
+
# 依赖
|
|
2
|
+
node_modules
|
|
3
|
+
.foliko/node_modules
|
|
4
|
+
|
|
5
|
+
# 日志
|
|
6
|
+
*.log
|
|
7
|
+
npm-debug.log*
|
|
8
|
+
|
|
9
|
+
# 环境变量(包含敏感信息)
|
|
10
|
+
.env
|
|
11
|
+
.env.local
|
|
12
|
+
|
|
13
|
+
# Git
|
|
14
|
+
.git
|
|
15
|
+
.gitignore
|
|
16
|
+
|
|
17
|
+
# IDE
|
|
18
|
+
.idea
|
|
19
|
+
.vscode
|
|
20
|
+
*.swp
|
|
21
|
+
*.swo
|
|
22
|
+
|
|
23
|
+
# 文档(可选保留)
|
|
24
|
+
*.md
|
|
25
|
+
!README.md
|
|
26
|
+
|
|
27
|
+
# 测试
|
|
28
|
+
test
|
|
29
|
+
tests
|
|
30
|
+
coverage
|
|
31
|
+
|
|
32
|
+
# 临时文件
|
|
33
|
+
tmp
|
|
34
|
+
temp
|
|
35
|
+
*.tmp
|
|
36
|
+
|
|
37
|
+
# Docker 相关(避免递归)
|
|
38
|
+
Dockerfile*
|
|
39
|
+
docker-compose*
|
|
40
|
+
.dockerignore
|
|
41
|
+
|
|
42
|
+
# CI/CD
|
|
43
|
+
.github
|
|
44
|
+
.gitlab-ci.yml
|
|
45
|
+
.travis.yml
|
package/.env.example
CHANGED
|
@@ -1,56 +1,56 @@
|
|
|
1
|
-
# ========== AI Configuration ==========
|
|
2
|
-
# 最大输出 tokens(影响 AI 回复长度)
|
|
3
|
-
MAX_OUTPUT_TOKENS=8192
|
|
4
|
-
# AI Provider: minimax, deepseek, openai, anthropic 等
|
|
5
|
-
FOLIKO_PROVIDER=minimax
|
|
6
|
-
|
|
7
|
-
# AI Model(如果未设置,使用 provider 默认值)
|
|
8
|
-
# MiniMax: MiniMax-M2.7
|
|
9
|
-
# DeepSeek: deepseek-chat, deepseek-coder 等
|
|
10
|
-
FOLIKO_MODEL=MiniMax-M2.7
|
|
11
|
-
|
|
12
|
-
# API Base URL(如果未设置,使用 provider 默认值)
|
|
13
|
-
# MiniMax: https://api.minimaxi.com/v1
|
|
14
|
-
# DeepSeek: https://api.deepseek.com/v1
|
|
15
|
-
FOLIKO_BASE_URL=https://api.minimaxi.com/v1
|
|
16
|
-
|
|
17
|
-
# API Key(通用,如果未设置则尝试 provider 专用 key)
|
|
18
|
-
FOLIKO_API_KEY=sk-your-api-key
|
|
19
|
-
|
|
20
|
-
# Provider 专用 API Key(可选,如果 FOLIKO_API_KEY 未设置则使用这些)
|
|
21
|
-
DEEPSEEK_API_KEY=sk-your-deepseek-api-key
|
|
22
|
-
MINIMAX_API_KEY=sk-your-minimax-api-key
|
|
23
|
-
|
|
24
|
-
# ========== Email Configuration ==========
|
|
25
|
-
# SMTP Settings (for sending emails)
|
|
26
|
-
SMTP_HOST=smtp.gmail.com
|
|
27
|
-
SMTP_PORT=587
|
|
28
|
-
SMTP_SECURE=false
|
|
29
|
-
SMTP_USER=your-email@gmail.com
|
|
30
|
-
SMTP_PASS=your-app-password
|
|
31
|
-
|
|
32
|
-
# IMAP Settings (for reading emails)
|
|
33
|
-
IMAP_HOST=imap.gmail.com
|
|
34
|
-
IMAP_PORT=993
|
|
35
|
-
IMAP_USER=your-email@gmail.com
|
|
36
|
-
IMAP_PASS=your-app-password
|
|
37
|
-
|
|
38
|
-
# Default sender email address
|
|
39
|
-
FROM_EMAIL=your-email@gmail.com
|
|
40
|
-
|
|
41
|
-
# ========== Telegram Bot (optional) ==========
|
|
42
|
-
TELEGRAM_BOT_TOKEN=your-telegram-bot-token
|
|
43
|
-
|
|
44
|
-
# ========== Feishu Bot (optional) ==========
|
|
45
|
-
FEISHU_APP_ID=cli_xxxxxxxxxxx
|
|
46
|
-
FEISHU_APP_SECRET=app_secret
|
|
47
|
-
|
|
48
|
-
# ========== Web Server (optional) ==========
|
|
49
|
-
# Web 服务端口,默认 8088
|
|
50
|
-
WEB_PORT=3000
|
|
51
|
-
|
|
52
|
-
# Web 服务主机,默认 127.0.0.1
|
|
53
|
-
WEB_HOST=127.0.0.1
|
|
54
|
-
|
|
55
|
-
# 公网访问的 base URL(用于生成 webhook URL 等),不设置则使用 host:port,最好部署在docker中可以暴露自定义域名
|
|
56
|
-
WEB_BASE_URL=https://your-domain.com
|
|
1
|
+
# ========== AI Configuration ==========
|
|
2
|
+
# 最大输出 tokens(影响 AI 回复长度)
|
|
3
|
+
MAX_OUTPUT_TOKENS=8192
|
|
4
|
+
# AI Provider: minimax, deepseek, openai, anthropic 等
|
|
5
|
+
FOLIKO_PROVIDER=minimax
|
|
6
|
+
|
|
7
|
+
# AI Model(如果未设置,使用 provider 默认值)
|
|
8
|
+
# MiniMax: MiniMax-M2.7
|
|
9
|
+
# DeepSeek: deepseek-chat, deepseek-coder 等
|
|
10
|
+
FOLIKO_MODEL=MiniMax-M2.7
|
|
11
|
+
|
|
12
|
+
# API Base URL(如果未设置,使用 provider 默认值)
|
|
13
|
+
# MiniMax: https://api.minimaxi.com/v1
|
|
14
|
+
# DeepSeek: https://api.deepseek.com/v1
|
|
15
|
+
FOLIKO_BASE_URL=https://api.minimaxi.com/v1
|
|
16
|
+
|
|
17
|
+
# API Key(通用,如果未设置则尝试 provider 专用 key)
|
|
18
|
+
FOLIKO_API_KEY=sk-your-api-key
|
|
19
|
+
|
|
20
|
+
# Provider 专用 API Key(可选,如果 FOLIKO_API_KEY 未设置则使用这些)
|
|
21
|
+
DEEPSEEK_API_KEY=sk-your-deepseek-api-key
|
|
22
|
+
MINIMAX_API_KEY=sk-your-minimax-api-key
|
|
23
|
+
|
|
24
|
+
# ========== Email Configuration ==========
|
|
25
|
+
# SMTP Settings (for sending emails)
|
|
26
|
+
SMTP_HOST=smtp.gmail.com
|
|
27
|
+
SMTP_PORT=587
|
|
28
|
+
SMTP_SECURE=false
|
|
29
|
+
SMTP_USER=your-email@gmail.com
|
|
30
|
+
SMTP_PASS=your-app-password
|
|
31
|
+
|
|
32
|
+
# IMAP Settings (for reading emails)
|
|
33
|
+
IMAP_HOST=imap.gmail.com
|
|
34
|
+
IMAP_PORT=993
|
|
35
|
+
IMAP_USER=your-email@gmail.com
|
|
36
|
+
IMAP_PASS=your-app-password
|
|
37
|
+
|
|
38
|
+
# Default sender email address
|
|
39
|
+
FROM_EMAIL=your-email@gmail.com
|
|
40
|
+
|
|
41
|
+
# ========== Telegram Bot (optional) ==========
|
|
42
|
+
TELEGRAM_BOT_TOKEN=your-telegram-bot-token
|
|
43
|
+
|
|
44
|
+
# ========== Feishu Bot (optional) ==========
|
|
45
|
+
FEISHU_APP_ID=cli_xxxxxxxxxxx
|
|
46
|
+
FEISHU_APP_SECRET=app_secret
|
|
47
|
+
|
|
48
|
+
# ========== Web Server (optional) ==========
|
|
49
|
+
# Web 服务端口,默认 8088
|
|
50
|
+
WEB_PORT=3000
|
|
51
|
+
|
|
52
|
+
# Web 服务主机,默认 127.0.0.1
|
|
53
|
+
WEB_HOST=127.0.0.1
|
|
54
|
+
|
|
55
|
+
# 公网访问的 base URL(用于生成 webhook URL 等),不设置则使用 host:port,最好部署在docker中可以暴露自定义域名
|
|
56
|
+
WEB_BASE_URL=https://your-domain.com
|
package/cli/src/ui/chat-ui.js
CHANGED
|
@@ -9,12 +9,14 @@ const { TUI, ProcessTerminal, Editor, Text, CombinedAutocompleteProvider, matche
|
|
|
9
9
|
const { cleanResponse, logger } = require('../../../src/utils');
|
|
10
10
|
const { logEmitter } = require('../../../src/utils/logger');
|
|
11
11
|
const { renderLine } = require('../utils/markdown');
|
|
12
|
-
const { MessageBubble } = require('./message-bubble');
|
|
12
|
+
const { MessageBubble } = require('./components/message-bubble');
|
|
13
13
|
const Queue=require('js-queue');
|
|
14
14
|
const hl = require('cli-highlight');
|
|
15
15
|
const { renderDiffWithHeader } = require('../utils/render-diff');
|
|
16
|
-
const { FooterBar } = require('./footer-bar');
|
|
17
|
-
const { StatusBar } = require('./status-bar');
|
|
16
|
+
const { FooterBar } = require('./components/footer-bar');
|
|
17
|
+
const { StatusBar } = require('./components/status-bar');
|
|
18
|
+
const { AgentMentionProvider } = require('./components/agent-mention-provider');
|
|
19
|
+
const { ChainedAutocompleteProvider } = require('./components/chained-autocomplete-provider');
|
|
18
20
|
const queue=new Queue();
|
|
19
21
|
// Foliko 主色(蓝绿)
|
|
20
22
|
const folikoPrimary = chalk.hex('#2A9D8F');
|
|
@@ -103,10 +105,12 @@ class ChatUI {
|
|
|
103
105
|
{ name: "reload", description: "重载技能" },
|
|
104
106
|
];
|
|
105
107
|
|
|
106
|
-
this.autocompleteProvider = new
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
108
|
+
this.autocompleteProvider = new ChainedAutocompleteProvider([
|
|
109
|
+
// 优先拦截 @子Agent 补全
|
|
110
|
+
new AgentMentionProvider(agent.framework),
|
|
111
|
+
// 文件路径 / slash 命令补全
|
|
112
|
+
new CombinedAutocompleteProvider([...baseCommands, ...this.skillCommands], process.cwd()),
|
|
113
|
+
]);
|
|
110
114
|
|
|
111
115
|
const editor=this.editor = new Editor(this.tui, editorTheme,{paddingX:1});
|
|
112
116
|
this.editor.setAutocompleteProvider(this.autocompleteProvider);
|
|
@@ -192,13 +196,13 @@ class ChatUI {
|
|
|
192
196
|
ERROR: 'red',
|
|
193
197
|
}
|
|
194
198
|
// 监听错误日志,显示到 tooler
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
199
|
+
this._logHandler = (data) => {
|
|
200
|
+
if(!Object.keys(level_dict).includes(data.level))return;
|
|
201
|
+
const level=folikoTerracotta(`[${data.level}]`)
|
|
202
|
+
const level_key=level_dict[data.level]||'dim'
|
|
203
|
+
this.statusBar.notifier.show(`${level} ${chalk[level_key](data.message)}`)
|
|
204
|
+
};
|
|
205
|
+
logger.on('log', this._logHandler);
|
|
202
206
|
|
|
203
207
|
// 监听通知事件,显示到通知区域
|
|
204
208
|
this._notificationHandler = (data) => {
|
|
@@ -455,62 +459,20 @@ class ChatUI {
|
|
|
455
459
|
|
|
456
460
|
|
|
457
461
|
_clearContext() {
|
|
458
|
-
|
|
459
|
-
this.statusBar.tooler.hide();
|
|
460
|
-
this.statusBar.loader.hide();
|
|
462
|
+
|
|
461
463
|
|
|
462
464
|
const { sessionId } = this;
|
|
465
|
+
this.agent.clearContext(sessionId);
|
|
463
466
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
// 1. 清除 AgentChatHandler 的消息存储(需要传 sessionId)
|
|
470
|
-
if (this.agent._chatHandler) {
|
|
471
|
-
this.agent._chatHandler.clearHistory(sessionId);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// 2. 清除 SessionContext 的消息和压缩计数
|
|
475
|
-
if (this.agent.framework) {
|
|
476
|
-
const sessionCtx = this.agent.framework.getSessionContext(sessionId);
|
|
477
|
-
if (sessionCtx) {
|
|
478
|
-
sessionCtx.clearMessages();
|
|
479
|
-
if (sessionCtx.compressionState) {
|
|
480
|
-
sessionCtx.compressionState.count = 0;
|
|
481
|
-
}
|
|
482
|
-
if (sessionCtx.metadata) {
|
|
483
|
-
sessionCtx.metadata.compressionCount = 0;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
// 同步清除 framework 缓存的 sessionContexts
|
|
487
|
-
const cachedCtx = this.agent.framework._sessionContexts?.get(sessionId);
|
|
488
|
-
if (cachedCtx) {
|
|
489
|
-
cachedCtx.clearMessages();
|
|
490
|
-
if (cachedCtx.compressionState) {
|
|
491
|
-
cachedCtx.compressionState.count = 0;
|
|
492
|
-
}
|
|
493
|
-
if (cachedCtx.metadata) {
|
|
494
|
-
cachedCtx.metadata.compressionCount = 0;
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
// 清除 SessionPlugin 中的消息
|
|
498
|
-
if (this.sessionPlugin) {
|
|
499
|
-
const manager = this.sessionPlugin._getSessionManager(sessionId);
|
|
500
|
-
if (manager) {
|
|
501
|
-
manager.clearMessages();
|
|
502
|
-
}
|
|
503
|
-
}
|
|
467
|
+
if (this.sessionPlugin) {
|
|
468
|
+
const manager = this.sessionPlugin._getSessionManager(sessionId);
|
|
469
|
+
if (manager) manager.clearMessages();
|
|
504
470
|
}
|
|
505
471
|
|
|
506
|
-
// 清空 messageContainer 中的消息,保留 welcome
|
|
507
472
|
const welcomeChild = this.messageContainer.children[0];
|
|
508
473
|
this.messageContainer.clear();
|
|
509
|
-
if (welcomeChild)
|
|
510
|
-
this.messageContainer.addChild(welcomeChild);
|
|
511
|
-
}
|
|
474
|
+
if (welcomeChild) this.messageContainer.addChild(welcomeChild);
|
|
512
475
|
|
|
513
|
-
// 重置 footer 统计
|
|
514
476
|
if (this.footerBar) {
|
|
515
477
|
this.footerBar.reset();
|
|
516
478
|
const footerLines = this.footerBar.render(this.tui.width || 80);
|
|
@@ -518,72 +480,37 @@ class ChatUI {
|
|
|
518
480
|
this._footerText.setText(footerLines[0]);
|
|
519
481
|
}
|
|
520
482
|
}
|
|
483
|
+
this.statusBar.tooler.hide();
|
|
484
|
+
this.statusBar.loader.hide();
|
|
521
485
|
this.tui.requestRender();
|
|
522
486
|
}
|
|
523
487
|
|
|
524
488
|
_compressContext() {
|
|
525
|
-
|
|
526
|
-
this.statusBar.tooler.hide();
|
|
527
|
-
this.statusBar.loader.hide();
|
|
489
|
+
|
|
528
490
|
|
|
529
491
|
const { sessionId } = this;
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
messages = sessionCtx.getMessages();
|
|
548
|
-
// 同步到 messageStore
|
|
549
|
-
messageStore.messages = messages;
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
const beforeCount = messages ? messages.length : 0;
|
|
555
|
-
this.statusBar.tooler.show(`${colored('[压缩]', YELLOW)} 压缩前: ${beforeCount} 条`);
|
|
556
|
-
|
|
557
|
-
if (messages && messages.length > 0) {
|
|
558
|
-
// 使用 ContextCompressor 压缩
|
|
559
|
-
if (chatHandler._contextCompressor) {
|
|
560
|
-
chatHandler._contextCompressor.compress(sessionId, messages, messageStore).then(() => {
|
|
561
|
-
const afterCount = messages.length;
|
|
562
|
-
this.statusBar.tooler.setText(`${colored('[压缩]', YELLOW)} 压缩后: ${afterCount} 条 (保留${chatHandler._contextCompressor._keepRecentMessages}条)`);
|
|
563
|
-
// 同步回 SessionContext
|
|
564
|
-
if (this.agent.framework) {
|
|
565
|
-
const sessionCtx = this.agent.framework.getSessionContext(sessionId);
|
|
566
|
-
if (sessionCtx) {
|
|
567
|
-
sessionCtx.replaceMessages(messages);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
setTimeout(() => {
|
|
571
|
-
this.statusBar.tooler.hide();
|
|
572
|
-
this.create_message(`${colored('[提示]', CYAN)} 上下文已压缩`,chalk.dim('● '))
|
|
573
|
-
this.tui.requestRender();
|
|
574
|
-
}, 2000);
|
|
575
|
-
}).catch(err => {
|
|
576
|
-
this.statusBar.tooler.setText(`${colored('[错误]', RED)} 压缩失败: ${err.message}`);
|
|
577
|
-
});
|
|
578
|
-
} else {
|
|
579
|
-
this.statusBar.tooler.setText(`${colored('[错误]', RED)} 无 ContextCompressor`);
|
|
580
|
-
}
|
|
581
|
-
} else {
|
|
492
|
+
this.statusBar.tooler.show(`${colored('[压缩]', YELLOW)} 压缩中...`);
|
|
493
|
+
|
|
494
|
+
this.agent.compressContext(sessionId).then(({ before, after, keepRecent }) => {
|
|
495
|
+
this.statusBar.tooler.setText(
|
|
496
|
+
`${colored('[压缩]', YELLOW)} 压缩后: ${after} 条 (保留${keepRecent}条)`
|
|
497
|
+
);
|
|
498
|
+
setTimeout(() => {
|
|
499
|
+
this.statusBar.tooler.hide();
|
|
500
|
+
this.statusBar.loader.hide();
|
|
501
|
+
this.create_message(`${colored('[提示]', CYAN)} 上下文已压缩`, chalk.dim('● '));
|
|
502
|
+
this.tui.requestRender();
|
|
503
|
+
}, 2000);
|
|
504
|
+
}).catch(err => {
|
|
505
|
+
this.statusBar.tooler.hide();
|
|
506
|
+
this.statusBar.loader.hide();
|
|
507
|
+
const msg = err.message || String(err);
|
|
508
|
+
if (msg.includes('No messages')) {
|
|
582
509
|
this.statusBar.tooler.setText(`${colored('[提示]', CYAN)} 无消息可压缩`);
|
|
510
|
+
} else {
|
|
511
|
+
this.statusBar.tooler.setText(`${colored('[错误]', RED)} ${msg}`);
|
|
583
512
|
}
|
|
584
|
-
}
|
|
585
|
-
this.statusBar.tooler.setText(`${colored('[错误]', RED)} 无 AgentChatHandler`);
|
|
586
|
-
}
|
|
513
|
+
});
|
|
587
514
|
}
|
|
588
515
|
/**
|
|
589
516
|
* 刷新 skill 命令列表(用于热重载)
|
|
@@ -602,13 +529,16 @@ class ChatUI {
|
|
|
602
529
|
*/
|
|
603
530
|
_refreshAutocomplete() {
|
|
604
531
|
if (!this.editor) return;
|
|
605
|
-
|
|
606
|
-
// 重新构建
|
|
532
|
+
|
|
533
|
+
// 重新构建 ChainedAutocompleteProvider(含 AgentMentionProvider)
|
|
607
534
|
const { CombinedAutocompleteProvider } = require('@earendil-works/pi-tui');
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
535
|
+
const { AgentMentionProvider } = require('./components/agent-mention-provider');
|
|
536
|
+
const { ChainedAutocompleteProvider } = require('./components/chained-autocomplete-provider');
|
|
537
|
+
|
|
538
|
+
this.autocompleteProvider = new ChainedAutocompleteProvider([
|
|
539
|
+
new AgentMentionProvider(this.agent.framework),
|
|
540
|
+
new CombinedAutocompleteProvider([...this.baseCommands, ...this.skillCommands], process.cwd()),
|
|
541
|
+
]);
|
|
612
542
|
this.editor.setAutocompleteProvider(this.autocompleteProvider);
|
|
613
543
|
}
|
|
614
544
|
/**
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentMentionProvider — @子Agent 提及补全提供者
|
|
3
|
+
*
|
|
4
|
+
* 处理 @agent_name 语法,在输入 @ 时触发子Agent 列表补全。
|
|
5
|
+
* 实现 AutocompleteProvider 接口,与 CombinedAutocompleteProvider 协同工作。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { fuzzyFilter } = require('@earendil-works/pi-tui/dist/fuzzy.js');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} AutocompleteItem
|
|
12
|
+
* @property {string} value
|
|
13
|
+
* @property {string} label
|
|
14
|
+
* @property {string} [description]
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} AutocompleteSuggestions
|
|
19
|
+
* @property {AutocompleteItem[]} items
|
|
20
|
+
* @property {string} prefix
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {Object} AutocompleteProvider
|
|
25
|
+
* @property {Function} getSuggestions
|
|
26
|
+
* @property {Function} applyCompletion
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 从文本中提取 @ 前缀(用于检测 @ 子Agent 触发)
|
|
31
|
+
* @param {string} textBeforeCursor
|
|
32
|
+
* @returns {string|null} '@prefix' 或 null
|
|
33
|
+
*/
|
|
34
|
+
function extractAtPrefixForAgent(textBeforeCursor) {
|
|
35
|
+
// 找最后一个有效的 @ 前缀
|
|
36
|
+
// 有效的 @:前面是行首/空白/非字母数字字符(排除邮箱)
|
|
37
|
+
// 支持多 @ 行:'@foo @bar' → 光标在末尾空格时返回 '@'
|
|
38
|
+
const lastAtIndex = textBeforeCursor.lastIndexOf('@');
|
|
39
|
+
if (lastAtIndex === -1) return null;
|
|
40
|
+
|
|
41
|
+
const prevChar = textBeforeCursor[lastAtIndex - 1];
|
|
42
|
+
|
|
43
|
+
// 邮箱内嵌 @(前一个字符是字母数字)→ 排除
|
|
44
|
+
if (prevChar && /[a-zA-Z0-9]/.test(prevChar)) return null;
|
|
45
|
+
|
|
46
|
+
const afterAt = textBeforeCursor.slice(lastAtIndex + 1);
|
|
47
|
+
|
|
48
|
+
// @ 后紧跟非空白 → 正在输入 agent 名,正常返回前缀
|
|
49
|
+
if (/\S/.test(afterAt)) {
|
|
50
|
+
return textBeforeCursor.slice(lastAtIndex);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// @ 后是空白或空 → 独立的 @,触发空前缀列表
|
|
54
|
+
// @ 必须在行首、或前面是空白/非字母数字字符
|
|
55
|
+
if (lastAtIndex === 0 || !prevChar || /\s/.test(prevChar) || /[^\sa-zA-Z0-9]/.test(prevChar)) {
|
|
56
|
+
return '@';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
class AgentMentionProvider {
|
|
63
|
+
/**
|
|
64
|
+
* @param {Object} framework - Foliko Framework 实例
|
|
65
|
+
* @param {Object} fallbackProvider - 备用的 AutocompleteProvider(如 CombinedAutocompleteProvider)
|
|
66
|
+
*/
|
|
67
|
+
constructor(framework, fallbackProvider = null) {
|
|
68
|
+
this.framework = framework;
|
|
69
|
+
this.fallbackProvider = fallbackProvider;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 获取所有子Agent(排除主Agent)
|
|
74
|
+
* @returns {Array<{name: string, description: string}>}
|
|
75
|
+
*/
|
|
76
|
+
getSubAgents() {
|
|
77
|
+
if (!this.framework?._agents) return [];
|
|
78
|
+
|
|
79
|
+
const mainAgent = this.framework._mainAgent;
|
|
80
|
+
return this.framework._agents
|
|
81
|
+
.filter(agent => agent !== mainAgent && agent.name)
|
|
82
|
+
.map(agent => ({
|
|
83
|
+
name: agent.name.replace(/^subagent_/, '').replace(/^session_/, ''),
|
|
84
|
+
description: agent._role || agent.description || agent.name,
|
|
85
|
+
}));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 判断 @ 前缀是否看起来像子Agent 名称(而非文件路径)
|
|
90
|
+
* @param {string} atPrefix
|
|
91
|
+
* @returns {boolean}
|
|
92
|
+
*/
|
|
93
|
+
_looksLikeAgentName(atPrefix) {
|
|
94
|
+
// 如果包含路径分隔符或点号扩展名,认为是文件路径
|
|
95
|
+
if (atPrefix.includes('/') || atPrefix.includes('\\') || atPrefix.includes('.')) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 获取补全建议
|
|
103
|
+
* @returns {Promise<AutocompleteSuggestions|null>}
|
|
104
|
+
*/
|
|
105
|
+
async getSuggestions(lines, cursorLine, cursorCol, options) {
|
|
106
|
+
try {
|
|
107
|
+
const currentLine = lines[cursorLine] || '';
|
|
108
|
+
const textBeforeCursor = currentLine.slice(0, cursorCol);
|
|
109
|
+
const atPrefix = extractAtPrefixForAgent(textBeforeCursor);
|
|
110
|
+
|
|
111
|
+
if (!atPrefix || typeof atPrefix !== 'string') {
|
|
112
|
+
// 没有 @ 前缀,委托给 fallback provider
|
|
113
|
+
return this.fallbackProvider?.getSuggestions?.(lines, cursorLine, cursorCol, options) ?? null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const rawPrefix = atPrefix.startsWith('@') ? atPrefix.slice(1) : atPrefix;
|
|
117
|
+
|
|
118
|
+
// 如果看起来像文件路径(包含 / \ .),委托给 fallback
|
|
119
|
+
if (!this._looksLikeAgentName(rawPrefix)) {
|
|
120
|
+
return this.fallbackProvider?.getSuggestions?.(lines, cursorLine, cursorCol, options) ?? null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const subAgents = this.getSubAgents();
|
|
124
|
+
if (!subAgents || subAgents.length === 0) {
|
|
125
|
+
return this.fallbackProvider?.getSuggestions?.(lines, cursorLine, cursorCol, options) ?? null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const items = subAgents.map(agent => ({
|
|
129
|
+
value: agent.name,
|
|
130
|
+
label: agent.name,
|
|
131
|
+
description: agent.description || undefined,
|
|
132
|
+
}));
|
|
133
|
+
|
|
134
|
+
const filtered = fuzzyFilter(items, rawPrefix, (item) => item.label).map(item => ({
|
|
135
|
+
value: item.value,
|
|
136
|
+
label: item.label,
|
|
137
|
+
...(item.description && { description: item.description }),
|
|
138
|
+
}));
|
|
139
|
+
|
|
140
|
+
if (filtered.length === 0) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
items: filtered,
|
|
146
|
+
prefix: atPrefix,
|
|
147
|
+
};
|
|
148
|
+
} catch (err) {
|
|
149
|
+
// 出错时委托给 fallback
|
|
150
|
+
return this.fallbackProvider?.getSuggestions?.(lines, cursorLine, cursorCol, options) ?? null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 应用补全
|
|
156
|
+
* @returns {{ lines: string[], cursorLine: number, cursorCol: number }}
|
|
157
|
+
*/
|
|
158
|
+
applyCompletion(lines, cursorLine, cursorCol, item, prefix) {
|
|
159
|
+
const currentLine = lines[cursorLine] || '';
|
|
160
|
+
const beforePrefix = currentLine.slice(0, cursorCol - prefix.length);
|
|
161
|
+
const afterCursor = currentLine.slice(cursorCol);
|
|
162
|
+
|
|
163
|
+
const newLine = `${beforePrefix}@${item.value} ${afterCursor}`;
|
|
164
|
+
const newLines = [...lines];
|
|
165
|
+
newLines[cursorLine] = newLine;
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
lines: newLines,
|
|
169
|
+
cursorLine,
|
|
170
|
+
cursorCol: beforePrefix.length + item.value.length + 2,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = { AgentMentionProvider };
|