@huyooo/ai-chat-core 0.2.19 → 0.2.21

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/src/types.ts ADDED
@@ -0,0 +1,531 @@
1
+ /**
2
+ * AI Chat Core 类型定义
3
+ */
4
+
5
+ import type { McpServerConfig } from './mcp/types';
6
+ import {
7
+ getVisibleModels,
8
+ getModelFamily,
9
+ getModelEntry,
10
+ modelSupportsThinking,
11
+ modelSupportsVision,
12
+ modelSupportsNativeSearch,
13
+ getModelSearchStrategy,
14
+ type ModelRegistryEntry,
15
+ } from './providers/model-registry';
16
+ import type { ProviderType } from './router';
17
+
18
+ // ==================== 模式与模型 ====================
19
+
20
+ /** 对话模式 */
21
+ export type ChatMode = 'agent' | 'ask';
22
+
23
+ // 重新导出 router 和 Model Registry 类型
24
+ export type { ProviderType };
25
+ export type { ModelFamilyId, ModelFamilyConfig, ModelRegistryEntry } from './providers/model-registry';
26
+ export { getModelFamily, getModelEntry, modelSupportsThinking, modelSupportsNativeSearch, getModelSearchStrategy };
27
+
28
+ /** 模型选项(前端显示用) */
29
+ export interface ModelOption {
30
+ /** 模型 ID(发送给 API) */
31
+ modelId: string;
32
+ /** 显示名称 */
33
+ displayName: string;
34
+ /** 是否支持深度思考 */
35
+ supportsThinking: boolean;
36
+ /** 是否支持图片理解 */
37
+ supportsVision: boolean;
38
+ /** 用于模型项 hover 的特性描述(由后端定义,前端只渲染) */
39
+ tooltip?: {
40
+ features?: string[];
41
+ /** 开销信息(数组,分行显示) */
42
+ cost?: string[];
43
+ description?: string;
44
+ };
45
+ }
46
+
47
+ /** 生成模型 tooltip 的 features 列表 */
48
+ function buildModelFeatures(entry: ModelRegistryEntry): string[] {
49
+ const family = getModelFamily(entry.id);
50
+ const supportsVision = modelSupportsVision(entry.id);
51
+ const supportsThinking = family?.supportsThinking ?? false;
52
+ const supportsNativeSearch = family?.supportsNativeSearch ?? false;
53
+
54
+ const features: string[] = [];
55
+ // 顺序:多模态 > 深度思考 > 长上下文 > 联网搜索
56
+ if (supportsVision) features.push('多模态');
57
+ if (supportsThinking) features.push('深度思考');
58
+ if (entry.contextWindow) features.push(`长上下文(${entry.contextWindow})`);
59
+ if (supportsNativeSearch) features.push('联网搜索');
60
+
61
+ return features;
62
+ }
63
+
64
+ /**
65
+ * 预定义模型列表
66
+ */
67
+ export const MODELS: ModelOption[] = getVisibleModels().map(entry => {
68
+ const family = getModelFamily(entry.id);
69
+ const supportsThinking = family?.supportsThinking ?? false;
70
+ const supportsVision = modelSupportsVision(entry.id);
71
+
72
+ return {
73
+ modelId: entry.id,
74
+ displayName: entry.displayName,
75
+ supportsThinking,
76
+ supportsVision,
77
+ tooltip: {
78
+ features: buildModelFeatures(entry),
79
+ cost: entry.pricing,
80
+ },
81
+ };
82
+ });
83
+
84
+ /** 根据 modelId 获取模型 */
85
+ export function getModelByModelId(modelId: string): ModelOption | undefined {
86
+ return MODELS.find(m => m.modelId === modelId);
87
+ }
88
+
89
+ // ==================== 配置 ====================
90
+
91
+ /** 工具批准回调函数 */
92
+ export type ToolApprovalCallback = (toolCall: {
93
+ id: string;
94
+ name: string;
95
+ args: Record<string, unknown>;
96
+ }) => Promise<boolean>;
97
+
98
+ /** Agent 配置 */
99
+ export interface AgentConfig {
100
+ /** 豆包/火山引擎 API Key (用于豆包和 DeepSeek) */
101
+ arkApiKey: string;
102
+ /** 豆包 API URL */
103
+ arkApiUrl?: string;
104
+ /** 通义千问 API Key */
105
+ qwenApiKey?: string;
106
+ /** 通义千问 API URL */
107
+ qwenApiUrl?: string;
108
+ /** OpenRouter API Key */
109
+ openrouterApiKey?: string;
110
+ /** OpenRouter API URL */
111
+ openrouterApiUrl?: string;
112
+ /** Vercel API Key(用于 Vercel AI Gateway 访问 Claude) */
113
+ vercelApiKey?: string;
114
+ /** Tavily API Key(用于统一 Web Search) */
115
+ tavilyApiKey?: string;
116
+ /** Gemini API Key (用于图片/视频,注意:中国大陆无法直接访问) */
117
+ geminiApiKey: string;
118
+ /** 当前工作目录(Current Working Directory) */
119
+ cwd?: string;
120
+ /**
121
+ * 工具列表(Vite 插件风格)
122
+ *
123
+ * 支持多种形式:
124
+ * - 单个工具:getCwdTool
125
+ * - 工具插件:searchPlugin({ dataDir, workspace })
126
+ * - Promise:await asyncPlugin()
127
+ *
128
+ * @example
129
+ * ```typescript
130
+ * tools: [
131
+ * getCwdTool,
132
+ * executeCommandTool,
133
+ * searchPlugin({ dataDir: '/path', workspace: '/project' }),
134
+ * ]
135
+ * ```
136
+ */
137
+ tools?: ToolConfigItem[];
138
+ /**
139
+ * 工具批准回调(manual 模式使用)
140
+ * 返回 true 表示批准执行,false 表示跳过
141
+ */
142
+ onToolApprovalRequest?: ToolApprovalCallback;
143
+ /**
144
+ * 动态获取自动运行配置回调
145
+ * 每次检查工具批准时调用,获取最新配置
146
+ */
147
+ getAutoRunConfig?: () => Promise<AutoRunConfig | undefined>;
148
+ /**
149
+ * MCP Server 配置列表
150
+ *
151
+ * 初始化时自动连接所有 MCP Server,发现工具并注册到 Agent。
152
+ *
153
+ * @example
154
+ * ```typescript
155
+ * mcpServers: [
156
+ * { name: 'filesystem', transport: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-filesystem', '/Users/me'] },
157
+ * { name: 'github', transport: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-github'], env: { GITHUB_TOKEN: '...' } },
158
+ * { name: 'remote-db', transport: 'sse', url: 'http://localhost:3001/mcp' },
159
+ * ]
160
+ * ```
161
+ */
162
+ mcpServers?: McpServerConfig[];
163
+ }
164
+
165
+ /** 深度思考模式 */
166
+ export type ThinkingMode = 'enabled' | 'disabled';
167
+
168
+ /** 自动运行模式 */
169
+ export type AutoRunMode = 'run-everything' | 'manual';
170
+
171
+ /** 自动运行配置 */
172
+ export interface AutoRunConfig {
173
+ /**
174
+ * 自动运行模式
175
+ * - 'run-everything': 运行所有内容(自动执行)
176
+ * - 'manual': 手动批准(每次执行前询问)
177
+ */
178
+ mode?: AutoRunMode;
179
+ }
180
+
181
+ /** 聊天消息格式(用于传递历史) */
182
+ export interface ChatHistoryMessage {
183
+ role: 'user' | 'assistant' | 'system' | 'tool';
184
+ content: string;
185
+ }
186
+
187
+ /**
188
+ * 用户工具定义(透传模式)
189
+ *
190
+ * 只包含 schema,不包含 execute 函数
191
+ * 由客户端执行,服务端透传 tool_call_request 事件
192
+ */
193
+ export interface UserToolDefinition {
194
+ /** 工具名称 */
195
+ name: string;
196
+ /** 工具描述(供 AI 理解) */
197
+ description: string;
198
+ /** 参数定义(JSON Schema,支持嵌套) */
199
+ parameters: JsonSchemaObject;
200
+ }
201
+
202
+ /** 聊天配置(每次聊天可变的选项) */
203
+ export interface ChatOptions {
204
+ /** 对话模式 */
205
+ mode?: ChatMode;
206
+ /** 使用的模型 */
207
+ model?: string;
208
+ /** 模型提供商 */
209
+ provider?: ProviderType;
210
+ /** 是否启用联网搜索 */
211
+ enableWebSearch?: boolean;
212
+ /**
213
+ * 深度思考模式
214
+ * - 'enabled': 强制开启深度思考
215
+ * - 'disabled': 强制关闭深度思考
216
+ */
217
+ thinkingMode?: ThinkingMode;
218
+ /** 启用的工具名称列表(agent 模式有效,ask 模式强制禁用) */
219
+ enabledTools?: string[];
220
+ /** 自动运行配置 */
221
+ autoRunConfig?: AutoRunConfig;
222
+ /** 对话历史(从数据库加载,无状态架构) */
223
+ history?: ChatHistoryMessage[];
224
+ /**
225
+ * 用户自定义工具(透传模式)
226
+ *
227
+ * 这些工具不在服务端执行,而是:
228
+ * 1. 传递给 AI 模型
229
+ * 2. AI 返回 tool_call 时,发送 tool_call_request 事件给客户端
230
+ * 3. 客户端执行后,发新请求继续对话
231
+ */
232
+ userTools?: UserToolDefinition[];
233
+ /**
234
+ * 上次请求的计划快照(可选,仅用于断点续传)
235
+ *
236
+ * 正常流程不需要传:AI 会在单次请求内自主创建和管理计划。
237
+ *
238
+ * 仅在请求被中断后需要恢复时使用:
239
+ * - 客户端工具透传模式(请求中断,等客户端执行后重新发请求)
240
+ * - 用户 abort 后想继续
241
+ */
242
+ /**
243
+ * Skills 内容(启用的 Skills 的 content 数组,由前端/服务端拼好传入)
244
+ *
245
+ * 自动注入到 system prompt 的【用户指令】段落。
246
+ * 前端合并「默认启用的 skills + @ 手动选的 skills」后传入。
247
+ */
248
+ skillContents?: string[];
249
+ // 注意:每个 provider 内部使用自己的最优参数
250
+ // - ARK: reasoning.effort = 'high'
251
+ // - Qwen: thinking_budget = 38400
252
+ // - Gemini: thinkingLevel = 'high'
253
+ // 外部只需关心 thinkingMode 开关即可
254
+ }
255
+
256
+ // ==================== 工具相关 ====================
257
+
258
+ /**
259
+ * 副作用定义(参考 REST API 规范)
260
+ *
261
+ * @example
262
+ * // 通知
263
+ * { type: 'notification', success: true, message: '图片生成完成' }
264
+ *
265
+ * // 文件系统变更
266
+ * { type: 'filesystem', success: true, data: { paths: ['/path/to/file'] } }
267
+ *
268
+ * // 失败
269
+ * { type: 'notification', success: false, message: 'API 超时' }
270
+ */
271
+ export interface SideEffect {
272
+ /** 副作用类型 */
273
+ type: string;
274
+ /** 是否成功 */
275
+ success: boolean;
276
+ /** 附加数据 */
277
+ data?: unknown;
278
+ /** 消息(成功时为提示,失败时为错误信息) */
279
+ message?: string;
280
+ }
281
+
282
+ /**
283
+ * 工具执行结果(支持动态副作用)
284
+ *
285
+ * 工具可以返回此类型来动态声明副作用
286
+ */
287
+ export interface ToolResult {
288
+ /** 执行结果(字符串) */
289
+ result: string;
290
+ /** 动态副作用(覆盖静态声明) */
291
+ sideEffects?: SideEffect[];
292
+ }
293
+
294
+ /** 工具执行上下文 */
295
+ export interface ToolContext {
296
+ /** 当前工作目录(Current Working Directory) */
297
+ cwd: string;
298
+ /** Gemini 客户端(供多模态工具使用) */
299
+ geminiClient?: unknown;
300
+ /** 执行 Shell 命令(内置方法) */
301
+ executeCommand: (
302
+ command: string,
303
+ cwd?: string
304
+ ) => Promise<{ success: boolean; output?: string; error?: string }>;
305
+ /** 中断信号(用于取消长时间操作) */
306
+ signal?: AbortSignal;
307
+ }
308
+
309
+ /** JSON Schema 属性定义(支持嵌套,符合 JSON Schema 规范) */
310
+ export interface JsonSchemaProperty {
311
+ type?: string;
312
+ description?: string;
313
+ enum?: unknown[];
314
+ items?: JsonSchemaProperty;
315
+ properties?: Record<string, JsonSchemaProperty>;
316
+ required?: string[];
317
+ default?: unknown;
318
+ }
319
+
320
+ /** JSON Schema 对象类型(工具参数的根类型) */
321
+ export interface JsonSchemaObject {
322
+ type: 'object';
323
+ properties: Record<string, JsonSchemaProperty>;
324
+ required?: string[];
325
+ description?: string;
326
+ }
327
+
328
+ /**
329
+ * 工具接口(用户可实现此接口创建自定义工具)
330
+ *
331
+ * 示例:
332
+ * ```typescript
333
+ * const myTool: Tool = {
334
+ * name: 'my_tool',
335
+ * description: '我的自定义工具',
336
+ * parameters: { type: 'object', properties: {}, required: [] },
337
+ * sideEffects: ['filesystem'], // 声明副作用
338
+ * resultType: 'my_result', // 结果类型(用于前端渲染)
339
+ * execute: async (args, ctx) => '执行结果'
340
+ * };
341
+ * ```
342
+ */
343
+ export interface Tool {
344
+ /** 工具名称 */
345
+ name: string;
346
+ /** 工具描述(供 AI 理解) */
347
+ description: string;
348
+ /** 参数定义(JSON Schema,支持嵌套对象和数组) */
349
+ parameters: JsonSchemaObject;
350
+ /**
351
+ * 静态副作用声明(可选)
352
+ *
353
+ * 用于通知前端该工具执行后可能产生的副作用
354
+ *
355
+ * @example
356
+ * sideEffects: [{ type: 'notification', message: '图片生成完成', level: 'success' }]
357
+ *
358
+ * 注意:如果 execute 返回 ToolResult 带有 sideEffects,会覆盖此静态声明
359
+ */
360
+ sideEffects?: SideEffect[];
361
+ /**
362
+ * 结果类型(用于前端渲染)
363
+ *
364
+ * 当工具执行成功时,前端会根据此类型生成对应的 ContentPart
365
+ * 例如:resultType: 'weather' 会生成 { type: 'weather', ...result }
366
+ *
367
+ * 如果不指定,工具结果仅显示在 ToolCallPart 的折叠面板中
368
+ */
369
+ resultType?: string;
370
+ /**
371
+ * 执行函数
372
+ *
373
+ * 返回值:
374
+ * - string: 简单返回结果,使用静态 sideEffects
375
+ * - ToolResult: 带动态副作用的结果,覆盖静态 sideEffects
376
+ */
377
+ execute: (args: Record<string, unknown>, context: ToolContext) => Promise<string | ToolResult>;
378
+ }
379
+
380
+ /** 工具执行器接口(可由使用方实现,用于自定义命令执行环境) */
381
+ export interface ToolExecutor {
382
+ /** 执行 Shell 命令 */
383
+ executeCommand(
384
+ command: string,
385
+ cwd?: string,
386
+ signal?: AbortSignal,
387
+ hooks?: {
388
+ onStdout?: (chunk: string) => void;
389
+ onStderr?: (chunk: string) => void;
390
+ }
391
+ ): Promise<{ success: boolean; output?: string; error?: string }>;
392
+ }
393
+
394
+ // ==================== 工具插件(Vite 插件风格) ====================
395
+
396
+ /**
397
+ * 工具插件实例
398
+ *
399
+ * 由 createXxxPlugin() 返回,可直接在 tools 数组中展开使用
400
+ */
401
+ export interface ToolPlugin {
402
+ /** 插件提供的工具数组 */
403
+ tools: Tool[];
404
+ /** 插件控制方法(可选,由各插件自定义) */
405
+ [key: string]: unknown;
406
+ }
407
+
408
+ /**
409
+ * 创建单个工具插件(辅助函数)
410
+ *
411
+ * @example
412
+ * ```typescript
413
+ * export function getCwdTool(): ToolPlugin {
414
+ * return tool({
415
+ * name: 'get_cwd',
416
+ * description: '...',
417
+ * parameters: { ... },
418
+ * execute: async (args, context) => { ... }
419
+ * });
420
+ * }
421
+ * ```
422
+ */
423
+ export function tool(t: Tool): ToolPlugin {
424
+ return { tools: [t] };
425
+ }
426
+
427
+ /**
428
+ * 创建多个工具插件(辅助函数)
429
+ *
430
+ * @example
431
+ * ```typescript
432
+ * export function myPlugin(): ToolPlugin {
433
+ * return tools([
434
+ * { name: 'tool1', ... },
435
+ * { name: 'tool2', ... },
436
+ * ]);
437
+ * }
438
+ * ```
439
+ */
440
+ export function tools(ts: Tool[]): ToolPlugin {
441
+ return { tools: ts };
442
+ }
443
+
444
+ /**
445
+ * 工具配置项(Vite 插件风格,只支持插件形式)
446
+ *
447
+ * @example
448
+ * ```typescript
449
+ * tools: [
450
+ * getCwdTool(), // 工具插件(函数调用)
451
+ * searchPlugin({ dataDir, workspace }), // 异步插件(返回 Promise<ToolPlugin>)
452
+ * ]
453
+ * ```
454
+ */
455
+ export type ToolConfigItem = Tool | ToolPlugin | Promise<Tool | ToolPlugin>;
456
+
457
+ /**
458
+ * 解析工具配置,统一转换为 Tool[]
459
+ *
460
+ * 支持三种形式:
461
+ * - Tool: 单个工具(直接使用)
462
+ * - ToolPlugin: 工具插件(展开 .tools 数组)
463
+ * - Promise<Tool | ToolPlugin>: 异步工具/插件
464
+ */
465
+ export async function resolveTools(items: ToolConfigItem[]): Promise<Tool[]> {
466
+ const tools: Tool[] = [];
467
+
468
+ for (const item of items) {
469
+ const resolved = await item;
470
+
471
+ if ('tools' in resolved && Array.isArray(resolved.tools)) {
472
+ // ToolPlugin
473
+ tools.push(...resolved.tools);
474
+ } else if ('name' in resolved && 'execute' in resolved) {
475
+ // Tool
476
+ tools.push(resolved as Tool);
477
+ } else {
478
+ console.warn('无法识别的工具配置项:', resolved);
479
+ }
480
+ }
481
+
482
+ return tools;
483
+ }
484
+
485
+ /** 工具定义(OpenAI 格式,内部使用) */
486
+ export interface ToolDefinition {
487
+ type: 'function';
488
+ function: {
489
+ name: string;
490
+ description: string;
491
+ parameters: JsonSchemaObject;
492
+ };
493
+ }
494
+
495
+ // ==================== 消息相关 ====================
496
+
497
+ /** 聊天消息(内部格式) */
498
+ export interface ChatMessage {
499
+ /** 消息 ID */
500
+ id?: string;
501
+ role: 'system' | 'user' | 'assistant' | 'tool';
502
+ content: string;
503
+ /** 深度思考内容 */
504
+ reasoning_content?: string;
505
+ tool_call_id?: string;
506
+ tool_calls?: Array<{
507
+ id: string;
508
+ type: 'function';
509
+ function: {
510
+ name: string;
511
+ arguments: string;
512
+ };
513
+ /** Gemini 模型需要的 thought_signature(用于工具调用循环) */
514
+ thought_signature?: string;
515
+ }>;
516
+ }
517
+
518
+ // ==================== API 特定类型 ====================
519
+
520
+ /** 火山引擎 Responses API 工具定义 */
521
+ export interface ResponsesApiTool {
522
+ type: 'function' | 'web_search';
523
+ // Function 类型
524
+ name?: string;
525
+ description?: string;
526
+ parameters?: Record<string, unknown>;
527
+ // Web Search 类型
528
+ max_keyword?: number;
529
+ limit?: number;
530
+ sources?: string[];
531
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,86 @@
1
+ /**
2
+ * 调试日志工具
3
+ *
4
+ * 通过 DebugLogger.enable(true) 全局开启,
5
+ * 通过 DebugLogger.setLogFile(path) 同时写入 JSONL 文件,
6
+ * 各模块通过 DebugLogger.module('Name') 创建带前缀的 logger。
7
+ */
8
+
9
+ import { appendFileSync } from 'node:fs';
10
+
11
+ let enabled = false;
12
+ let logFilePath: string | null = null;
13
+
14
+ export class DebugLogger {
15
+ private prefix: string;
16
+
17
+ private constructor(prefix: string) {
18
+ this.prefix = prefix;
19
+ }
20
+
21
+ /** 全局启停调试日志 */
22
+ static enable(value: boolean): void {
23
+ enabled = value;
24
+ }
25
+
26
+ /** 查询是否已启用 */
27
+ static isEnabled(): boolean {
28
+ return enabled;
29
+ }
30
+
31
+ /** 设置日志文件路径(JSONL 格式,追加写入) */
32
+ static setLogFile(filePath: string): void {
33
+ logFilePath = filePath;
34
+ }
35
+
36
+ /** 创建模块专用 logger */
37
+ static module(name: string): DebugLogger {
38
+ return new DebugLogger(`[${name}]`);
39
+ }
40
+
41
+ debug(...args: unknown[]): void {
42
+ if (enabled) {
43
+ console.debug(this.prefix, ...args);
44
+ this.writeToFile('debug', args);
45
+ }
46
+ }
47
+
48
+ info(...args: unknown[]): void {
49
+ if (enabled) {
50
+ console.info(this.prefix, ...args);
51
+ this.writeToFile('info', args);
52
+ }
53
+ }
54
+
55
+ warn(...args: unknown[]): void {
56
+ if (enabled) {
57
+ console.warn(this.prefix, ...args);
58
+ this.writeToFile('warn', args);
59
+ }
60
+ }
61
+
62
+ error(...args: unknown[]): void {
63
+ if (enabled) {
64
+ console.error(this.prefix, ...args);
65
+ this.writeToFile('error', args);
66
+ }
67
+ }
68
+
69
+ private writeToFile(level: string, args: unknown[]): void {
70
+ if (!logFilePath) return;
71
+ try {
72
+ const entry = JSON.stringify({
73
+ t: Date.now(),
74
+ level,
75
+ module: this.prefix,
76
+ args: args.map(a => {
77
+ try { return typeof a === 'string' ? a : JSON.parse(JSON.stringify(a)); }
78
+ catch { return String(a); }
79
+ }),
80
+ });
81
+ appendFileSync(logFilePath, entry + '\n');
82
+ } catch {
83
+ // 写文件失败不影响主流程
84
+ }
85
+ }
86
+ }