@zhin.js/core 1.0.25 → 1.0.27

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 (202) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +84 -342
  3. package/lib/adapter.d.ts +17 -0
  4. package/lib/adapter.d.ts.map +1 -1
  5. package/lib/adapter.js +84 -2
  6. package/lib/adapter.js.map +1 -1
  7. package/lib/ai/agent.d.ts +126 -0
  8. package/lib/ai/agent.d.ts.map +1 -0
  9. package/lib/ai/agent.js +645 -0
  10. package/lib/ai/agent.js.map +1 -0
  11. package/lib/ai/context-manager.d.ts +213 -0
  12. package/lib/ai/context-manager.d.ts.map +1 -0
  13. package/lib/ai/context-manager.js +313 -0
  14. package/lib/ai/context-manager.js.map +1 -0
  15. package/lib/ai/conversation-memory.d.ts +181 -0
  16. package/lib/ai/conversation-memory.d.ts.map +1 -0
  17. package/lib/ai/conversation-memory.js +581 -0
  18. package/lib/ai/conversation-memory.js.map +1 -0
  19. package/lib/ai/follow-up.d.ts +131 -0
  20. package/lib/ai/follow-up.d.ts.map +1 -0
  21. package/lib/ai/follow-up.js +265 -0
  22. package/lib/ai/follow-up.js.map +1 -0
  23. package/lib/ai/index.d.ts +29 -0
  24. package/lib/ai/index.d.ts.map +1 -0
  25. package/lib/ai/index.js +34 -0
  26. package/lib/ai/index.js.map +1 -0
  27. package/lib/ai/init.d.ts +30 -0
  28. package/lib/ai/init.d.ts.map +1 -0
  29. package/lib/ai/init.js +424 -0
  30. package/lib/ai/init.js.map +1 -0
  31. package/lib/ai/output.d.ts +93 -0
  32. package/lib/ai/output.d.ts.map +1 -0
  33. package/lib/ai/output.js +176 -0
  34. package/lib/ai/output.js.map +1 -0
  35. package/lib/ai/providers/anthropic.d.ts +23 -0
  36. package/lib/ai/providers/anthropic.d.ts.map +1 -0
  37. package/lib/ai/providers/anthropic.js +322 -0
  38. package/lib/ai/providers/anthropic.js.map +1 -0
  39. package/lib/ai/providers/base.d.ts +43 -0
  40. package/lib/ai/providers/base.d.ts.map +1 -0
  41. package/lib/ai/providers/base.js +135 -0
  42. package/lib/ai/providers/base.js.map +1 -0
  43. package/lib/ai/providers/index.d.ts +12 -0
  44. package/lib/ai/providers/index.d.ts.map +1 -0
  45. package/lib/ai/providers/index.js +9 -0
  46. package/lib/ai/providers/index.js.map +1 -0
  47. package/lib/ai/providers/ollama.d.ts +25 -0
  48. package/lib/ai/providers/ollama.d.ts.map +1 -0
  49. package/lib/ai/providers/ollama.js +243 -0
  50. package/lib/ai/providers/ollama.js.map +1 -0
  51. package/lib/ai/providers/openai.d.ts +46 -0
  52. package/lib/ai/providers/openai.d.ts.map +1 -0
  53. package/lib/ai/providers/openai.js +132 -0
  54. package/lib/ai/providers/openai.js.map +1 -0
  55. package/lib/ai/rate-limiter.d.ts +38 -0
  56. package/lib/ai/rate-limiter.d.ts.map +1 -0
  57. package/lib/ai/rate-limiter.js +86 -0
  58. package/lib/ai/rate-limiter.js.map +1 -0
  59. package/lib/ai/service.d.ts +81 -0
  60. package/lib/ai/service.d.ts.map +1 -0
  61. package/lib/ai/service.js +274 -0
  62. package/lib/ai/service.js.map +1 -0
  63. package/lib/ai/session.d.ts +186 -0
  64. package/lib/ai/session.d.ts.map +1 -0
  65. package/lib/ai/session.js +443 -0
  66. package/lib/ai/session.js.map +1 -0
  67. package/lib/ai/tone-detector.d.ts +19 -0
  68. package/lib/ai/tone-detector.d.ts.map +1 -0
  69. package/lib/ai/tone-detector.js +72 -0
  70. package/lib/ai/tone-detector.js.map +1 -0
  71. package/lib/ai/tools.d.ts +45 -0
  72. package/lib/ai/tools.d.ts.map +1 -0
  73. package/lib/ai/tools.js +206 -0
  74. package/lib/ai/tools.js.map +1 -0
  75. package/lib/ai/types.d.ts +264 -0
  76. package/lib/ai/types.d.ts.map +1 -0
  77. package/lib/ai/types.js +6 -0
  78. package/lib/ai/types.js.map +1 -0
  79. package/lib/ai/user-profile.d.ts +56 -0
  80. package/lib/ai/user-profile.d.ts.map +1 -0
  81. package/lib/ai/user-profile.js +130 -0
  82. package/lib/ai/user-profile.js.map +1 -0
  83. package/lib/ai/zhin-agent.d.ts +165 -0
  84. package/lib/ai/zhin-agent.d.ts.map +1 -0
  85. package/lib/ai/zhin-agent.js +707 -0
  86. package/lib/ai/zhin-agent.js.map +1 -0
  87. package/lib/built/ai-trigger.d.ts.map +1 -1
  88. package/lib/built/ai-trigger.js +7 -3
  89. package/lib/built/ai-trigger.js.map +1 -1
  90. package/lib/built/command.d.ts +33 -17
  91. package/lib/built/command.d.ts.map +1 -1
  92. package/lib/built/command.js +71 -44
  93. package/lib/built/command.js.map +1 -1
  94. package/lib/built/component.d.ts +42 -15
  95. package/lib/built/component.d.ts.map +1 -1
  96. package/lib/built/component.js +84 -52
  97. package/lib/built/component.js.map +1 -1
  98. package/lib/built/config.d.ts +64 -5
  99. package/lib/built/config.d.ts.map +1 -1
  100. package/lib/built/config.js +129 -12
  101. package/lib/built/config.js.map +1 -1
  102. package/lib/built/cron.d.ts +41 -18
  103. package/lib/built/cron.d.ts.map +1 -1
  104. package/lib/built/cron.js +106 -63
  105. package/lib/built/cron.js.map +1 -1
  106. package/lib/built/database.d.ts +55 -6
  107. package/lib/built/database.d.ts.map +1 -1
  108. package/lib/built/database.js +93 -22
  109. package/lib/built/database.js.map +1 -1
  110. package/lib/built/dispatcher.d.ts +118 -0
  111. package/lib/built/dispatcher.d.ts.map +1 -0
  112. package/lib/built/dispatcher.js +196 -0
  113. package/lib/built/dispatcher.js.map +1 -0
  114. package/lib/built/permission.d.ts +45 -5
  115. package/lib/built/permission.d.ts.map +1 -1
  116. package/lib/built/permission.js +56 -11
  117. package/lib/built/permission.js.map +1 -1
  118. package/lib/built/skill.d.ts +117 -0
  119. package/lib/built/skill.d.ts.map +1 -0
  120. package/lib/built/skill.js +191 -0
  121. package/lib/built/skill.js.map +1 -0
  122. package/lib/built/tool.d.ts +71 -164
  123. package/lib/built/tool.d.ts.map +1 -1
  124. package/lib/built/tool.js +212 -297
  125. package/lib/built/tool.js.map +1 -1
  126. package/lib/feature.d.ts +75 -0
  127. package/lib/feature.d.ts.map +1 -0
  128. package/lib/feature.js +69 -0
  129. package/lib/feature.js.map +1 -0
  130. package/lib/index.d.ts +4 -0
  131. package/lib/index.d.ts.map +1 -1
  132. package/lib/index.js +7 -0
  133. package/lib/index.js.map +1 -1
  134. package/lib/plugin.d.ts +25 -17
  135. package/lib/plugin.d.ts.map +1 -1
  136. package/lib/plugin.js +180 -20
  137. package/lib/plugin.js.map +1 -1
  138. package/lib/types.d.ts +4 -9
  139. package/lib/types.d.ts.map +1 -1
  140. package/package.json +6 -6
  141. package/src/adapter.ts +101 -2
  142. package/src/ai/agent.ts +772 -0
  143. package/src/ai/context-manager.ts +440 -0
  144. package/src/ai/conversation-memory.ts +774 -0
  145. package/src/ai/follow-up.ts +357 -0
  146. package/src/ai/index.ts +128 -0
  147. package/src/ai/init.ts +502 -0
  148. package/src/ai/output.ts +261 -0
  149. package/src/ai/providers/anthropic.ts +375 -0
  150. package/src/ai/providers/base.ts +173 -0
  151. package/src/ai/providers/index.ts +13 -0
  152. package/src/ai/providers/ollama.ts +292 -0
  153. package/src/ai/providers/openai.ts +167 -0
  154. package/src/ai/rate-limiter.ts +129 -0
  155. package/src/ai/service.ts +319 -0
  156. package/src/ai/session.ts +544 -0
  157. package/src/ai/tone-detector.ts +89 -0
  158. package/src/ai/tools.ts +218 -0
  159. package/src/ai/types.ts +296 -0
  160. package/src/ai/user-profile.ts +181 -0
  161. package/src/ai/zhin-agent.ts +845 -0
  162. package/src/built/ai-trigger.ts +6 -3
  163. package/src/built/command.ts +75 -69
  164. package/src/built/component.ts +94 -76
  165. package/src/built/config.ts +288 -128
  166. package/src/built/cron.ts +117 -101
  167. package/src/built/database.ts +128 -33
  168. package/src/built/dispatcher.ts +332 -0
  169. package/src/built/permission.ts +146 -54
  170. package/src/built/skill.ts +280 -0
  171. package/src/built/tool.ts +245 -366
  172. package/src/feature.ts +113 -0
  173. package/src/index.ts +7 -0
  174. package/src/plugin.ts +198 -33
  175. package/src/types.ts +6 -10
  176. package/tests/adapter.test.ts +153 -1
  177. package/tests/ai/agent.test.ts +614 -0
  178. package/tests/ai/ai-trigger.test.ts +368 -0
  179. package/tests/ai/context-manager.test.ts +413 -0
  180. package/tests/ai/conversation-memory.test.ts +128 -0
  181. package/tests/ai/follow-up.test.ts +175 -0
  182. package/tests/ai/integration.test.ts +584 -0
  183. package/tests/ai/output.test.ts +128 -0
  184. package/tests/ai/providers.integration.test.ts +227 -0
  185. package/tests/ai/rate-limiter.test.ts +108 -0
  186. package/tests/ai/session.test.ts +375 -0
  187. package/tests/ai/setup.ts +308 -0
  188. package/tests/ai/tone-detector.test.ts +80 -0
  189. package/tests/ai/tool.test.ts +800 -0
  190. package/tests/ai/tools-builtin.test.ts +346 -0
  191. package/tests/ai/user-profile.test.ts +73 -0
  192. package/tests/ai/zhin-agent.test.ts +177 -0
  193. package/tests/config.test.ts +46 -0
  194. package/tests/cron.test.ts +94 -5
  195. package/tests/dispatcher.test.ts +146 -0
  196. package/tests/feature.test.ts +145 -0
  197. package/tests/features-builtin.test.ts +191 -0
  198. package/tests/plugin.test.ts +88 -14
  199. package/tests/skill-feature.test.ts +179 -0
  200. package/tests/tool-feature.test.ts +254 -0
  201. package/test/minimal-bot.ts +0 -31
  202. package/test/stress-test.ts +0 -123
@@ -0,0 +1,261 @@
1
+ /**
2
+ * @zhin.js/ai - Multi-modal Output Pipeline
3
+ *
4
+ * 统一输出类型系统:AI 回复 → OutputElement[] → Adapter 渲染
5
+ *
6
+ * 支持的输出类型:
7
+ * - text: 纯文本 / markdown
8
+ * - image: 图片 URL 或 base64
9
+ * - audio: 音频 URL 或 base64
10
+ * - video: 视频 URL
11
+ * - card: 结构化卡片 (标题 + 正文 + 字段 + 按钮)
12
+ * - file: 文件附件
13
+ */
14
+
15
+ // ============================================================================
16
+ // OutputElement 类型定义
17
+ // ============================================================================
18
+
19
+ export interface TextElement {
20
+ type: 'text';
21
+ content: string;
22
+ /** markdown / plain */
23
+ format?: 'markdown' | 'plain';
24
+ }
25
+
26
+ export interface ImageElement {
27
+ type: 'image';
28
+ url: string;
29
+ /** base64 数据 (url 不可用时的后备) */
30
+ base64?: string;
31
+ alt?: string;
32
+ width?: number;
33
+ height?: number;
34
+ }
35
+
36
+ export interface AudioElement {
37
+ type: 'audio';
38
+ url: string;
39
+ base64?: string;
40
+ duration?: number;
41
+ /** 语音转文字的后备文本 */
42
+ fallbackText?: string;
43
+ }
44
+
45
+ export interface VideoElement {
46
+ type: 'video';
47
+ url: string;
48
+ coverUrl?: string;
49
+ duration?: number;
50
+ fallbackText?: string;
51
+ }
52
+
53
+ export interface CardField {
54
+ label: string;
55
+ value: string;
56
+ inline?: boolean;
57
+ }
58
+
59
+ export interface CardButton {
60
+ text: string;
61
+ /** 点击后发送的命令 */
62
+ command?: string;
63
+ /** 点击后打开的 URL */
64
+ url?: string;
65
+ }
66
+
67
+ export interface CardElement {
68
+ type: 'card';
69
+ title: string;
70
+ description?: string;
71
+ fields?: CardField[];
72
+ imageUrl?: string;
73
+ buttons?: CardButton[];
74
+ color?: string;
75
+ }
76
+
77
+ export interface FileElement {
78
+ type: 'file';
79
+ url: string;
80
+ name: string;
81
+ size?: number;
82
+ mimeType?: string;
83
+ }
84
+
85
+ export type OutputElement =
86
+ | TextElement
87
+ | ImageElement
88
+ | AudioElement
89
+ | VideoElement
90
+ | CardElement
91
+ | FileElement;
92
+
93
+ // ============================================================================
94
+ // 输出解析器 — 将 AI 原始回复转换为 OutputElement[]
95
+ // ============================================================================
96
+
97
+ /**
98
+ * 从 AI 回复文本中解析出结构化的 OutputElement[]
99
+ *
100
+ * 识别规则:
101
+ * - ![alt](url) → ImageElement
102
+ * - [audio](url) → AudioElement
103
+ * - [video](url) → VideoElement
104
+ * - ```card ... ``` → CardElement (JSON)
105
+ * - [file:name](url) → FileElement
106
+ * - 其余文本 → TextElement
107
+ */
108
+ export function parseOutput(raw: string): OutputElement[] {
109
+ if (!raw || !raw.trim()) return [{ type: 'text', content: '', format: 'plain' }];
110
+
111
+ const elements: OutputElement[] = [];
112
+ let remaining = raw;
113
+
114
+ // eslint-disable-next-line no-constant-condition
115
+ while (remaining.length > 0) {
116
+ // ── 尝试匹配 card 代码块 ──
117
+ const cardMatch = remaining.match(/^([\s\S]*?)```card\s*\n([\s\S]*?)```/);
118
+ if (cardMatch) {
119
+ // 前缀文本
120
+ if (cardMatch[1].trim()) {
121
+ elements.push({ type: 'text', content: cardMatch[1].trim(), format: 'markdown' });
122
+ }
123
+ // 解析卡片 JSON
124
+ try {
125
+ const cardData = JSON.parse(cardMatch[2]);
126
+ elements.push({ ...cardData, type: 'card' });
127
+ } catch {
128
+ elements.push({ type: 'text', content: cardMatch[2], format: 'plain' });
129
+ }
130
+ remaining = remaining.slice(cardMatch[0].length);
131
+ continue;
132
+ }
133
+
134
+ // ── 尝试匹配 image: ![alt](url) ──
135
+ const imgMatch = remaining.match(/^([\s\S]*?)!\[([^\]]*)\]\(([^)]+)\)/);
136
+ if (imgMatch && imgMatch[1].length < 500) {
137
+ if (imgMatch[1].trim()) {
138
+ elements.push({ type: 'text', content: imgMatch[1].trim(), format: 'markdown' });
139
+ }
140
+ elements.push({ type: 'image', url: imgMatch[3], alt: imgMatch[2] || undefined });
141
+ remaining = remaining.slice(imgMatch[0].length);
142
+ continue;
143
+ }
144
+
145
+ // ── 尝试匹配 audio: [audio](url) ──
146
+ const audioMatch = remaining.match(/^([\s\S]*?)\[audio\]\(([^)]+)\)/i);
147
+ if (audioMatch && audioMatch[1].length < 500) {
148
+ if (audioMatch[1].trim()) {
149
+ elements.push({ type: 'text', content: audioMatch[1].trim(), format: 'markdown' });
150
+ }
151
+ elements.push({ type: 'audio', url: audioMatch[2] });
152
+ remaining = remaining.slice(audioMatch[0].length);
153
+ continue;
154
+ }
155
+
156
+ // ── 尝试匹配 video: [video](url) ──
157
+ const videoMatch = remaining.match(/^([\s\S]*?)\[video\]\(([^)]+)\)/i);
158
+ if (videoMatch && videoMatch[1].length < 500) {
159
+ if (videoMatch[1].trim()) {
160
+ elements.push({ type: 'text', content: videoMatch[1].trim(), format: 'markdown' });
161
+ }
162
+ elements.push({ type: 'video', url: videoMatch[2] });
163
+ remaining = remaining.slice(videoMatch[0].length);
164
+ continue;
165
+ }
166
+
167
+ // ── 尝试匹配 file: [file:name](url) ──
168
+ const fileMatch = remaining.match(/^([\s\S]*?)\[file:([^\]]+)\]\(([^)]+)\)/i);
169
+ if (fileMatch && fileMatch[1].length < 500) {
170
+ if (fileMatch[1].trim()) {
171
+ elements.push({ type: 'text', content: fileMatch[1].trim(), format: 'markdown' });
172
+ }
173
+ elements.push({ type: 'file', url: fileMatch[3], name: fileMatch[2] });
174
+ remaining = remaining.slice(fileMatch[0].length);
175
+ continue;
176
+ }
177
+
178
+ // ── 无匹配 → 全部作为文本 ──
179
+ elements.push({ type: 'text', content: remaining.trim(), format: 'markdown' });
180
+ break;
181
+ }
182
+
183
+ return elements.length > 0 ? elements : [{ type: 'text', content: raw, format: 'plain' }];
184
+ }
185
+
186
+ // ============================================================================
187
+ // OutputElement 渲染工具
188
+ // ============================================================================
189
+
190
+ /**
191
+ * 将 OutputElement[] 降级为纯文本(用于不支持富媒体的平台)
192
+ */
193
+ export function renderToPlainText(elements: OutputElement[]): string {
194
+ return elements.map(el => {
195
+ switch (el.type) {
196
+ case 'text':
197
+ return el.content;
198
+ case 'image':
199
+ return el.alt ? `[图片: ${el.alt}]` : `[图片: ${el.url}]`;
200
+ case 'audio':
201
+ return el.fallbackText || `[音频: ${el.url}]`;
202
+ case 'video':
203
+ return el.fallbackText || `[视频: ${el.url}]`;
204
+ case 'card': {
205
+ const parts = [el.title];
206
+ if (el.description) parts.push(el.description);
207
+ if (el.fields?.length) {
208
+ for (const f of el.fields) parts.push(`${f.label}: ${f.value}`);
209
+ }
210
+ return parts.join('\n');
211
+ }
212
+ case 'file':
213
+ return `[文件: ${el.name}] ${el.url}`;
214
+ default:
215
+ return '';
216
+ }
217
+ }).filter(Boolean).join('\n');
218
+ }
219
+
220
+ /**
221
+ * 将 OutputElement[] 渲染为 Satori 兼容的 HTML 片段
222
+ */
223
+ export function renderToSatori(elements: OutputElement[]): string {
224
+ return elements.map(el => {
225
+ switch (el.type) {
226
+ case 'text':
227
+ return `<p>${escapeHtml(el.content)}</p>`;
228
+ case 'image':
229
+ return `<img src="${escapeHtml(el.url)}"${el.alt ? ` alt="${escapeHtml(el.alt)}"` : ''}/>`;
230
+ case 'audio':
231
+ return el.fallbackText
232
+ ? `<p>${escapeHtml(el.fallbackText)}</p>`
233
+ : `<audio src="${escapeHtml(el.url)}"/>`;
234
+ case 'video':
235
+ return el.fallbackText
236
+ ? `<p>${escapeHtml(el.fallbackText)}</p>`
237
+ : `<video src="${escapeHtml(el.url)}"/>`;
238
+ case 'card': {
239
+ let html = `<div class="card">`;
240
+ html += `<h3>${escapeHtml(el.title)}</h3>`;
241
+ if (el.description) html += `<p>${escapeHtml(el.description)}</p>`;
242
+ if (el.imageUrl) html += `<img src="${escapeHtml(el.imageUrl)}"/>`;
243
+ if (el.fields?.length) {
244
+ for (const f of el.fields) {
245
+ html += `<p><b>${escapeHtml(f.label)}</b>: ${escapeHtml(f.value)}</p>`;
246
+ }
247
+ }
248
+ html += `</div>`;
249
+ return html;
250
+ }
251
+ case 'file':
252
+ return `<a href="${escapeHtml(el.url)}">${escapeHtml(el.name)}</a>`;
253
+ default:
254
+ return '';
255
+ }
256
+ }).filter(Boolean).join('\n');
257
+ }
258
+
259
+ function escapeHtml(s: string): string {
260
+ return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
261
+ }
@@ -0,0 +1,375 @@
1
+ /**
2
+ * @zhin.js/ai - Anthropic Provider
3
+ * 支持 Claude 系列模型
4
+ */
5
+
6
+ import { BaseProvider } from './base.js';
7
+ import type {
8
+ ProviderConfig,
9
+ ChatCompletionRequest,
10
+ ChatCompletionResponse,
11
+ ChatCompletionChunk,
12
+ ChatMessage,
13
+ ToolDefinition,
14
+ ContentPart,
15
+ } from '../types.js';
16
+
17
+ export interface AnthropicConfig extends ProviderConfig {
18
+ anthropicVersion?: string;
19
+ }
20
+
21
+ /**
22
+ * Anthropic API 格式转换
23
+ */
24
+ function toAnthropicMessages(messages: ChatMessage[]): {
25
+ system?: string;
26
+ messages: any[];
27
+ } {
28
+ let system: string | undefined;
29
+ const anthropicMessages: any[] = [];
30
+
31
+ for (const msg of messages) {
32
+ if (msg.role === 'system') {
33
+ system = typeof msg.content === 'string'
34
+ ? msg.content
35
+ : msg.content.map(p => p.type === 'text' ? p.text : '').join('');
36
+ continue;
37
+ }
38
+
39
+ if (msg.role === 'tool') {
40
+ anthropicMessages.push({
41
+ role: 'user',
42
+ content: [{
43
+ type: 'tool_result',
44
+ tool_use_id: msg.tool_call_id,
45
+ content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content),
46
+ }],
47
+ });
48
+ continue;
49
+ }
50
+
51
+ let content: any;
52
+ if (typeof msg.content === 'string') {
53
+ content = msg.content;
54
+ } else {
55
+ content = msg.content.map((part: ContentPart) => {
56
+ if (part.type === 'text') {
57
+ return { type: 'text', text: part.text };
58
+ }
59
+ if (part.type === 'image_url') {
60
+ // Anthropic 需要 base64 格式
61
+ const url = part.image_url.url;
62
+ if (url.startsWith('data:')) {
63
+ const [meta, data] = url.split(',');
64
+ // 使用更安全的正则表达式,避免 ReDoS
65
+ const mediaType = meta.match(/^data:([^;]+);/)?.[1] || 'image/png';
66
+ return {
67
+ type: 'image',
68
+ source: { type: 'base64', media_type: mediaType, data },
69
+ };
70
+ }
71
+ return {
72
+ type: 'image',
73
+ source: { type: 'url', url },
74
+ };
75
+ }
76
+ return { type: 'text', text: '' };
77
+ });
78
+ }
79
+
80
+ // 处理 tool_calls
81
+ if (msg.tool_calls?.length) {
82
+ const toolUseContent = msg.tool_calls.map(tc => ({
83
+ type: 'tool_use',
84
+ id: tc.id,
85
+ name: tc.function.name,
86
+ input: JSON.parse(tc.function.arguments),
87
+ }));
88
+
89
+ if (typeof content === 'string' && content) {
90
+ content = [{ type: 'text', text: content }, ...toolUseContent];
91
+ } else if (Array.isArray(content)) {
92
+ content = [...content, ...toolUseContent];
93
+ } else {
94
+ content = toolUseContent;
95
+ }
96
+ }
97
+
98
+ anthropicMessages.push({
99
+ role: msg.role === 'assistant' ? 'assistant' : 'user',
100
+ content,
101
+ });
102
+ }
103
+
104
+ return { system, messages: anthropicMessages };
105
+ }
106
+
107
+ /**
108
+ * 转换工具定义
109
+ */
110
+ function toAnthropicTools(tools?: ToolDefinition[]): any[] | undefined {
111
+ if (!tools?.length) return undefined;
112
+
113
+ return tools.map(tool => ({
114
+ name: tool.function.name,
115
+ description: tool.function.description,
116
+ input_schema: tool.function.parameters,
117
+ }));
118
+ }
119
+
120
+ /**
121
+ * 转换 Anthropic 响应为 OpenAI 格式
122
+ */
123
+ function fromAnthropicResponse(response: any): ChatCompletionResponse {
124
+ const content: ContentPart[] = [];
125
+ const toolCalls: any[] = [];
126
+
127
+ for (const block of response.content || []) {
128
+ if (block.type === 'text') {
129
+ content.push({ type: 'text', text: block.text });
130
+ } else if (block.type === 'tool_use') {
131
+ toolCalls.push({
132
+ id: block.id,
133
+ type: 'function',
134
+ function: {
135
+ name: block.name,
136
+ arguments: JSON.stringify(block.input),
137
+ },
138
+ });
139
+ }
140
+ }
141
+
142
+ const textContent = content
143
+ .filter(c => c.type === 'text')
144
+ .map(c => (c as { type: 'text'; text: string }).text)
145
+ .join('');
146
+
147
+ return {
148
+ id: response.id,
149
+ object: 'chat.completion',
150
+ created: Date.now(),
151
+ model: response.model,
152
+ choices: [{
153
+ index: 0,
154
+ message: {
155
+ role: 'assistant',
156
+ content: textContent,
157
+ tool_calls: toolCalls.length ? toolCalls : undefined,
158
+ },
159
+ finish_reason: response.stop_reason === 'tool_use' ? 'tool_calls' : 'stop',
160
+ }],
161
+ usage: {
162
+ prompt_tokens: response.usage?.input_tokens || 0,
163
+ completion_tokens: response.usage?.output_tokens || 0,
164
+ total_tokens: (response.usage?.input_tokens || 0) + (response.usage?.output_tokens || 0),
165
+ },
166
+ };
167
+ }
168
+
169
+ export class AnthropicProvider extends BaseProvider {
170
+ name = 'anthropic';
171
+ models = [
172
+ 'claude-opus-4-20250514',
173
+ 'claude-sonnet-4-20250514',
174
+ 'claude-3-7-sonnet-20250219',
175
+ 'claude-3-5-sonnet-20241022',
176
+ 'claude-3-5-haiku-20241022',
177
+ 'claude-3-opus-20240229',
178
+ 'claude-3-sonnet-20240229',
179
+ 'claude-3-haiku-20240307',
180
+ ];
181
+
182
+ private baseUrl: string;
183
+ private anthropicVersion: string;
184
+
185
+ constructor(config: AnthropicConfig = {}) {
186
+ super(config);
187
+ this.baseUrl = config.baseUrl || 'https://api.anthropic.com';
188
+ this.anthropicVersion = config.anthropicVersion || '2023-06-01';
189
+ }
190
+
191
+ protected async fetch<T>(url: string, options: RequestInit & { json?: any } = {}): Promise<T> {
192
+ const { json, ...fetchOptions } = options;
193
+
194
+ const headers: Record<string, string> = {
195
+ 'Content-Type': 'application/json',
196
+ 'x-api-key': this.config.apiKey || '',
197
+ 'anthropic-version': this.anthropicVersion,
198
+ ...this.config.headers,
199
+ ...(options.headers as Record<string, string>),
200
+ };
201
+
202
+ const controller = new AbortController();
203
+ const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
204
+
205
+ try {
206
+ const response = await globalThis.fetch(url, {
207
+ ...fetchOptions,
208
+ headers,
209
+ body: json ? JSON.stringify(json) : fetchOptions.body,
210
+ signal: controller.signal,
211
+ });
212
+
213
+ if (!response.ok) {
214
+ const error = await response.text();
215
+ throw new Error(`Anthropic API Error (${response.status}): ${error}`);
216
+ }
217
+
218
+ return response.json() as Promise<T>;
219
+ } finally {
220
+ clearTimeout(timeoutId);
221
+ }
222
+ }
223
+
224
+ async chat(request: ChatCompletionRequest): Promise<ChatCompletionResponse> {
225
+ const { system, messages } = toAnthropicMessages(request.messages);
226
+
227
+ const anthropicRequest: any = {
228
+ model: request.model,
229
+ messages,
230
+ max_tokens: request.max_tokens || 4096,
231
+ };
232
+
233
+ if (system) {
234
+ anthropicRequest.system = system;
235
+ }
236
+
237
+ if (request.temperature !== undefined) {
238
+ anthropicRequest.temperature = request.temperature;
239
+ }
240
+
241
+ if (request.top_p !== undefined) {
242
+ anthropicRequest.top_p = request.top_p;
243
+ }
244
+
245
+ if (request.stop) {
246
+ anthropicRequest.stop_sequences = Array.isArray(request.stop) ? request.stop : [request.stop];
247
+ }
248
+
249
+ const tools = toAnthropicTools(request.tools);
250
+ if (tools) {
251
+ anthropicRequest.tools = tools;
252
+ }
253
+
254
+ const response = await this.fetch<any>(`${this.baseUrl}/v1/messages`, {
255
+ method: 'POST',
256
+ json: anthropicRequest,
257
+ });
258
+
259
+ return fromAnthropicResponse(response);
260
+ }
261
+
262
+ async *chatStream(request: ChatCompletionRequest): AsyncIterable<ChatCompletionChunk> {
263
+ const { system, messages } = toAnthropicMessages(request.messages);
264
+
265
+ const anthropicRequest: any = {
266
+ model: request.model,
267
+ messages,
268
+ max_tokens: request.max_tokens || 4096,
269
+ stream: true,
270
+ };
271
+
272
+ if (system) {
273
+ anthropicRequest.system = system;
274
+ }
275
+
276
+ if (request.temperature !== undefined) {
277
+ anthropicRequest.temperature = request.temperature;
278
+ }
279
+
280
+ const tools = toAnthropicTools(request.tools);
281
+ if (tools) {
282
+ anthropicRequest.tools = tools;
283
+ }
284
+
285
+ const headers: Record<string, string> = {
286
+ 'Content-Type': 'application/json',
287
+ 'x-api-key': this.config.apiKey || '',
288
+ 'anthropic-version': this.anthropicVersion,
289
+ };
290
+
291
+ const response = await globalThis.fetch(`${this.baseUrl}/v1/messages`, {
292
+ method: 'POST',
293
+ headers,
294
+ body: JSON.stringify(anthropicRequest),
295
+ });
296
+
297
+ if (!response.ok) {
298
+ const error = await response.text();
299
+ throw new Error(`Anthropic API Error (${response.status}): ${error}`);
300
+ }
301
+
302
+ if (!response.body) {
303
+ throw new Error('Response body is empty');
304
+ }
305
+
306
+ const reader = response.body.getReader();
307
+ const decoder = new TextDecoder();
308
+ let buffer = '';
309
+ let messageId = '';
310
+ let model = request.model;
311
+
312
+ while (true) {
313
+ const { done, value } = await reader.read();
314
+ if (done) break;
315
+
316
+ buffer += decoder.decode(value, { stream: true });
317
+ const lines = buffer.split('\n');
318
+ buffer = lines.pop() || '';
319
+
320
+ for (const line of lines) {
321
+ const trimmed = line.trim();
322
+ if (trimmed.startsWith('data: ')) {
323
+ const data = trimmed.slice(6);
324
+ if (data === '[DONE]') continue;
325
+
326
+ try {
327
+ const event = JSON.parse(data);
328
+
329
+ if (event.type === 'message_start') {
330
+ messageId = event.message?.id || '';
331
+ model = event.message?.model || model;
332
+ } else if (event.type === 'content_block_delta') {
333
+ if (event.delta?.type === 'text_delta') {
334
+ yield {
335
+ id: messageId,
336
+ object: 'chat.completion.chunk',
337
+ created: Date.now(),
338
+ model,
339
+ choices: [{
340
+ index: 0,
341
+ delta: { content: event.delta.text },
342
+ finish_reason: null,
343
+ }],
344
+ };
345
+ }
346
+ } else if (event.type === 'message_delta') {
347
+ yield {
348
+ id: messageId,
349
+ object: 'chat.completion.chunk',
350
+ created: Date.now(),
351
+ model,
352
+ choices: [{
353
+ index: 0,
354
+ delta: {},
355
+ finish_reason: event.delta?.stop_reason === 'tool_use' ? 'tool_calls' : 'stop',
356
+ }],
357
+ usage: event.usage ? {
358
+ prompt_tokens: event.usage.input_tokens || 0,
359
+ completion_tokens: event.usage.output_tokens || 0,
360
+ total_tokens: (event.usage.input_tokens || 0) + (event.usage.output_tokens || 0),
361
+ } : undefined,
362
+ };
363
+ }
364
+ } catch {
365
+ // 忽略解析错误
366
+ }
367
+ }
368
+ }
369
+ }
370
+ }
371
+
372
+ async listModels(): Promise<string[]> {
373
+ return this.models;
374
+ }
375
+ }