closer-code 1.0.0 → 1.0.1

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 (99) hide show
  1. package/.closer-code.example.json +32 -0
  2. package/DUAL_OPTIMIZATION_COMPLETE.md +293 -0
  3. package/README.md +167 -557
  4. package/README_OPENAI.md +163 -0
  5. package/THINKING_THROTTLING_OPTIMIZATION.md +244 -0
  6. package/THROTTLING_1_5S_OPTIMIZATION.md +401 -0
  7. package/TOOLS_IMPROVEMENTS_SUMMARY.md +273 -0
  8. package/cloco.md +5 -1
  9. package/config.example.json +15 -94
  10. package/config.mcp.example.json +81 -0
  11. package/dist/bash-runner.js +5 -126
  12. package/dist/batch-cli.js +286 -20658
  13. package/dist/closer-cli.js +329 -21135
  14. package/dist/index.js +308 -31036
  15. package/docs/ANTHROPIC_TOOL_ERROR_HANDLING.md +220 -0
  16. package/docs/BUILD_COMMANDS.md +79 -0
  17. package/docs/CTRL_Z_SUPPORT.md +189 -0
  18. package/docs/DEEPSEEK_R1_INTEGRATION.md +427 -0
  19. package/docs/FIX_OPENAI_TOOL_ERROR_HANDLING.md +375 -0
  20. package/docs/FIX_OPENAI_TOOL_RESULT.md +198 -0
  21. package/docs/INPUT_ENHANCEMENTS.md +192 -0
  22. package/docs/MCP_IMPLEMENTATION_SUMMARY.md +428 -0
  23. package/docs/MCP_INTEGRATION.md +418 -0
  24. package/docs/MCP_QUICKSTART.md +299 -0
  25. package/docs/MCP_README.md +166 -0
  26. package/docs/MINIFY_BUILD.md +180 -0
  27. package/docs/MULTILINE_INPUT_FEATURE.md +119 -0
  28. package/docs/OPENAI_CLIENT.md +258 -0
  29. package/docs/PROJECT_LOCAL_CONFIG.md +471 -0
  30. package/docs/PROJECT_LOCAL_CONFIG_SUMMARY.md +407 -0
  31. package/docs/REFACTOR_CONVERSATION.md +306 -0
  32. package/docs/REGION_EDIT_DESIGN.md +475 -0
  33. package/docs/SIGNAL_HANDLING.md +171 -0
  34. package/docs/STREAM_UPDATE_THROTTLE.md +273 -0
  35. package/docs/TOOLS_REFACTOR_PLAN.md +520 -0
  36. package/ds_r1.md +249 -0
  37. package/examples/abort-fence-example.js +294 -0
  38. package/package.json +18 -4
  39. package/src/ai-client-legacy.js +6 -1
  40. package/src/ai-client-openai.js +672 -0
  41. package/src/ai-client.js +30 -13
  42. package/src/closer-cli.jsx +450 -162
  43. package/src/components/fullscreen-conversation.jsx +157 -0
  44. package/src/components/ink-text-input/index.jsx +324 -0
  45. package/src/components/multiline-text-input.jsx +614 -0
  46. package/src/components/progress-bar.jsx +135 -0
  47. package/src/components/tool-detail-view.jsx +82 -0
  48. package/src/components/tool-renderers/bash-renderer.jsx +197 -0
  49. package/src/components/tool-renderers/file-edit-renderer.jsx +247 -0
  50. package/src/components/tool-renderers/file-read-renderer.jsx +261 -0
  51. package/src/components/tool-renderers/file-write-renderer.jsx +222 -0
  52. package/src/components/tool-renderers/index.jsx +178 -0
  53. package/src/components/tool-renderers/list-renderer.jsx +274 -0
  54. package/src/components/tool-renderers/search-renderer.jsx +248 -0
  55. package/src/config.js +182 -20
  56. package/src/conversation/abort-fence.js +158 -0
  57. package/src/conversation/core.js +377 -0
  58. package/src/conversation/index.js +33 -0
  59. package/src/conversation/mcp-integration.js +96 -0
  60. package/src/conversation/plan-manager.js +295 -0
  61. package/src/conversation/stream-handler.js +154 -0
  62. package/src/conversation/tool-executor.js +264 -0
  63. package/src/conversation.js +23 -958
  64. package/src/hooks/use-throttled-state.js +158 -0
  65. package/src/input/enhanced-input.jsx +268 -0
  66. package/src/input/history.js +342 -0
  67. package/src/logger.js +20 -0
  68. package/src/mcp/client.js +275 -0
  69. package/src/mcp/tools-adapter.js +149 -0
  70. package/src/planner.js +18 -5
  71. package/src/prompt-builder.js +159 -0
  72. package/src/tools.js +457 -25
  73. package/src/utils/json-parser.js +231 -0
  74. package/src/utils/json-repair.js +146 -0
  75. package/src/utils/platform.js +259 -0
  76. package/test/test-ctrl-bf.js +121 -0
  77. package/test/test-deepseek-reasoning.js +118 -0
  78. package/test/test-history-navigation.js +80 -0
  79. package/test/test-input-fix.js +105 -0
  80. package/test/test-input-history.js +98 -0
  81. package/test/test-mcp.js +115 -0
  82. package/test/test-openai-client.js +152 -0
  83. package/test/test-openai-tool-result.js +199 -0
  84. package/test/test-project-config.js +106 -0
  85. package/test/test-shortcuts.js +79 -0
  86. package/test/test-stream-throttle.js +124 -0
  87. package/test/test-tool-error-handling.js +95 -0
  88. package/test/verify-input-fix.sh +35 -0
  89. package/test-abort-fence.js +263 -0
  90. package/test-abort-fix.js +54 -0
  91. package/test-abort-new-conversation.js +75 -0
  92. package/test-ctrl-z.js +54 -0
  93. package/test-file-read.js +105 -0
  94. package/test-tool-display.js +127 -0
  95. package/src/closer-cli.jsx.backup +0 -948
  96. package/test/workflows/longtalk/cloco.md +0 -19
  97. package/test/workflows/longtalk/emoji_500.txt +0 -63
  98. package/test/workflows/longtalk/emoji_list.txt +0 -20
  99. package/test-ctrl-c.jsx +0 -126
@@ -0,0 +1,152 @@
1
+ /**
2
+ * OpenAI 客户端测试脚本
3
+ *
4
+ * 用法:
5
+ * 1. 设置环境变量: export CLOSER_OPENAI_API_KEY=your_key
6
+ * 2. 运行测试: node test/test-openai-client.js
7
+ */
8
+
9
+ import { OpenAIClient } from '../src/ai-client-openai.js';
10
+
11
+ // 测试配置
12
+ const config = {
13
+ apiKey: process.env.CLOSER_OPENAI_API_KEY || 'sk-test',
14
+ baseURL: 'https://api.openai.com/v1',
15
+ model: 'gpt-4o',
16
+ maxTokens: 1000
17
+ };
18
+
19
+ async function testBasicChat() {
20
+ console.log('\n🧪 测试 1: 基础对话');
21
+ console.log('─'.repeat(50));
22
+
23
+ try {
24
+ const client = new OpenAIClient(config);
25
+ const messages = [
26
+ { role: 'user', content: 'Say "Hello, OpenAI!" in a creative way.' }
27
+ ];
28
+
29
+ const response = await client.chat(messages, {
30
+ temperature: 0.7
31
+ });
32
+
33
+ console.log('✅ 成功!');
34
+ console.log('响应:', JSON.stringify(response, null, 2));
35
+ return true;
36
+ } catch (error) {
37
+ console.error('❌ 失败:', error.message);
38
+ return false;
39
+ }
40
+ }
41
+
42
+ async function testToolCalling() {
43
+ console.log('\n🧪 测试 2: 工具调用');
44
+ console.log('─'.repeat(50));
45
+
46
+ try {
47
+ const { z } = await import('zod');
48
+ const { tool } = await import('@openai/agents');
49
+
50
+ const client = new OpenAIClient(config);
51
+
52
+ // 定义一个简单的工具
53
+ const getWeatherTool = tool({
54
+ name: 'get_weather',
55
+ description: 'Get the weather for a given city',
56
+ parameters: z.object({
57
+ city: z.string().describe('The name of the city')
58
+ }),
59
+ execute: async (input) => {
60
+ return `The weather in ${input.city} is sunny and 25°C`;
61
+ }
62
+ });
63
+
64
+ const messages = [
65
+ { role: 'user', content: 'What is the weather in Tokyo?' }
66
+ ];
67
+
68
+ const response = await client.chatWithTools(
69
+ messages,
70
+ [getWeatherTool],
71
+ { temperature: 0.7 }
72
+ );
73
+
74
+ console.log('✅ 成功!');
75
+ console.log('响应:', JSON.stringify(response, null, 2));
76
+ return true;
77
+ } catch (error) {
78
+ console.error('❌ 失败:', error.message);
79
+ console.error('详情:', error.cause || error);
80
+ return false;
81
+ }
82
+ }
83
+
84
+ async function testStreaming() {
85
+ console.log('\n🧪 测试 3: 流式响应');
86
+ console.log('─'.repeat(50));
87
+
88
+ try {
89
+ const client = new OpenAIClient(config);
90
+ const messages = [
91
+ { role: 'user', content: 'Count from 1 to 10 slowly.' }
92
+ ];
93
+
94
+ console.log('流式输出: ');
95
+ await client.chatStream(messages, { temperature: 0.7 }, (chunk) => {
96
+ if (chunk.type === 'text') {
97
+ process.stdout.write(chunk.delta);
98
+ }
99
+ });
100
+
101
+ console.log('\n✅ 成功!');
102
+ return true;
103
+ } catch (error) {
104
+ console.error('\n❌ 失败:', error.message);
105
+ console.error('详情:', error.cause || error);
106
+ return false;
107
+ }
108
+ }
109
+
110
+ async function main() {
111
+ console.log('🚀 OpenAI 客户端测试');
112
+ console.log('═'.repeat(50));
113
+
114
+ // 检查 API key
115
+ if (!process.env.CLOSER_OPENAI_API_KEY || process.env.CLOSER_OPENAI_API_KEY === 'sk-test') {
116
+ console.warn('\n⚠️ 警告: 未设置 CLOSER_OPENAI_API_KEY 环境变量');
117
+ console.warn('请设置: export CLOSER_OPENAI_API_KEY=your_key');
118
+ console.warn('继续运行测试(预期会失败)...\n');
119
+ }
120
+
121
+ const results = {
122
+ basicChat: false,
123
+ toolCalling: false,
124
+ streaming: false
125
+ };
126
+
127
+ // 运行测试
128
+ results.basicChat = await testBasicChat();
129
+ results.toolCalling = await testToolCalling();
130
+ results.streaming = await testStreaming();
131
+
132
+ // 总结
133
+ console.log('\n' + '═'.repeat(50));
134
+ console.log('📊 测试结果总结:');
135
+ console.log('─'.repeat(50));
136
+ console.log(`基础对话: ${results.basicChat ? '✅ 通过' : '❌ 失败'}`);
137
+ console.log(`工具调用: ${results.toolCalling ? '✅ 通过' : '❌ 失败'}`);
138
+ console.log(`流式响应: ${results.streaming ? '✅ 通过' : '❌ 失败'}`);
139
+
140
+ const passed = Object.values(results).filter(r => r).length;
141
+ const total = Object.keys(results).length;
142
+
143
+ console.log(`\n总计: ${passed}/${total} 测试通过`);
144
+
145
+ if (passed === total) {
146
+ console.log('🎉 所有测试通过!');
147
+ } else {
148
+ console.log('⚠️ 部分测试失败,请检查配置和网络连接');
149
+ }
150
+ }
151
+
152
+ main().catch(console.error);
@@ -0,0 +1,199 @@
1
+ /**
2
+ * 测试 OpenAI 工具结果消息格式修复
3
+ *
4
+ * 这个测试验证修复后的 _convertMessageFormat 方法
5
+ * 是否能正确处理 tool_result 类型的消息
6
+ */
7
+
8
+ import { OpenAIClient } from '../src/ai-client-openai.js';
9
+
10
+ // 创建测试客户端
11
+ const client = new OpenAIClient({
12
+ apiKey: 'test-key',
13
+ baseURL: 'https://api.openai.com/v1',
14
+ model: 'gpt-4o'
15
+ });
16
+
17
+ console.log('🧪 测试 OpenAI 工具结果消息格式转换\n');
18
+
19
+ // 测试用例
20
+ const testCases = [
21
+ {
22
+ name: '测试1: 简单的 tool_result 消息',
23
+ input: {
24
+ role: 'user',
25
+ content: [
26
+ {
27
+ type: 'tool_result',
28
+ tool_use_id: 'call_123',
29
+ content: 'Command executed successfully'
30
+ }
31
+ ]
32
+ },
33
+ expected: {
34
+ role: 'tool',
35
+ tool_call_id: 'call_123',
36
+ content: 'Command executed successfully'
37
+ }
38
+ },
39
+ {
40
+ name: '测试2: tool_result 带对象内容',
41
+ input: {
42
+ role: 'user',
43
+ content: [
44
+ {
45
+ type: 'tool_result',
46
+ tool_use_id: 'call_456',
47
+ content: { success: true, data: 'test' }
48
+ }
49
+ ]
50
+ },
51
+ expected: {
52
+ role: 'tool',
53
+ tool_call_id: 'call_456',
54
+ content: '{"success":true,"data":"test"}'
55
+ }
56
+ },
57
+ {
58
+ name: '测试3: tool_use 消息(助手调用工具)',
59
+ input: {
60
+ role: 'assistant',
61
+ content: [
62
+ {
63
+ type: 'tool_use',
64
+ id: 'call_789',
65
+ name: 'bash',
66
+ input: { command: 'ls -la' }
67
+ }
68
+ ]
69
+ },
70
+ expected: {
71
+ role: 'assistant',
72
+ tool_calls: [
73
+ {
74
+ id: 'call_789',
75
+ type: 'function',
76
+ function: {
77
+ name: 'bash',
78
+ arguments: '{"command":"ls -la"}'
79
+ }
80
+ }
81
+ ]
82
+ }
83
+ },
84
+ {
85
+ name: '测试4: 普通文本消息',
86
+ input: {
87
+ role: 'user',
88
+ content: 'Hello, how are you?'
89
+ },
90
+ expected: {
91
+ role: 'user',
92
+ content: 'Hello, how are you?'
93
+ }
94
+ },
95
+ {
96
+ name: '测试5: 混合文本和 tool_use',
97
+ input: {
98
+ role: 'assistant',
99
+ content: [
100
+ {
101
+ type: 'text',
102
+ text: 'I will execute the command:'
103
+ },
104
+ {
105
+ type: 'tool_use',
106
+ id: 'call_abc',
107
+ name: 'bash',
108
+ input: { command: 'pwd' }
109
+ }
110
+ ]
111
+ },
112
+ expected: {
113
+ role: 'assistant',
114
+ tool_calls: [
115
+ {
116
+ id: 'call_abc',
117
+ type: 'function',
118
+ function: {
119
+ name: 'bash',
120
+ arguments: '{"command":"pwd"}'
121
+ }
122
+ }
123
+ ],
124
+ content: 'I will execute the command:'
125
+ }
126
+ }
127
+ ];
128
+
129
+ // 运行测试
130
+ let passed = 0;
131
+ let failed = 0;
132
+
133
+ for (const testCase of testCases) {
134
+ console.log(`📋 ${testCase.name}`);
135
+ console.log(` 输入:`, JSON.stringify(testCase.input, null, 2));
136
+
137
+ try {
138
+ const result = client._convertMessageFormat(testCase.input);
139
+ console.log(` 输出:`, JSON.stringify(result, null, 2));
140
+
141
+ // 验证结果
142
+ let isCorrect = true;
143
+
144
+ if (testCase.expected.role !== result.role) {
145
+ console.log(` ❌ role 不匹配: 期望 "${testCase.expected.role}", 实际 "${result.role}"`);
146
+ isCorrect = false;
147
+ }
148
+
149
+ if (testCase.expected.tool_call_id && testCase.expected.tool_call_id !== result.tool_call_id) {
150
+ console.log(` ❌ tool_call_id 不匹配`);
151
+ isCorrect = false;
152
+ }
153
+
154
+ if (testCase.expected.content && testCase.expected.content !== result.content) {
155
+ console.log(` ❌ content 不匹配`);
156
+ isCorrect = false;
157
+ }
158
+
159
+ if (testCase.expected.tool_calls) {
160
+ if (!result.tool_calls || result.tool_calls.length !== testCase.expected.tool_calls.length) {
161
+ console.log(` ❌ tool_calls 数量不匹配`);
162
+ isCorrect = false;
163
+ } else {
164
+ for (let i = 0; i < testCase.expected.tool_calls.length; i++) {
165
+ const expected = testCase.expected.tool_calls[i];
166
+ const actual = result.tool_calls[i];
167
+ if (expected.id !== actual.id || expected.function.name !== actual.function.name) {
168
+ console.log(` ❌ tool_call[${i}] 不匹配`);
169
+ isCorrect = false;
170
+ }
171
+ }
172
+ }
173
+ }
174
+
175
+ if (isCorrect) {
176
+ console.log(` ✅ 通过\n`);
177
+ passed++;
178
+ } else {
179
+ console.log(` ❌ 失败\n`);
180
+ failed++;
181
+ }
182
+ } catch (error) {
183
+ console.log(` ❌ 错误: ${error.message}\n`);
184
+ failed++;
185
+ }
186
+ }
187
+
188
+ // 输出测试结果
189
+ console.log('='.repeat(60));
190
+ console.log(`📊 测试结果: ${passed} 通过, ${failed} 失败`);
191
+ console.log('='.repeat(60));
192
+
193
+ if (failed === 0) {
194
+ console.log('\n✅ 所有测试通过!工具结果消息格式修复成功。');
195
+ process.exit(0);
196
+ } else {
197
+ console.log('\n❌ 部分测试失败,请检查代码。');
198
+ process.exit(1);
199
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * 项目本地配置测试
3
+ */
4
+
5
+ import { loadConfig, loadProjectConfig, findProjectConfigFile, getConfigPaths } from '../src/config.js';
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+
9
+ async function testProjectConfig() {
10
+ console.log('=== 项目本地配置测试 ===\n');
11
+
12
+ // 1. 测试查找项目配置文件
13
+ console.log('1️⃣ 测试查找项目配置文件\n');
14
+ const projectConfigPath = findProjectConfigFile();
15
+ console.log(`当前目录: ${process.cwd()}`);
16
+ console.log(`项目配置文件: ${projectConfigPath || '(未找到)'}\n`);
17
+
18
+ // 2. 测试加载项目配置
19
+ console.log('2️⃣ 测试加载项目配置\n');
20
+ const projectConfig = loadProjectConfig();
21
+ console.log(`项目配置内容:`);
22
+ console.log(JSON.stringify(projectConfig, null, 2));
23
+ console.log();
24
+
25
+ // 3. 测试配置合并
26
+ console.log('3️⃣ 测试配置合并\n');
27
+ const mergedConfig = loadConfig();
28
+ console.log(`合并后的配置:`);
29
+ console.log(`- MCP 启用: ${mergedConfig.mcp?.enabled}`);
30
+ console.log(`- MCP Servers 数量: ${Object.keys(mergedConfig.mcp?.servers || {}).length}`);
31
+ if (mergedConfig.mcp?.servers) {
32
+ for (const [name, server] of Object.entries(mergedConfig.mcp.servers)) {
33
+ const status = server.enabled ? '✅' : '❌';
34
+ console.log(` ${status} ${name}`);
35
+ }
36
+ }
37
+ console.log();
38
+
39
+ // 4. 测试配置路径
40
+ console.log('4️⃣ 测试配置路径\n');
41
+ const configPaths = getConfigPaths();
42
+ console.log('配置文件路径:');
43
+ console.log(`- 全局配置: ${configPaths.global}`);
44
+ console.log(`- 项目配置: ${configPaths.project || '(未找到)'}`);
45
+ console.log(`- 活动配置: ${configPaths.active}`);
46
+ console.log();
47
+
48
+ // 5. 测试创建项目配置文件
49
+ console.log('5️⃣ 测试创建项目配置文件\n');
50
+ const testConfigPath = '.closer-code.test.json';
51
+ const testConfig = {
52
+ mcp: {
53
+ enabled: true,
54
+ servers: {
55
+ test_server: {
56
+ enabled: true,
57
+ command: 'echo',
58
+ args: ['test'],
59
+ env: {}
60
+ }
61
+ }
62
+ }
63
+ };
64
+
65
+ try {
66
+ // 创建测试配置文件
67
+ fs.writeFileSync(testConfigPath, JSON.stringify(testConfig, null, 2));
68
+ console.log(`✅ 创建测试配置文件: ${testConfigPath}`);
69
+
70
+ // 重新加载配置
71
+ const newProjectConfig = loadProjectConfig();
72
+ console.log(`✅ 重新加载项目配置:`);
73
+ console.log(JSON.stringify(newProjectConfig, null, 2));
74
+
75
+ // 验证配置是否正确加载
76
+ if (newProjectConfig.mcp?.servers?.test_server) {
77
+ console.log(`✅ 测试服务器配置加载成功`);
78
+ } else {
79
+ console.log(`❌ 测试服务器配置加载失败`);
80
+ }
81
+
82
+ // 清理测试文件
83
+ fs.unlinkSync(testConfigPath);
84
+ console.log(`✅ 清理测试文件: ${testConfigPath}`);
85
+ } catch (error) {
86
+ console.error(`❌ 测试失败: ${error.message}`);
87
+ }
88
+
89
+ console.log();
90
+
91
+ // 6. 测试配置优先级
92
+ console.log('6️⃣ 测试配置优先级\n');
93
+ console.log('配置优先级: 项目本地 > 全局 > 默认');
94
+ console.log();
95
+ console.log('示例:');
96
+ console.log('1. 默认配置: MCP 启用,无 servers');
97
+ console.log('2. 全局配置: 添加 filesystem server');
98
+ console.log('3. 项目配置: 添加 git server');
99
+ console.log('4. 最终配置: 包含 filesystem (全局) 和 git (项目)');
100
+ console.log();
101
+
102
+ console.log('=== 测试完成 ===');
103
+ }
104
+
105
+ // 运行测试
106
+ testProjectConfig().catch(console.error);
@@ -0,0 +1,79 @@
1
+ /**
2
+ * 测试输入框快捷键功能
3
+ *
4
+ * 这个脚本展示 EnhancedTextInput 组件的所有快捷键:
5
+ * - Ctrl+A: 跳到行首
6
+ * - Ctrl+E: 跳到行尾
7
+ * - Ctrl+U: 删除到行首
8
+ * - Ctrl+K: 删除到行尾
9
+ * - Ctrl+W: 删除前一个单词
10
+ */
11
+
12
+ // 模拟测试
13
+ console.log('🧪 测试输入框快捷键功能\n');
14
+
15
+ // 测试用例
16
+ const testCases = [
17
+ {
18
+ name: 'Ctrl+A: 跳到行首',
19
+ initialValue: 'Hello World',
20
+ initialCursor: 11,
21
+ key: { ctrl: true, input: 'a' },
22
+ expectedCursor: 0,
23
+ expectedValue: 'Hello World'
24
+ },
25
+ {
26
+ name: 'Ctrl+E: 跳到行尾',
27
+ initialValue: 'Hello World',
28
+ initialCursor: 0,
29
+ key: { ctrl: true, input: 'e' },
30
+ expectedCursor: 11,
31
+ expectedValue: 'Hello World'
32
+ },
33
+ {
34
+ name: 'Ctrl+U: 删除到行首(光标在中间)',
35
+ initialValue: 'Hello World',
36
+ initialCursor: 6,
37
+ key: { ctrl: true, input: 'u' },
38
+ expectedCursor: 0,
39
+ expectedValue: 'World'
40
+ },
41
+ {
42
+ name: 'Ctrl+K: 删除到行尾(光标在中间)',
43
+ initialValue: 'Hello World',
44
+ initialCursor: 5,
45
+ key: { ctrl: true, input: 'k' },
46
+ expectedCursor: 5,
47
+ expectedValue: 'Hello'
48
+ },
49
+ {
50
+ name: 'Ctrl+W: 删除前一个单词',
51
+ initialValue: 'Hello World Test',
52
+ initialCursor: 12,
53
+ key: { ctrl: true, input: 'w' },
54
+ expectedCursor: 6,
55
+ expectedValue: 'Hello Test'
56
+ }
57
+ ];
58
+
59
+ console.log('📋 测试用例:\n');
60
+ testCases.forEach((test, index) => {
61
+ console.log(`${index + 1}. ${test.name}`);
62
+ console.log(` 初始值: "${test.initialValue}"`);
63
+ console.log(` 初始光标: ${test.initialCursor}`);
64
+ console.log(` 快捷键: Ctrl+${test.key.input.toUpperCase()}`);
65
+ console.log(` 期望光标: ${test.expectedCursor}`);
66
+ console.log(` 期望值: "${test.expectedValue}"`);
67
+ console.log('');
68
+ });
69
+
70
+ console.log('✅ 所有快捷键已实现!\n');
71
+ console.log('📝 使用方法:\n');
72
+ console.log('1. 运行 closer CLI: node dist/closer-cli.js');
73
+ console.log('2. 在输入框中测试以下快捷键:\n');
74
+ console.log(' Ctrl+A - 跳到行首');
75
+ console.log(' Ctrl+E - 跳到行尾');
76
+ console.log(' Ctrl+U - 删除到行首');
77
+ console.log(' Ctrl+K - 删除到行尾');
78
+ console.log(' Ctrl+W - 删除前一个单词\n');
79
+ console.log('🎉 快捷键扩展完成!\n');
@@ -0,0 +1,124 @@
1
+ /**
2
+ * 测试流式更新节流功能(Buffer + Throttle)
3
+ *
4
+ * 验证消息逐字打印的更新频率限制
5
+ */
6
+
7
+ import { createConversation } from '../src/conversation.js';
8
+ import { loadConfig } from '../src/config.js';
9
+
10
+ // 加载配置
11
+ const config = loadConfig();
12
+
13
+ // 显示当前配置
14
+ console.log('🔧 流式更新配置:');
15
+ console.log(` 更新间隔: ${config.ui.streamUpdate.interval}ms`);
16
+ console.log(` 缓冲区大小: ${config.ui.streamUpdate.bufferSize} tokens`);
17
+ console.log(` 标点更新: ${config.ui.streamUpdate.updateOnPunctuation ? '启用' : '禁用'}`);
18
+ console.log('');
19
+
20
+ // 模拟 token 流
21
+ function simulateTokenStream() {
22
+ const testText = '这是一个测试句子。这是第二个句子!这是第三个句子?这是第四个句子。';
23
+
24
+ console.log('📝 模拟 token 流:');
25
+ console.log(` 原始文本: "${testText}"`);
26
+ console.log(` 字符数: ${testText.length}`);
27
+ console.log('');
28
+
29
+ // 创建模拟的 Conversation 对象(只测试节流逻辑)
30
+ const mockConversation = {
31
+ streamUpdate: {
32
+ lastUpdateTime: 0,
33
+ queuedTokens: [],
34
+ interval: config.ui.streamUpdate.interval,
35
+ bufferSize: config.ui.streamUpdate.bufferSize,
36
+ updateOnPunctuation: config.ui.streamUpdate.updateOnPunctuation
37
+ }
38
+ };
39
+
40
+ const updates = [];
41
+ const startTime = Date.now();
42
+
43
+ // 逐字符发送(模拟 token 流)
44
+ for (let i = 0; i < testText.length; i++) {
45
+ const char = testText[i];
46
+ const now = Date.now();
47
+ const timeSinceLastUpdate = now - mockConversation.streamUpdate.lastUpdateTime;
48
+
49
+ // 累积 token
50
+ mockConversation.streamUpdate.queuedTokens.push(char);
51
+ const combinedContent = mockConversation.streamUpdate.queuedTokens.join('');
52
+
53
+ // 检查是否应该更新(满足任一条件)
54
+ const shouldUpdate =
55
+ timeSinceLastUpdate >= mockConversation.streamUpdate.interval || // 条件1: 时间间隔
56
+ mockConversation.streamUpdate.queuedTokens.length >= mockConversation.streamUpdate.bufferSize || // 条件2: 缓冲区满
57
+ (mockConversation.streamUpdate.updateOnPunctuation && /[.!?。!?]\s*$/.test(combinedContent)); // 条件3: 句子结束
58
+
59
+ if (shouldUpdate) {
60
+ updates.push({
61
+ time: now - startTime,
62
+ content: combinedContent,
63
+ tokenCount: mockConversation.streamUpdate.queuedTokens.length,
64
+ reason: [
65
+ timeSinceLastUpdate >= mockConversation.streamUpdate.interval ? '时间间隔' : null,
66
+ mockConversation.streamUpdate.queuedTokens.length >= mockConversation.streamUpdate.bufferSize ? '缓冲区满' : null,
67
+ (mockConversation.streamUpdate.updateOnPunctuation && /[.!?。!?]\s*$/.test(combinedContent)) ? '标点符号' : null
68
+ ].filter(Boolean).join(' + ')
69
+ });
70
+
71
+ mockConversation.streamUpdate.queuedTokens = [];
72
+ mockConversation.streamUpdate.lastUpdateTime = now;
73
+ }
74
+
75
+ // 模拟延迟(每个 token 间隔 50ms)
76
+ // 注意:实际测试时不需要延迟,这里只是为了演示
77
+ }
78
+
79
+ // 发送剩余的 tokens
80
+ if (mockConversation.streamUpdate.queuedTokens.length > 0) {
81
+ updates.push({
82
+ time: Date.now() - startTime,
83
+ content: mockConversation.streamUpdate.queuedTokens.join(''),
84
+ tokenCount: mockConversation.streamUpdate.queuedTokens.length,
85
+ reason: '剩余内容'
86
+ });
87
+ }
88
+
89
+ return updates;
90
+ }
91
+
92
+ // 运行测试
93
+ try {
94
+ const updates = simulateTokenStream();
95
+
96
+ console.log('📊 更新统计:');
97
+ console.log(` 总更新次数: ${updates.length}`);
98
+ console.log(` 原始 token 数: ${updates.reduce((sum, u) => sum + u.tokenCount, 0)}`);
99
+ console.log('');
100
+
101
+ console.log('📋 更新详情:');
102
+ updates.forEach((update, index) => {
103
+ console.log(` 更新 #${index + 1}:`);
104
+ console.log(` 时间: ${update.time}ms`);
105
+ console.log(` Token 数: ${update.tokenCount}`);
106
+ console.log(` 原因: ${update.reason}`);
107
+ console.log(` 内容: "${update.content}"`);
108
+ console.log('');
109
+ });
110
+
111
+ console.log('✅ 测试完成!');
112
+ console.log('');
113
+ console.log('💡 说明:');
114
+ console.log(' - 每次更新都包含多个累积的 tokens');
115
+ console.log(' - 更新由以下条件触发(满足任一即可):');
116
+ console.log(' 1. 时间间隔达到设定值(默认 1000ms)');
117
+ console.log(' 2. 缓冲区达到设定大小(默认 50 tokens)');
118
+ console.log(' 3. 遇到句子结束标点(., !, ?, 。, !, ?)');
119
+ console.log(' - 响应结束后会发送所有剩余的 tokens');
120
+
121
+ } catch (error) {
122
+ console.error('❌ 测试失败:', error);
123
+ process.exit(1);
124
+ }