foliko 1.1.67 → 1.1.69
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 +19 -10
- package/.dockerignore +45 -45
- package/.env.example +56 -56
- package/CLAUDE.md +2 -2
- package/README.md +13 -13
- package/SPEC.md +3 -3
- package/cli/src/commands/chat.js +2 -20
- package/cli/src/commands/list.js +7 -6
- package/cli/src/commands/plugin.js +3 -2
- package/cli/src/daemon.js +2 -2
- package/cli/src/ui/chat-ui-old.js +15 -4
- package/cli/src/ui/chat-ui.js +236 -203
- package/cli/src/ui/footer-bar.js +20 -46
- package/cli/src/ui/message-bubble.js +24 -2
- package/cli/src/ui/status-bar.js +177 -0
- package/cli/src/utils/config.js +29 -0
- package/cli/src/utils/plugin-config.js +1 -1
- package/docker-compose.yml +33 -33
- package/docs/features.md +120 -120
- package/docs/quick-reference.md +160 -160
- package/docs/user-manual.md +1391 -1391
- package/examples/ambient-example.js +2 -2
- package/examples/bootstrap.js +3 -3
- package/examples/test-chat.js +1 -1
- package/examples/test-reload.js +1 -1
- package/examples/test-telegram.js +1 -1
- package/examples/test-tg-bot.js +1 -1
- package/examples/test-tg-simple.js +2 -2
- package/examples/test-tg.js +1 -1
- package/examples/test-think.js +1 -1
- package/examples/test-weixin-feishu.js +3 -3
- package/package.json +3 -1
- package/plugins/ambient-agent/index.js +1 -1
- package/plugins/audit-plugin.js +84 -29
- package/plugins/coordinator-plugin.js +14 -12
- package/plugins/data-splitter-plugin.js +323 -0
- package/plugins/default-plugins.js +23 -12
- package/plugins/email/index.js +1 -1
- package/plugins/extension-executor-plugin.js +87 -9
- package/plugins/feishu-plugin.js +118 -16
- package/plugins/file-system-plugin.js +68 -50
- package/plugins/gate-trading.js +10 -10
- package/plugins/install-plugin.js +7 -7
- package/plugins/memory-plugin.js +9 -12
- package/plugins/plugin-manager-plugin.js +12 -14
- package/plugins/python-executor-plugin.js +1 -1
- package/plugins/python-plugin-loader.js +1 -1
- package/plugins/qq-plugin.js +151 -24
- package/plugins/rules-plugin.js +8 -8
- package/plugins/scheduler-plugin.js +24 -20
- package/plugins/session-plugin.js +313 -397
- package/plugins/storage-plugin.js +235 -175
- package/plugins/subagent-plugin.js +17 -13
- package/plugins/telegram-plugin.js +116 -17
- package/plugins/think-plugin.js +64 -60
- package/plugins/tools-plugin.js +8 -8
- package/plugins/web-plugin.js +2 -2
- package/plugins/weixin-plugin.js +107 -24
- package/skills/find-skills/AGENTS.md +2 -2
- package/skills/find-skills/SKILL.md +133 -133
- package/skills/foliko-dev/AGENTS.md +236 -236
- package/skills/foliko-dev/SKILL.md +19 -19
- package/skills/mcp-usage/SKILL.md +200 -200
- package/skills/plugin-guide/SKILL.md +4 -4
- package/skills/python-plugin-dev/SKILL.md +5 -5
- package/skills/skill-guide/SKILL.md +104 -6
- package/skills/subagent-guide/SKILL.md +237 -237
- package/skills/workflow-guide/SKILL.md +646 -646
- package/src/capabilities/skill-manager.js +124 -17
- package/src/capabilities/workflow-engine.js +3 -3
- package/src/core/agent-chat.js +72 -26
- package/src/core/agent.js +17 -27
- package/src/core/branch-summary-auto.js +206 -0
- package/src/core/chat-session.js +45 -169
- package/src/core/command-registry.js +200 -0
- package/src/core/constants.js +198 -0
- package/src/core/context-compressor.js +702 -326
- package/src/core/context-manager.js +0 -1
- package/src/core/enhanced-context-compressor.js +210 -0
- package/src/core/framework.js +260 -84
- package/src/core/jsonl-storage.js +253 -0
- package/src/core/plugin-base.js +7 -5
- package/src/core/plugin-manager.js +15 -10
- package/src/core/provider-registry.js +159 -0
- package/src/core/provider.js +2 -0
- package/src/core/session-entry.js +225 -0
- package/src/core/session-manager.js +701 -0
- package/src/core/storage-manager.js +494 -0
- package/src/core/sub-agent-config.js +1 -1
- package/src/core/subagent.js +16 -135
- package/src/core/token-counter.js +177 -58
- package/src/core/tool-executor.js +2 -70
- package/src/core/ui-extension-context.js +174 -0
- package/src/executors/mcp-executor.js +27 -16
- package/src/utils/chat-queue.js +11 -22
- package/src/utils/data-splitter.js +345 -0
- package/src/utils/logger.js +152 -180
- package/src/utils/message-validator.js +283 -0
- package/src/utils/plugin-helpers.js +2 -2
- package/src/utils/retry.js +168 -22
- package/website_v2/docs/api.html +1 -1
- package/website_v2/docs/configuration.html +2 -2
- package/website_v2/docs/plugin-development.html +4 -4
- package/website_v2/docs/project-structure.html +2 -2
- package/website_v2/docs/skill-development.html +2 -2
- package/website_v2/index.html +1 -1
- package/website_v2/styles/animations.css +7 -7
- package/.agent/agents/backend-dev.md +0 -102
- package/.agent/agents/data-analyst.md +0 -117
- package/.agent/agents/devops.md +0 -115
- package/.agent/agents/frontend-dev.md +0 -94
- package/.agent/agents/network-requester.md +0 -44
- package/.agent/agents/poster-designer.md +0 -52
- package/.agent/agents/product-manager.md +0 -85
- package/.agent/agents/qa-engineer.md +0 -100
- package/.agent/agents/security-engineer.md +0 -99
- package/.agent/agents/team-lead.md +0 -137
- package/.agent/agents/ui-designer.md +0 -116
- package/.agent/data/default.json +0 -58
- package/.agent/data/email/processed-emails.json +0 -1
- package/.agent/data/plugins-state.json +0 -199
- package/.agent/data/scheduler/tasks.json +0 -1
- package/.agent/data/web/web-config.json +0 -5
- package/.agent/data/weixin/images/file_1776188148383jpg +0 -0
- package/.agent/data/weixin/images/file_1776188458326.jpg +0 -0
- package/.agent/data/weixin/images/file_1776188689423.jpg +0 -0
- package/.agent/data/weixin/images/file_1776188813604.jpg +0 -0
- package/.agent/data/weixin/images/file_1776189097450.jpg +0 -0
- package/.agent/data/weixin/videos/file_1776188318431.mp4 +0 -0
- package/.agent/data/weixin.json +0 -6
- package/.agent/mcp_config.json +0 -14
- package/.agent/memory/user/mof6gk94-kneeuh.md +0 -9
- package/.agent/package.json +0 -8
- package/.agent/plugins/marknative/README.md +0 -134
- package/.agent/plugins/marknative/fonts/SegoeUI Emoji.ttf +0 -0
- package/.agent/plugins/marknative/fonts.zip +0 -0
- package/.agent/plugins/marknative/index.js +0 -256
- package/.agent/plugins/marknative/package.json +0 -12
- package/.agent/plugins/test-plugin.py +0 -99
- package/.agent/plugins.json +0 -14
- package/.agent/python-scripts/test_sample.py +0 -24
- package/.agent/sessions/cli_default.json +0 -247
- package/.agent/skills/agent-browser/SKILL.md +0 -311
- package/.agent/skills/agent-browser/TEST_PLAN.md +0 -200
- package/.agent/skills/sysinfo/SKILL.md +0 -38
- package/.agent/skills/sysinfo/system-info.sh +0 -130
- package/.agent/skills/workflow/SKILL.md +0 -324
- package/.agent/test-agent.js +0 -35
- package/.agent/weixin.json +0 -6
- package/.agent/workflows/email-digest.json +0 -50
- package/.agent/workflows/file-backup.json +0 -21
- package/.agent/workflows/get-ip-notify.json +0 -32
- package/.agent/workflows/news-aggregator.json +0 -93
- package/.agent/workflows/news-dashboard-v2.json +0 -94
- package/.agent/workflows/notification-batch.json +0 -32
- package/src/core/session-context.js +0 -346
- package/src/core/session-storage.js +0 -295
package/cli/src/ui/chat-ui.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
const chalk = require('chalk').default;
|
|
6
6
|
const figlet = require('figlet');
|
|
7
7
|
const { CLEAR_LINE, CYAN, DIM, GREEN, RED, YELLOW, colored } = require('../utils/ansi');
|
|
8
|
-
const { TUI, ProcessTerminal, Editor,
|
|
9
|
-
const { cleanResponse } = require('../../../src/utils');
|
|
8
|
+
const { TUI, ProcessTerminal, Editor, Text, CombinedAutocompleteProvider, matchesKey, Container } = require('@earendil-works/pi-tui');
|
|
9
|
+
const { cleanResponse, logger } = require('../../../src/utils');
|
|
10
10
|
const { logEmitter } = require('../../../src/utils/logger');
|
|
11
11
|
const { renderLine } = require('../utils/markdown');
|
|
12
12
|
const { MessageBubble } = require('./message-bubble');
|
|
@@ -14,6 +14,7 @@ const Queue=require('js-queue');
|
|
|
14
14
|
const hl = require('cli-highlight');
|
|
15
15
|
const { renderDiffWithHeader } = require('../utils/render-diff');
|
|
16
16
|
const { FooterBar } = require('./footer-bar');
|
|
17
|
+
const { StatusBar } = require('./status-bar');
|
|
17
18
|
const queue=new Queue();
|
|
18
19
|
// Foliko 主色(蓝绿)
|
|
19
20
|
const folikoPrimary = chalk.hex('#2A9D8F');
|
|
@@ -70,15 +71,15 @@ class ChatUI {
|
|
|
70
71
|
const terminal = new ProcessTerminal();
|
|
71
72
|
const tui=this.tui = new TUI(terminal);
|
|
72
73
|
this.messageContainer= new Container(0)
|
|
73
|
-
this.statusContainer= new Container(0)
|
|
74
74
|
const text=`${folikoTerracotta("Ctrl+C")} 退出 | ${folikoTerracotta("/")} 指令列表 | ${folikoTerracotta("Enter")} 发送 | ${folikoTerracotta("Shift+Enter")} 换行`
|
|
75
75
|
this.messageContainer.addChild(
|
|
76
76
|
new Text(FOLIKO_LOGO + folikoGold(`\n欢迎使用Foliko!\n`)+folikoSand(text),3,2),
|
|
77
77
|
);
|
|
78
78
|
this.tui.addChild(this.messageContainer)
|
|
79
|
-
this.tui.addChild(this.statusContainer)
|
|
80
|
-
|
|
81
79
|
|
|
80
|
+
// 状态栏组件(通知/工具调用/思考中),插在消息区和编辑器之间
|
|
81
|
+
this.statusBar = new StatusBar(tui, this.editor);
|
|
82
|
+
this.tui.addChild(this.statusBar.container);
|
|
82
83
|
|
|
83
84
|
this._currentBotMessage=null
|
|
84
85
|
|
|
@@ -88,12 +89,24 @@ class ChatUI {
|
|
|
88
89
|
// 会话管理
|
|
89
90
|
this.sessionId = options.sessionId || 'cli_default';
|
|
90
91
|
this.sessionPlugin = null;
|
|
92
|
+
|
|
93
|
+
// 获取 skill 命令列表
|
|
94
|
+
const skillCommands = [];
|
|
95
|
+
if (agent.framework) {
|
|
96
|
+
const extExecutor = agent.framework.pluginManager?.get('extension-executor');
|
|
97
|
+
if (extExecutor && typeof extExecutor.getSkillCommands === 'function') {
|
|
98
|
+
skillCommands.push(...extExecutor.getSkillCommands());
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const baseCommands = [
|
|
103
|
+
{ name: "compress", description: "压缩记录" },
|
|
104
|
+
{ name: "clear", description: "清除记录" },
|
|
105
|
+
{ name: "exit", description: "退出" },
|
|
106
|
+
];
|
|
107
|
+
|
|
91
108
|
const autocompleteProvider = new CombinedAutocompleteProvider(
|
|
92
|
-
[
|
|
93
|
-
{ name: "compress", description: "压缩记录" },
|
|
94
|
-
{ name: "clear", description: "清除记录" },
|
|
95
|
-
{ name: "exit", description: "退出" },
|
|
96
|
-
],
|
|
109
|
+
[...baseCommands, ...skillCommands],
|
|
97
110
|
process.cwd(),
|
|
98
111
|
);
|
|
99
112
|
|
|
@@ -135,132 +148,6 @@ class ChatUI {
|
|
|
135
148
|
if (initialFooter.length > 0) {
|
|
136
149
|
this._footerText.setText(initialFooter[0]);
|
|
137
150
|
}
|
|
138
|
-
const tooler=new Loader(
|
|
139
|
-
tui,
|
|
140
|
-
(s) => chalk.green(s), // indicatorColorFn
|
|
141
|
-
(s) => chalk.yellow(s), // messageColorFn
|
|
142
|
-
"工具调用...",
|
|
143
|
-
{
|
|
144
|
-
frames:["|", "/", "-", "\\"].map(str=>folikoTerracotta(str))
|
|
145
|
-
}
|
|
146
|
-
)
|
|
147
|
-
const loader=new Loader(
|
|
148
|
-
tui,
|
|
149
|
-
(s) => chalk.cyan(s), // indicatorColorFn
|
|
150
|
-
(s) => chalk.dim(s), // messageColorFn
|
|
151
|
-
"正在思考中...",
|
|
152
|
-
{
|
|
153
|
-
frames:["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"].map(str=>folikoPrimary(str))
|
|
154
|
-
}
|
|
155
|
-
)
|
|
156
|
-
// Spacer 占位,用于隐藏时保持布局
|
|
157
|
-
const toolerSpacer = new Spacer(0);
|
|
158
|
-
const loaderSpacer = new Spacer(0);
|
|
159
|
-
const notifierSpacer = new Spacer(0);
|
|
160
|
-
|
|
161
|
-
this.statusContainer.addChild(toolerSpacer); // tooler 位置
|
|
162
|
-
this.statusContainer.addChild(loaderSpacer); // loader 位置
|
|
163
|
-
this.statusContainer.addChild(notifierSpacer); // notifier 位置
|
|
164
|
-
|
|
165
|
-
const self = this;
|
|
166
|
-
|
|
167
|
-
this.tooler = {
|
|
168
|
-
dom: tooler,
|
|
169
|
-
spacer: toolerSpacer,
|
|
170
|
-
showed:false,
|
|
171
|
-
index:1,
|
|
172
|
-
show(text) {
|
|
173
|
-
this.showed = true;
|
|
174
|
-
self.statusContainer.children[this.index] = self.tooler.dom;
|
|
175
|
-
this.dom.setMessage(text);
|
|
176
|
-
tui.requestRender();
|
|
177
|
-
return self.tooler.dom;
|
|
178
|
-
},
|
|
179
|
-
hide() {
|
|
180
|
-
this.showed = false;
|
|
181
|
-
self.statusContainer.children[this.index] = self.tooler.spacer;
|
|
182
|
-
tui.requestRender();
|
|
183
|
-
},
|
|
184
|
-
setText(text) {
|
|
185
|
-
if(!this.showed){
|
|
186
|
-
this.show(text);
|
|
187
|
-
} else {
|
|
188
|
-
this.dom.setMessage(text);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
};
|
|
192
|
-
|
|
193
|
-
this.notifier = {
|
|
194
|
-
dom: null,
|
|
195
|
-
spacer: notifierSpacer,
|
|
196
|
-
showed: false,
|
|
197
|
-
_timeout: null,
|
|
198
|
-
index:0,
|
|
199
|
-
show(text, duration = 5000) {
|
|
200
|
-
if (self.notifier._timeout) {
|
|
201
|
-
clearTimeout(self.notifier._timeout);
|
|
202
|
-
}
|
|
203
|
-
if (!self.notifier.dom) {
|
|
204
|
-
self.notifier.dom = new Loader(
|
|
205
|
-
tui,
|
|
206
|
-
(s) => chalk.cyan(s),
|
|
207
|
-
(s) => chalk.yellow(s),
|
|
208
|
-
text,
|
|
209
|
-
);
|
|
210
|
-
}
|
|
211
|
-
self.notifier.dom.setMessage(text);
|
|
212
|
-
self.statusContainer.children[this.index] = self.notifier.dom;
|
|
213
|
-
self.notifier.showed = true;
|
|
214
|
-
tui.requestRender();
|
|
215
|
-
self.notifier._timeout = setTimeout(() => {
|
|
216
|
-
self.notifier.hide();
|
|
217
|
-
}, duration);
|
|
218
|
-
},
|
|
219
|
-
hide() {
|
|
220
|
-
if (self.notifier._timeout) {
|
|
221
|
-
clearTimeout(self.notifier._timeout);
|
|
222
|
-
self.notifier._timeout = null;
|
|
223
|
-
}
|
|
224
|
-
self.notifier.showed = false;
|
|
225
|
-
self.statusContainer.children[this.index] = self.notifier.spacer;
|
|
226
|
-
tui.requestRender();
|
|
227
|
-
},
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
this.loader = {
|
|
231
|
-
dom: loader,
|
|
232
|
-
spacer: loaderSpacer,
|
|
233
|
-
startTime: null,
|
|
234
|
-
timer: null,
|
|
235
|
-
index:2,
|
|
236
|
-
show() {
|
|
237
|
-
self.statusContainer.children[this.index] = self.loader.dom;
|
|
238
|
-
editor.disableSubmit = true;
|
|
239
|
-
self.startTime = Date.now();
|
|
240
|
-
self.loader.dom.setMessage("正在思考中... (0秒)");
|
|
241
|
-
self.loader.dom.start();
|
|
242
|
-
// 每秒更新计时
|
|
243
|
-
self.loader.timer = setInterval(() => {
|
|
244
|
-
const elapsed = Math.floor((Date.now() - self.startTime) / 1000);
|
|
245
|
-
const seconds = elapsed % 60;
|
|
246
|
-
const minutes = Math.floor(elapsed / 60);
|
|
247
|
-
const timeStr = minutes > 0 ? `${minutes}分${seconds}秒` : `${seconds}秒`;
|
|
248
|
-
self.loader.dom.setMessage(`正在思考中... (${timeStr})`);
|
|
249
|
-
}, 1000);
|
|
250
|
-
tui.requestRender();
|
|
251
|
-
return self.loader.dom;
|
|
252
|
-
},
|
|
253
|
-
hide() {
|
|
254
|
-
editor.disableSubmit = false;
|
|
255
|
-
if (self.loader.timer) {
|
|
256
|
-
clearInterval(self.loader.timer);
|
|
257
|
-
self.loader.timer = null;
|
|
258
|
-
}
|
|
259
|
-
self.loader.dom.stop();
|
|
260
|
-
self.statusContainer.children[this.index] = self.loader.spacer;
|
|
261
|
-
tui.requestRender();
|
|
262
|
-
}
|
|
263
|
-
};
|
|
264
151
|
tui.requestRender();
|
|
265
152
|
// 中断状态(供监听器访问)
|
|
266
153
|
this.interrupted = false;
|
|
@@ -270,6 +157,11 @@ class ChatUI {
|
|
|
270
157
|
|
|
271
158
|
// 流式输出缓冲区(可重置)
|
|
272
159
|
this._lineBuffer = '';
|
|
160
|
+
// 流式渲染缓冲区:合并高频 chunk 后统一刷新 Markdown,减少重复解析
|
|
161
|
+
this._streamBufferTimer = null;
|
|
162
|
+
this._pendingTextUpdate = false;
|
|
163
|
+
// renderLine 的共享状态(引用传递,跨调用保持 codeBlock/think 状态)
|
|
164
|
+
this._renderState = { inThink: true, inCodeBlock: false };
|
|
273
165
|
// agent.framework.on("console:log",async (...args)=>{
|
|
274
166
|
// this.create_message(args.join(' '),chalk.dim('● '),true);
|
|
275
167
|
// })
|
|
@@ -296,18 +188,19 @@ class ChatUI {
|
|
|
296
188
|
this._setupSessionListeners();
|
|
297
189
|
const level_dict={
|
|
298
190
|
DEBUG: 'cyan',
|
|
299
|
-
|
|
191
|
+
INFO: 'dim',
|
|
192
|
+
LOG: 'dim',
|
|
300
193
|
WARN: 'yellow',
|
|
301
194
|
ERROR: 'red',
|
|
302
195
|
}
|
|
303
196
|
// 监听错误日志,显示到 tooler
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
197
|
+
this._logHandler = (data) => {
|
|
198
|
+
if(!Object.keys(level_dict).includes(data.level))return;
|
|
199
|
+
const level=folikoTerracotta(`[${data.level}]`)
|
|
200
|
+
const level_key=level_dict[data.level]||'dim'
|
|
201
|
+
this.statusBar.notifier.show(`${level} ${chalk[level_key](data.message)}`)
|
|
202
|
+
};
|
|
203
|
+
logger.on('log', this._logHandler);
|
|
311
204
|
|
|
312
205
|
// 监听通知事件,显示到通知区域
|
|
313
206
|
this._notificationHandler = (data) => {
|
|
@@ -318,7 +211,7 @@ class ChatUI {
|
|
|
318
211
|
}
|
|
319
212
|
const time = timestamp ? new Date(timestamp).toLocaleTimeString('zh-CN') : '';
|
|
320
213
|
const notificationText = `🔔 [${source}] ${title}${time ? ` (${time})` : ''}`;
|
|
321
|
-
this.notifier.show(notificationText);
|
|
214
|
+
this.statusBar.notifier.show(notificationText);
|
|
322
215
|
// 显示消息内容
|
|
323
216
|
this.create_message(`**${title}**\n${message}`, "🔔 ");
|
|
324
217
|
};
|
|
@@ -344,8 +237,13 @@ class ChatUI {
|
|
|
344
237
|
* 清理监听器
|
|
345
238
|
*/
|
|
346
239
|
dispose() {
|
|
240
|
+
// 清理流式缓冲区定时器
|
|
241
|
+
if (this._streamBufferTimer) {
|
|
242
|
+
clearTimeout(this._streamBufferTimer);
|
|
243
|
+
this._streamBufferTimer = null;
|
|
244
|
+
}
|
|
347
245
|
if (this._logHandler && this.agent.framework) {
|
|
348
|
-
|
|
246
|
+
logger.off('log', this._logHandler);
|
|
349
247
|
this._logHandler = null;
|
|
350
248
|
}
|
|
351
249
|
if (this._notificationHandler && this.agent.framework) {
|
|
@@ -359,8 +257,14 @@ class ChatUI {
|
|
|
359
257
|
}
|
|
360
258
|
|
|
361
259
|
clear_message_done(){
|
|
362
|
-
this.
|
|
363
|
-
this.
|
|
260
|
+
this._flushStreamBuffer(); // 确保最后的文本被刷新
|
|
261
|
+
if (this._streamBufferTimer) {
|
|
262
|
+
clearTimeout(this._streamBufferTimer);
|
|
263
|
+
this._streamBufferTimer = null;
|
|
264
|
+
}
|
|
265
|
+
this._pendingTextUpdate = false;
|
|
266
|
+
this.statusBar.loader.hide();
|
|
267
|
+
this.statusBar.tooler.hide();
|
|
364
268
|
this._lineBuffer=""
|
|
365
269
|
this.isResponding=false
|
|
366
270
|
this._currentBotMessage=null
|
|
@@ -379,6 +283,106 @@ class ChatUI {
|
|
|
379
283
|
return
|
|
380
284
|
}
|
|
381
285
|
|
|
286
|
+
/**
|
|
287
|
+
* 统一命令执行入口
|
|
288
|
+
*/
|
|
289
|
+
async _executeCommand(trimmed) {
|
|
290
|
+
// 基础命令
|
|
291
|
+
const cmd = trimmed.toLowerCase();
|
|
292
|
+
if (cmd === '/clear') {
|
|
293
|
+
this._clearContext()
|
|
294
|
+
this.create_message(`${colored('[提示]', CYAN)} 对话上下文已清除\n`,chalk.dim('● '))
|
|
295
|
+
return true;
|
|
296
|
+
}
|
|
297
|
+
if (cmd === '/compress') {
|
|
298
|
+
this._compressContext()
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
if (cmd === '/exit') {
|
|
302
|
+
this.create_message(`${colored('[再见]', CYAN)} 感谢使用 Foliko!\n`,chalk.dim('● '))
|
|
303
|
+
this.tui.stop();
|
|
304
|
+
setTimeout(() => process.exit(0), 500);
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// /help 命令显示所有注册的命令
|
|
309
|
+
if (cmd === '/help') {
|
|
310
|
+
const { getCommandRegistry } = require('../../../src/core/command-registry');
|
|
311
|
+
const registry = getCommandRegistry();
|
|
312
|
+
const commands = registry.getAllCommands();
|
|
313
|
+
|
|
314
|
+
let helpText = `${colored('[帮助]', CYAN)} 可用命令:\n`;
|
|
315
|
+
helpText += ` ${colored('/clear', CYAN)} - 清除对话上下文\n`;
|
|
316
|
+
helpText += ` ${colored('/compress', CYAN)} - 压缩上下文\n`;
|
|
317
|
+
helpText += ` ${colored('/exit', CYAN)} - 退出\n`;
|
|
318
|
+
|
|
319
|
+
// 添加 skill 命令
|
|
320
|
+
const extExecutor = this.agent.framework?.pluginManager?.get('extension-executor');
|
|
321
|
+
if (extExecutor && typeof extExecutor.getSkillCommandsHelp === 'function') {
|
|
322
|
+
const skillHelp = extExecutor.getSkillCommandsHelp();
|
|
323
|
+
if (skillHelp) {
|
|
324
|
+
helpText += `\n${colored('[技能命令]', CYAN)}\n`;
|
|
325
|
+
helpText += skillHelp.split('\n').map(line => ` ${line}`).join('\n') + '\n';
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (commands.length > 0) {
|
|
330
|
+
helpText += `\n${colored('[扩展命令]', CYAN)}\n`;
|
|
331
|
+
for (const cmd of commands) {
|
|
332
|
+
const pluginTag = cmd.registeredBy ? ` [${cmd.registeredBy}]` : '';
|
|
333
|
+
helpText += ` ${colored('/' + cmd.name, CYAN)}${pluginTag} - ${cmd.description || '无描述'}\n`;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
this.create_message(helpText, chalk.dim('● '));
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 尝试通过 CommandRegistry 执行命令
|
|
341
|
+
const cmdName = trimmed.slice(1).split(' ')[0];
|
|
342
|
+
const cmdArgs = trimmed.slice(cmdName.length + 2);
|
|
343
|
+
if (trimmed.startsWith('/') && cmdName) {
|
|
344
|
+
// 检查 skill 命令 (格式: skillname:cmdname)
|
|
345
|
+
if (cmdName.includes(':')) {
|
|
346
|
+
try {
|
|
347
|
+
const result = await this.agent.framework.executeTool('ext_call', {
|
|
348
|
+
plugin: 'skill',
|
|
349
|
+
tool: cmdName,
|
|
350
|
+
args: { args: cmdArgs }
|
|
351
|
+
});
|
|
352
|
+
if (result.success !== false) {
|
|
353
|
+
this.create_message(`${colored('[结果]', CYAN)} ${result.data || result}\n`, chalk.dim('● '));
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
} catch (err) {
|
|
357
|
+
log.warn('Skill command failed:', err.message);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// 检查 CommandRegistry 中的命令
|
|
362
|
+
const { getCommandRegistry } = require('../../../src/core/command-registry');
|
|
363
|
+
const registry = getCommandRegistry();
|
|
364
|
+
const cmd = registry.getCommand(cmdName);
|
|
365
|
+
if (cmd) {
|
|
366
|
+
try {
|
|
367
|
+
const context = {
|
|
368
|
+
sessionId: this.sessionId,
|
|
369
|
+
agent: this.agent,
|
|
370
|
+
ui: this,
|
|
371
|
+
framework: this.agent.framework,
|
|
372
|
+
};
|
|
373
|
+
registry.execute(cmdName, cmdArgs, context).catch(err => {
|
|
374
|
+
this.create_message(`${colored('[命令错误]', 'red')} ${err.message}\n`, chalk.dim('● '));
|
|
375
|
+
});
|
|
376
|
+
} catch (err) {
|
|
377
|
+
this.create_message(`${colored('[命令错误]', 'red')} ${err.message}\n`, chalk.dim('● '));
|
|
378
|
+
}
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
|
|
382
386
|
async handleOnSubmit(value){
|
|
383
387
|
try{
|
|
384
388
|
if (this.isResponding) {
|
|
@@ -389,21 +393,8 @@ class ChatUI {
|
|
|
389
393
|
return;
|
|
390
394
|
}
|
|
391
395
|
|
|
392
|
-
//
|
|
393
|
-
|
|
394
|
-
if (cmd === '/clear') {
|
|
395
|
-
this._clearContext()
|
|
396
|
-
this.create_message(`${colored('[提示]', CYAN)} 对话上下文已清除\n`,chalk.dim('● '))
|
|
397
|
-
return;
|
|
398
|
-
}
|
|
399
|
-
if (cmd === '/compress') {
|
|
400
|
-
this._compressContext()
|
|
401
|
-
return;
|
|
402
|
-
}
|
|
403
|
-
if (cmd === '/exit') {
|
|
404
|
-
this.create_message(`${colored('[再见]', CYAN)} 感谢使用 Foliko!\n`,chalk.dim('● '))
|
|
405
|
-
this.tui.stop();
|
|
406
|
-
setTimeout(() => process.exit(0), 500);
|
|
396
|
+
// 尝试执行命令
|
|
397
|
+
if (await this._executeCommand(trimmed)) {
|
|
407
398
|
return;
|
|
408
399
|
}
|
|
409
400
|
|
|
@@ -427,11 +418,8 @@ class ChatUI {
|
|
|
427
418
|
}
|
|
428
419
|
|
|
429
420
|
create_message(text, icon="", isBot=false, bgFn=null){
|
|
430
|
-
// 使用 MessageBubble 组件实现左侧图标 + 右侧 markdown 布局
|
|
431
|
-
// bgFn: 可选背景色函数 (line) => coloredLine,用于代码块等整块背景
|
|
432
421
|
const bubble = new MessageBubble(icon, text, isBot, markdownTheme, 0, 0, 1, bgFn);
|
|
433
|
-
|
|
434
|
-
children.splice(children.length, 0, bubble);
|
|
422
|
+
this.messageContainer.addChild(bubble);
|
|
435
423
|
this.tui.requestRender();
|
|
436
424
|
return bubble
|
|
437
425
|
}
|
|
@@ -440,11 +428,16 @@ class ChatUI {
|
|
|
440
428
|
|
|
441
429
|
_clearContext() {
|
|
442
430
|
// 隐藏 loader 和 tooler
|
|
443
|
-
this.tooler.hide();
|
|
444
|
-
this.loader.hide();
|
|
431
|
+
this.statusBar.tooler.hide();
|
|
432
|
+
this.statusBar.loader.hide();
|
|
445
433
|
|
|
446
434
|
const { sessionId } = this;
|
|
447
435
|
|
|
436
|
+
// Clear ChatSession message store (memory + file)
|
|
437
|
+
if (this.agent._chatSession) {
|
|
438
|
+
this.agent._chatSession.clearSessionMessages(sessionId, true);
|
|
439
|
+
}
|
|
440
|
+
|
|
448
441
|
// 1. 清除 AgentChatHandler 的消息存储(需要传 sessionId)
|
|
449
442
|
if (this.agent._chatHandler) {
|
|
450
443
|
this.agent._chatHandler.clearHistory(sessionId);
|
|
@@ -455,29 +448,38 @@ class ChatUI {
|
|
|
455
448
|
const sessionCtx = this.agent.framework.getSessionContext(sessionId);
|
|
456
449
|
if (sessionCtx) {
|
|
457
450
|
sessionCtx.clearMessages();
|
|
458
|
-
sessionCtx.compressionState
|
|
459
|
-
|
|
451
|
+
if (sessionCtx.compressionState) {
|
|
452
|
+
sessionCtx.compressionState.count = 0;
|
|
453
|
+
}
|
|
454
|
+
if (sessionCtx.metadata) {
|
|
455
|
+
sessionCtx.metadata.compressionCount = 0;
|
|
456
|
+
}
|
|
460
457
|
}
|
|
461
458
|
// 同步清除 framework 缓存的 sessionContexts
|
|
462
459
|
const cachedCtx = this.agent.framework._sessionContexts?.get(sessionId);
|
|
463
460
|
if (cachedCtx) {
|
|
464
461
|
cachedCtx.clearMessages();
|
|
465
|
-
cachedCtx.compressionState
|
|
466
|
-
|
|
462
|
+
if (cachedCtx.compressionState) {
|
|
463
|
+
cachedCtx.compressionState.count = 0;
|
|
464
|
+
}
|
|
465
|
+
if (cachedCtx.metadata) {
|
|
466
|
+
cachedCtx.metadata.compressionCount = 0;
|
|
467
|
+
}
|
|
467
468
|
}
|
|
468
469
|
// 清除 SessionPlugin 中的消息
|
|
469
470
|
if (this.sessionPlugin) {
|
|
470
|
-
const
|
|
471
|
-
if (
|
|
472
|
-
|
|
471
|
+
const manager = this.sessionPlugin._getSessionManager(sessionId);
|
|
472
|
+
if (manager) {
|
|
473
|
+
manager.clearMessages();
|
|
473
474
|
}
|
|
474
475
|
}
|
|
475
476
|
}
|
|
476
477
|
|
|
477
478
|
// 清空 messageContainer 中的消息,保留 welcome
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
|
|
479
|
+
const welcomeChild = this.messageContainer.children[0];
|
|
480
|
+
this.messageContainer.clear();
|
|
481
|
+
if (welcomeChild) {
|
|
482
|
+
this.messageContainer.addChild(welcomeChild);
|
|
481
483
|
}
|
|
482
484
|
|
|
483
485
|
// 重置 footer 统计
|
|
@@ -493,8 +495,8 @@ class ChatUI {
|
|
|
493
495
|
|
|
494
496
|
_compressContext() {
|
|
495
497
|
// 隐藏 loader 和 tooler
|
|
496
|
-
this.tooler.hide();
|
|
497
|
-
this.loader.hide();
|
|
498
|
+
this.statusBar.tooler.hide();
|
|
499
|
+
this.statusBar.loader.hide();
|
|
498
500
|
|
|
499
501
|
const { sessionId } = this;
|
|
500
502
|
|
|
@@ -522,14 +524,14 @@ class ChatUI {
|
|
|
522
524
|
}
|
|
523
525
|
|
|
524
526
|
const beforeCount = messages ? messages.length : 0;
|
|
525
|
-
this.tooler.show(`${colored('[压缩]', YELLOW)} 压缩前: ${beforeCount} 条`);
|
|
527
|
+
this.statusBar.tooler.show(`${colored('[压缩]', YELLOW)} 压缩前: ${beforeCount} 条`);
|
|
526
528
|
|
|
527
529
|
if (messages && messages.length > 0) {
|
|
528
530
|
// 使用 ContextCompressor 压缩
|
|
529
531
|
if (chatHandler._contextCompressor) {
|
|
530
532
|
chatHandler._contextCompressor.compress(sessionId, messages, messageStore).then(() => {
|
|
531
533
|
const afterCount = messages.length;
|
|
532
|
-
this.tooler.setText(`${colored('[压缩]', YELLOW)} 压缩后: ${afterCount} 条 (保留${chatHandler._contextCompressor._keepRecentMessages}条)`);
|
|
534
|
+
this.statusBar.tooler.setText(`${colored('[压缩]', YELLOW)} 压缩后: ${afterCount} 条 (保留${chatHandler._contextCompressor._keepRecentMessages}条)`);
|
|
533
535
|
// 同步回 SessionContext
|
|
534
536
|
if (this.agent.framework) {
|
|
535
537
|
const sessionCtx = this.agent.framework.getSessionContext(sessionId);
|
|
@@ -538,49 +540,79 @@ class ChatUI {
|
|
|
538
540
|
}
|
|
539
541
|
}
|
|
540
542
|
setTimeout(() => {
|
|
541
|
-
this.tooler.hide();
|
|
543
|
+
this.statusBar.tooler.hide();
|
|
542
544
|
this.create_message(`${colored('[提示]', CYAN)} 上下文已压缩`,chalk.dim('● '))
|
|
543
545
|
this.tui.requestRender();
|
|
544
546
|
}, 2000);
|
|
545
547
|
}).catch(err => {
|
|
546
|
-
this.tooler.setText(`${colored('[错误]', RED)} 压缩失败: ${err.message}`);
|
|
548
|
+
this.statusBar.tooler.setText(`${colored('[错误]', RED)} 压缩失败: ${err.message}`);
|
|
547
549
|
});
|
|
548
550
|
} else {
|
|
549
|
-
this.tooler.setText(`${colored('[错误]', RED)} 无 ContextCompressor`);
|
|
551
|
+
this.statusBar.tooler.setText(`${colored('[错误]', RED)} 无 ContextCompressor`);
|
|
550
552
|
}
|
|
551
553
|
} else {
|
|
552
|
-
this.tooler.setText(`${colored('[提示]', CYAN)} 无消息可压缩`);
|
|
554
|
+
this.statusBar.tooler.setText(`${colored('[提示]', CYAN)} 无消息可压缩`);
|
|
553
555
|
}
|
|
554
556
|
} else {
|
|
555
|
-
this.tooler.setText(`${colored('[错误]', RED)} 无 AgentChatHandler`);
|
|
557
|
+
this.statusBar.tooler.setText(`${colored('[错误]', RED)} 无 AgentChatHandler`);
|
|
556
558
|
}
|
|
557
559
|
}
|
|
560
|
+
/**
|
|
561
|
+
* 刷新流式输出缓冲区:合并高频 chunk 后统一渲染,减少 Markdown 重复解析
|
|
562
|
+
*/
|
|
563
|
+
_flushStreamBuffer() {
|
|
564
|
+
this._streamBufferTimer = null;
|
|
565
|
+
if (!this._lineBuffer) return;
|
|
566
|
+
|
|
567
|
+
// 渲染剩余的 partial line(没有 \n 结尾的尾部文字)
|
|
568
|
+
const rendered = renderLine(this._lineBuffer, this._renderState, true);
|
|
569
|
+
if (!this._currentBotMessage) {
|
|
570
|
+
this._currentBotMessage = this.create_message(rendered, colored('● ', GREEN), true);
|
|
571
|
+
} else {
|
|
572
|
+
this._currentBotMessage.appendContent(rendered);
|
|
573
|
+
}
|
|
574
|
+
this._lineBuffer = '';
|
|
575
|
+
}
|
|
576
|
+
|
|
558
577
|
/**
|
|
559
578
|
* 设置 session scope 事件监听
|
|
560
579
|
*/
|
|
561
580
|
_setupSessionListeners() {
|
|
562
|
-
const renderState = { inThink: true, inCodeBlock: false };
|
|
563
|
-
|
|
564
581
|
this.sessionScope.on('stream:chunk', ({ chunk }) => {
|
|
565
582
|
if (this.interrupted) return;
|
|
566
583
|
|
|
567
584
|
if (chunk.type === 'text') {
|
|
568
|
-
this._lineBuffer += chunk.text
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
585
|
+
this._lineBuffer += chunk.text
|
|
586
|
+
|
|
587
|
+
// 逐行处理:遇到 \n 立即渲染完整行,实现即时反馈
|
|
588
|
+
// 行尾补回 \n 确保 TUI 正确换行(空行也要渲染,保留段落分隔)
|
|
589
|
+
// while (this._lineBuffer.includes('\n')) {
|
|
590
|
+
// if (this.interrupted) break;
|
|
591
|
+
|
|
592
|
+
// const nlIndex = this._lineBuffer.indexOf('\n');
|
|
593
|
+
// const line = this._lineBuffer.substring(0, nlIndex);
|
|
594
|
+
// this._lineBuffer = this._lineBuffer.substring(nlIndex + 1);
|
|
595
|
+
|
|
596
|
+
// const rendered = line;
|
|
597
|
+
// if (!this._currentBotMessage) {
|
|
598
|
+
// this._currentBotMessage = this.create_message(rendered, colored('● ', GREEN), true);
|
|
599
|
+
// } else {
|
|
600
|
+
// this._currentBotMessage.appendContent(rendered);
|
|
601
|
+
// }
|
|
602
|
+
|
|
603
|
+
// }
|
|
604
|
+
|
|
605
|
+
// 兜底:partial line(无 \n 结尾)用 timer 刷新,防止尾部文字卡住
|
|
606
|
+
if (this._lineBuffer && !this._streamBufferTimer) {
|
|
607
|
+
this._streamBufferTimer = setTimeout(() => this._flushStreamBuffer(), 0);
|
|
576
608
|
}
|
|
577
609
|
} else if (chunk.type === 'tool-call') {
|
|
578
610
|
const args = chunk.input ? JSON.stringify(chunk.input).slice(0, 50) : '';
|
|
579
|
-
this.tooler.show(`${chalk.yellow('[Tool]')} ${folikoGold(chunk.toolName)} ${chalk.gray(args+'...')}`)
|
|
611
|
+
this.statusBar.tooler.show(`${chalk.yellow('[Tool]')} ${folikoGold(chunk.toolName)} ${chalk.gray(args+'...')}`)
|
|
580
612
|
} else if(chunk.type==='tool-result'){
|
|
581
613
|
const result = chunk.result;
|
|
582
614
|
const shortResult = typeof result === 'string' ? result.slice(0, 30) : JSON.stringify(result).slice(0, 30);
|
|
583
|
-
this.tooler.show(`${chalk.green('[Tool]')} ${folikoGold(chunk.toolName)} ${chalk.gray(shortResult+'...')}`)
|
|
615
|
+
this.statusBar.tooler.show(`${chalk.green('[Tool]')} ${folikoGold(chunk.toolName)} ${chalk.gray(shortResult+'...')}`)
|
|
584
616
|
|
|
585
617
|
// 如果是 edit/edit_file 的结果并且包含 diff,渲染彩色 diff
|
|
586
618
|
if (chunk.toolName === 'edit' || chunk.toolName === 'edit_file') {
|
|
@@ -592,8 +624,9 @@ class ChatUI {
|
|
|
592
624
|
if (resultObj && resultObj.diff) {
|
|
593
625
|
const diffContent = renderDiffWithHeader(resultObj.diff, resultObj.filePath);
|
|
594
626
|
const bgColor = chalk.bgHex('#1a1a2e');
|
|
595
|
-
this.
|
|
596
|
-
this.
|
|
627
|
+
this._currentBotMessage.appendContent(['\n',diffContent,'\n\n'].join(''));
|
|
628
|
+
// this.create_message(diffContent, chalk.yellow('▸ '), true, (line) => bgColor(line));
|
|
629
|
+
this.statusBar.tooler.show(`${chalk.green('[Diff]')} ${folikoGold(resultObj.filePath)}`);
|
|
597
630
|
}
|
|
598
631
|
}
|
|
599
632
|
// setTimeout(() => this.tooler.hide(), 3000);
|
|
@@ -610,7 +643,7 @@ class ChatUI {
|
|
|
610
643
|
}
|
|
611
644
|
});
|
|
612
645
|
this.sessionScope.on('message:start', async () => {
|
|
613
|
-
this.loader.show();
|
|
646
|
+
this.statusBar.loader.show();
|
|
614
647
|
})
|
|
615
648
|
this.sessionScope.on('message:complete', async ({ content, requestId }) => {
|
|
616
649
|
try{
|