@zhin.js/core 1.0.24 → 1.0.26
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/CHANGELOG.md +22 -0
- package/README.md +84 -342
- package/lib/adapter.d.ts +45 -1
- package/lib/adapter.d.ts.map +1 -1
- package/lib/adapter.js +182 -1
- package/lib/adapter.js.map +1 -1
- package/lib/ai/agent.d.ts +126 -0
- package/lib/ai/agent.d.ts.map +1 -0
- package/lib/ai/agent.js +645 -0
- package/lib/ai/agent.js.map +1 -0
- package/lib/ai/context-manager.d.ts +213 -0
- package/lib/ai/context-manager.d.ts.map +1 -0
- package/lib/ai/context-manager.js +313 -0
- package/lib/ai/context-manager.js.map +1 -0
- package/lib/ai/conversation-memory.d.ts +181 -0
- package/lib/ai/conversation-memory.d.ts.map +1 -0
- package/lib/ai/conversation-memory.js +581 -0
- package/lib/ai/conversation-memory.js.map +1 -0
- package/lib/ai/follow-up.d.ts +131 -0
- package/lib/ai/follow-up.d.ts.map +1 -0
- package/lib/ai/follow-up.js +265 -0
- package/lib/ai/follow-up.js.map +1 -0
- package/lib/ai/index.d.ts +29 -0
- package/lib/ai/index.d.ts.map +1 -0
- package/lib/ai/index.js +34 -0
- package/lib/ai/index.js.map +1 -0
- package/lib/ai/init.d.ts +30 -0
- package/lib/ai/init.d.ts.map +1 -0
- package/lib/ai/init.js +424 -0
- package/lib/ai/init.js.map +1 -0
- package/lib/ai/output.d.ts +93 -0
- package/lib/ai/output.d.ts.map +1 -0
- package/lib/ai/output.js +176 -0
- package/lib/ai/output.js.map +1 -0
- package/lib/ai/providers/anthropic.d.ts +23 -0
- package/lib/ai/providers/anthropic.d.ts.map +1 -0
- package/lib/ai/providers/anthropic.js +322 -0
- package/lib/ai/providers/anthropic.js.map +1 -0
- package/lib/ai/providers/base.d.ts +43 -0
- package/lib/ai/providers/base.d.ts.map +1 -0
- package/lib/ai/providers/base.js +135 -0
- package/lib/ai/providers/base.js.map +1 -0
- package/lib/ai/providers/index.d.ts +12 -0
- package/lib/ai/providers/index.d.ts.map +1 -0
- package/lib/ai/providers/index.js +9 -0
- package/lib/ai/providers/index.js.map +1 -0
- package/lib/ai/providers/ollama.d.ts +25 -0
- package/lib/ai/providers/ollama.d.ts.map +1 -0
- package/lib/ai/providers/ollama.js +243 -0
- package/lib/ai/providers/ollama.js.map +1 -0
- package/lib/ai/providers/openai.d.ts +46 -0
- package/lib/ai/providers/openai.d.ts.map +1 -0
- package/lib/ai/providers/openai.js +132 -0
- package/lib/ai/providers/openai.js.map +1 -0
- package/lib/ai/rate-limiter.d.ts +38 -0
- package/lib/ai/rate-limiter.d.ts.map +1 -0
- package/lib/ai/rate-limiter.js +86 -0
- package/lib/ai/rate-limiter.js.map +1 -0
- package/lib/ai/service.d.ts +81 -0
- package/lib/ai/service.d.ts.map +1 -0
- package/lib/ai/service.js +274 -0
- package/lib/ai/service.js.map +1 -0
- package/lib/ai/session.d.ts +186 -0
- package/lib/ai/session.d.ts.map +1 -0
- package/lib/ai/session.js +443 -0
- package/lib/ai/session.js.map +1 -0
- package/lib/ai/tone-detector.d.ts +19 -0
- package/lib/ai/tone-detector.d.ts.map +1 -0
- package/lib/ai/tone-detector.js +72 -0
- package/lib/ai/tone-detector.js.map +1 -0
- package/lib/ai/tools.d.ts +45 -0
- package/lib/ai/tools.d.ts.map +1 -0
- package/lib/ai/tools.js +206 -0
- package/lib/ai/tools.js.map +1 -0
- package/lib/ai/types.d.ts +264 -0
- package/lib/ai/types.d.ts.map +1 -0
- package/lib/ai/types.js +6 -0
- package/lib/ai/types.js.map +1 -0
- package/lib/ai/user-profile.d.ts +56 -0
- package/lib/ai/user-profile.d.ts.map +1 -0
- package/lib/ai/user-profile.js +130 -0
- package/lib/ai/user-profile.js.map +1 -0
- package/lib/ai/zhin-agent.d.ts +165 -0
- package/lib/ai/zhin-agent.d.ts.map +1 -0
- package/lib/ai/zhin-agent.js +707 -0
- package/lib/ai/zhin-agent.js.map +1 -0
- package/lib/built/adapter-process.d.ts +4 -0
- package/lib/built/adapter-process.d.ts.map +1 -1
- package/lib/built/adapter-process.js +94 -0
- package/lib/built/adapter-process.js.map +1 -1
- package/lib/built/ai-trigger.d.ts +89 -0
- package/lib/built/ai-trigger.d.ts.map +1 -0
- package/lib/built/ai-trigger.js +166 -0
- package/lib/built/ai-trigger.js.map +1 -0
- package/lib/built/command.d.ts +33 -17
- package/lib/built/command.d.ts.map +1 -1
- package/lib/built/command.js +71 -44
- package/lib/built/command.js.map +1 -1
- package/lib/built/component.d.ts +42 -15
- package/lib/built/component.d.ts.map +1 -1
- package/lib/built/component.js +84 -52
- package/lib/built/component.js.map +1 -1
- package/lib/built/config.d.ts +54 -5
- package/lib/built/config.d.ts.map +1 -1
- package/lib/built/config.js +76 -10
- package/lib/built/config.js.map +1 -1
- package/lib/built/cron.d.ts +41 -18
- package/lib/built/cron.d.ts.map +1 -1
- package/lib/built/cron.js +106 -63
- package/lib/built/cron.js.map +1 -1
- package/lib/built/database.d.ts +55 -6
- package/lib/built/database.d.ts.map +1 -1
- package/lib/built/database.js +93 -22
- package/lib/built/database.js.map +1 -1
- package/lib/built/dispatcher.d.ts +118 -0
- package/lib/built/dispatcher.d.ts.map +1 -0
- package/lib/built/dispatcher.js +196 -0
- package/lib/built/dispatcher.js.map +1 -0
- package/lib/built/permission.d.ts +45 -5
- package/lib/built/permission.d.ts.map +1 -1
- package/lib/built/permission.js +56 -11
- package/lib/built/permission.js.map +1 -1
- package/lib/built/skill.d.ts +117 -0
- package/lib/built/skill.d.ts.map +1 -0
- package/lib/built/skill.js +191 -0
- package/lib/built/skill.js.map +1 -0
- package/lib/built/tool.d.ts +188 -0
- package/lib/built/tool.d.ts.map +1 -0
- package/lib/built/tool.js +749 -0
- package/lib/built/tool.js.map +1 -0
- package/lib/feature.d.ts +75 -0
- package/lib/feature.d.ts.map +1 -0
- package/lib/feature.js +69 -0
- package/lib/feature.js.map +1 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +11 -0
- package/lib/index.js.map +1 -1
- package/lib/plugin.d.ts +53 -18
- package/lib/plugin.d.ts.map +1 -1
- package/lib/plugin.js +301 -31
- package/lib/plugin.js.map +1 -1
- package/lib/types.d.ts +248 -9
- package/lib/types.d.ts.map +1 -1
- package/lib/utils.d.ts.map +1 -1
- package/lib/utils.js +38 -12
- package/lib/utils.js.map +1 -1
- package/package.json +4 -4
- package/src/adapter.ts +206 -2
- package/src/ai/agent.ts +772 -0
- package/src/ai/context-manager.ts +440 -0
- package/src/ai/conversation-memory.ts +774 -0
- package/src/ai/follow-up.ts +357 -0
- package/src/ai/index.ts +128 -0
- package/src/ai/init.ts +502 -0
- package/src/ai/output.ts +261 -0
- package/src/ai/providers/anthropic.ts +375 -0
- package/src/ai/providers/base.ts +173 -0
- package/src/ai/providers/index.ts +13 -0
- package/src/ai/providers/ollama.ts +292 -0
- package/src/ai/providers/openai.ts +167 -0
- package/src/ai/rate-limiter.ts +129 -0
- package/src/ai/service.ts +319 -0
- package/src/ai/session.ts +544 -0
- package/src/ai/tone-detector.ts +89 -0
- package/src/ai/tools.ts +218 -0
- package/src/ai/types.ts +296 -0
- package/src/ai/user-profile.ts +181 -0
- package/src/ai/zhin-agent.ts +845 -0
- package/src/built/adapter-process.ts +99 -0
- package/src/built/ai-trigger.ts +259 -0
- package/src/built/command.ts +75 -69
- package/src/built/component.ts +94 -76
- package/src/built/config.ts +238 -128
- package/src/built/cron.ts +117 -101
- package/src/built/database.ts +128 -33
- package/src/built/dispatcher.ts +332 -0
- package/src/built/permission.ts +146 -54
- package/src/built/skill.ts +280 -0
- package/src/built/tool.ts +928 -0
- package/src/feature.ts +113 -0
- package/src/index.ts +11 -0
- package/src/plugin.ts +359 -69
- package/src/types.ts +306 -11
- package/src/utils.ts +37 -13
- package/tests/adapter.test.ts +153 -1
- package/tests/ai/agent.test.ts +614 -0
- package/tests/ai/ai-trigger.test.ts +368 -0
- package/tests/ai/context-manager.test.ts +413 -0
- package/tests/ai/conversation-memory.test.ts +128 -0
- package/tests/ai/follow-up.test.ts +175 -0
- package/tests/ai/integration.test.ts +584 -0
- package/tests/ai/output.test.ts +128 -0
- package/tests/ai/providers.integration.test.ts +227 -0
- package/tests/ai/rate-limiter.test.ts +108 -0
- package/tests/ai/session.test.ts +375 -0
- package/tests/ai/setup.ts +308 -0
- package/tests/ai/tone-detector.test.ts +80 -0
- package/tests/ai/tool.test.ts +800 -0
- package/tests/ai/tools-builtin.test.ts +346 -0
- package/tests/ai/user-profile.test.ts +73 -0
- package/tests/ai/zhin-agent.test.ts +177 -0
- package/tests/component-new.test.ts +17 -6
- package/tests/config.test.ts +46 -0
- package/tests/cron.test.ts +94 -5
- package/tests/dispatcher.test.ts +146 -0
- package/tests/feature.test.ts +145 -0
- package/tests/features-builtin.test.ts +191 -0
- package/tests/plugin.test.ts +88 -14
- package/tests/skill-feature.test.ts +179 -0
- package/tests/tool-feature.test.ts +254 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RateLimiter — AI 请求频率限制
|
|
3
|
+
*
|
|
4
|
+
* 防止单用户过度消耗 GPU/API 资源。
|
|
5
|
+
*
|
|
6
|
+
* 策略:
|
|
7
|
+
* 1. 滑动窗口速率限制 — 每分钟 N 次请求
|
|
8
|
+
* 2. 优雅降级 — 超限时返回友好提示而非静默丢弃
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Logger } from '@zhin.js/logger';
|
|
12
|
+
|
|
13
|
+
const logger = new Logger(null, 'RateLimiter');
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// 配置
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
export interface RateLimitConfig {
|
|
20
|
+
/** 每分钟最大请求数(默认 20) */
|
|
21
|
+
maxRequestsPerMinute?: number;
|
|
22
|
+
/** 冷却时间(秒),超限后需等待(默认 10) */
|
|
23
|
+
cooldownSeconds?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DEFAULTS: Required<RateLimitConfig> = {
|
|
27
|
+
maxRequestsPerMinute: 20,
|
|
28
|
+
cooldownSeconds: 10,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// 类型
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
interface UserBucket {
|
|
36
|
+
/** 最近请求的时间戳列表(ms) */
|
|
37
|
+
timestamps: number[];
|
|
38
|
+
/** 冷却结束时间(ms),0 表示无冷却 */
|
|
39
|
+
cooldownUntil: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface RateLimitResult {
|
|
43
|
+
allowed: boolean;
|
|
44
|
+
/** 如果被拒绝,返回友好提示 */
|
|
45
|
+
message?: string;
|
|
46
|
+
/** 需等待的秒数 */
|
|
47
|
+
retryAfterSeconds?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// RateLimiter
|
|
52
|
+
// ============================================================================
|
|
53
|
+
|
|
54
|
+
export class RateLimiter {
|
|
55
|
+
private config: Required<RateLimitConfig>;
|
|
56
|
+
private buckets: Map<string, UserBucket> = new Map();
|
|
57
|
+
private cleanupTimer?: ReturnType<typeof setInterval>;
|
|
58
|
+
|
|
59
|
+
constructor(config?: RateLimitConfig) {
|
|
60
|
+
this.config = { ...DEFAULTS, ...config };
|
|
61
|
+
// 定期清理过期的 bucket(每 5 分钟)
|
|
62
|
+
this.cleanupTimer = setInterval(() => this.cleanup(), 5 * 60 * 1000);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 检查请求是否被允许
|
|
67
|
+
*/
|
|
68
|
+
check(userId: string): RateLimitResult {
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
let bucket = this.buckets.get(userId);
|
|
71
|
+
|
|
72
|
+
if (!bucket) {
|
|
73
|
+
bucket = { timestamps: [], cooldownUntil: 0 };
|
|
74
|
+
this.buckets.set(userId, bucket);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 1. 检查冷却期
|
|
78
|
+
if (bucket.cooldownUntil > now) {
|
|
79
|
+
const waitSec = Math.ceil((bucket.cooldownUntil - now) / 1000);
|
|
80
|
+
return {
|
|
81
|
+
allowed: false,
|
|
82
|
+
message: `请稍等 ${waitSec} 秒后再发消息哦~消息太频繁啦 😊`,
|
|
83
|
+
retryAfterSeconds: waitSec,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 2. 滑动窗口:清理 1 分钟前的时间戳
|
|
88
|
+
const windowStart = now - 60_000;
|
|
89
|
+
bucket.timestamps = bucket.timestamps.filter(t => t > windowStart);
|
|
90
|
+
|
|
91
|
+
// 3. 检查速率
|
|
92
|
+
if (bucket.timestamps.length >= this.config.maxRequestsPerMinute) {
|
|
93
|
+
bucket.cooldownUntil = now + this.config.cooldownSeconds * 1000;
|
|
94
|
+
const waitSec = this.config.cooldownSeconds;
|
|
95
|
+
logger.warn(`User ${userId} rate limited: ${bucket.timestamps.length} requests in 1 min`);
|
|
96
|
+
return {
|
|
97
|
+
allowed: false,
|
|
98
|
+
message: `你发消息太快啦,请等 ${waitSec} 秒后再试~`,
|
|
99
|
+
retryAfterSeconds: waitSec,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 4. 记录这次请求
|
|
104
|
+
bucket.timestamps.push(now);
|
|
105
|
+
return { allowed: true };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 清理长期不活跃的 bucket
|
|
110
|
+
*/
|
|
111
|
+
private cleanup(): void {
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
const staleThreshold = 10 * 60 * 1000; // 10 分钟
|
|
114
|
+
for (const [userId, bucket] of this.buckets) {
|
|
115
|
+
const latest = bucket.timestamps[bucket.timestamps.length - 1] ?? 0;
|
|
116
|
+
if (now - latest > staleThreshold && bucket.cooldownUntil < now) {
|
|
117
|
+
this.buckets.delete(userId);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
dispose(): void {
|
|
123
|
+
if (this.cleanupTimer) {
|
|
124
|
+
clearInterval(this.cleanupTimer);
|
|
125
|
+
this.cleanupTimer = undefined;
|
|
126
|
+
}
|
|
127
|
+
this.buckets.clear();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AIService — AI 服务核心类
|
|
3
|
+
* 统一管理多个模型提供商,提供会话和 Agent 能力
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Logger } from '@zhin.js/logger';
|
|
7
|
+
import type { Plugin } from '../plugin.js';
|
|
8
|
+
import type { Tool, ToolContext } from '../types.js';
|
|
9
|
+
import type { AITriggerConfig } from '../built/ai-trigger.js';
|
|
10
|
+
import type {
|
|
11
|
+
AIProvider,
|
|
12
|
+
AIConfig,
|
|
13
|
+
ChatMessage,
|
|
14
|
+
ChatCompletionRequest,
|
|
15
|
+
ChatCompletionResponse,
|
|
16
|
+
ChatCompletionChunk,
|
|
17
|
+
AgentTool,
|
|
18
|
+
} from './types.js';
|
|
19
|
+
import {
|
|
20
|
+
OpenAIProvider,
|
|
21
|
+
DeepSeekProvider,
|
|
22
|
+
MoonshotProvider,
|
|
23
|
+
ZhipuProvider,
|
|
24
|
+
AnthropicProvider,
|
|
25
|
+
OllamaProvider,
|
|
26
|
+
} from './providers/index.js';
|
|
27
|
+
import {
|
|
28
|
+
SessionManager,
|
|
29
|
+
createMemorySessionManager,
|
|
30
|
+
createDatabaseSessionManager,
|
|
31
|
+
} from './session.js';
|
|
32
|
+
import { Agent, createAgent } from './agent.js';
|
|
33
|
+
import { getBuiltinTools } from './tools.js';
|
|
34
|
+
import type { ContextManager, ContextConfig } from './context-manager.js';
|
|
35
|
+
|
|
36
|
+
const aiLogger = new Logger(null, 'AI');
|
|
37
|
+
|
|
38
|
+
export class AIService {
|
|
39
|
+
private providers: Map<string, AIProvider> = new Map();
|
|
40
|
+
private defaultProvider: string;
|
|
41
|
+
public sessions: SessionManager;
|
|
42
|
+
public contextManager?: ContextManager;
|
|
43
|
+
private builtinTools: AgentTool[];
|
|
44
|
+
private sessionConfig: { maxHistory?: number; expireMs?: number };
|
|
45
|
+
private contextConfig: ContextConfig;
|
|
46
|
+
private triggerConfig: AITriggerConfig;
|
|
47
|
+
private plugin?: Plugin;
|
|
48
|
+
private customTools: Map<string, AgentTool> = new Map();
|
|
49
|
+
|
|
50
|
+
constructor(config: AIConfig = {}) {
|
|
51
|
+
this.defaultProvider = config.defaultProvider || 'openai';
|
|
52
|
+
this.sessionConfig = config.sessions || {};
|
|
53
|
+
this.contextConfig = config.context || {};
|
|
54
|
+
this.triggerConfig = config.trigger || {};
|
|
55
|
+
this.sessions = createMemorySessionManager(this.sessionConfig);
|
|
56
|
+
this.builtinTools = getBuiltinTools().map(tool => this.convertToolToAgentTool(tool.toTool()));
|
|
57
|
+
|
|
58
|
+
if (config.providers?.openai?.apiKey) {
|
|
59
|
+
this.registerProvider(new OpenAIProvider(config.providers.openai));
|
|
60
|
+
}
|
|
61
|
+
if (config.providers?.anthropic?.apiKey) {
|
|
62
|
+
this.registerProvider(new AnthropicProvider(config.providers.anthropic));
|
|
63
|
+
}
|
|
64
|
+
if (config.providers?.deepseek?.apiKey) {
|
|
65
|
+
this.registerProvider(new DeepSeekProvider(config.providers.deepseek));
|
|
66
|
+
}
|
|
67
|
+
if (config.providers?.moonshot?.apiKey) {
|
|
68
|
+
this.registerProvider(new MoonshotProvider(config.providers.moonshot));
|
|
69
|
+
}
|
|
70
|
+
if (config.providers?.zhipu?.apiKey) {
|
|
71
|
+
this.registerProvider(new ZhipuProvider(config.providers.zhipu));
|
|
72
|
+
}
|
|
73
|
+
if (config.providers?.ollama) {
|
|
74
|
+
this.registerProvider(new OllamaProvider(config.providers.ollama));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
isReady(): boolean {
|
|
79
|
+
return this.providers.size > 0;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private static readonly PRE_EXEC_TIMEOUT = 10_000;
|
|
83
|
+
|
|
84
|
+
async process(
|
|
85
|
+
content: string,
|
|
86
|
+
context: ToolContext,
|
|
87
|
+
tools: Tool[],
|
|
88
|
+
): Promise<string | AsyncIterable<string>> {
|
|
89
|
+
const { platform, senderId, sceneId } = context;
|
|
90
|
+
const sessionId = SessionManager.generateId(platform || '', senderId || '', sceneId);
|
|
91
|
+
const allTools = this.collectAllToolsWithExternal(tools);
|
|
92
|
+
const baseSystemPrompt = '你是一个友好的中文 AI 助手,请始终使用中文回复。';
|
|
93
|
+
|
|
94
|
+
if (allTools.length === 0) {
|
|
95
|
+
return this.finishAndSave(sessionId, content, baseSystemPrompt, sceneId);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const callerPermissionLevel = context.senderPermissionLevel
|
|
99
|
+
? (AIService.PERM_MAP[context.senderPermissionLevel] ?? 0)
|
|
100
|
+
: (context.isOwner ? 4 : context.isBotAdmin ? 3 : context.isGroupOwner ? 2 : context.isGroupAdmin ? 1 : 0);
|
|
101
|
+
|
|
102
|
+
const relevantTools = Agent.filterTools(content, allTools, {
|
|
103
|
+
callerPermissionLevel,
|
|
104
|
+
maxTools: 8,
|
|
105
|
+
minScore: 0.1,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (relevantTools.length === 0) {
|
|
109
|
+
return this.finishAndSave(sessionId, content, baseSystemPrompt, sceneId);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const noParamTools: AgentTool[] = [];
|
|
113
|
+
const paramTools: AgentTool[] = [];
|
|
114
|
+
for (const tool of relevantTools) {
|
|
115
|
+
const required = tool.parameters?.required;
|
|
116
|
+
(!required || required.length === 0) ? noParamTools.push(tool) : paramTools.push(tool);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let preExecutedData = '';
|
|
120
|
+
const preExecutedCalls: { tool: string; args: Record<string, any>; result: any }[] = [];
|
|
121
|
+
|
|
122
|
+
if (noParamTools.length > 0) {
|
|
123
|
+
const results = await Promise.allSettled(
|
|
124
|
+
noParamTools.map(async (tool) => {
|
|
125
|
+
const result = await Promise.race([
|
|
126
|
+
tool.execute({}),
|
|
127
|
+
new Promise<never>((_, reject) =>
|
|
128
|
+
setTimeout(() => reject(new Error('预执行超时')), AIService.PRE_EXEC_TIMEOUT)),
|
|
129
|
+
]);
|
|
130
|
+
return { name: tool.name, result };
|
|
131
|
+
}),
|
|
132
|
+
);
|
|
133
|
+
for (const r of results) {
|
|
134
|
+
if (r.status === 'fulfilled') {
|
|
135
|
+
const s = typeof r.value.result === 'string' ? r.value.result : JSON.stringify(r.value.result);
|
|
136
|
+
preExecutedData += `\n【${r.value.name}】${s}`;
|
|
137
|
+
preExecutedCalls.push({ tool: r.value.name, args: {}, result: r.value.result });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let finalResponse: string;
|
|
143
|
+
|
|
144
|
+
if (paramTools.length === 0 && preExecutedData) {
|
|
145
|
+
const singleShotPrompt = `你是一个友好的中文 AI 助手。\n\n以下是根据用户问题自动获取的实时数据:\n${preExecutedData}\n\n请基于以上数据回答用户的问题。用自然流畅的中文组织信息,突出重点。`;
|
|
146
|
+
finalResponse = await this.simpleChat(content, singleShotPrompt);
|
|
147
|
+
} else {
|
|
148
|
+
const agentSystemPrompt = `你是一个友好的中文 AI 助手。
|
|
149
|
+
${preExecutedData ? `\n已自动获取的数据:${preExecutedData}\n` : ''}
|
|
150
|
+
## 关键要求
|
|
151
|
+
- 调用工具后**必须**基于结果给出完整中文回答
|
|
152
|
+
- 用自然语言总结,突出关键信息`;
|
|
153
|
+
|
|
154
|
+
const agent = this.createAgent({
|
|
155
|
+
systemPrompt: agentSystemPrompt,
|
|
156
|
+
tools: paramTools.length > 0 ? paramTools : relevantTools,
|
|
157
|
+
useBuiltinTools: false,
|
|
158
|
+
collectExternalTools: false,
|
|
159
|
+
maxIterations: 3,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const agentResult = await agent.run(content);
|
|
163
|
+
finalResponse = agentResult.content || this.formatToolCallsFallback(
|
|
164
|
+
[...preExecutedCalls, ...agentResult.toolCalls],
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
await this.sessions.addMessage(sessionId, { role: 'user', content });
|
|
169
|
+
await this.sessions.addMessage(sessionId, { role: 'assistant', content: finalResponse });
|
|
170
|
+
if (this.contextManager && sceneId) {
|
|
171
|
+
this.contextManager.autoSummarizeIfNeeded(sceneId).catch(() => {});
|
|
172
|
+
}
|
|
173
|
+
return finalResponse;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private async finishAndSave(sessionId: string, content: string, systemPrompt: string, sceneId?: string): Promise<string> {
|
|
177
|
+
const response = await this.simpleChat(content, systemPrompt);
|
|
178
|
+
await this.sessions.addMessage(sessionId, { role: 'user', content });
|
|
179
|
+
await this.sessions.addMessage(sessionId, { role: 'assistant', content: response });
|
|
180
|
+
if (this.contextManager && sceneId) {
|
|
181
|
+
this.contextManager.autoSummarizeIfNeeded(sceneId).catch(() => {});
|
|
182
|
+
}
|
|
183
|
+
return response;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private formatToolCallsFallback(toolCalls: { tool: string; args: any; result: any }[]): string {
|
|
187
|
+
if (toolCalls.length === 0) return '处理完成。';
|
|
188
|
+
return toolCalls.map(tc => {
|
|
189
|
+
const s = typeof tc.result === 'string' ? tc.result : JSON.stringify(tc.result, null, 2);
|
|
190
|
+
return `【${tc.tool}】\n${s}`;
|
|
191
|
+
}).join('\n\n');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private async simpleChat(content: string, systemPrompt: string): Promise<string> {
|
|
195
|
+
const provider = this.getProvider();
|
|
196
|
+
const response = await this.chat({
|
|
197
|
+
model: provider.models[0],
|
|
198
|
+
messages: [
|
|
199
|
+
{ role: 'system', content: systemPrompt },
|
|
200
|
+
{ role: 'user', content },
|
|
201
|
+
],
|
|
202
|
+
});
|
|
203
|
+
const msgContent = response.choices[0]?.message?.content;
|
|
204
|
+
return typeof msgContent === 'string' ? msgContent : '';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private collectAllToolsWithExternal(externalTools: Tool[]): AgentTool[] {
|
|
208
|
+
const tools: AgentTool[] = [];
|
|
209
|
+
tools.push(...this.builtinTools);
|
|
210
|
+
tools.push(...this.customTools.values());
|
|
211
|
+
for (const tool of externalTools) {
|
|
212
|
+
if (tool.name.startsWith('cmd_') || tool.name.startsWith('process_')) continue;
|
|
213
|
+
tools.push(this.convertToolToAgentTool(tool));
|
|
214
|
+
}
|
|
215
|
+
if (tools.length > 30) return tools.slice(0, 30);
|
|
216
|
+
return tools;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private static readonly PERM_MAP: Record<string, number> = {
|
|
220
|
+
'user': 0, 'group_admin': 1, 'group_owner': 2, 'bot_admin': 3, 'owner': 4,
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
private convertToolToAgentTool(tool: Tool): AgentTool {
|
|
224
|
+
const agentTool: AgentTool = {
|
|
225
|
+
name: tool.name,
|
|
226
|
+
description: tool.description,
|
|
227
|
+
parameters: tool.parameters as any,
|
|
228
|
+
execute: tool.execute,
|
|
229
|
+
};
|
|
230
|
+
if (tool.tags?.length) agentTool.tags = tool.tags;
|
|
231
|
+
if (tool.permissionLevel) agentTool.permissionLevel = AIService.PERM_MAP[tool.permissionLevel] ?? 0;
|
|
232
|
+
if ((tool as any).keywords?.length) agentTool.keywords = (tool as any).keywords;
|
|
233
|
+
return agentTool;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
setSessionManager(manager: SessionManager): void { this.sessions.dispose(); this.sessions = manager; }
|
|
237
|
+
setContextManager(manager: ContextManager): void {
|
|
238
|
+
this.contextManager = manager;
|
|
239
|
+
const defaultProvider = this.providers.get(this.defaultProvider);
|
|
240
|
+
if (defaultProvider) manager.setAIProvider(defaultProvider);
|
|
241
|
+
}
|
|
242
|
+
setPlugin(plugin: Plugin): void { this.plugin = plugin; }
|
|
243
|
+
registerTool(tool: AgentTool): () => void { this.customTools.set(tool.name, tool); return () => { this.customTools.delete(tool.name); }; }
|
|
244
|
+
|
|
245
|
+
collectAllTools(): AgentTool[] {
|
|
246
|
+
const tools: AgentTool[] = [...this.builtinTools, ...this.customTools.values()];
|
|
247
|
+
if (this.plugin) {
|
|
248
|
+
for (const tool of this.plugin.collectAllTools()) tools.push(this.convertToolToAgentTool(tool));
|
|
249
|
+
}
|
|
250
|
+
return tools;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
getContextConfig(): ContextConfig { return this.contextConfig; }
|
|
254
|
+
getSessionConfig() { return this.sessionConfig; }
|
|
255
|
+
getTriggerConfig(): AITriggerConfig { return this.triggerConfig; }
|
|
256
|
+
|
|
257
|
+
registerProvider(provider: AIProvider): void { this.providers.set(provider.name, provider); }
|
|
258
|
+
getProvider(name?: string): AIProvider {
|
|
259
|
+
const providerName = name || this.defaultProvider;
|
|
260
|
+
const provider = this.providers.get(providerName);
|
|
261
|
+
if (!provider) throw new Error(`AI Provider "${providerName}" not found. Available: ${this.listProviders().join(', ')}`);
|
|
262
|
+
return provider;
|
|
263
|
+
}
|
|
264
|
+
listProviders(): string[] { return Array.from(this.providers.keys()); }
|
|
265
|
+
|
|
266
|
+
async listModels(providerName?: string): Promise<{ provider: string; models: string[] }[]> {
|
|
267
|
+
const result: { provider: string; models: string[] }[] = [];
|
|
268
|
+
if (providerName) {
|
|
269
|
+
const provider = this.getProvider(providerName);
|
|
270
|
+
result.push({ provider: providerName, models: await provider.listModels?.() || provider.models });
|
|
271
|
+
} else {
|
|
272
|
+
for (const [name, provider] of this.providers) {
|
|
273
|
+
result.push({ provider: name, models: await provider.listModels?.() || provider.models });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async chat(request: ChatCompletionRequest, providerName?: string): Promise<ChatCompletionResponse> {
|
|
280
|
+
return this.getProvider(providerName).chat(request);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async *chatStream(request: ChatCompletionRequest, providerName?: string): AsyncIterable<ChatCompletionChunk> {
|
|
284
|
+
yield* this.getProvider(providerName).chatStream(request);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async ask(question: string, options: { provider?: string; model?: string; systemPrompt?: string; temperature?: number } = {}): Promise<string> {
|
|
288
|
+
const messages: ChatMessage[] = [];
|
|
289
|
+
if (options.systemPrompt) messages.push({ role: 'system', content: options.systemPrompt });
|
|
290
|
+
messages.push({ role: 'user', content: question });
|
|
291
|
+
const provider = this.getProvider(options.provider);
|
|
292
|
+
const response = await provider.chat({ model: options.model || provider.models[0], messages, temperature: options.temperature });
|
|
293
|
+
const content = response.choices[0]?.message?.content;
|
|
294
|
+
return typeof content === 'string' ? content : '';
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
createAgent(options: { provider?: string; model?: string; systemPrompt?: string; tools?: AgentTool[]; useBuiltinTools?: boolean; collectExternalTools?: boolean; maxIterations?: number } = {}): Agent {
|
|
298
|
+
const provider = this.getProvider(options.provider);
|
|
299
|
+
let tools: AgentTool[] = [];
|
|
300
|
+
if (options.useBuiltinTools !== false) tools.push(...this.builtinTools);
|
|
301
|
+
if (options.collectExternalTools !== false) { tools.push(...this.customTools.values()); if (this.plugin) { for (const t of this.plugin.collectAllTools()) tools.push(this.convertToolToAgentTool(t)); } }
|
|
302
|
+
if (options.tools?.length) tools.push(...options.tools);
|
|
303
|
+
return createAgent(provider, { model: options.model, systemPrompt: options.systemPrompt, tools, maxIterations: options.maxIterations });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async runAgent(task: string, options: { provider?: string; model?: string; tools?: AgentTool[]; systemPrompt?: string } = {}): Promise<{ content: string; toolCalls: any[]; usage: any }> {
|
|
307
|
+
return this.createAgent(options).run(task);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async healthCheck(): Promise<Record<string, boolean>> {
|
|
311
|
+
const results: Record<string, boolean> = {};
|
|
312
|
+
for (const [name, provider] of this.providers) {
|
|
313
|
+
try { results[name] = await provider.healthCheck?.() ?? true; } catch { results[name] = false; }
|
|
314
|
+
}
|
|
315
|
+
return results;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
dispose(): void { this.sessions.dispose(); this.providers.clear(); }
|
|
319
|
+
}
|