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.
Files changed (157) hide show
  1. package/.claude/settings.local.json +19 -10
  2. package/.dockerignore +45 -45
  3. package/.env.example +56 -56
  4. package/CLAUDE.md +2 -2
  5. package/README.md +13 -13
  6. package/SPEC.md +3 -3
  7. package/cli/src/commands/chat.js +2 -20
  8. package/cli/src/commands/list.js +7 -6
  9. package/cli/src/commands/plugin.js +3 -2
  10. package/cli/src/daemon.js +2 -2
  11. package/cli/src/ui/chat-ui-old.js +15 -4
  12. package/cli/src/ui/chat-ui.js +236 -203
  13. package/cli/src/ui/footer-bar.js +20 -46
  14. package/cli/src/ui/message-bubble.js +24 -2
  15. package/cli/src/ui/status-bar.js +177 -0
  16. package/cli/src/utils/config.js +29 -0
  17. package/cli/src/utils/plugin-config.js +1 -1
  18. package/docker-compose.yml +33 -33
  19. package/docs/features.md +120 -120
  20. package/docs/quick-reference.md +160 -160
  21. package/docs/user-manual.md +1391 -1391
  22. package/examples/ambient-example.js +2 -2
  23. package/examples/bootstrap.js +3 -3
  24. package/examples/test-chat.js +1 -1
  25. package/examples/test-reload.js +1 -1
  26. package/examples/test-telegram.js +1 -1
  27. package/examples/test-tg-bot.js +1 -1
  28. package/examples/test-tg-simple.js +2 -2
  29. package/examples/test-tg.js +1 -1
  30. package/examples/test-think.js +1 -1
  31. package/examples/test-weixin-feishu.js +3 -3
  32. package/package.json +3 -1
  33. package/plugins/ambient-agent/index.js +1 -1
  34. package/plugins/audit-plugin.js +84 -29
  35. package/plugins/coordinator-plugin.js +14 -12
  36. package/plugins/data-splitter-plugin.js +323 -0
  37. package/plugins/default-plugins.js +23 -12
  38. package/plugins/email/index.js +1 -1
  39. package/plugins/extension-executor-plugin.js +87 -9
  40. package/plugins/feishu-plugin.js +118 -16
  41. package/plugins/file-system-plugin.js +68 -50
  42. package/plugins/gate-trading.js +10 -10
  43. package/plugins/install-plugin.js +7 -7
  44. package/plugins/memory-plugin.js +9 -12
  45. package/plugins/plugin-manager-plugin.js +12 -14
  46. package/plugins/python-executor-plugin.js +1 -1
  47. package/plugins/python-plugin-loader.js +1 -1
  48. package/plugins/qq-plugin.js +151 -24
  49. package/plugins/rules-plugin.js +8 -8
  50. package/plugins/scheduler-plugin.js +24 -20
  51. package/plugins/session-plugin.js +313 -397
  52. package/plugins/storage-plugin.js +235 -175
  53. package/plugins/subagent-plugin.js +17 -13
  54. package/plugins/telegram-plugin.js +116 -17
  55. package/plugins/think-plugin.js +64 -60
  56. package/plugins/tools-plugin.js +8 -8
  57. package/plugins/web-plugin.js +2 -2
  58. package/plugins/weixin-plugin.js +107 -24
  59. package/skills/find-skills/AGENTS.md +2 -2
  60. package/skills/find-skills/SKILL.md +133 -133
  61. package/skills/foliko-dev/AGENTS.md +236 -236
  62. package/skills/foliko-dev/SKILL.md +19 -19
  63. package/skills/mcp-usage/SKILL.md +200 -200
  64. package/skills/plugin-guide/SKILL.md +4 -4
  65. package/skills/python-plugin-dev/SKILL.md +5 -5
  66. package/skills/skill-guide/SKILL.md +104 -6
  67. package/skills/subagent-guide/SKILL.md +237 -237
  68. package/skills/workflow-guide/SKILL.md +646 -646
  69. package/src/capabilities/skill-manager.js +124 -17
  70. package/src/capabilities/workflow-engine.js +3 -3
  71. package/src/core/agent-chat.js +72 -26
  72. package/src/core/agent.js +17 -27
  73. package/src/core/branch-summary-auto.js +206 -0
  74. package/src/core/chat-session.js +45 -169
  75. package/src/core/command-registry.js +200 -0
  76. package/src/core/constants.js +198 -0
  77. package/src/core/context-compressor.js +702 -326
  78. package/src/core/context-manager.js +0 -1
  79. package/src/core/enhanced-context-compressor.js +210 -0
  80. package/src/core/framework.js +260 -84
  81. package/src/core/jsonl-storage.js +253 -0
  82. package/src/core/plugin-base.js +7 -5
  83. package/src/core/plugin-manager.js +15 -10
  84. package/src/core/provider-registry.js +159 -0
  85. package/src/core/provider.js +2 -0
  86. package/src/core/session-entry.js +225 -0
  87. package/src/core/session-manager.js +701 -0
  88. package/src/core/storage-manager.js +494 -0
  89. package/src/core/sub-agent-config.js +1 -1
  90. package/src/core/subagent.js +16 -135
  91. package/src/core/token-counter.js +177 -58
  92. package/src/core/tool-executor.js +2 -70
  93. package/src/core/ui-extension-context.js +174 -0
  94. package/src/executors/mcp-executor.js +27 -16
  95. package/src/utils/chat-queue.js +11 -22
  96. package/src/utils/data-splitter.js +345 -0
  97. package/src/utils/logger.js +152 -180
  98. package/src/utils/message-validator.js +283 -0
  99. package/src/utils/plugin-helpers.js +2 -2
  100. package/src/utils/retry.js +168 -22
  101. package/website_v2/docs/api.html +1 -1
  102. package/website_v2/docs/configuration.html +2 -2
  103. package/website_v2/docs/plugin-development.html +4 -4
  104. package/website_v2/docs/project-structure.html +2 -2
  105. package/website_v2/docs/skill-development.html +2 -2
  106. package/website_v2/index.html +1 -1
  107. package/website_v2/styles/animations.css +7 -7
  108. package/.agent/agents/backend-dev.md +0 -102
  109. package/.agent/agents/data-analyst.md +0 -117
  110. package/.agent/agents/devops.md +0 -115
  111. package/.agent/agents/frontend-dev.md +0 -94
  112. package/.agent/agents/network-requester.md +0 -44
  113. package/.agent/agents/poster-designer.md +0 -52
  114. package/.agent/agents/product-manager.md +0 -85
  115. package/.agent/agents/qa-engineer.md +0 -100
  116. package/.agent/agents/security-engineer.md +0 -99
  117. package/.agent/agents/team-lead.md +0 -137
  118. package/.agent/agents/ui-designer.md +0 -116
  119. package/.agent/data/default.json +0 -58
  120. package/.agent/data/email/processed-emails.json +0 -1
  121. package/.agent/data/plugins-state.json +0 -199
  122. package/.agent/data/scheduler/tasks.json +0 -1
  123. package/.agent/data/web/web-config.json +0 -5
  124. package/.agent/data/weixin/images/file_1776188148383jpg +0 -0
  125. package/.agent/data/weixin/images/file_1776188458326.jpg +0 -0
  126. package/.agent/data/weixin/images/file_1776188689423.jpg +0 -0
  127. package/.agent/data/weixin/images/file_1776188813604.jpg +0 -0
  128. package/.agent/data/weixin/images/file_1776189097450.jpg +0 -0
  129. package/.agent/data/weixin/videos/file_1776188318431.mp4 +0 -0
  130. package/.agent/data/weixin.json +0 -6
  131. package/.agent/mcp_config.json +0 -14
  132. package/.agent/memory/user/mof6gk94-kneeuh.md +0 -9
  133. package/.agent/package.json +0 -8
  134. package/.agent/plugins/marknative/README.md +0 -134
  135. package/.agent/plugins/marknative/fonts/SegoeUI Emoji.ttf +0 -0
  136. package/.agent/plugins/marknative/fonts.zip +0 -0
  137. package/.agent/plugins/marknative/index.js +0 -256
  138. package/.agent/plugins/marknative/package.json +0 -12
  139. package/.agent/plugins/test-plugin.py +0 -99
  140. package/.agent/plugins.json +0 -14
  141. package/.agent/python-scripts/test_sample.py +0 -24
  142. package/.agent/sessions/cli_default.json +0 -247
  143. package/.agent/skills/agent-browser/SKILL.md +0 -311
  144. package/.agent/skills/agent-browser/TEST_PLAN.md +0 -200
  145. package/.agent/skills/sysinfo/SKILL.md +0 -38
  146. package/.agent/skills/sysinfo/system-info.sh +0 -130
  147. package/.agent/skills/workflow/SKILL.md +0 -324
  148. package/.agent/test-agent.js +0 -35
  149. package/.agent/weixin.json +0 -6
  150. package/.agent/workflows/email-digest.json +0 -50
  151. package/.agent/workflows/file-backup.json +0 -21
  152. package/.agent/workflows/get-ip-notify.json +0 -32
  153. package/.agent/workflows/news-aggregator.json +0 -93
  154. package/.agent/workflows/news-dashboard-v2.json +0 -94
  155. package/.agent/workflows/notification-batch.json +0 -32
  156. package/src/core/session-context.js +0 -346
  157. 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; // overhead per tool_calls block
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 = { TokenCounter, encode, encodeForJSON };
298
+ module.exports = {
299
+ TokenCounter,
300
+ encode,
301
+ encodeForJSON,
302
+ estimateTokens,
303
+ calculateContextTokens,
304
+ getAssistantUsage,
305
+ getLastAssistantUsage,
306
+ estimateContextTokens,
307
+ shouldCompact,
308
+ safeJsonStringify
309
+ };
@@ -9,6 +9,7 @@
9
9
 
10
10
  const { EventEmitter } = require('../utils/event-emitter');
11
11
  const { logger } = require('../utils/logger');
12
+ const { validateToolCalls } = require('../utils/message-validator');
12
13
 
13
14
  class ToolExecutor extends EventEmitter {
14
15
  /**
@@ -183,76 +184,7 @@ class ToolExecutor extends EventEmitter {
183
184
  * @returns {Array} 验证后的消息
184
185
  */
185
186
  validateToolCalls(messages) {
186
- let fixedCount = 0;
187
- // 收集被跳过的 toolCallId,用于清理对应的 tool-result
188
- const invalidatedToolCallIds = new Set();
189
-
190
- for (const msg of messages) {
191
- // 清理 assistant 消息中的不完整 tool-call
192
- if (msg.role === 'assistant' && Array.isArray(msg.content)) {
193
- for (const item of msg.content) {
194
- // 兼容 tool-call 和 tool-use 两种类型
195
- if (item.type !== 'tool-call' && item.type !== 'tool-use') {
196
- continue;
197
- }
198
-
199
- const input = item.input;
200
- if (typeof input !== 'string') {
201
- continue;
202
- }
203
-
204
- // 检查 input 是否是有效的 JSON(不是不完整的)
205
- const trimmed = input.trim();
206
- if (trimmed === '{' || trimmed === '' || !trimmed.startsWith('{')) {
207
- // 不完整的 JSON,移除这个 tool-call
208
- // 记录 toolCallId,以便后续清理对应的 tool-result
209
- if (item.toolCallId) {
210
- invalidatedToolCallIds.add(item.toolCallId);
211
- }
212
- logger.warn(
213
- `_validateToolCalls: invalid tool-call input="${input}", toolCallId=${item.toolCallId}, converting to text`
214
- );
215
- item.type = 'text';
216
- item.text = `(工具调用 ${item.toolName} 参数不完整,已跳过)`;
217
- delete item.toolCallId;
218
- delete item.toolName;
219
- delete item.input;
220
- fixedCount++;
221
- }
222
- }
223
- }
224
- }
225
-
226
- // 如果有无效的 tool-call,清理对应的 tool-result
227
- if (invalidatedToolCallIds.size > 0) {
228
- logger.warn(
229
- `_validateToolCalls: removing ${invalidatedToolCallIds.size} tool-results with invalidated toolCallIds`
230
- );
231
- for (const msg of messages) {
232
- if (msg.role === 'tool' && Array.isArray(msg.content)) {
233
- // 过滤掉引用了无效 toolCallId 的 tool-result
234
- const oldLen = msg.content.length;
235
- msg.content = msg.content.filter((item) => {
236
- if (item.type !== 'tool-result' && item.type !== 'tool_result') {
237
- return true;
238
- }
239
- // 如果 tool-result 引用的 toolCallId 已被标记为无效,则移除
240
- if (item.toolCallId && invalidatedToolCallIds.has(item.toolCallId)) {
241
- logger.warn(
242
- `_validateToolCalls: removing orphaned tool-result with toolCallId=${item.toolCallId}`
243
- );
244
- fixedCount++;
245
- return false;
246
- }
247
- return true;
248
- });
249
- }
250
- }
251
- }
252
-
253
- if (fixedCount > 0) {
254
- logger.info(`_validateToolCalls: Fixed ${fixedCount} incomplete tool calls/results`);
255
- }
187
+ return validateToolCalls(messages);
256
188
  }
257
189
 
258
190
  /**
@@ -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 };