flashclaw 1.6.0 → 1.6.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.
@@ -0,0 +1,461 @@
1
+ /**
2
+ * Anthropic AI Provider 插件
3
+ * 实现 AIProviderPlugin 接口,支持 Claude 等模型
4
+ */
5
+
6
+ import Anthropic from '@anthropic-ai/sdk';
7
+ import type {
8
+ AIProviderPlugin,
9
+ ChatMessage,
10
+ ChatOptions,
11
+ StreamEvent,
12
+ ToolExecutor,
13
+ HeartbeatCallback,
14
+ PluginConfig,
15
+ ImageBlock,
16
+ TextBlock,
17
+ } from '../../src/plugins/types';
18
+
19
+ // ==================== 内部状态 ====================
20
+
21
+ let client: Anthropic | null = null;
22
+ let model: string = 'claude-sonnet-4-20250514';
23
+
24
+ // ==================== 内部常量和工具函数 ====================
25
+
26
+ const MAX_TOOL_CALL_DEPTH = 20;
27
+ const MAX_TOOL_RESULT_CHARS = 4000;
28
+ const KEEP_RECENT_TOOL_ROUNDS = 2;
29
+
30
+ function truncateToolResult(content: string, maxChars: number): string {
31
+ if (content.length <= maxChars) return content;
32
+ return content.slice(0, maxChars) + `\n...(内容已截断,原始 ${content.length} 字符)`;
33
+ }
34
+
35
+ function compressToolHistory(
36
+ messages: Anthropic.MessageParam[],
37
+ keepRecentRounds: number,
38
+ ): Anthropic.MessageParam[] {
39
+ const toolRoundIndices: number[] = [];
40
+ for (let i = 0; i < messages.length; i++) {
41
+ const msg = messages[i];
42
+ if (msg.role === 'assistant' && Array.isArray(msg.content)) {
43
+ const hasToolUse = (msg.content as Anthropic.ContentBlock[]).some(
44
+ (b) => b.type === 'tool_use'
45
+ );
46
+ if (hasToolUse) toolRoundIndices.push(i);
47
+ }
48
+ }
49
+
50
+ if (toolRoundIndices.length <= keepRecentRounds) return messages;
51
+
52
+ const compressCount = toolRoundIndices.length - keepRecentRounds;
53
+ const toCompress = new Set(toolRoundIndices.slice(0, compressCount));
54
+
55
+ const result: Anthropic.MessageParam[] = [];
56
+ for (let i = 0; i < messages.length; i++) {
57
+ const msg = messages[i];
58
+
59
+ if (toCompress.has(i) && msg.role === 'assistant' && Array.isArray(msg.content)) {
60
+ const summaryParts: string[] = [];
61
+ for (const block of msg.content as Anthropic.ContentBlock[]) {
62
+ if (block.type === 'tool_use') {
63
+ const inputStr = JSON.stringify(block.input);
64
+ const inputPreview = inputStr.length > 80 ? inputStr.slice(0, 80) + '...' : inputStr;
65
+ summaryParts.push(`[已执行工具 ${block.name}(${inputPreview})]`);
66
+ } else if (block.type === 'text' && (block as Anthropic.TextBlock).text) {
67
+ summaryParts.push((block as Anthropic.TextBlock).text);
68
+ }
69
+ }
70
+ result.push({ role: 'assistant', content: summaryParts.join('\n') || '[工具调用]' });
71
+
72
+ if (i + 1 < messages.length && messages[i + 1].role === 'user') {
73
+ const nextMsg = messages[i + 1];
74
+ if (Array.isArray(nextMsg.content)) {
75
+ const resultParts: string[] = [];
76
+ for (const block of nextMsg.content as Anthropic.ToolResultBlockParam[]) {
77
+ if (block.type === 'tool_result') {
78
+ const contentStr = typeof block.content === 'string' ? block.content : '';
79
+ const preview = contentStr.length > 100 ? contentStr.slice(0, 100) + '...' : contentStr;
80
+ resultParts.push(block.is_error ? `[失败: ${preview}]` : `[成功: ${preview}]`);
81
+ }
82
+ }
83
+ result.push({ role: 'user', content: resultParts.join('\n') || '[工具结果]' });
84
+ i++;
85
+ } else {
86
+ result.push(nextMsg);
87
+ i++;
88
+ }
89
+ }
90
+ } else {
91
+ result.push(msg);
92
+ }
93
+ }
94
+
95
+ return result;
96
+ }
97
+
98
+ function extractText(response: Anthropic.Message): string {
99
+ const textBlocks = response.content.filter(
100
+ (block): block is Anthropic.TextBlock => block.type === 'text'
101
+ );
102
+ return textBlocks.map(block => block.text).join('');
103
+ }
104
+
105
+ async function streamFollowUp(
106
+ messages: Anthropic.MessageParam[],
107
+ options?: ChatOptions,
108
+ heartbeat?: HeartbeatCallback
109
+ ): Promise<Anthropic.Message> {
110
+ if (!client) {
111
+ throw new Error('Provider not initialized. Call init() first.');
112
+ }
113
+
114
+ const params: Anthropic.MessageCreateParams = {
115
+ model: model,
116
+ max_tokens: options?.maxTokens ?? 4096,
117
+ messages,
118
+ stream: true,
119
+ };
120
+
121
+ if (options?.system) {
122
+ params.system = options.system;
123
+ }
124
+
125
+ if (options?.tools && options.tools.length > 0) {
126
+ params.tools = options.tools as unknown as Anthropic.Tool[];
127
+ }
128
+
129
+ const stream = await client.messages.create(params);
130
+
131
+ let finalMessage: Anthropic.Message | null = null;
132
+ const contentBlocks: Array<Anthropic.TextBlock | Anthropic.ToolUseBlock> = [];
133
+ const partialJsonParts = new Map<number, string[]>();
134
+
135
+ for await (const event of stream as AsyncIterable<Anthropic.MessageStreamEvent>) {
136
+ heartbeat?.();
137
+
138
+ if (event.type === 'message_start') {
139
+ finalMessage = event.message as Anthropic.Message;
140
+ } else if (event.type === 'content_block_start') {
141
+ const block = event.content_block;
142
+ if (block.type === 'text') {
143
+ contentBlocks[event.index] = { type: 'text' as const, text: '', citations: null };
144
+ } else if (block.type === 'tool_use') {
145
+ contentBlocks[event.index] = {
146
+ type: 'tool_use' as const,
147
+ id: block.id,
148
+ name: block.name,
149
+ input: {},
150
+ };
151
+ partialJsonParts.set(event.index, []);
152
+ }
153
+ } else if (event.type === 'content_block_delta') {
154
+ const delta = event.delta;
155
+ if ('text' in delta) {
156
+ const block = contentBlocks[event.index];
157
+ if (block?.type === 'text') {
158
+ block.text += delta.text;
159
+ }
160
+ } else if ('partial_json' in delta) {
161
+ const parts = partialJsonParts.get(event.index);
162
+ if (parts) {
163
+ parts.push(delta.partial_json);
164
+ }
165
+ }
166
+ } else if (event.type === 'content_block_stop') {
167
+ const block = contentBlocks[event.index];
168
+ if (block?.type === 'tool_use') {
169
+ const parts = partialJsonParts.get(event.index);
170
+ if (parts && parts.length > 0) {
171
+ try {
172
+ block.input = JSON.parse(parts.join(''));
173
+ } catch {
174
+ block.input = {};
175
+ }
176
+ }
177
+ }
178
+ } else if (event.type === 'message_delta') {
179
+ if (finalMessage) {
180
+ finalMessage.stop_reason = event.delta.stop_reason ?? finalMessage.stop_reason;
181
+ if (event.usage) {
182
+ finalMessage.usage.output_tokens = event.usage.output_tokens;
183
+ }
184
+ }
185
+ }
186
+ }
187
+
188
+ if (!finalMessage) {
189
+ throw new Error('工具链后续请求未收到响应');
190
+ }
191
+
192
+ finalMessage.content = contentBlocks.filter(
193
+ (block): block is Anthropic.TextBlock | Anthropic.ToolUseBlock => block != null
194
+ );
195
+
196
+ return finalMessage;
197
+ }
198
+
199
+ async function handleToolUseInternal(
200
+ response: Anthropic.Message,
201
+ messages: Anthropic.MessageParam[],
202
+ executeTool: ToolExecutor,
203
+ options?: ChatOptions,
204
+ depth: number = 0,
205
+ heartbeat?: HeartbeatCallback
206
+ ): Promise<string> {
207
+ if (depth >= MAX_TOOL_CALL_DEPTH) {
208
+ return extractText(response) || `[工具调用链过深(超过 ${MAX_TOOL_CALL_DEPTH} 轮),已强制终止]`;
209
+ }
210
+
211
+ const toolUseBlocks = response.content.filter(
212
+ (block): block is Anthropic.ToolUseBlock => block.type === 'tool_use'
213
+ );
214
+
215
+ if (toolUseBlocks.length === 0) {
216
+ return extractText(response);
217
+ }
218
+
219
+ let newMessages: Anthropic.MessageParam[] = [
220
+ ...messages,
221
+ {
222
+ role: 'assistant' as const,
223
+ content: response.content,
224
+ },
225
+ ];
226
+
227
+ const toolResults: Anthropic.ToolResultBlockParam[] = [];
228
+
229
+ for (const toolUse of toolUseBlocks) {
230
+ heartbeat?.();
231
+ try {
232
+ const result = await executeTool(toolUse.name, toolUse.input);
233
+ const content = typeof result === 'string' ? result : JSON.stringify(result);
234
+ toolResults.push({
235
+ type: 'tool_result',
236
+ tool_use_id: toolUse.id,
237
+ content: truncateToolResult(content, MAX_TOOL_RESULT_CHARS),
238
+ });
239
+ } catch (error) {
240
+ toolResults.push({
241
+ type: 'tool_result',
242
+ tool_use_id: toolUse.id,
243
+ content: `工具执行失败: ${(error as Error).message}`,
244
+ is_error: true,
245
+ });
246
+ }
247
+ }
248
+
249
+ heartbeat?.();
250
+
251
+ newMessages.push({
252
+ role: 'user',
253
+ content: toolResults,
254
+ });
255
+
256
+ if (depth >= KEEP_RECENT_TOOL_ROUNDS) {
257
+ newMessages = compressToolHistory(newMessages, KEEP_RECENT_TOOL_ROUNDS);
258
+ }
259
+
260
+ const nextResponse = await streamFollowUp(newMessages, options, heartbeat);
261
+
262
+ if (nextResponse.stop_reason === 'tool_use') {
263
+ return handleToolUseInternal(nextResponse, newMessages, executeTool, options, depth + 1, heartbeat);
264
+ }
265
+
266
+ return extractText(nextResponse);
267
+ }
268
+
269
+ // ==================== Provider 实现 ====================
270
+
271
+ const anthropicProvider: AIProviderPlugin = {
272
+ name: 'anthropic-provider',
273
+ version: '1.0.0',
274
+ description: 'Anthropic AI Provider - 支持 Claude 等模型',
275
+
276
+ async init(config: PluginConfig): Promise<void> {
277
+ const apiKey = config.apiKey as string || process.env.ANTHROPIC_AUTH_TOKEN || process.env.ANTHROPIC_API_KEY;
278
+ if (!apiKey) {
279
+ throw new Error('Missing API key: ANTHROPIC_AUTH_TOKEN or ANTHROPIC_API_KEY');
280
+ }
281
+
282
+ client = new Anthropic({
283
+ apiKey,
284
+ baseURL: config.baseURL as string || process.env.ANTHROPIC_BASE_URL,
285
+ maxRetries: 0,
286
+ timeout: config.timeout ? Number(config.timeout) : 60000,
287
+ });
288
+
289
+ model = config.model as string || process.env.AI_MODEL || process.env.ANTHROPIC_MODEL || 'claude-sonnet-4-20250514';
290
+ },
291
+
292
+ async chat(
293
+ messages: ChatMessage[],
294
+ options?: ChatOptions
295
+ ): Promise<Anthropic.Message> {
296
+ if (!client) {
297
+ throw new Error('Provider not initialized. Call init() first.');
298
+ }
299
+
300
+ const params: Anthropic.MessageCreateParams = {
301
+ model: model,
302
+ max_tokens: options?.maxTokens ?? 4096,
303
+ messages: messages.map(msg => ({
304
+ role: msg.role,
305
+ content: msg.content,
306
+ })),
307
+ };
308
+
309
+ if (options?.system) {
310
+ params.system = options.system;
311
+ }
312
+
313
+ if (options?.tools && options.tools.length > 0) {
314
+ params.tools = options.tools as unknown as Anthropic.Tool[];
315
+ }
316
+
317
+ if (options?.temperature !== undefined) {
318
+ params.temperature = options.temperature;
319
+ }
320
+
321
+ if (options?.stopSequences && options.stopSequences.length > 0) {
322
+ params.stop_sequences = options.stopSequences;
323
+ }
324
+
325
+ return await client.messages.create(params);
326
+ },
327
+
328
+ async *chatStream(
329
+ messages: ChatMessage[],
330
+ options?: ChatOptions
331
+ ): AsyncGenerator<StreamEvent> {
332
+ if (!client) {
333
+ throw new Error('Provider not initialized. Call init() first.');
334
+ }
335
+
336
+ const params: Anthropic.MessageCreateParams = {
337
+ model: model,
338
+ max_tokens: options?.maxTokens ?? 4096,
339
+ messages: messages.map(msg => ({
340
+ role: msg.role,
341
+ content: msg.content,
342
+ })),
343
+ stream: true,
344
+ };
345
+
346
+ if (options?.system) {
347
+ params.system = options.system;
348
+ }
349
+
350
+ if (options?.tools && options.tools.length > 0) {
351
+ params.tools = options.tools as unknown as Anthropic.Tool[];
352
+ }
353
+
354
+ if (options?.temperature !== undefined) {
355
+ params.temperature = options.temperature;
356
+ }
357
+
358
+ const stream = await client.messages.create(params);
359
+
360
+ let finalMessage: Anthropic.Message | null = null;
361
+ const contentBlocks: Array<Anthropic.TextBlock | Anthropic.ToolUseBlock> = [];
362
+ const partialJsonParts = new Map<number, string[]>();
363
+
364
+ for await (const event of stream as AsyncIterable<Anthropic.MessageStreamEvent>) {
365
+ if (event.type === 'message_start') {
366
+ finalMessage = event.message as Anthropic.Message;
367
+ } else if (event.type === 'content_block_start') {
368
+ const block = event.content_block;
369
+ if (block.type === 'text') {
370
+ contentBlocks[event.index] = { type: 'text' as const, text: '', citations: null };
371
+ } else if (block.type === 'tool_use') {
372
+ contentBlocks[event.index] = {
373
+ type: 'tool_use' as const,
374
+ id: block.id,
375
+ name: block.name,
376
+ input: {},
377
+ };
378
+ partialJsonParts.set(event.index, []);
379
+ }
380
+ } else if (event.type === 'content_block_delta') {
381
+ const delta = event.delta;
382
+ if ('text' in delta) {
383
+ const block = contentBlocks[event.index];
384
+ if (block?.type === 'text') {
385
+ block.text += delta.text;
386
+ }
387
+ yield { type: 'text', text: delta.text };
388
+ } else if ('partial_json' in delta) {
389
+ const parts = partialJsonParts.get(event.index);
390
+ if (parts) {
391
+ parts.push(delta.partial_json);
392
+ }
393
+ }
394
+ } else if (event.type === 'content_block_stop') {
395
+ const block = contentBlocks[event.index];
396
+ if (block?.type === 'tool_use') {
397
+ const parts = partialJsonParts.get(event.index);
398
+ if (parts && parts.length > 0) {
399
+ try {
400
+ block.input = JSON.parse(parts.join(''));
401
+ } catch {
402
+ block.input = {};
403
+ }
404
+ }
405
+ }
406
+ } else if (event.type === 'message_delta') {
407
+ if (finalMessage) {
408
+ finalMessage.stop_reason = event.delta.stop_reason ?? finalMessage.stop_reason;
409
+ if (event.usage) {
410
+ finalMessage.usage.output_tokens = event.usage.output_tokens;
411
+ }
412
+ }
413
+ }
414
+ }
415
+
416
+ if (finalMessage) {
417
+ finalMessage.content = contentBlocks.filter(
418
+ (block): block is Anthropic.TextBlock | Anthropic.ToolUseBlock => block != null
419
+ );
420
+
421
+ for (const block of finalMessage.content) {
422
+ if (block.type === 'tool_use') {
423
+ yield {
424
+ type: 'tool_use',
425
+ id: block.id,
426
+ name: block.name,
427
+ input: block.input,
428
+ };
429
+ }
430
+ }
431
+
432
+ yield { type: 'done', message: finalMessage };
433
+ }
434
+ },
435
+
436
+ async handleToolUse(
437
+ response: unknown,
438
+ messages: ChatMessage[],
439
+ executeTool: ToolExecutor,
440
+ options?: ChatOptions,
441
+ heartbeat?: HeartbeatCallback
442
+ ): Promise<string> {
443
+ const anthropicResponse = response as Anthropic.Message;
444
+ const apiMessages: Anthropic.MessageParam[] = messages.map(msg => ({
445
+ role: msg.role as 'user' | 'assistant',
446
+ content: msg.content,
447
+ }));
448
+
449
+ return handleToolUseInternal(anthropicResponse, apiMessages, executeTool, options, 0, heartbeat);
450
+ },
451
+
452
+ getModel(): string {
453
+ return model;
454
+ },
455
+
456
+ setModel(newModel: string): void {
457
+ model = newModel;
458
+ },
459
+ };
460
+
461
+ export default anthropicProvider;
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "anthropic-provider",
3
+ "version": "1.0.0",
4
+ "type": "provider",
5
+ "description": "Anthropic AI Provider - 支持 Claude 等模型",
6
+ "main": "index.ts"
7
+ }