foliko 1.1.68 → 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 +152 -38
- package/cli/src/ui/footer-bar.js +13 -14
- 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 +75 -24
- package/plugins/default-plugins.js +21 -11
- package/plugins/email/index.js +1 -1
- package/plugins/extension-executor-plugin.js +85 -7
- package/plugins/feishu-plugin.js +118 -16
- package/plugins/install-plugin.js +4 -4
- package/plugins/memory-plugin.js +1 -1
- package/plugins/plugin-manager-plugin.js +3 -3
- package/plugins/python-executor-plugin.js +1 -1
- package/plugins/python-plugin-loader.js +1 -1
- package/plugins/qq-plugin.js +143 -16
- package/plugins/rules-plugin.js +1 -1
- package/plugins/scheduler-plugin.js +2 -2
- package/plugins/session-plugin.js +309 -393
- package/plugins/storage-plugin.js +235 -176
- package/plugins/subagent-plugin.js +4 -4
- package/plugins/telegram-plugin.js +116 -17
- package/plugins/think-plugin.js +1 -1
- package/plugins/web-plugin.js +2 -2
- package/plugins/weixin-plugin.js +102 -19
- 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 +101 -2
- package/src/capabilities/workflow-engine.js +1 -1
- package/src/core/agent-chat.js +17 -14
- package/src/core/branch-summary-auto.js +206 -0
- package/src/core/chat-session.js +38 -8
- package/src/core/command-registry.js +200 -0
- package/src/core/constants.js +11 -11
- package/src/core/context-compressor.js +700 -149
- package/src/core/context-manager.js +0 -1
- package/src/core/enhanced-context-compressor.js +210 -0
- package/src/core/framework.js +140 -83
- package/src/core/jsonl-storage.js +253 -0
- 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/token-counter.js +177 -58
- package/src/core/ui-extension-context.js +174 -0
- package/src/executors/mcp-executor.js +15 -6
- package/src/utils/logger.js +152 -180
- package/src/utils/plugin-helpers.js +2 -2
- 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 -207
- 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 -4121
- 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
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TokenCounter - Token 计算工具
|
|
3
|
-
*
|
|
4
|
-
* 职责:
|
|
5
|
-
* 1. 计算文本的 token 数量
|
|
6
|
-
* 2. 计算消息数组的 token 总数
|
|
7
|
-
* 3. 计算工具定义的 token 总数
|
|
3
|
+
* Ported from pi's compaction/compaction.ts with enhancements
|
|
8
4
|
*/
|
|
9
5
|
|
|
10
6
|
/**
|
|
@@ -22,56 +18,191 @@ function encode(text, bytesPerToken = 4) {
|
|
|
22
18
|
|
|
23
19
|
/**
|
|
24
20
|
* 计算 JSON 字符串的 token 数量
|
|
25
|
-
* @param {string} text - JSON 字符串
|
|
26
|
-
* @returns {number} token 数量
|
|
27
21
|
*/
|
|
28
22
|
function encodeForJSON(text) {
|
|
29
23
|
if (!text) return 0;
|
|
30
|
-
// JSON 字符串需要额外计算引号和转义
|
|
31
24
|
const encoded = encode(text);
|
|
32
25
|
return encoded + Math.ceil(Buffer.byteLength(JSON.stringify(text), 'utf8') / 100);
|
|
33
26
|
}
|
|
34
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Safe JSON stringify that handles unserializable values
|
|
30
|
+
*/
|
|
31
|
+
function safeJsonStringify(value) {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.stringify(value) ?? 'undefined';
|
|
34
|
+
} catch {
|
|
35
|
+
return '[unserializable]';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Estimate token count for one message using a conservative character heuristic
|
|
41
|
+
* Ported from pi
|
|
42
|
+
*/
|
|
43
|
+
function estimateTokens(message) {
|
|
44
|
+
let chars = 0;
|
|
45
|
+
|
|
46
|
+
switch (message.role) {
|
|
47
|
+
case 'user': {
|
|
48
|
+
const content = message.content;
|
|
49
|
+
if (typeof content === 'string') {
|
|
50
|
+
chars = content.length;
|
|
51
|
+
} else if (Array.isArray(content)) {
|
|
52
|
+
for (const block of content) {
|
|
53
|
+
if (block.type === 'text' && block.text) {
|
|
54
|
+
chars += block.text.length;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return Math.ceil(chars / 4);
|
|
59
|
+
}
|
|
60
|
+
case 'assistant': {
|
|
61
|
+
const assistant = message;
|
|
62
|
+
for (const block of assistant.content || []) {
|
|
63
|
+
if (block.type === 'text') {
|
|
64
|
+
chars += block.text.length;
|
|
65
|
+
} else if (block.type === 'thinking') {
|
|
66
|
+
chars += block.thinking.length;
|
|
67
|
+
} else if (block.type === 'toolCall') {
|
|
68
|
+
chars += block.name.length + safeJsonStringify(block.arguments).length;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return Math.ceil(chars / 4);
|
|
72
|
+
}
|
|
73
|
+
case 'custom':
|
|
74
|
+
case 'toolResult': {
|
|
75
|
+
if (typeof message.content === 'string') {
|
|
76
|
+
chars = message.content.length;
|
|
77
|
+
} else {
|
|
78
|
+
for (const block of message.content || []) {
|
|
79
|
+
if (block.type === 'text' && block.text) {
|
|
80
|
+
chars += block.text.length;
|
|
81
|
+
}
|
|
82
|
+
if (block.type === 'image') {
|
|
83
|
+
chars += 4800;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return Math.ceil(chars / 4);
|
|
88
|
+
}
|
|
89
|
+
case 'bashExecution': {
|
|
90
|
+
chars = message.command.length + (message.output?.length || 0);
|
|
91
|
+
return Math.ceil(chars / 4);
|
|
92
|
+
}
|
|
93
|
+
case 'branchSummary':
|
|
94
|
+
case 'compactionSummary': {
|
|
95
|
+
chars = message.summary.length;
|
|
96
|
+
return Math.ceil(chars / 4);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Calculate total context tokens from provider usage
|
|
105
|
+
*/
|
|
106
|
+
function calculateContextTokens(usage) {
|
|
107
|
+
return usage.totalTokens || (usage.input || 0) + (usage.output || 0) + (usage.cacheRead || 0) + (usage.cacheWrite || 0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Get assistant usage from message
|
|
112
|
+
*/
|
|
113
|
+
function getAssistantUsage(msg) {
|
|
114
|
+
if (msg.role === 'assistant' && msg.usage) {
|
|
115
|
+
if (msg.stopReason !== 'aborted' && msg.stopReason !== 'error' && msg.usage) {
|
|
116
|
+
return msg.usage;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Return usage from the last successful assistant message in session entries
|
|
124
|
+
*/
|
|
125
|
+
function getLastAssistantUsage(entries) {
|
|
126
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
127
|
+
const entry = entries[i];
|
|
128
|
+
if (entry.type === 'message') {
|
|
129
|
+
const usage = getAssistantUsage(entry.message);
|
|
130
|
+
if (usage) return usage;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Get last assistant usage info from messages
|
|
138
|
+
*/
|
|
139
|
+
function getLastAssistantUsageInfo(messages) {
|
|
140
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
141
|
+
const usage = getAssistantUsage(messages[i]);
|
|
142
|
+
if (usage) return { usage, index: i };
|
|
143
|
+
}
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Estimate context tokens for messages using provider usage when available
|
|
149
|
+
*/
|
|
150
|
+
function estimateContextTokens(messages) {
|
|
151
|
+
const usageInfo = getLastAssistantUsageInfo(messages);
|
|
152
|
+
|
|
153
|
+
if (!usageInfo) {
|
|
154
|
+
let estimated = 0;
|
|
155
|
+
for (const message of messages) {
|
|
156
|
+
estimated += estimateTokens(message);
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
tokens: estimated,
|
|
160
|
+
usageTokens: 0,
|
|
161
|
+
trailingTokens: estimated,
|
|
162
|
+
lastUsageIndex: null
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const usageTokens = calculateContextTokens(usageInfo.usage);
|
|
167
|
+
let trailingTokens = 0;
|
|
168
|
+
for (let i = usageInfo.index + 1; i < messages.length; i++) {
|
|
169
|
+
trailingTokens += estimateTokens(messages[i]);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
tokens: usageTokens + trailingTokens,
|
|
174
|
+
usageTokens,
|
|
175
|
+
trailingTokens,
|
|
176
|
+
lastUsageIndex: usageInfo.index
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Return whether context usage exceeds the configured compaction threshold
|
|
182
|
+
*/
|
|
183
|
+
function shouldCompact(contextTokens, contextWindow, settings) {
|
|
184
|
+
if (!settings.enabled) return false;
|
|
185
|
+
return contextTokens > contextWindow - settings.reserveTokens;
|
|
186
|
+
}
|
|
187
|
+
|
|
35
188
|
class TokenCounter {
|
|
36
|
-
/**
|
|
37
|
-
* @param {Object} config - 配置
|
|
38
|
-
* @param {Object} config.toolSchema - 工具 schema(用于工具 token 计算)
|
|
39
|
-
*/
|
|
40
189
|
constructor(config = {}) {
|
|
41
190
|
this.toolSchema = config.toolSchema || null;
|
|
42
191
|
}
|
|
43
192
|
|
|
44
|
-
/**
|
|
45
|
-
* 计算文本 token
|
|
46
|
-
* @param {string} text - 文本
|
|
47
|
-
* @param {number} bytesPerToken - 每 token 字节数
|
|
48
|
-
* @returns {number} token 数量
|
|
49
|
-
*/
|
|
50
193
|
countText(text, bytesPerToken = 4) {
|
|
51
194
|
return encode(text, bytesPerToken);
|
|
52
195
|
}
|
|
53
196
|
|
|
54
|
-
/**
|
|
55
|
-
* 计算消息数组的 token 总数
|
|
56
|
-
* @param {Array} messages - 消息数组
|
|
57
|
-
* @returns {number} token 总数
|
|
58
|
-
*/
|
|
59
197
|
countMessages(messages) {
|
|
60
198
|
if (!Array.isArray(messages)) return 0;
|
|
61
199
|
return messages.reduce((sum, msg) => sum + this.countMessage(msg), 0);
|
|
62
200
|
}
|
|
63
201
|
|
|
64
|
-
/**
|
|
65
|
-
* 计算单条消息的 token
|
|
66
|
-
* @param {Object} msg - 消息
|
|
67
|
-
* @returns {number} token 数量
|
|
68
|
-
*/
|
|
69
202
|
countMessage(msg) {
|
|
70
203
|
if (!msg) return 0;
|
|
71
204
|
|
|
72
205
|
let total = 0;
|
|
73
|
-
|
|
74
|
-
// 角色和格式开销
|
|
75
206
|
total += 4;
|
|
76
207
|
|
|
77
208
|
if (typeof msg.content === 'string') {
|
|
@@ -82,9 +213,8 @@ class TokenCounter {
|
|
|
82
213
|
}
|
|
83
214
|
}
|
|
84
215
|
|
|
85
|
-
// tool_calls 开销
|
|
86
216
|
if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
|
|
87
|
-
total += 15;
|
|
217
|
+
total += 15;
|
|
88
218
|
for (const tc of msg.tool_calls) {
|
|
89
219
|
if (tc.function) {
|
|
90
220
|
total += this.countText(tc.function.name) + this.countText(tc.function.arguments);
|
|
@@ -92,7 +222,6 @@ class TokenCounter {
|
|
|
92
222
|
}
|
|
93
223
|
}
|
|
94
224
|
|
|
95
|
-
// tool_call_id 开销
|
|
96
225
|
if (msg.tool_call_id) {
|
|
97
226
|
total += 15;
|
|
98
227
|
}
|
|
@@ -100,11 +229,6 @@ class TokenCounter {
|
|
|
100
229
|
return total;
|
|
101
230
|
}
|
|
102
231
|
|
|
103
|
-
/**
|
|
104
|
-
* 计算 content block 的 token
|
|
105
|
-
* @param {Object} block - content block
|
|
106
|
-
* @returns {number} token 数量
|
|
107
|
-
*/
|
|
108
232
|
countContentBlock(block) {
|
|
109
233
|
if (!block) return 0;
|
|
110
234
|
|
|
@@ -126,34 +250,23 @@ class TokenCounter {
|
|
|
126
250
|
}
|
|
127
251
|
return 0;
|
|
128
252
|
case 'image':
|
|
129
|
-
// 图片按 token 估算
|
|
130
253
|
return 85;
|
|
131
254
|
default:
|
|
132
255
|
return this.countText(JSON.stringify(block));
|
|
133
256
|
}
|
|
134
257
|
}
|
|
135
258
|
|
|
136
|
-
/**
|
|
137
|
-
* 计算工具定义的 token 总数
|
|
138
|
-
* @param {Array} tools - 工具数组
|
|
139
|
-
* @returns {number} token 总数
|
|
140
|
-
*/
|
|
141
259
|
countTools(tools) {
|
|
142
260
|
if (!tools || !Array.isArray(tools)) return 0;
|
|
143
261
|
|
|
144
262
|
let total = 0;
|
|
145
263
|
for (const tool of tools) {
|
|
146
|
-
|
|
147
|
-
total += 20; // overhead
|
|
148
|
-
|
|
264
|
+
total += 20;
|
|
149
265
|
if (tool.description) {
|
|
150
266
|
total += this.countText(tool.description);
|
|
151
267
|
}
|
|
152
|
-
|
|
153
|
-
// 参数
|
|
154
268
|
if (tool.inputSchema) {
|
|
155
|
-
const schema =
|
|
156
|
-
tool.inputSchema.jsonSchema || tool.inputSchema.inputSchema || tool.inputSchema;
|
|
269
|
+
const schema = tool.inputSchema.jsonSchema || tool.inputSchema.inputSchema || tool.inputSchema;
|
|
157
270
|
if (schema.properties) {
|
|
158
271
|
for (const [name, prop] of Object.entries(schema.properties)) {
|
|
159
272
|
total += this.countText(name) + 10;
|
|
@@ -168,11 +281,6 @@ class TokenCounter {
|
|
|
168
281
|
return total;
|
|
169
282
|
}
|
|
170
283
|
|
|
171
|
-
/**
|
|
172
|
-
* 估算完整请求的 token(消息 + 工具 + 系统提示)
|
|
173
|
-
* @param {Object} params - 请求参数
|
|
174
|
-
* @returns {Object} - { messagesTokens, toolsTokens, systemPromptTokens, total }
|
|
175
|
-
*/
|
|
176
284
|
estimateRequest({ messages, tools, systemPrompt }) {
|
|
177
285
|
const messagesTokens = this.countMessages(messages);
|
|
178
286
|
const toolsTokens = this.countTools(tools);
|
|
@@ -182,9 +290,20 @@ class TokenCounter {
|
|
|
182
290
|
messagesTokens,
|
|
183
291
|
toolsTokens,
|
|
184
292
|
systemPromptTokens,
|
|
185
|
-
total: messagesTokens + toolsTokens + systemPromptTokens
|
|
293
|
+
total: messagesTokens + toolsTokens + systemPromptTokens
|
|
186
294
|
};
|
|
187
295
|
}
|
|
188
296
|
}
|
|
189
297
|
|
|
190
|
-
module.exports = {
|
|
298
|
+
module.exports = {
|
|
299
|
+
TokenCounter,
|
|
300
|
+
encode,
|
|
301
|
+
encodeForJSON,
|
|
302
|
+
estimateTokens,
|
|
303
|
+
calculateContextTokens,
|
|
304
|
+
getAssistantUsage,
|
|
305
|
+
getLastAssistantUsage,
|
|
306
|
+
estimateContextTokens,
|
|
307
|
+
shouldCompact,
|
|
308
|
+
safeJsonStringify
|
|
309
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ExtensionUIContext - UI Extension Context for Plugins
|
|
3
|
+
* 允许插件扩展 TUI 界面
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { logger } = require('../utils/logger');
|
|
7
|
+
|
|
8
|
+
class ExtensionUIContext {
|
|
9
|
+
/**
|
|
10
|
+
* @param {Object} chatUI - ChatUI 实例
|
|
11
|
+
* @param {string} pluginName - 插件名称
|
|
12
|
+
*/
|
|
13
|
+
constructor(chatUI, pluginName) {
|
|
14
|
+
this._ui = chatUI;
|
|
15
|
+
this._pluginName = pluginName;
|
|
16
|
+
this._components = new Map();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 显示选择对话框
|
|
21
|
+
* @param {string} title - 对话框标题
|
|
22
|
+
* @param {Array<{label: string, value: any}>} options - 选项
|
|
23
|
+
* @returns {Promise<any>} 选中的值
|
|
24
|
+
*/
|
|
25
|
+
async select(title, options) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
// 简化实现:使用 TUI 的 prompt 机制
|
|
28
|
+
if (!this._ui || !this._ui.tui) {
|
|
29
|
+
reject(new Error('ChatUI not available'));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 构建选项文本
|
|
34
|
+
let prompt = `${title}\n`;
|
|
35
|
+
options.forEach((opt, i) => {
|
|
36
|
+
prompt += `${i + 1}. ${opt.label}\n`;
|
|
37
|
+
});
|
|
38
|
+
prompt += '请选择: ';
|
|
39
|
+
|
|
40
|
+
// 创建一个简单的选择机制
|
|
41
|
+
this._ui.editor.setPrompt(prompt);
|
|
42
|
+
this._ui.editor.once('submit', (value) => {
|
|
43
|
+
const idx = parseInt(value, 10) - 1;
|
|
44
|
+
if (idx >= 0 && idx < options.length) {
|
|
45
|
+
resolve(options[idx].value);
|
|
46
|
+
} else {
|
|
47
|
+
resolve(options[0]?.value);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
this._ui.tui.setFocus(this._ui.editor);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 显示确认对话框
|
|
57
|
+
* @param {string} message - 确认消息
|
|
58
|
+
* @returns {Promise<boolean>} 是否确认
|
|
59
|
+
*/
|
|
60
|
+
async confirm(message) {
|
|
61
|
+
const result = await this.select(message, [
|
|
62
|
+
{ label: '是', value: true },
|
|
63
|
+
{ label: '否', value: false },
|
|
64
|
+
]);
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 获取文本输入
|
|
70
|
+
* @param {string} prompt - 输入提示
|
|
71
|
+
* @returns {Promise<string>} 用户输入
|
|
72
|
+
*/
|
|
73
|
+
async input(prompt) {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
if (!this._ui || !this._ui.editor) {
|
|
76
|
+
reject(new Error('ChatUI not available'));
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const originalPrompt = this._ui.editor.getPrompt?.() || '';
|
|
81
|
+
this._ui.editor.setPrompt(prompt);
|
|
82
|
+
|
|
83
|
+
const handler = (value) => {
|
|
84
|
+
this._ui.editor.off('submit', handler);
|
|
85
|
+
if (originalPrompt) {
|
|
86
|
+
this._ui.editor.setPrompt(originalPrompt);
|
|
87
|
+
}
|
|
88
|
+
resolve(value);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
this._ui.editor.once('submit', handler);
|
|
92
|
+
this._ui.tui.setFocus(this._ui.editor);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 显示通知
|
|
98
|
+
* @param {string} title - 通知标题
|
|
99
|
+
* @param {string} message - 通知消息
|
|
100
|
+
* @param {number} [duration=3000] - 显示时长(ms)
|
|
101
|
+
*/
|
|
102
|
+
notify(title, message, duration = 3000) {
|
|
103
|
+
if (this._ui && this._ui.statusBar) {
|
|
104
|
+
this._ui.statusBar.tooler?.show(`🔔 ${title}: ${message}`);
|
|
105
|
+
setTimeout(() => {
|
|
106
|
+
this._ui.statusBar.tooler?.hide();
|
|
107
|
+
}, duration);
|
|
108
|
+
} else {
|
|
109
|
+
logger.info(`[Notification] ${title}: ${message}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 设置状态栏
|
|
115
|
+
* @param {string} status - 状态文本
|
|
116
|
+
*/
|
|
117
|
+
setStatus(status) {
|
|
118
|
+
if (this._ui && this._ui.statusBar) {
|
|
119
|
+
this._ui.statusBar.tooler?.show(status);
|
|
120
|
+
} else {
|
|
121
|
+
logger.debug(`[Status] ${status}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 添加自定义组件
|
|
127
|
+
* @param {string} id - 组件标识
|
|
128
|
+
* @param {Object} component - TUI 组件
|
|
129
|
+
*/
|
|
130
|
+
addComponent(id, component) {
|
|
131
|
+
if (this._ui && this._ui.tui) {
|
|
132
|
+
this._ui.tui.addChild(component);
|
|
133
|
+
this._components.set(id, component);
|
|
134
|
+
}
|
|
135
|
+
return this;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 移除自定义组件
|
|
140
|
+
*/
|
|
141
|
+
removeComponent(id) {
|
|
142
|
+
const comp = this._components.get(id);
|
|
143
|
+
if (comp && this._ui && this._ui.tui) {
|
|
144
|
+
this._ui.tui.removeChild(comp);
|
|
145
|
+
this._components.delete(id);
|
|
146
|
+
}
|
|
147
|
+
return this;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 获取 TUI 实例(用于更高级的定制)
|
|
152
|
+
*/
|
|
153
|
+
getTUI() {
|
|
154
|
+
return this._ui?.tui || null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 在消息区域显示消息
|
|
159
|
+
*/
|
|
160
|
+
showMessage(message, type = 'info') {
|
|
161
|
+
if (this._ui) {
|
|
162
|
+
const colors = {
|
|
163
|
+
info: 'cyan',
|
|
164
|
+
success: 'green',
|
|
165
|
+
warning: 'yellow',
|
|
166
|
+
error: 'red',
|
|
167
|
+
};
|
|
168
|
+
const color = colors[type] || 'cyan';
|
|
169
|
+
this._ui.create_message(`[${this._pluginName}] ${message}\n`, chalk?.[color] || ((t) => t));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = { ExtensionUIContext };
|
|
@@ -229,7 +229,7 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
async start(framework) {
|
|
232
|
-
//
|
|
232
|
+
// 异步加载所有 MCP 服务器,不阻塞框架启动
|
|
233
233
|
if (this._config.servers) {
|
|
234
234
|
for (const serverConfig of this._config.servers) {
|
|
235
235
|
if (serverConfig.enabled === false) {
|
|
@@ -238,7 +238,10 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
238
238
|
this._serverConfigs.set(serverConfig.name, { ...serverConfig });
|
|
239
239
|
continue;
|
|
240
240
|
}
|
|
241
|
-
await
|
|
241
|
+
// 异步连接,不 await
|
|
242
|
+
this.addServer(serverConfig).catch(err => {
|
|
243
|
+
log.error(`[MCP] Async server connection failed for ${serverConfig.name}:`, err.message);
|
|
244
|
+
});
|
|
242
245
|
}
|
|
243
246
|
}
|
|
244
247
|
|
|
@@ -458,14 +461,14 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
458
461
|
framework.registerTool({
|
|
459
462
|
name: 'mcp_reload',
|
|
460
463
|
description:
|
|
461
|
-
'重新加载 MCP 服务器配置。当配置文件 .
|
|
464
|
+
'重新加载 MCP 服务器配置。当配置文件 .foliko/mcp_config.json 修改后使用此工具重载配置',
|
|
462
465
|
inputSchema: z.object({}),
|
|
463
466
|
execute: async () => {
|
|
464
467
|
log.info(' mcp_reload called');
|
|
465
468
|
try {
|
|
466
469
|
const fs = require('fs');
|
|
467
470
|
const path = require('path');
|
|
468
|
-
const configPath = path.resolve('.
|
|
471
|
+
const configPath = path.resolve('.foliko/mcp_config.json');
|
|
469
472
|
|
|
470
473
|
log.info(' Reading config from:', configPath);
|
|
471
474
|
if (!fs.existsSync(configPath)) {
|
|
@@ -1265,7 +1268,7 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
1265
1268
|
async _saveMCPServerEnabled(serverName, enabled) {
|
|
1266
1269
|
const fs = require('fs');
|
|
1267
1270
|
const path = require('path');
|
|
1268
|
-
const configPath = path.resolve('.
|
|
1271
|
+
const configPath = path.resolve('.foliko/mcp_config.json');
|
|
1269
1272
|
|
|
1270
1273
|
try {
|
|
1271
1274
|
if (!fs.existsSync(configPath)) {
|
|
@@ -1295,7 +1298,13 @@ class MCPExecutorPlugin extends Plugin {
|
|
|
1295
1298
|
// 断开所有 MCP 连接
|
|
1296
1299
|
for (const [name, clientInfo] of this._clients) {
|
|
1297
1300
|
try {
|
|
1298
|
-
|
|
1301
|
+
const closePromise = clientInfo.client.close?.() || clientInfo.client.destroy?.();
|
|
1302
|
+
if (closePromise && typeof closePromise.then === 'function') {
|
|
1303
|
+
await Promise.race([
|
|
1304
|
+
closePromise,
|
|
1305
|
+
new Promise((resolve) => setTimeout(() => resolve(), 3000)),
|
|
1306
|
+
]);
|
|
1307
|
+
}
|
|
1299
1308
|
} catch (e) {
|
|
1300
1309
|
// ignore
|
|
1301
1310
|
}
|