@zhin.js/agent 0.0.20 → 0.1.2

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 (116) hide show
  1. package/README.md +5 -2
  2. package/lib/cron-engine.d.ts +16 -1
  3. package/lib/cron-engine.d.ts.map +1 -1
  4. package/lib/cron-engine.js +47 -13
  5. package/lib/cron-engine.js.map +1 -1
  6. package/lib/discover-skills.d.ts +3 -1
  7. package/lib/discover-skills.d.ts.map +1 -1
  8. package/lib/discover-skills.js +7 -9
  9. package/lib/discover-skills.js.map +1 -1
  10. package/lib/discover-tools.d.ts +1 -6
  11. package/lib/discover-tools.d.ts.map +1 -1
  12. package/lib/discover-tools.js +2 -6
  13. package/lib/discover-tools.js.map +1 -1
  14. package/lib/index.d.ts +2 -4
  15. package/lib/index.d.ts.map +1 -1
  16. package/lib/index.js +1 -2
  17. package/lib/index.js.map +1 -1
  18. package/lib/init/create-zhin-agent.d.ts.map +1 -1
  19. package/lib/init/create-zhin-agent.js +58 -21
  20. package/lib/init/create-zhin-agent.js.map +1 -1
  21. package/lib/init/register-ai-trigger.d.ts.map +1 -1
  22. package/lib/init/register-ai-trigger.js +10 -3
  23. package/lib/init/register-ai-trigger.js.map +1 -1
  24. package/lib/init/register-builtin-tools.d.ts.map +1 -1
  25. package/lib/init/register-builtin-tools.js +46 -14
  26. package/lib/init/register-builtin-tools.js.map +1 -1
  27. package/lib/init/register-db-models.d.ts.map +1 -1
  28. package/lib/init/register-db-models.js +1 -3
  29. package/lib/init/register-db-models.js.map +1 -1
  30. package/lib/init/register-db-upgrade.d.ts.map +1 -1
  31. package/lib/init/register-db-upgrade.js +1 -8
  32. package/lib/init/register-db-upgrade.js.map +1 -1
  33. package/lib/init/register-management-tools.d.ts.map +1 -1
  34. package/lib/init/register-management-tools.js +33 -20
  35. package/lib/init/register-management-tools.js.map +1 -1
  36. package/lib/service.d.ts +4 -0
  37. package/lib/service.d.ts.map +1 -1
  38. package/lib/service.js +3 -8
  39. package/lib/service.js.map +1 -1
  40. package/lib/zhin-agent/builtin-tools.d.ts +0 -2
  41. package/lib/zhin-agent/builtin-tools.d.ts.map +1 -1
  42. package/lib/zhin-agent/builtin-tools.js +0 -55
  43. package/lib/zhin-agent/builtin-tools.js.map +1 -1
  44. package/lib/zhin-agent/config.d.ts +4 -1
  45. package/lib/zhin-agent/config.d.ts.map +1 -1
  46. package/lib/zhin-agent/config.js +2 -1
  47. package/lib/zhin-agent/config.js.map +1 -1
  48. package/lib/zhin-agent/index.d.ts +11 -6
  49. package/lib/zhin-agent/index.d.ts.map +1 -1
  50. package/lib/zhin-agent/index.js +147 -81
  51. package/lib/zhin-agent/index.js.map +1 -1
  52. package/lib/zhin-agent/prompt.d.ts.map +1 -1
  53. package/lib/zhin-agent/prompt.js +31 -76
  54. package/lib/zhin-agent/prompt.js.map +1 -1
  55. package/lib/zhin-agent/tool-collector.d.ts.map +1 -1
  56. package/lib/zhin-agent/tool-collector.js +7 -7
  57. package/lib/zhin-agent/tool-collector.js.map +1 -1
  58. package/package.json +7 -4
  59. package/CHANGELOG.md +0 -170
  60. package/lib/follow-up.d.ts +0 -131
  61. package/lib/follow-up.d.ts.map +0 -1
  62. package/lib/follow-up.js +0 -265
  63. package/lib/follow-up.js.map +0 -1
  64. package/src/agent.ts +0 -6
  65. package/src/bootstrap.ts +0 -309
  66. package/src/builtin-tools.ts +0 -958
  67. package/src/compaction.ts +0 -28
  68. package/src/context-manager.ts +0 -15
  69. package/src/conversation-memory.ts +0 -5
  70. package/src/cron-engine.ts +0 -338
  71. package/src/discover-agents.ts +0 -138
  72. package/src/discover-skills.ts +0 -325
  73. package/src/discover-tools.ts +0 -302
  74. package/src/discovery-utils.ts +0 -96
  75. package/src/file-policy.ts +0 -333
  76. package/src/follow-up.ts +0 -357
  77. package/src/hooks.ts +0 -223
  78. package/src/index.ts +0 -183
  79. package/src/init/create-zhin-agent.ts +0 -136
  80. package/src/init/register-ai-service.ts +0 -53
  81. package/src/init/register-ai-trigger.ts +0 -253
  82. package/src/init/register-builtin-tools.ts +0 -308
  83. package/src/init/register-db-models.ts +0 -31
  84. package/src/init/register-db-upgrade.ts +0 -77
  85. package/src/init/register-management-tools.ts +0 -71
  86. package/src/init/register-message-recorder.ts +0 -31
  87. package/src/init/register-tool-service.ts +0 -9
  88. package/src/init/shared-refs.ts +0 -20
  89. package/src/init/types.ts +0 -18
  90. package/src/init.ts +0 -50
  91. package/src/output.ts +0 -15
  92. package/src/rate-limiter.ts +0 -5
  93. package/src/service.ts +0 -224
  94. package/src/session.ts +0 -13
  95. package/src/storage.ts +0 -9
  96. package/src/subagent.ts +0 -209
  97. package/src/tone-detector.ts +0 -5
  98. package/src/tools.ts +0 -214
  99. package/src/user-profile.ts +0 -182
  100. package/src/zhin-agent/builtin-tools.ts +0 -247
  101. package/src/zhin-agent/config.ts +0 -121
  102. package/src/zhin-agent/exec-policy.ts +0 -285
  103. package/src/zhin-agent/index.ts +0 -559
  104. package/src/zhin-agent/prompt.ts +0 -305
  105. package/src/zhin-agent/tool-collector.ts +0 -249
  106. package/tests/ai/follow-up.test.ts +0 -175
  107. package/tests/ai/integration.test.ts +0 -582
  108. package/tests/ai/multimodal.test.ts +0 -106
  109. package/tests/ai/setup.ts +0 -186
  110. package/tests/ai/subagent.test.ts +0 -270
  111. package/tests/ai/tools-builtin.test.ts +0 -310
  112. package/tests/ai/user-profile.test.ts +0 -73
  113. package/tests/ai/zhin-agent.test.ts +0 -306
  114. package/tests/exec-policy.test.ts +0 -355
  115. package/tests/file-policy.test.ts +0 -405
  116. package/tsconfig.json +0 -22
package/src/index.ts DELETED
@@ -1,183 +0,0 @@
1
- /**
2
- * @zhin.js/agent — AI Agent composition
3
- * Composes providers, tools, skills from @zhin.js/core into session, ZhinAgent, init.
4
- */
5
-
6
- // Re-export types from core for convenience
7
- export type {
8
- AIConfig,
9
- AIProvider,
10
- ChatMessage,
11
- ContentPart,
12
- AgentTool,
13
- AgentConfig,
14
- AgentResult,
15
- } from '@zhin.js/core';
16
-
17
- export { Agent, createAgent, formatToolTitle } from './agent.js';
18
- export type { AgentState, AgentEvents } from './agent.js';
19
-
20
- export { AIService } from './service.js';
21
-
22
- export {
23
- SessionManager,
24
- MemorySessionManager,
25
- DatabaseSessionManager,
26
- createMemorySessionManager,
27
- createDatabaseSessionManager,
28
- AI_SESSION_MODEL,
29
- } from './session.js';
30
-
31
- export {
32
- ContextManager,
33
- createContextManager,
34
- CHAT_MESSAGE_MODEL,
35
- CONTEXT_SUMMARY_MODEL,
36
- } from './context-manager.js';
37
- export type { ContextConfig, MessageRecord } from './context-manager.js';
38
-
39
- export { ZhinAgent } from './zhin-agent/index.js';
40
- export type { ZhinAgentConfig, OnChunkCallback } from './zhin-agent/index.js';
41
-
42
- export { PERM_MAP, DEFAULT_CONFIG as ZHIN_AGENT_DEFAULT_CONFIG, SECTION_SEP } from './zhin-agent/config.js';
43
- export {
44
- checkExecPolicy, applyExecPolicyToTools, resolveExecAllowlist, EXEC_PRESETS,
45
- isDangerousCommand, stripEnvVarPrefix, stripSafeWrappers, splitCompoundCommand, extractCommandName,
46
- type ExecPolicyResult,
47
- } from './zhin-agent/exec-policy.js';
48
- export { collectRelevantTools, toAgentTool } from './zhin-agent/tool-collector.js';
49
- export { buildRichSystemPrompt, buildContextHint, buildEnhancedPersona, buildUserMessageWithHistory, contentToText } from './zhin-agent/prompt.js';
50
- export type { RichSystemPromptContext } from './zhin-agent/prompt.js';
51
- export { createChatHistoryTool, createUserProfileTool, createScheduleFollowUpTool, createSpawnTaskTool } from './zhin-agent/builtin-tools.js';
52
-
53
- export {
54
- ConversationMemory,
55
- AI_MESSAGE_MODEL,
56
- AI_SUMMARY_MODEL,
57
- } from './conversation-memory.js';
58
- export type { ConversationMemoryConfig } from './conversation-memory.js';
59
-
60
- export { UserProfileStore, AI_USER_PROFILE_MODEL } from './user-profile.js';
61
-
62
- export { RateLimiter } from './rate-limiter.js';
63
- export type { RateLimitConfig, RateLimitResult } from './rate-limiter.js';
64
-
65
- export { FollowUpManager, AI_FOLLOWUP_MODEL } from './follow-up.js';
66
- export type { FollowUpRecord, FollowUpSender } from './follow-up.js';
67
-
68
- export { SubagentManager } from './subagent.js';
69
- export type {
70
- SubagentOrigin,
71
- SubagentResultSender,
72
- SpawnOptions,
73
- SubagentManagerOptions,
74
- } from './subagent.js';
75
-
76
- export {
77
- PersistentCronEngine,
78
- readCronJobsFile,
79
- writeCronJobsFile,
80
- getCronJobsFilePath,
81
- generateCronJobId,
82
- createCronTools,
83
- setCronManager,
84
- getCronManager,
85
- CRON_JOBS_FILENAME,
86
- } from './cron-engine.js';
87
- export type {
88
- CronJobRecord,
89
- CronRunner,
90
- AddCronFn,
91
- PersistentCronEngineOptions,
92
- CronManager,
93
- } from './cron-engine.js';
94
-
95
- export {
96
- estimateTokens,
97
- estimateMessagesTokens,
98
- splitMessagesByTokenShare,
99
- chunkMessagesByMaxTokens,
100
- computeAdaptiveChunkRatio,
101
- resolveContextWindowTokens,
102
- evaluateContextWindowGuard,
103
- summarizeWithFallback,
104
- summarizeInStages,
105
- pruneHistoryForContext,
106
- compactSession,
107
- DEFAULT_CONTEXT_TOKENS,
108
- } from './compaction.js';
109
- export type { ContextWindowSource, ContextWindowInfo, ContextWindowGuardResult, PruneResult } from './compaction.js';
110
-
111
- export {
112
- loadBootstrapFiles,
113
- buildContextFiles,
114
- buildBootstrapContextSection,
115
- loadSoulPersona,
116
- loadToolsGuide,
117
- loadAgentsMemory,
118
- clearBootstrapCache,
119
- } from './bootstrap.js';
120
- export type { BootstrapFile, ContextFile } from './bootstrap.js';
121
-
122
- export {
123
- registerAIHook,
124
- unregisterAIHook,
125
- triggerAIHook,
126
- createAIHookEvent,
127
- clearAIHooks,
128
- getRegisteredAIHookKeys,
129
- } from './hooks.js';
130
- export type {
131
- AIHookEvent,
132
- AIHookEventType,
133
- AIHookHandler,
134
- MessageReceivedEvent,
135
- MessageSentEvent,
136
- SessionCompactEvent,
137
- SessionNewEvent,
138
- AgentBootstrapEvent,
139
- ToolCallEvent,
140
- } from './hooks.js';
141
-
142
- export { detectTone } from './tone-detector.js';
143
- export type { Tone } from './tone-detector.js';
144
-
145
- export {
146
- parseOutput,
147
- renderToPlainText,
148
- renderToSatori,
149
- } from './output.js';
150
- export type {
151
- OutputElement,
152
- TextElement,
153
- ImageElement,
154
- AudioElement,
155
- VideoElement,
156
- CardElement,
157
- FileElement,
158
- CardField,
159
- CardButton,
160
- } from './output.js';
161
-
162
- export {
163
- calculatorTool,
164
- timeTool,
165
- searchTool,
166
- codeRunnerTool,
167
- httpTool,
168
- memoryTool,
169
- getBuiltinTools,
170
- getAllBuiltinTools,
171
- } from './tools.js';
172
-
173
- export { initAgentModule } from './init.js';
174
-
175
- export {
176
- MemoryStorageBackend,
177
- DatabaseStorageBackend,
178
- createSwappableBackend,
179
- } from './storage.js';
180
- export type {
181
- StorageBackend,
182
- DbModel,
183
- } from './storage.js';
@@ -1,136 +0,0 @@
1
- /**
2
- * Create ZhinAgent global brain and wire up sub-systems
3
- * (follow-up sender, subagent manager, cron engine, scheduler).
4
- */
5
- import * as path from 'path';
6
- import { getPlugin, Scheduler, getScheduler, setScheduler, type MessageType, type SendOptions } from '@zhin.js/core';
7
- import { ZhinAgent } from '../zhin-agent/index.js';
8
- import { createBuiltinTools } from '../builtin-tools.js';
9
- import { collectPluginSkillSearchRoots } from '../discovery-utils.js';
10
- import { resolveSkillInstructionMaxChars, DEFAULT_CONFIG } from '../zhin-agent/config.js';
11
- import { PersistentCronEngine, setCronManager } from '../cron-engine.js';
12
- import type { AIServiceRefs } from './shared-refs.js';
13
-
14
- export function createZhinAgentContext(refs: AIServiceRefs): void {
15
- const plugin = getPlugin();
16
- const { useContext, root, logger } = plugin;
17
-
18
- useContext('ai', (ai) => {
19
- if (!ai.isReady()) {
20
- logger.warn('AI Service not ready, ZhinAgent not created');
21
- return;
22
- }
23
-
24
- const provider = ai.getProvider();
25
- const agentConfig = ai.getAgentConfig();
26
- const agent = new ZhinAgent(provider, agentConfig);
27
- refs.zhinAgent = agent;
28
-
29
- const skillRegistry = root.inject('skill');
30
- if (skillRegistry) agent.setSkillRegistry(skillRegistry);
31
-
32
- // Follow-up reminder sender
33
- agent.setFollowUpSender(async (record) => {
34
- const adapter = root.inject(record.platform) as { sendMessage?: (opts: SendOptions) => Promise<string> } | undefined;
35
- if (!adapter || typeof adapter.sendMessage !== 'function') {
36
- logger.warn(`[跟进提醒] 找不到适配器: ${record.platform}`);
37
- return;
38
- }
39
- await adapter.sendMessage({
40
- context: record.platform,
41
- bot: record.bot_id,
42
- id: record.scene_id,
43
- type: record.scene_type as MessageType,
44
- content: `⏰ 定时提醒:${record.message}`,
45
- });
46
- });
47
-
48
- // Subagent manager for background tasks
49
- agent.initSubagentManager(() => {
50
- const modelName = provider.models[0] || '';
51
- const fullConfig = { ...DEFAULT_CONFIG, ...agentConfig } as Required<import('../zhin-agent/config.js').ZhinAgentConfig>;
52
- const zhinTools = createBuiltinTools({
53
- plugin,
54
- skillInstructionMaxChars: resolveSkillInstructionMaxChars(fullConfig, modelName),
55
- pluginSkillRootsResolver: () => collectPluginSkillSearchRoots(root),
56
- });
57
- return zhinTools.map(zt => {
58
- const t = zt.toTool();
59
- return {
60
- name: t.name,
61
- description: t.description,
62
- parameters: t.parameters,
63
- execute: t.execute as (args: Record<string, any>) => Promise<unknown>,
64
- tags: t.tags,
65
- keywords: t.keywords,
66
- };
67
- });
68
- });
69
- agent.setSubagentSender(async (origin, content) => {
70
- const adapter = root.inject(origin.platform) as { sendMessage?: (opts: SendOptions) => Promise<string> } | undefined;
71
- if (!adapter || typeof adapter.sendMessage !== 'function') {
72
- logger.warn(`[子任务] 找不到适配器: ${origin.platform}`);
73
- return;
74
- }
75
- await adapter.sendMessage({
76
- context: origin.platform,
77
- bot: origin.botId,
78
- id: origin.sceneId,
79
- type: origin.sceneType as MessageType,
80
- content,
81
- });
82
- });
83
-
84
- // Persistent cron engine
85
- let cronEngine: PersistentCronEngine | null = null;
86
- const cronFeature = root.inject('cron') as import('@zhin.js/core').CronFeature | undefined;
87
- if (cronFeature && typeof cronFeature.add === 'function') {
88
- const dataDir = path.join(process.cwd(), 'data');
89
- const addCron: import('../cron-engine.js').AddCronFn = (c) => cronFeature.add(c, 'cron-engine');
90
- const runner = async (prompt: string) => {
91
- if (!refs.zhinAgent) return;
92
- await refs.zhinAgent.process(prompt, {
93
- platform: 'cron',
94
- senderId: 'system',
95
- sceneId: 'cron',
96
- });
97
- };
98
- cronEngine = new PersistentCronEngine({ dataDir, addCron, runner });
99
- cronEngine.load();
100
- setCronManager({ cronFeature, engine: cronEngine });
101
- }
102
-
103
- // Unified scheduler (at/every/cron)
104
- const dataDir = path.join(process.cwd(), 'data');
105
- const scheduler = new Scheduler({
106
- storePath: path.join(dataDir, 'scheduler-jobs.json'),
107
- workspace: process.cwd(),
108
- onJob: async (job) => {
109
- if (!refs.zhinAgent) return;
110
- await refs.zhinAgent.process(job.payload.message, {
111
- platform: 'cron',
112
- senderId: 'system',
113
- sceneId: 'scheduler',
114
- });
115
- },
116
- });
117
- setScheduler(scheduler);
118
- scheduler.start().catch((e) => logger.warn('Scheduler start failed: ' + (e as Error).message));
119
-
120
- logger.debug('ZhinAgent created');
121
- return () => {
122
- setCronManager(null);
123
- if (cronEngine) {
124
- cronEngine.unload();
125
- cronEngine = null;
126
- }
127
- const s = getScheduler();
128
- if (s) {
129
- s.stop();
130
- setScheduler(null);
131
- }
132
- agent.dispose();
133
- refs.zhinAgent = null;
134
- };
135
- });
136
- }
@@ -1,53 +0,0 @@
1
- /**
2
- * Register AIService as a plugin context.
3
- */
4
- import './types.js';
5
- import { getPlugin, type Plugin } from '@zhin.js/core';
6
- import type { AIConfig } from '@zhin.js/core';
7
- import { AIService } from '../service.js';
8
- import type { AIServiceRefs } from './shared-refs.js';
9
-
10
- export function registerAIService(refs: AIServiceRefs): void {
11
- const plugin = getPlugin();
12
- const { provide, root, logger } = plugin;
13
-
14
- provide<'ai'>({
15
- name: 'ai',
16
- description: 'AI Service - Multi-model LLM integration',
17
- async mounted(_p: Plugin) {
18
- const configService = root.inject('config');
19
- const appConfig =
20
- configService?.getPrimary<{ ai?: AIConfig }>() || {};
21
- const config = appConfig.ai || {};
22
-
23
- if (config.enabled === false) {
24
- logger.info('AI Service is disabled');
25
- return undefined as unknown as AIService;
26
- }
27
-
28
- const service = new AIService(config);
29
- refs.aiService = service;
30
- service.setPlugin(root);
31
-
32
- const providers = service.listProviders();
33
- if (providers.length === 0) {
34
- logger.warn(
35
- 'No AI providers configured. Please add API keys in zhin.config (yml/json/toml)',
36
- );
37
- } else {
38
- logger.info(
39
- `AI Service started with providers: ${providers.join(', ')}`,
40
- );
41
- }
42
-
43
- return service;
44
- },
45
- async dispose(service) {
46
- if (service) {
47
- service.dispose();
48
- refs.aiService = null;
49
- logger.info('AI Service stopped');
50
- }
51
- },
52
- });
53
- }
@@ -1,253 +0,0 @@
1
- /**
2
- * Register AI trigger handler via MessageDispatcher or fallback middleware.
3
- */
4
- import './types.js';
5
- import { getPlugin, Message, shouldTriggerAI, inferSenderPermissions, parseRichMediaContent, mergeAITriggerConfig } from '@zhin.js/core';
6
- import type { Tool, ToolContext } from '@zhin.js/core';
7
- import type { ContentPart } from '@zhin.js/core';
8
- import type { OutputElement } from '@zhin.js/ai';
9
- import type { AIServiceRefs } from './shared-refs.js';
10
-
11
- /**
12
- * Extract multimodal ContentPart[] from a Message's structured $content segments.
13
- * Handles image, video, audio, and face/sticker types.
14
- * Falls back to raw string parsing for image URLs when $content has no media segments.
15
- */
16
- function extractMediaParts(message: Message<any>): ContentPart[] {
17
- const parts: ContentPart[] = [];
18
-
19
- // 1. Extract from structured $content segments
20
- if (Array.isArray(message.$content)) {
21
- for (const seg of message.$content) {
22
- if (typeof seg === 'string' || !seg || !seg.type) continue;
23
- const { type, data } = seg;
24
- switch (type) {
25
- case 'image': {
26
- const url = data?.url || data?.file || data?.src;
27
- if (url) parts.push({ type: 'image_url', image_url: { url } });
28
- break;
29
- }
30
- case 'video': {
31
- const url = data?.url || data?.file || data?.src;
32
- if (url) parts.push({ type: 'video_url', video_url: { url } });
33
- break;
34
- }
35
- case 'audio':
36
- case 'record':
37
- case 'voice': {
38
- const dataStr = data?.data || data?.base64;
39
- if (dataStr) {
40
- const fmt = data?.format === 'wav' ? 'wav' : 'mp3';
41
- parts.push({ type: 'audio', audio: { data: dataStr, format: fmt } });
42
- } else {
43
- const url = data?.url || data?.file || data?.src;
44
- if (url) {
45
- // Audio URL: describe as text since most LLMs can't play audio URLs directly
46
- parts.push({ type: 'text', text: `[用户发送了一段语音: ${url}]` });
47
- }
48
- }
49
- break;
50
- }
51
- case 'face':
52
- case 'sticker':
53
- case 'emoji': {
54
- const id = String(data?.id ?? data?.face_id ?? '');
55
- const text = data?.text || data?.name || data?.describe;
56
- if (id) parts.push({ type: 'face', face: { id, text } });
57
- break;
58
- }
59
- }
60
- }
61
- }
62
-
63
- // 2. Fallback: parse image URLs from $raw for adapters that don't use structured $content
64
- if (parts.length === 0) {
65
- const raw = typeof message.$raw === 'string' ? message.$raw : JSON.stringify(message.$raw || '');
66
-
67
- const xmlMatches = raw.match(/<image[^>]+url="([^"]+)"/g);
68
- if (xmlMatches) {
69
- for (const m of xmlMatches) {
70
- const urlMatch = m.match(/url="([^"]+)"/);
71
- if (urlMatch) parts.push({ type: 'image_url', image_url: { url: urlMatch[1] } });
72
- }
73
- }
74
-
75
- const cqMatches = raw.match(/\[CQ:image[^\]]*url=([^\],]+)/g);
76
- if (cqMatches) {
77
- for (const m of cqMatches) {
78
- const urlMatch = m.match(/url=([^\],]+)/);
79
- if (urlMatch) parts.push({ type: 'image_url', image_url: { url: urlMatch[1] } });
80
- }
81
- }
82
- }
83
-
84
- return parts;
85
- }
86
-
87
- function renderOutput(elements: OutputElement[]): string {
88
- const parts: string[] = [];
89
- for (const el of elements) {
90
- switch (el.type) {
91
- case 'text':
92
- if (el.content) parts.push(el.content);
93
- break;
94
- case 'image':
95
- parts.push(`<image url="${el.url}"/>`);
96
- break;
97
- case 'audio':
98
- parts.push(`<audio url="${el.url}"/>`);
99
- break;
100
- case 'video':
101
- parts.push(`<video url="${el.url}"/>`);
102
- break;
103
- case 'card': {
104
- const cp = [`📋 ${el.title}`];
105
- if (el.description) cp.push(el.description);
106
- if (el.fields?.length)
107
- for (const f of el.fields) cp.push(` ${f.label}: ${f.value}`);
108
- if (el.imageUrl) cp.push(`<image url="${el.imageUrl}"/>`);
109
- parts.push(cp.join('\n'));
110
- break;
111
- }
112
- case 'file':
113
- parts.push(`📎 ${el.name}: ${el.url}`);
114
- break;
115
- }
116
- }
117
- return parts.join('\n') || '';
118
- }
119
-
120
- export function registerAITrigger(refs: AIServiceRefs): void {
121
- const plugin = getPlugin();
122
- const { useContext, root, logger } = plugin;
123
-
124
- useContext('ai', (ai) => {
125
- const rawConfig = ai.getTriggerConfig();
126
- const triggerConfig = mergeAITriggerConfig(rawConfig);
127
- if (!triggerConfig.enabled) {
128
- logger.info('AI Trigger is disabled');
129
- return;
130
- }
131
-
132
- const dispatcherSvc = root.inject('dispatcher') as
133
- | { replyWithPolish?: (m: Message<any>, s: 'ai' | 'command', c: unknown) => Promise<unknown> }
134
- | undefined;
135
-
136
- const handleAIMessage = async (
137
- message: Message<any>,
138
- content: string,
139
- ) => {
140
- const replyOutbound = async (payload: unknown) => {
141
- if (dispatcherSvc && typeof dispatcherSvc.replyWithPolish === 'function') {
142
- return dispatcherSvc.replyWithPolish(message, 'ai', payload as any);
143
- }
144
- return message.$reply(payload as any);
145
- };
146
-
147
- const t0 = performance.now();
148
- if (!ai.isReady()) return;
149
- if (triggerConfig.thinkingMessage)
150
- await replyOutbound(triggerConfig.thinkingMessage);
151
-
152
- const permissions = inferSenderPermissions(message, triggerConfig);
153
-
154
- // 从 bot 配置中查找 owner(bots[].owner)
155
- const adapterInstance = root.inject(message.$adapter) as
156
- | { bots?: Map<string, { $config?: Record<string, any> }> }
157
- | undefined;
158
- const botConfig = adapterInstance?.bots?.get(message.$bot)?.$config as Record<string, any> | undefined;
159
- const botOwner: string | undefined = botConfig?.owner;
160
-
161
- // 用 bot 级别 owner 覆盖权限判断
162
- const isOwner = botOwner ? String(message.$sender.id) === String(botOwner) : permissions.isOwner;
163
- const permissionLevel = isOwner ? 'owner' as const : permissions.permissionLevel;
164
-
165
- const toolContext: ToolContext = {
166
- platform: message.$adapter,
167
- botId: message.$bot,
168
- sceneId: message.$channel?.id || message.$sender.id,
169
- senderId: message.$sender.id,
170
- message,
171
- scope: permissions.scope,
172
- senderPermissionLevel: permissionLevel,
173
- isGroupAdmin: permissions.isGroupAdmin,
174
- isGroupOwner: permissions.isGroupOwner,
175
- isBotAdmin: isOwner || permissions.isBotAdmin,
176
- isOwner,
177
- };
178
-
179
- const tCollect = performance.now();
180
- const toolService = root.inject('tool');
181
- let externalTools: Tool[] = [];
182
- if (toolService) {
183
- externalTools = toolService.collectAll(root);
184
- externalTools = toolService.filterByContext(externalTools, toolContext);
185
- }
186
- logger.debug(`[AI Handler] 工具收集: ${externalTools.length} 个, ${(performance.now() - tCollect).toFixed(0)}ms`);
187
-
188
- try {
189
- const timeout = new Promise<never>((_, rej) =>
190
- setTimeout(() => rej(new Error('AI 响应超时')), triggerConfig.timeout),
191
- );
192
-
193
- let responseText: string;
194
- if (refs.zhinAgent) {
195
- const mediaParts = extractMediaParts(message);
196
- let elements: OutputElement[];
197
- if (mediaParts.length > 0) {
198
- const parts: ContentPart[] = [];
199
- if (content) parts.push({ type: 'text', text: content });
200
- parts.push(...mediaParts);
201
- elements = await Promise.race([
202
- refs.zhinAgent.processMultimodal(parts, toolContext),
203
- timeout,
204
- ]);
205
- } else {
206
- elements = await Promise.race([
207
- refs.zhinAgent.process(content, toolContext, externalTools),
208
- timeout,
209
- ]);
210
- }
211
- responseText = renderOutput(elements);
212
- } else {
213
- const response = await Promise.race([
214
- ai.process(content, toolContext, externalTools),
215
- timeout,
216
- ]);
217
- responseText = typeof response === 'string' ? response : '';
218
- }
219
-
220
- if (responseText) await replyOutbound(parseRichMediaContent(responseText));
221
- logger.info(`[AI Handler] 总耗时: ${(performance.now() - t0).toFixed(0)}ms`);
222
- } catch (error) {
223
- const msg = error instanceof Error ? error.message : String(error);
224
- logger.warn(`[AI Handler] 失败 (${(performance.now() - t0).toFixed(0)}ms): ${msg}`);
225
- await replyOutbound(triggerConfig.errorTemplate.replace('{error}', msg));
226
- }
227
- };
228
-
229
- const dispatcher = root.inject('dispatcher');
230
-
231
- if (dispatcher && typeof dispatcher.setAIHandler === 'function') {
232
- dispatcher.setAITriggerMatcher((message: Message<any>) =>
233
- shouldTriggerAI(message, triggerConfig),
234
- );
235
- dispatcher.setAIHandler(handleAIMessage);
236
- logger.debug('AI Handler registered via MessageDispatcher');
237
- return () => { logger.info('AI Handler unregistered'); };
238
- }
239
-
240
- const aiMw = async (
241
- message: Message<any>,
242
- next: () => Promise<void>,
243
- ) => {
244
- const { triggered, content } = shouldTriggerAI(message, triggerConfig);
245
- if (!triggered) return await next();
246
- await handleAIMessage(message, content);
247
- await next();
248
- };
249
- const dispose = root.addMiddleware(aiMw);
250
- logger.debug('AI Trigger middleware registered (fallback mode)');
251
- return () => { dispose(); };
252
- });
253
- }