@zhin.js/agent 0.0.0 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +226 -0
- package/lib/agent.d.ts +9 -7
- package/lib/agent.d.ts.map +1 -1
- package/lib/agent.js +63 -29
- package/lib/agent.js.map +1 -1
- package/lib/bootstrap.js +1 -1
- package/lib/bootstrap.js.map +1 -1
- package/lib/builtin-tools.d.ts.map +1 -1
- package/lib/builtin-tools.js +34 -31
- package/lib/builtin-tools.js.map +1 -1
- package/lib/compaction.js +1 -1
- package/lib/compaction.js.map +1 -1
- package/lib/context-manager.js +1 -1
- package/lib/context-manager.js.map +1 -1
- package/lib/conversation-memory.d.ts +11 -0
- package/lib/conversation-memory.d.ts.map +1 -1
- package/lib/conversation-memory.js +39 -1
- package/lib/conversation-memory.js.map +1 -1
- package/lib/cron-engine.d.ts.map +1 -1
- package/lib/cron-engine.js +5 -3
- package/lib/cron-engine.js.map +1 -1
- package/lib/follow-up.js +1 -1
- package/lib/follow-up.js.map +1 -1
- package/lib/hooks.js +1 -1
- package/lib/hooks.js.map +1 -1
- package/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/init/create-zhin-agent.d.ts +3 -0
- package/lib/init/create-zhin-agent.d.ts.map +1 -0
- package/lib/init/create-zhin-agent.js +127 -0
- package/lib/init/create-zhin-agent.js.map +1 -0
- package/lib/init/register-ai-service.d.ts +7 -0
- package/lib/init/register-ai-service.d.ts.map +1 -0
- package/lib/init/register-ai-service.js +42 -0
- package/lib/init/register-ai-service.js.map +1 -0
- package/lib/init/register-ai-trigger.d.ts +7 -0
- package/lib/init/register-ai-trigger.d.ts.map +1 -0
- package/lib/init/register-ai-trigger.js +163 -0
- package/lib/init/register-ai-trigger.js.map +1 -0
- package/lib/init/register-builtin-tools.d.ts +3 -0
- package/lib/init/register-builtin-tools.d.ts.map +1 -0
- package/lib/init/register-builtin-tools.js +179 -0
- package/lib/init/register-builtin-tools.js.map +1 -0
- package/lib/init/register-db-models.d.ts +2 -0
- package/lib/init/register-db-models.d.ts.map +1 -0
- package/lib/init/register-db-models.js +28 -0
- package/lib/init/register-db-models.js.map +1 -0
- package/lib/init/register-db-upgrade.d.ts +10 -0
- package/lib/init/register-db-upgrade.d.ts.map +1 -0
- package/lib/init/register-db-upgrade.js +67 -0
- package/lib/init/register-db-upgrade.js.map +1 -0
- package/lib/init/register-management-tools.d.ts +6 -0
- package/lib/init/register-management-tools.d.ts.map +1 -0
- package/lib/init/register-management-tools.js +68 -0
- package/lib/init/register-management-tools.js.map +1 -0
- package/lib/init/register-message-recorder.d.ts +3 -0
- package/lib/init/register-message-recorder.d.ts.map +1 -0
- package/lib/init/register-message-recorder.js +27 -0
- package/lib/init/register-message-recorder.js.map +1 -0
- package/lib/init/register-tool-service.d.ts +2 -0
- package/lib/init/register-tool-service.d.ts.map +1 -0
- package/lib/init/register-tool-service.js +9 -0
- package/lib/init/register-tool-service.js.map +1 -0
- package/lib/init/shared-refs.d.ts +14 -0
- package/lib/init/shared-refs.d.ts.map +1 -0
- package/lib/init/shared-refs.js +7 -0
- package/lib/init/shared-refs.js.map +1 -0
- package/lib/init/types.d.ts +15 -0
- package/lib/init/types.d.ts.map +1 -0
- package/lib/init/types.js +7 -0
- package/lib/init/types.js.map +1 -0
- package/lib/init.d.ts +14 -17
- package/lib/init.d.ts.map +1 -1
- package/lib/init.js +34 -670
- package/lib/init.js.map +1 -1
- package/lib/rate-limiter.js +1 -1
- package/lib/rate-limiter.js.map +1 -1
- package/lib/service.js +2 -2
- package/lib/service.js.map +1 -1
- package/lib/session.d.ts +7 -0
- package/lib/session.d.ts.map +1 -1
- package/lib/session.js +26 -4
- package/lib/session.js.map +1 -1
- package/lib/storage.d.ts +68 -0
- package/lib/storage.d.ts.map +1 -0
- package/lib/storage.js +105 -0
- package/lib/storage.js.map +1 -0
- package/lib/subagent.js +1 -1
- package/lib/subagent.js.map +1 -1
- package/lib/tools.d.ts.map +1 -1
- package/lib/tools.js +8 -9
- package/lib/tools.js.map +1 -1
- package/lib/user-profile.js +1 -1
- package/lib/user-profile.js.map +1 -1
- package/lib/zhin-agent/builtin-tools.js.map +1 -1
- package/lib/zhin-agent/config.d.ts +1 -1
- package/lib/zhin-agent/config.d.ts.map +1 -1
- package/lib/zhin-agent/config.js.map +1 -1
- package/lib/zhin-agent/index.js +1 -1
- package/lib/zhin-agent/index.js.map +1 -1
- package/lib/zhin-agent/tool-collector.js +3 -3
- package/lib/zhin-agent/tool-collector.js.map +1 -1
- package/package.json +4 -3
- package/src/agent.ts +53 -30
- package/src/bootstrap.ts +1 -1
- package/src/builtin-tools.ts +44 -40
- package/src/compaction.ts +1 -1
- package/src/context-manager.ts +1 -1
- package/src/conversation-memory.ts +43 -1
- package/src/cron-engine.ts +5 -3
- package/src/follow-up.ts +1 -1
- package/src/hooks.ts +1 -1
- package/src/index.ts +10 -0
- package/src/init/create-zhin-agent.ts +133 -0
- package/src/init/register-ai-service.ts +53 -0
- package/src/init/register-ai-trigger.ts +179 -0
- package/src/init/register-builtin-tools.ts +177 -0
- package/src/init/register-db-models.ts +31 -0
- package/src/init/register-db-upgrade.ts +77 -0
- package/src/init/register-management-tools.ts +71 -0
- package/src/init/register-message-recorder.ts +31 -0
- package/src/init/register-tool-service.ts +9 -0
- package/src/init/shared-refs.ts +20 -0
- package/src/init/types.ts +18 -0
- package/src/init.ts +35 -735
- package/src/rate-limiter.ts +1 -1
- package/src/service.ts +4 -4
- package/src/session.ts +26 -4
- package/src/storage.ts +135 -0
- package/src/subagent.ts +1 -1
- package/src/tools.ts +7 -10
- package/src/user-profile.ts +1 -1
- package/src/zhin-agent/builtin-tools.ts +2 -2
- package/src/zhin-agent/config.ts +3 -3
- package/src/zhin-agent/index.ts +1 -1
- package/src/zhin-agent/tool-collector.ts +8 -8
|
@@ -0,0 +1,179 @@
|
|
|
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 '../output.js';
|
|
9
|
+
import type { AIServiceRefs } from './shared-refs.js';
|
|
10
|
+
|
|
11
|
+
function extractImageUrls(message: Message<any>): string[] {
|
|
12
|
+
const urls: string[] = [];
|
|
13
|
+
const raw = typeof message.$raw === 'string' ? message.$raw : JSON.stringify(message.$raw || '');
|
|
14
|
+
|
|
15
|
+
const xmlMatches = raw.match(/<image[^>]+url="([^"]+)"/g);
|
|
16
|
+
if (xmlMatches) {
|
|
17
|
+
for (const m of xmlMatches) {
|
|
18
|
+
const urlMatch = m.match(/url="([^"]+)"/);
|
|
19
|
+
if (urlMatch) urls.push(urlMatch[1]);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const cqMatches = raw.match(/\[CQ:image[^\]]*url=([^\],]+)/g);
|
|
24
|
+
if (cqMatches) {
|
|
25
|
+
for (const m of cqMatches) {
|
|
26
|
+
const urlMatch = m.match(/url=([^\],]+)/);
|
|
27
|
+
if (urlMatch) urls.push(urlMatch[1]);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return urls;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function renderOutput(elements: OutputElement[]): string {
|
|
35
|
+
const parts: string[] = [];
|
|
36
|
+
for (const el of elements) {
|
|
37
|
+
switch (el.type) {
|
|
38
|
+
case 'text':
|
|
39
|
+
if (el.content) parts.push(el.content);
|
|
40
|
+
break;
|
|
41
|
+
case 'image':
|
|
42
|
+
parts.push(`<image url="${el.url}"/>`);
|
|
43
|
+
break;
|
|
44
|
+
case 'audio':
|
|
45
|
+
parts.push(`<audio url="${el.url}"/>`);
|
|
46
|
+
break;
|
|
47
|
+
case 'video':
|
|
48
|
+
parts.push(`<video url="${el.url}"/>`);
|
|
49
|
+
break;
|
|
50
|
+
case 'card': {
|
|
51
|
+
const cp = [`📋 ${el.title}`];
|
|
52
|
+
if (el.description) cp.push(el.description);
|
|
53
|
+
if (el.fields?.length)
|
|
54
|
+
for (const f of el.fields) cp.push(` ${f.label}: ${f.value}`);
|
|
55
|
+
if (el.imageUrl) cp.push(`<image url="${el.imageUrl}"/>`);
|
|
56
|
+
parts.push(cp.join('\n'));
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
case 'file':
|
|
60
|
+
parts.push(`📎 ${el.name}: ${el.url}`);
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return parts.join('\n') || '';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function registerAITrigger(refs: AIServiceRefs): void {
|
|
68
|
+
const plugin = getPlugin();
|
|
69
|
+
const { useContext, root, logger } = plugin;
|
|
70
|
+
|
|
71
|
+
useContext('ai', (ai) => {
|
|
72
|
+
const rawConfig = ai.getTriggerConfig();
|
|
73
|
+
const triggerConfig = mergeAITriggerConfig(rawConfig);
|
|
74
|
+
if (!triggerConfig.enabled) {
|
|
75
|
+
logger.info('AI Trigger is disabled');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const handleAIMessage = async (
|
|
80
|
+
message: Message<any>,
|
|
81
|
+
content: string,
|
|
82
|
+
) => {
|
|
83
|
+
const t0 = performance.now();
|
|
84
|
+
if (!ai.isReady()) return;
|
|
85
|
+
if (triggerConfig.thinkingMessage)
|
|
86
|
+
await message.$reply(triggerConfig.thinkingMessage);
|
|
87
|
+
|
|
88
|
+
const permissions = inferSenderPermissions(message, triggerConfig);
|
|
89
|
+
const toolContext: ToolContext = {
|
|
90
|
+
platform: message.$adapter,
|
|
91
|
+
botId: message.$bot,
|
|
92
|
+
sceneId: message.$channel?.id || message.$sender.id,
|
|
93
|
+
senderId: message.$sender.id,
|
|
94
|
+
message,
|
|
95
|
+
scope: permissions.scope,
|
|
96
|
+
senderPermissionLevel: permissions.permissionLevel,
|
|
97
|
+
isGroupAdmin: permissions.isGroupAdmin,
|
|
98
|
+
isGroupOwner: permissions.isGroupOwner,
|
|
99
|
+
isBotAdmin: permissions.isBotAdmin,
|
|
100
|
+
isOwner: permissions.isOwner,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const tCollect = performance.now();
|
|
104
|
+
const toolService = root.inject('tool');
|
|
105
|
+
let externalTools: Tool[] = [];
|
|
106
|
+
if (toolService) {
|
|
107
|
+
externalTools = toolService.collectAll(root);
|
|
108
|
+
externalTools = toolService.filterByContext(externalTools, toolContext);
|
|
109
|
+
}
|
|
110
|
+
logger.debug(`[AI Handler] 工具收集: ${externalTools.length} 个, ${(performance.now() - tCollect).toFixed(0)}ms`);
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const timeout = new Promise<never>((_, rej) =>
|
|
114
|
+
setTimeout(() => rej(new Error('AI 响应超时')), triggerConfig.timeout),
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
let responseText: string;
|
|
118
|
+
if (refs.zhinAgent) {
|
|
119
|
+
const imageUrls = extractImageUrls(message);
|
|
120
|
+
let elements: OutputElement[];
|
|
121
|
+
if (imageUrls.length > 0) {
|
|
122
|
+
const parts: ContentPart[] = [];
|
|
123
|
+
if (content) parts.push({ type: 'text', text: content });
|
|
124
|
+
for (const url of imageUrls) {
|
|
125
|
+
parts.push({ type: 'image_url', image_url: { url } });
|
|
126
|
+
}
|
|
127
|
+
elements = await Promise.race([
|
|
128
|
+
refs.zhinAgent.processMultimodal(parts, toolContext),
|
|
129
|
+
timeout,
|
|
130
|
+
]);
|
|
131
|
+
} else {
|
|
132
|
+
elements = await Promise.race([
|
|
133
|
+
refs.zhinAgent.process(content, toolContext, externalTools),
|
|
134
|
+
timeout,
|
|
135
|
+
]);
|
|
136
|
+
}
|
|
137
|
+
responseText = renderOutput(elements);
|
|
138
|
+
} else {
|
|
139
|
+
const response = await Promise.race([
|
|
140
|
+
ai.process(content, toolContext, externalTools),
|
|
141
|
+
timeout,
|
|
142
|
+
]);
|
|
143
|
+
responseText = typeof response === 'string' ? response : '';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (responseText) await message.$reply(parseRichMediaContent(responseText));
|
|
147
|
+
logger.info(`[AI Handler] 总耗时: ${(performance.now() - t0).toFixed(0)}ms`);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
150
|
+
logger.warn(`[AI Handler] 失败 (${(performance.now() - t0).toFixed(0)}ms): ${msg}`);
|
|
151
|
+
await message.$reply(triggerConfig.errorTemplate.replace('{error}', msg));
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const dispatcher = root.inject('dispatcher');
|
|
156
|
+
|
|
157
|
+
if (dispatcher && typeof dispatcher.setAIHandler === 'function') {
|
|
158
|
+
dispatcher.setAITriggerMatcher((message: Message<any>) =>
|
|
159
|
+
shouldTriggerAI(message, triggerConfig),
|
|
160
|
+
);
|
|
161
|
+
dispatcher.setAIHandler(handleAIMessage);
|
|
162
|
+
logger.debug('AI Handler registered via MessageDispatcher');
|
|
163
|
+
return () => { logger.info('AI Handler unregistered'); };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const aiMw = async (
|
|
167
|
+
message: Message<any>,
|
|
168
|
+
next: () => Promise<void>,
|
|
169
|
+
) => {
|
|
170
|
+
const { triggered, content } = shouldTriggerAI(message, triggerConfig);
|
|
171
|
+
if (!triggered) return await next();
|
|
172
|
+
await handleAIMessage(message, content);
|
|
173
|
+
await next();
|
|
174
|
+
};
|
|
175
|
+
const dispose = root.addMiddleware(aiMw);
|
|
176
|
+
logger.debug('AI Trigger middleware registered (fallback mode)');
|
|
177
|
+
return () => { dispose(); };
|
|
178
|
+
});
|
|
179
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register built-in system tools (file/shell/web/schedule/memory/skill)
|
|
3
|
+
* and workspace skills with hot-reload support.
|
|
4
|
+
*/
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as os from 'os';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import { getPlugin, type Tool, type SkillFeature } from '@zhin.js/core';
|
|
9
|
+
import { createBuiltinTools, discoverWorkspaceSkills, loadAlwaysSkillsContent, buildSkillsSummaryXML } from '../builtin-tools.js';
|
|
10
|
+
import { resolveSkillInstructionMaxChars, DEFAULT_CONFIG } from '../zhin-agent/config.js';
|
|
11
|
+
import { loadBootstrapFiles, buildContextFiles, buildBootstrapContextSection } from '../bootstrap.js';
|
|
12
|
+
import { triggerAIHook, createAIHookEvent } from '../hooks.js';
|
|
13
|
+
import { createCronTools } from '../cron-engine.js';
|
|
14
|
+
import type { AIServiceRefs } from './shared-refs.js';
|
|
15
|
+
|
|
16
|
+
export function registerBuiltinTools(refs: AIServiceRefs): void {
|
|
17
|
+
const plugin = getPlugin();
|
|
18
|
+
const { useContext, root, logger } = plugin;
|
|
19
|
+
|
|
20
|
+
useContext('ai', 'tool', (ai, toolService) => {
|
|
21
|
+
if (!ai || !toolService) return;
|
|
22
|
+
|
|
23
|
+
const provider = ai.getProvider();
|
|
24
|
+
const agentCfg = ai.getAgentConfig();
|
|
25
|
+
const fullCfg = { ...DEFAULT_CONFIG, ...agentCfg } as Required<import('../zhin-agent/config.js').ZhinAgentConfig>;
|
|
26
|
+
const modelName = provider.models[0] || '';
|
|
27
|
+
const builtinTools = createBuiltinTools({ skillInstructionMaxChars: resolveSkillInstructionMaxChars(fullCfg, modelName) });
|
|
28
|
+
const disposers: (() => void)[] = [];
|
|
29
|
+
for (const tool of builtinTools) disposers.push(toolService.addTool(tool, root.name));
|
|
30
|
+
const cronTools = createCronTools();
|
|
31
|
+
for (const tool of cronTools) disposers.push(toolService.addTool(tool, root.name));
|
|
32
|
+
logger.info(`Registered ${builtinTools.length} built-in + ${cronTools.length} cron tools`);
|
|
33
|
+
|
|
34
|
+
let skillWatchers: fs.FSWatcher[] = [];
|
|
35
|
+
let skillReloadDebounce: ReturnType<typeof setTimeout> | null = null;
|
|
36
|
+
|
|
37
|
+
async function syncWorkspaceSkills(): Promise<number> {
|
|
38
|
+
const skillFeature = root.inject?.('skill') as SkillFeature | undefined;
|
|
39
|
+
if (!skillFeature) return 0;
|
|
40
|
+
const existing = skillFeature.getByPlugin(root.name);
|
|
41
|
+
for (const s of existing) skillFeature.remove(s);
|
|
42
|
+
const skills = await discoverWorkspaceSkills();
|
|
43
|
+
if (skills.length === 0) return 0;
|
|
44
|
+
const allRegisteredTools = toolService.getAll();
|
|
45
|
+
const toolNameIndex = new Map<string, Tool>();
|
|
46
|
+
for (const t of allRegisteredTools) {
|
|
47
|
+
toolNameIndex.set(t.name, t);
|
|
48
|
+
const parts = t.name.split('_');
|
|
49
|
+
if (parts.length === 2) toolNameIndex.set(`${parts[1]}_${parts[0]}`, t);
|
|
50
|
+
}
|
|
51
|
+
for (const s of skills) {
|
|
52
|
+
const associatedTools: Tool[] = [];
|
|
53
|
+
const toolNames = s.toolNames || [];
|
|
54
|
+
for (const toolName of toolNames) {
|
|
55
|
+
let tool = toolService.get(toolName) || toolNameIndex.get(toolName);
|
|
56
|
+
if (tool) associatedTools.push(tool);
|
|
57
|
+
}
|
|
58
|
+
skillFeature.add({
|
|
59
|
+
name: s.name,
|
|
60
|
+
description: s.description,
|
|
61
|
+
tools: associatedTools,
|
|
62
|
+
keywords: s.keywords || [],
|
|
63
|
+
tags: s.tags || [],
|
|
64
|
+
pluginName: root.name,
|
|
65
|
+
}, root.name);
|
|
66
|
+
}
|
|
67
|
+
return skills.length;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
(async () => {
|
|
71
|
+
// Step 1: discover workspace skills
|
|
72
|
+
try {
|
|
73
|
+
const count = await syncWorkspaceSkills();
|
|
74
|
+
const skillFeature = root.inject?.('skill') as SkillFeature | undefined;
|
|
75
|
+
if (count > 0 && skillFeature) {
|
|
76
|
+
logger.info(`Registered ${count} workspace skills`);
|
|
77
|
+
}
|
|
78
|
+
} catch (e: any) {
|
|
79
|
+
logger.warn(`Failed to discover workspace skills: ${e.message}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Step 2: load bootstrap files
|
|
83
|
+
const loadedFiles: string[] = [];
|
|
84
|
+
try {
|
|
85
|
+
const workspaceDir = process.cwd();
|
|
86
|
+
const bootstrapFiles = await loadBootstrapFiles(workspaceDir);
|
|
87
|
+
const contextFiles = buildContextFiles(bootstrapFiles);
|
|
88
|
+
|
|
89
|
+
logger.debug(`Bootstrap files loaded (cwd: ${workspaceDir}): ${bootstrapFiles.map(f => f.name + (f.missing ? ' (missing)' : '')).join(', ')}`);
|
|
90
|
+
|
|
91
|
+
const soulFile = contextFiles.find(f => f.path === 'SOUL.md');
|
|
92
|
+
if (soulFile && refs.zhinAgent) loadedFiles.push('SOUL.md');
|
|
93
|
+
|
|
94
|
+
const toolsFile = contextFiles.find(f => f.path === 'TOOLS.md');
|
|
95
|
+
if (toolsFile) loadedFiles.push('TOOLS.md');
|
|
96
|
+
|
|
97
|
+
const agentsFile = contextFiles.find(f => f.path === 'AGENTS.md');
|
|
98
|
+
if (agentsFile) loadedFiles.push('AGENTS.md');
|
|
99
|
+
|
|
100
|
+
if (loadedFiles.length > 0) {
|
|
101
|
+
logger.info(`Loaded bootstrap: ${loadedFiles.join(', ')} → agent prompt`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (refs.zhinAgent && contextFiles.length > 0) {
|
|
105
|
+
const contextSection = buildBootstrapContextSection(contextFiles);
|
|
106
|
+
refs.zhinAgent.setBootstrapContext(contextSection);
|
|
107
|
+
}
|
|
108
|
+
} catch (e: any) {
|
|
109
|
+
logger.debug(`Bootstrap files not loaded: ${e.message}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Step 3: inject always-on skills content + XML summary
|
|
113
|
+
try {
|
|
114
|
+
const skillsForContext = await discoverWorkspaceSkills();
|
|
115
|
+
const alwaysContent = await loadAlwaysSkillsContent(skillsForContext);
|
|
116
|
+
const skillsXml = buildSkillsSummaryXML(skillsForContext);
|
|
117
|
+
if (refs.zhinAgent) {
|
|
118
|
+
refs.zhinAgent.setActiveSkillsContext(alwaysContent);
|
|
119
|
+
refs.zhinAgent.setSkillsSummaryXML(skillsXml);
|
|
120
|
+
}
|
|
121
|
+
} catch (e: unknown) {
|
|
122
|
+
logger.debug(`Skills context not set: ${e instanceof Error ? e.message : String(e)}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Trigger agent:bootstrap hook
|
|
126
|
+
const skillFeature2 = root.inject('skill') as SkillFeature | undefined;
|
|
127
|
+
await triggerAIHook(createAIHookEvent('agent', 'bootstrap', undefined, {
|
|
128
|
+
workspaceDir: process.cwd(),
|
|
129
|
+
toolCount: builtinTools.length,
|
|
130
|
+
skillCount: skillFeature2?.size ?? 0,
|
|
131
|
+
bootstrapFiles: loadedFiles,
|
|
132
|
+
}));
|
|
133
|
+
|
|
134
|
+
// Hot-reload skill directories
|
|
135
|
+
const workspaceSkillDir = path.join(process.cwd(), 'skills');
|
|
136
|
+
const localSkillDir = path.join(os.homedir(), '.zhin', 'skills');
|
|
137
|
+
const onSkillDirChange = () => {
|
|
138
|
+
if (skillReloadDebounce) clearTimeout(skillReloadDebounce);
|
|
139
|
+
skillReloadDebounce = setTimeout(async () => {
|
|
140
|
+
skillReloadDebounce = null;
|
|
141
|
+
try {
|
|
142
|
+
const count = await syncWorkspaceSkills();
|
|
143
|
+
const skillsForContext = await discoverWorkspaceSkills();
|
|
144
|
+
const alwaysContent = await loadAlwaysSkillsContent(skillsForContext);
|
|
145
|
+
const skillsXml = buildSkillsSummaryXML(skillsForContext);
|
|
146
|
+
if (refs.zhinAgent) {
|
|
147
|
+
refs.zhinAgent.setActiveSkillsContext(alwaysContent);
|
|
148
|
+
refs.zhinAgent.setSkillsSummaryXML(skillsXml);
|
|
149
|
+
}
|
|
150
|
+
await triggerAIHook(createAIHookEvent('agent', 'skills-reloaded', undefined, { skillCount: count }));
|
|
151
|
+
if (count >= 0) logger.info(`[技能热重载] 已更新,当前工作区技能数: ${count}`);
|
|
152
|
+
} catch (e: any) {
|
|
153
|
+
logger.warn(`[技能热重载] 失败: ${e.message}`);
|
|
154
|
+
}
|
|
155
|
+
}, 400);
|
|
156
|
+
};
|
|
157
|
+
for (const dir of [workspaceSkillDir, localSkillDir]) {
|
|
158
|
+
if (fs.existsSync(dir)) {
|
|
159
|
+
try {
|
|
160
|
+
const w = fs.watch(dir, { recursive: true }, onSkillDirChange);
|
|
161
|
+
skillWatchers.push(w);
|
|
162
|
+
logger.debug(`[技能热重载] 监听目录: ${dir}`);
|
|
163
|
+
} catch (e: any) {
|
|
164
|
+
logger.debug(`[技能热重载] 无法监听 ${dir}: ${e.message}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
})();
|
|
169
|
+
|
|
170
|
+
return () => {
|
|
171
|
+
disposers.forEach(d => d());
|
|
172
|
+
skillWatchers.forEach(w => w.close());
|
|
173
|
+
skillWatchers = [];
|
|
174
|
+
if (skillReloadDebounce) clearTimeout(skillReloadDebounce);
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Define AI-related database models (7 tables).
|
|
3
|
+
*/
|
|
4
|
+
import { getPlugin } from '@zhin.js/core';
|
|
5
|
+
import { AI_SESSION_MODEL } from '../session.js';
|
|
6
|
+
import { CHAT_MESSAGE_MODEL, CONTEXT_SUMMARY_MODEL } from '../context-manager.js';
|
|
7
|
+
import { AI_MESSAGE_MODEL, AI_SUMMARY_MODEL } from '../conversation-memory.js';
|
|
8
|
+
import { AI_USER_PROFILE_MODEL } from '../user-profile.js';
|
|
9
|
+
import { AI_FOLLOWUP_MODEL } from '../follow-up.js';
|
|
10
|
+
|
|
11
|
+
export function registerDbModels(): void {
|
|
12
|
+
const plugin = getPlugin();
|
|
13
|
+
const { logger } = plugin;
|
|
14
|
+
|
|
15
|
+
const defineModel = (plugin as unknown as Record<string, unknown>).defineModel as
|
|
16
|
+
| ((name: string, def: Record<string, unknown>) => void)
|
|
17
|
+
| undefined;
|
|
18
|
+
|
|
19
|
+
if (typeof defineModel === 'function') {
|
|
20
|
+
defineModel('chat_messages', CHAT_MESSAGE_MODEL);
|
|
21
|
+
defineModel('context_summaries', CONTEXT_SUMMARY_MODEL);
|
|
22
|
+
defineModel('ai_sessions', AI_SESSION_MODEL);
|
|
23
|
+
defineModel('ai_messages', AI_MESSAGE_MODEL);
|
|
24
|
+
defineModel('ai_summaries', AI_SUMMARY_MODEL);
|
|
25
|
+
defineModel('ai_user_profiles', AI_USER_PROFILE_MODEL);
|
|
26
|
+
defineModel('ai_followups', AI_FOLLOWUP_MODEL);
|
|
27
|
+
logger.debug('AI database models registered (7 tables)');
|
|
28
|
+
} else {
|
|
29
|
+
logger.debug('defineModel not available, AI will use in-memory storage');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upgrade AI components from in-memory to database storage when DB becomes ready.
|
|
3
|
+
*
|
|
4
|
+
* FIX: Replaced the previous `setTimeout(100)` race condition with
|
|
5
|
+
* a proper `useContext('database', 'ai', ...)` dual-dependency wait.
|
|
6
|
+
*/
|
|
7
|
+
import './types.js';
|
|
8
|
+
import { getPlugin } from '@zhin.js/core';
|
|
9
|
+
import type { AIConfig } from '@zhin.js/core';
|
|
10
|
+
import { createDatabaseSessionManager } from '../session.js';
|
|
11
|
+
import { createContextManager } from '../context-manager.js';
|
|
12
|
+
import type { AIServiceRefs } from './shared-refs.js';
|
|
13
|
+
|
|
14
|
+
export function registerDbUpgrade(refs: AIServiceRefs): void {
|
|
15
|
+
const plugin = getPlugin();
|
|
16
|
+
const { useContext, root, logger } = plugin;
|
|
17
|
+
|
|
18
|
+
useContext('database', 'ai', async (db: any, _ai) => {
|
|
19
|
+
const aiService = refs.aiService;
|
|
20
|
+
if (!aiService) return;
|
|
21
|
+
|
|
22
|
+
const configService = root.inject('config');
|
|
23
|
+
const appConfig =
|
|
24
|
+
configService?.getPrimary<{ ai?: AIConfig }>() || {};
|
|
25
|
+
const config = appConfig.ai || {};
|
|
26
|
+
|
|
27
|
+
if (config.sessions?.useDatabase === false) return;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const model = db.models?.get('ai_sessions');
|
|
31
|
+
if (!model) return;
|
|
32
|
+
|
|
33
|
+
const dbSession = createDatabaseSessionManager(
|
|
34
|
+
model,
|
|
35
|
+
aiService.getSessionConfig(),
|
|
36
|
+
);
|
|
37
|
+
aiService.setSessionManager(dbSession);
|
|
38
|
+
if (refs.zhinAgent) refs.zhinAgent.setSessionManager(dbSession);
|
|
39
|
+
|
|
40
|
+
const ctxCfg = aiService.getContextConfig();
|
|
41
|
+
if (ctxCfg.enabled !== false) {
|
|
42
|
+
const msgModel = db.models.get('chat_messages');
|
|
43
|
+
const sumModel = db.models.get('context_summaries');
|
|
44
|
+
if (msgModel && sumModel) {
|
|
45
|
+
const ctxMgr = createContextManager(msgModel, sumModel, ctxCfg);
|
|
46
|
+
aiService.setContextManager(ctxMgr);
|
|
47
|
+
if (refs.zhinAgent) refs.zhinAgent.setContextManager(ctxMgr);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (refs.zhinAgent) {
|
|
52
|
+
const aiMsgModel = db.models.get('ai_messages');
|
|
53
|
+
const aiSumModel = db.models.get('ai_summaries');
|
|
54
|
+
if (aiMsgModel && aiSumModel) {
|
|
55
|
+
refs.zhinAgent.upgradeMemoryToDatabase(aiMsgModel, aiSumModel);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const profileModel = db.models.get('ai_user_profiles');
|
|
59
|
+
if (profileModel) {
|
|
60
|
+
refs.zhinAgent.upgradeProfilesToDatabase(profileModel);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const followUpModel = db.models.get('ai_followups');
|
|
64
|
+
if (followUpModel) {
|
|
65
|
+
refs.zhinAgent.upgradeFollowUpsToDatabase(followUpModel);
|
|
66
|
+
refs.zhinAgent.restoreFollowUps().catch(e => {
|
|
67
|
+
logger.warn('FollowUp restore failed:', e);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
logger.debug('AI database storage activated (session, memory, profile, followup)');
|
|
73
|
+
} catch (e) {
|
|
74
|
+
logger.error('AI Session: database setup failed:', e);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register AI management tools (ai.models, ai.clear, ai.health).
|
|
3
|
+
*/
|
|
4
|
+
import './types.js';
|
|
5
|
+
import { getPlugin, Message, ZhinTool } from '@zhin.js/core';
|
|
6
|
+
import { SessionManager } from '../session.js';
|
|
7
|
+
|
|
8
|
+
export function registerManagementTools(): void {
|
|
9
|
+
const plugin = getPlugin();
|
|
10
|
+
const { useContext, root, logger } = plugin;
|
|
11
|
+
|
|
12
|
+
useContext('ai', 'tool', (ai, toolService) => {
|
|
13
|
+
if (!ai || !toolService) return;
|
|
14
|
+
|
|
15
|
+
const listModelsTool = new ZhinTool('ai.models')
|
|
16
|
+
.desc('列出所有可用的 AI 模型')
|
|
17
|
+
.keyword('模型', '可用模型', 'ai模型', 'model', 'models')
|
|
18
|
+
.tag('ai', 'management')
|
|
19
|
+
.execute(async () => {
|
|
20
|
+
const models = await ai.listModels();
|
|
21
|
+
return { providers: models.map(({ provider, models: ml }) => ({ name: provider, models: ml.slice(0, 10), total: ml.length })) };
|
|
22
|
+
})
|
|
23
|
+
.action(async () => {
|
|
24
|
+
const models = await ai.listModels();
|
|
25
|
+
let r = '🤖 可用模型:\n';
|
|
26
|
+
for (const { provider, models: ml } of models) {
|
|
27
|
+
r += `\n【${provider}】\n` + ml.slice(0, 5).map(m => ` • ${m}`).join('\n');
|
|
28
|
+
if (ml.length > 5) r += `\n ... 还有 ${ml.length - 5} 个`;
|
|
29
|
+
}
|
|
30
|
+
return r;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const clearSessionTool = new ZhinTool('ai.clear')
|
|
34
|
+
.desc('清除当前对话的历史记录')
|
|
35
|
+
.keyword('清除', '清空', '重置', 'clear', 'reset')
|
|
36
|
+
.tag('ai', 'session')
|
|
37
|
+
.execute(async (_args, context) => {
|
|
38
|
+
if (!context?.message) return { success: false, error: '无消息上下文' };
|
|
39
|
+
const msg = context.message as Message;
|
|
40
|
+
const sid = SessionManager.generateId(msg.$adapter, msg.$sender.id, msg.$channel?.id);
|
|
41
|
+
await ai.sessions.reset(sid);
|
|
42
|
+
return { success: true, message: '对话历史已清除' };
|
|
43
|
+
})
|
|
44
|
+
.action(async (message: Message) => {
|
|
45
|
+
const sid = SessionManager.generateId(message.$adapter, message.$sender.id, message.$channel?.id);
|
|
46
|
+
await ai.sessions.reset(sid);
|
|
47
|
+
return '✅ 对话历史已清除';
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const healthCheckTool = new ZhinTool('ai.health')
|
|
51
|
+
.desc('检查 AI 服务的健康状态')
|
|
52
|
+
.keyword('健康', '状态', '检查', 'health', 'status')
|
|
53
|
+
.tag('ai', 'management')
|
|
54
|
+
.execute(async () => {
|
|
55
|
+
const h = await ai.healthCheck();
|
|
56
|
+
return { providers: Object.entries(h).map(([n, ok]) => ({ name: n, healthy: ok })) };
|
|
57
|
+
})
|
|
58
|
+
.action(async () => {
|
|
59
|
+
const h = await ai.healthCheck();
|
|
60
|
+
return ['🏥 AI 服务健康状态:'].concat(
|
|
61
|
+
Object.entries(h).map(([p, ok]) => ` ${ok ? '✅' : '❌'} ${p}`),
|
|
62
|
+
).join('\n');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const tools = [listModelsTool, clearSessionTool, healthCheckTool];
|
|
66
|
+
const disposers: (() => void)[] = [];
|
|
67
|
+
for (const tool of tools) disposers.push(toolService.addTool(tool, root.name));
|
|
68
|
+
logger.debug(`Registered ${tools.length} AI management tools`);
|
|
69
|
+
return () => disposers.forEach(d => d());
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Register middleware that records messages to the context manager.
|
|
3
|
+
*/
|
|
4
|
+
import { getPlugin, Message } from '@zhin.js/core';
|
|
5
|
+
import type { MessageRecord } from '../context-manager.js';
|
|
6
|
+
import type { AIServiceRefs } from './shared-refs.js';
|
|
7
|
+
|
|
8
|
+
export function registerMessageRecorder(refs: AIServiceRefs): void {
|
|
9
|
+
const plugin = getPlugin();
|
|
10
|
+
const { root } = plugin;
|
|
11
|
+
|
|
12
|
+
root.addMiddleware(async (message: Message, next: () => Promise<void>) => {
|
|
13
|
+
await next();
|
|
14
|
+
if (refs.aiService?.contextManager) {
|
|
15
|
+
const record: MessageRecord = {
|
|
16
|
+
platform: message.$adapter,
|
|
17
|
+
scene_id: message.$channel?.id || message.$sender.id,
|
|
18
|
+
scene_type: message.$channel?.type || 'private',
|
|
19
|
+
scene_name: (message.$channel as { name?: string })?.name || '',
|
|
20
|
+
sender_id: message.$sender.id,
|
|
21
|
+
sender_name: message.$sender.name || message.$sender.id,
|
|
22
|
+
message:
|
|
23
|
+
typeof message.$raw === 'string'
|
|
24
|
+
? message.$raw
|
|
25
|
+
: JSON.stringify(message.$raw),
|
|
26
|
+
time: message.$timestamp || Date.now(),
|
|
27
|
+
};
|
|
28
|
+
refs.aiService.contextManager.recordMessage(record).catch(() => {});
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared mutable references between init sub-modules.
|
|
3
|
+
*
|
|
4
|
+
* These refs allow the split modules to coordinate without passing
|
|
5
|
+
* the instances through function parameters everywhere.
|
|
6
|
+
*/
|
|
7
|
+
import type { AIService } from '../service.js';
|
|
8
|
+
import type { ZhinAgent } from '../zhin-agent/index.js';
|
|
9
|
+
|
|
10
|
+
export interface AIServiceRefs {
|
|
11
|
+
aiService: AIService | null;
|
|
12
|
+
zhinAgent: ZhinAgent | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function createRefs(): AIServiceRefs {
|
|
16
|
+
return {
|
|
17
|
+
aiService: null,
|
|
18
|
+
zhinAgent: null,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consolidated type augmentation for the agent package.
|
|
3
|
+
* This ensures all init sub-modules can use typed inject/useContext
|
|
4
|
+
* without `as any` casts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { AIService } from '../service.js';
|
|
8
|
+
|
|
9
|
+
// Re-export the augmentation so it is applied when this file is imported
|
|
10
|
+
declare module '@zhin.js/core' {
|
|
11
|
+
namespace Plugin {
|
|
12
|
+
interface Contexts {
|
|
13
|
+
ai: AIService;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export {};
|