@zhin.js/agent 0.0.16 → 0.0.17

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 (45) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/lib/builtin-tools.d.ts +93 -0
  3. package/lib/builtin-tools.d.ts.map +1 -1
  4. package/lib/builtin-tools.js +358 -3
  5. package/lib/builtin-tools.js.map +1 -1
  6. package/lib/init/register-builtin-tools.d.ts.map +1 -1
  7. package/lib/init/register-builtin-tools.js +138 -2
  8. package/lib/init/register-builtin-tools.js.map +1 -1
  9. package/lib/init/register-db-models.js +3 -3
  10. package/lib/init/register-db-models.js.map +1 -1
  11. package/lib/init/register-db-upgrade.js +2 -2
  12. package/lib/init/register-db-upgrade.js.map +1 -1
  13. package/lib/init/register-management-tools.js +1 -1
  14. package/lib/init/register-management-tools.js.map +1 -1
  15. package/lib/service.d.ts +4 -8
  16. package/lib/service.d.ts.map +1 -1
  17. package/lib/service.js +23 -112
  18. package/lib/service.js.map +1 -1
  19. package/lib/subagent.js +1 -1
  20. package/lib/subagent.js.map +1 -1
  21. package/lib/zhin-agent/builtin-tools.d.ts +1 -1
  22. package/lib/zhin-agent/builtin-tools.d.ts.map +1 -1
  23. package/lib/zhin-agent/config.d.ts +8 -1
  24. package/lib/zhin-agent/config.d.ts.map +1 -1
  25. package/lib/zhin-agent/config.js +8 -1
  26. package/lib/zhin-agent/config.js.map +1 -1
  27. package/lib/zhin-agent/index.d.ts +3 -3
  28. package/lib/zhin-agent/index.d.ts.map +1 -1
  29. package/lib/zhin-agent/index.js +52 -29
  30. package/lib/zhin-agent/index.js.map +1 -1
  31. package/lib/zhin-agent/tool-collector.js +1 -1
  32. package/package.json +3 -3
  33. package/src/builtin-tools.ts +443 -3
  34. package/src/init/register-ai-trigger.ts +1 -1
  35. package/src/init/register-builtin-tools.ts +135 -2
  36. package/src/init/register-db-models.ts +3 -3
  37. package/src/init/register-db-upgrade.ts +2 -2
  38. package/src/init/register-management-tools.ts +1 -1
  39. package/src/init/register-message-recorder.ts +1 -1
  40. package/src/service.ts +28 -132
  41. package/src/subagent.ts +1 -1
  42. package/src/zhin-agent/builtin-tools.ts +1 -1
  43. package/src/zhin-agent/config.ts +10 -2
  44. package/src/zhin-agent/index.ts +51 -29
  45. package/src/zhin-agent/tool-collector.ts +1 -1
@@ -5,11 +5,14 @@
5
5
  import * as fs from 'fs';
6
6
  import * as os from 'os';
7
7
  import * as path from 'path';
8
- import { getPlugin, type Tool, type SkillFeature } from '@zhin.js/core';
8
+ import { getPlugin, type Tool, type SkillFeature, type AgentPreset } from '@zhin.js/core';
9
9
  import {
10
10
  collectPluginSkillSearchRoots,
11
11
  createBuiltinTools,
12
12
  discoverWorkspaceSkills,
13
+ discoverWorkspaceAgents,
14
+ discoverWorkspaceTools,
15
+ buildToolFromMeta,
13
16
  loadAlwaysSkillsContent,
14
17
  buildSkillsSummaryXML,
15
18
  } from '../builtin-tools.js';
@@ -33,6 +36,10 @@ export function registerBuiltinTools(refs: AIServiceRefs): void {
33
36
  const builtinTools = createBuiltinTools({
34
37
  skillInstructionMaxChars: resolveSkillInstructionMaxChars(fullCfg, modelName),
35
38
  pluginSkillRootsResolver: () => collectPluginSkillSearchRoots(root),
39
+ skillFileLookup: (name: string) => {
40
+ const skillFeature = root.inject?.('skill') as SkillFeature | undefined;
41
+ return skillFeature?.get(name)?.filePath;
42
+ },
36
43
  });
37
44
  const disposers: (() => void)[] = [];
38
45
  for (const tool of builtinTools) disposers.push(toolService.addTool(tool, root.name));
@@ -42,6 +49,7 @@ export function registerBuiltinTools(refs: AIServiceRefs): void {
42
49
 
43
50
  let skillWatchers: fs.FSWatcher[] = [];
44
51
  let skillReloadDebounce: ReturnType<typeof setTimeout> | null = null;
52
+ let toolReloadDebounce: ReturnType<typeof setTimeout> | null = null;
45
53
 
46
54
  async function syncWorkspaceSkills(): Promise<number> {
47
55
  const skillFeature = root.inject?.('skill') as SkillFeature | undefined;
@@ -71,11 +79,89 @@ export function registerBuiltinTools(refs: AIServiceRefs): void {
71
79
  keywords: s.keywords || [],
72
80
  tags: s.tags || [],
73
81
  pluginName: root.name,
82
+ filePath: s.filePath,
83
+ always: s.always,
74
84
  }, root.name);
75
85
  }
76
86
  return skills.length;
77
87
  }
78
88
 
89
+ // 文件化 Tool 的 disposer(用于热重载时移除旧 tool)
90
+ let toolFileDisposers: (() => void)[] = [];
91
+
92
+ /**
93
+ * Discover *.tool.md files and register them as tools.
94
+ */
95
+ async function syncWorkspaceTools(): Promise<number> {
96
+ // 移除之前文件化注册的 tool
97
+ for (const d of toolFileDisposers) d();
98
+ toolFileDisposers = [];
99
+
100
+ const toolMetas = await discoverWorkspaceTools(root);
101
+ if (toolMetas.length === 0) return 0;
102
+
103
+ let added = 0;
104
+ for (const meta of toolMetas) {
105
+ // 跳过已通过程序化方式注册的同名 tool
106
+ if (toolService.get(meta.name)) {
107
+ logger.debug(`Tool '${meta.name}' 已存在(程序化注册),跳过文件化版本`);
108
+ continue;
109
+ }
110
+ const tool = await buildToolFromMeta(meta);
111
+ if (!tool) continue;
112
+ const dispose = toolService.addTool(tool, root.name);
113
+ toolFileDisposers.push(dispose);
114
+ added++;
115
+ }
116
+ return added;
117
+ }
118
+
119
+ // 已发现的 Agent 预设(本地缓存,无需存储到 Plugin 树)
120
+ const discoveredAgents = new Map<string, AgentPreset>();
121
+
122
+ /**
123
+ * Discover *.agent.md files and register agent presets.
124
+ */
125
+ async function syncWorkspaceAgents(): Promise<number> {
126
+ const agentMetas = await discoverWorkspaceAgents(root);
127
+ if (agentMetas.length === 0) return 0;
128
+ const allRegisteredTools = toolService.getAll();
129
+ const toolNameIndex = new Map<string, import('@zhin.js/core').Tool>();
130
+ for (const t of allRegisteredTools) {
131
+ toolNameIndex.set(t.name, t);
132
+ }
133
+ let added = 0;
134
+ for (const meta of agentMetas) {
135
+ if (discoveredAgents.has(meta.name)) continue;
136
+ const associatedTools: import('@zhin.js/core').Tool[] = [];
137
+ for (const toolName of meta.toolNames || []) {
138
+ const tool = toolService.get(toolName) || toolNameIndex.get(toolName);
139
+ if (tool) associatedTools.push(tool);
140
+ }
141
+ // Read body as systemPrompt
142
+ let systemPrompt: string | undefined;
143
+ try {
144
+ const content = await fs.promises.readFile(meta.filePath, 'utf-8');
145
+ const body = content.replace(/^---\s*\n[\s\S]*?\n---\s*(?:\n|$)/, '').trim();
146
+ if (body) systemPrompt = body;
147
+ } catch { /* ignore */ }
148
+ discoveredAgents.set(meta.name, {
149
+ name: meta.name,
150
+ description: meta.description,
151
+ keywords: meta.keywords,
152
+ tags: meta.tags,
153
+ tools: associatedTools.length > 0 ? associatedTools : undefined,
154
+ systemPrompt,
155
+ model: meta.model,
156
+ provider: meta.provider,
157
+ maxIterations: meta.maxIterations,
158
+ filePath: meta.filePath,
159
+ });
160
+ added++;
161
+ }
162
+ return added;
163
+ }
164
+
79
165
  (async () => {
80
166
  // Step 1: discover workspace skills
81
167
  try {
@@ -88,6 +174,26 @@ export function registerBuiltinTools(refs: AIServiceRefs): void {
88
174
  logger.warn(`Failed to discover workspace skills: ${e.message}`);
89
175
  }
90
176
 
177
+ // Step 1b: discover *.tool.md file-based tools
178
+ try {
179
+ const toolCount = await syncWorkspaceTools();
180
+ if (toolCount > 0) {
181
+ logger.info(`Registered ${toolCount} workspace file-based tools`);
182
+ }
183
+ } catch (e: any) {
184
+ logger.warn(`Failed to discover workspace tools: ${e.message}`);
185
+ }
186
+
187
+ // Step 1c: discover *.agent.md agent presets
188
+ try {
189
+ const agentCount = await syncWorkspaceAgents();
190
+ if (agentCount > 0) {
191
+ logger.info(`Registered ${agentCount} workspace agent presets`);
192
+ }
193
+ } catch (e: any) {
194
+ logger.debug(`Failed to discover workspace agents: ${e.message}`);
195
+ }
196
+
91
197
  // Step 2: load bootstrap files
92
198
  const loadedFiles: string[] = [];
93
199
  try {
@@ -140,6 +246,30 @@ export function registerBuiltinTools(refs: AIServiceRefs): void {
140
246
  bootstrapFiles: loadedFiles,
141
247
  }));
142
248
 
249
+ // Hot-reload tool directories
250
+ const workspaceToolDir = path.join(process.cwd(), 'tools');
251
+ const onToolDirChange = () => {
252
+ if (toolReloadDebounce) clearTimeout(toolReloadDebounce);
253
+ toolReloadDebounce = setTimeout(async () => {
254
+ toolReloadDebounce = null;
255
+ try {
256
+ const count = await syncWorkspaceTools();
257
+ if (count >= 0) logger.info(`[Tool热重载] 已更新,工作区文件化Tool: ${count}`);
258
+ } catch (e: any) {
259
+ logger.warn(`[Tool热重载] 失败: ${e.message}`);
260
+ }
261
+ }, 400);
262
+ };
263
+ if (fs.existsSync(workspaceToolDir)) {
264
+ try {
265
+ const w = fs.watch(workspaceToolDir, { recursive: true }, onToolDirChange);
266
+ skillWatchers.push(w);
267
+ logger.debug(`[Tool热重载] 监听目录: ${workspaceToolDir}`);
268
+ } catch (e: any) {
269
+ logger.debug(`[Tool热重载] 无法监听 ${workspaceToolDir}: ${e.message}`);
270
+ }
271
+ }
272
+
143
273
  // Hot-reload skill directories
144
274
  const workspaceSkillDir = path.join(process.cwd(), 'skills');
145
275
  const localSkillDir = path.join(os.homedir(), '.zhin', 'skills');
@@ -157,7 +287,7 @@ export function registerBuiltinTools(refs: AIServiceRefs): void {
157
287
  refs.zhinAgent.setSkillsSummaryXML(skillsXml);
158
288
  }
159
289
  await triggerAIHook(createAIHookEvent('agent', 'skills-reloaded', undefined, { skillCount: count }));
160
- if (count >= 0) logger.info(`[技能热重载] 已更新,当前工作区技能数: ${count}`);
290
+ if (count >= 0) logger.info(`[技能热重载] 已更新,工作区技能: ${count}`);
161
291
  } catch (e: any) {
162
292
  logger.warn(`[技能热重载] 失败: ${e.message}`);
163
293
  }
@@ -178,9 +308,12 @@ export function registerBuiltinTools(refs: AIServiceRefs): void {
178
308
 
179
309
  return () => {
180
310
  disposers.forEach(d => d());
311
+ toolFileDisposers.forEach(d => d());
312
+ toolFileDisposers = [];
181
313
  skillWatchers.forEach(w => w.close());
182
314
  skillWatchers = [];
183
315
  if (skillReloadDebounce) clearTimeout(skillReloadDebounce);
316
+ if (toolReloadDebounce) clearTimeout(toolReloadDebounce);
184
317
  };
185
318
  });
186
319
  }
@@ -2,9 +2,9 @@
2
2
  * Define AI-related database models (7 tables).
3
3
  */
4
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';
5
+ import { AI_SESSION_MODEL } from '@zhin.js/ai';
6
+ import { CHAT_MESSAGE_MODEL, CONTEXT_SUMMARY_MODEL } from '@zhin.js/ai';
7
+ import { AI_MESSAGE_MODEL, AI_SUMMARY_MODEL } from '@zhin.js/ai';
8
8
  import { AI_USER_PROFILE_MODEL } from '../user-profile.js';
9
9
  import { AI_FOLLOWUP_MODEL } from '../follow-up.js';
10
10
 
@@ -7,8 +7,8 @@
7
7
  import './types.js';
8
8
  import { getPlugin } from '@zhin.js/core';
9
9
  import type { AIConfig } from '@zhin.js/core';
10
- import { createDatabaseSessionManager } from '../session.js';
11
- import { createContextManager } from '../context-manager.js';
10
+ import { createDatabaseSessionManager } from '@zhin.js/ai';
11
+ import { createContextManager } from '@zhin.js/ai';
12
12
  import type { AIServiceRefs } from './shared-refs.js';
13
13
 
14
14
  export function registerDbUpgrade(refs: AIServiceRefs): void {
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import './types.js';
5
5
  import { getPlugin, Message, ZhinTool } from '@zhin.js/core';
6
- import { SessionManager } from '../session.js';
6
+ import { SessionManager } from '@zhin.js/ai';
7
7
 
8
8
  export function registerManagementTools(): void {
9
9
  const plugin = getPlugin();
@@ -2,7 +2,7 @@
2
2
  * Register middleware that records messages to the context manager.
3
3
  */
4
4
  import { getPlugin, Message } from '@zhin.js/core';
5
- import type { MessageRecord } from '../context-manager.js';
5
+ import type { MessageRecord } from '@zhin.js/ai';
6
6
  import type { AIServiceRefs } from './shared-refs.js';
7
7
 
8
8
  export function registerMessageRecorder(refs: AIServiceRefs): void {
package/src/service.ts CHANGED
@@ -25,14 +25,28 @@ import {
25
25
  import {
26
26
  SessionManager,
27
27
  createMemorySessionManager,
28
- createDatabaseSessionManager,
29
- } from './session.js';
30
- import { Agent, createAgent } from './agent.js';
28
+ } from '@zhin.js/ai';
29
+ import { Agent, createAgent } from '@zhin.js/ai';
31
30
  import { getBuiltinTools } from './tools.js';
32
- import type { ContextManager, ContextConfig } from './context-manager.js';
31
+ import type { ContextManager, ContextConfig } from '@zhin.js/ai';
32
+ import { PERM_MAP } from './zhin-agent/config.js';
33
33
 
34
34
  const aiLogger = new Logger(null, 'AI');
35
35
 
36
+ /** Provider 注册表:key → 构造函数 + 是否需要 apiKey */
37
+ const PROVIDER_REGISTRY: Array<{
38
+ key: keyof NonNullable<AIConfig['providers']>;
39
+ factory: new (config: any) => AIProvider;
40
+ requireApiKey: boolean;
41
+ }> = [
42
+ { key: 'openai', factory: OpenAIProvider, requireApiKey: true },
43
+ { key: 'anthropic', factory: AnthropicProvider, requireApiKey: true },
44
+ { key: 'deepseek', factory: DeepSeekProvider, requireApiKey: true },
45
+ { key: 'moonshot', factory: MoonshotProvider, requireApiKey: true },
46
+ { key: 'zhipu', factory: ZhipuProvider, requireApiKey: true },
47
+ { key: 'ollama', factory: OllamaProvider, requireApiKey: false },
48
+ ];
49
+
36
50
  export class AIService {
37
51
  private providers: Map<string, AIProvider> = new Map();
38
52
  private defaultProvider: string;
@@ -55,23 +69,11 @@ export class AIService {
55
69
  this.sessions = createMemorySessionManager(this.sessionConfig);
56
70
  this.builtinTools = getBuiltinTools().map(tool => this.convertToolToAgentTool(tool.toTool()));
57
71
 
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));
72
+ for (const { key, factory, requireApiKey } of PROVIDER_REGISTRY) {
73
+ const providerConfig = config.providers?.[key];
74
+ if (!providerConfig) continue;
75
+ if (requireApiKey && !(providerConfig as any).apiKey) continue;
76
+ this.registerProvider(new factory(providerConfig as any));
75
77
  }
76
78
  }
77
79
 
@@ -79,98 +81,15 @@ export class AIService {
79
81
  return this.providers.size > 0;
80
82
  }
81
83
 
82
- private static readonly PRE_EXEC_TIMEOUT = 10_000;
83
-
84
84
  async process(
85
85
  content: string,
86
86
  context: ToolContext,
87
- tools: Tool[],
88
- ): Promise<string | AsyncIterable<string>> {
87
+ _tools: Tool[],
88
+ ): Promise<string> {
89
89
  const { platform, senderId, sceneId } = context;
90
90
  const sessionId = SessionManager.generateId(platform || '', senderId || '', sceneId);
91
- const allTools = this.collectAllToolsWithExternal(tools);
92
- const baseSystemPrompt = 'You are a helpful AI assistant. Reply in the language specified in [User profile] (key: language / preferred_language), or in the user\'s message language if not set.';
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${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 = `You are a helpful AI assistant. Reply in the language specified in [User profile] (key: language / preferred_language), or in the user's message language if not set.\n\nPre-fetched data:\n${preExecutedData}\n\nAnswer the user's question based on the data above. Do not mention data sources or tool names. Summarize clearly and highlight key information.`;
146
- finalResponse = await this.simpleChat(content, singleShotPrompt);
147
- } else {
148
- const agentSystemPrompt = `You are a helpful AI assistant. Reply in the language specified in [User profile] (key: language / preferred_language), or in the user's message language if not set.
149
- ${preExecutedData ? `\nPre-fetched data:\n${preExecutedData}\n` : ''}
150
- ## Requirements
151
- - After calling tools, give a complete answer based on the results; do not mention tool names or data sources
152
- - Summarize in natural language and highlight key information`;
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;
91
+ const systemPrompt = 'You are a helpful AI assistant. Reply in the language specified in [User profile] (key: language / preferred_language), or in the user\'s message language if not set.';
92
+ return this.finishAndSave(sessionId, content, systemPrompt, sceneId);
174
93
  }
175
94
 
176
95
  private async finishAndSave(sessionId: string, content: string, systemPrompt: string, sceneId?: string): Promise<string> {
@@ -183,13 +102,6 @@ ${preExecutedData ? `\nPre-fetched data:\n${preExecutedData}\n` : ''}
183
102
  return response;
184
103
  }
185
104
 
186
- private formatToolCallsFallback(toolCalls: { tool: string; args: any; result: any }[]): string {
187
- if (toolCalls.length === 0) return 'Done.';
188
- return toolCalls.map(tc => {
189
- return typeof tc.result === 'string' ? tc.result : JSON.stringify(tc.result, null, 2);
190
- }).join('\n\n');
191
- }
192
-
193
105
  private async simpleChat(content: string, systemPrompt: string): Promise<string> {
194
106
  const provider = this.getProvider();
195
107
  const response = await this.chat({
@@ -203,22 +115,6 @@ ${preExecutedData ? `\nPre-fetched data:\n${preExecutedData}\n` : ''}
203
115
  return typeof msgContent === 'string' ? msgContent : '';
204
116
  }
205
117
 
206
- private collectAllToolsWithExternal(externalTools: Tool[]): AgentTool[] {
207
- const tools: AgentTool[] = [];
208
- tools.push(...this.builtinTools);
209
- tools.push(...this.customTools.values());
210
- for (const tool of externalTools) {
211
- if (tool.name.startsWith('cmd_') || tool.name.startsWith('process_')) continue;
212
- tools.push(this.convertToolToAgentTool(tool));
213
- }
214
- if (tools.length > 30) return tools.slice(0, 30);
215
- return tools;
216
- }
217
-
218
- private static readonly PERM_MAP: Record<string, number> = {
219
- 'user': 0, 'group_admin': 1, 'group_owner': 2, 'bot_admin': 3, 'owner': 4,
220
- };
221
-
222
118
  private convertToolToAgentTool(tool: Tool): AgentTool {
223
119
  const agentTool: AgentTool = {
224
120
  name: tool.name,
@@ -227,7 +123,7 @@ ${preExecutedData ? `\nPre-fetched data:\n${preExecutedData}\n` : ''}
227
123
  execute: async (args) => tool.execute(args),
228
124
  };
229
125
  if (tool.tags?.length) agentTool.tags = tool.tags;
230
- if (tool.permissionLevel) agentTool.permissionLevel = AIService.PERM_MAP[tool.permissionLevel] ?? 0;
126
+ if (tool.permissionLevel) agentTool.permissionLevel = PERM_MAP[tool.permissionLevel] ?? 0;
231
127
  if (tool.keywords?.length) agentTool.keywords = tool.keywords;
232
128
  return agentTool;
233
129
  }
package/src/subagent.ts CHANGED
@@ -11,7 +11,7 @@
11
11
  import { randomUUID } from 'crypto';
12
12
  import { Logger } from '@zhin.js/core';
13
13
  import type { AIProvider, AgentTool } from '@zhin.js/core';
14
- import { createAgent } from './agent.js';
14
+ import { createAgent } from '@zhin.js/ai';
15
15
  import type { ZhinAgentConfig } from './zhin-agent/config.js';
16
16
  import { applyExecPolicyToTools } from './zhin-agent/exec-policy.js';
17
17
 
@@ -7,7 +7,7 @@
7
7
 
8
8
  import type { ToolContext } from '@zhin.js/core';
9
9
  import type { AgentTool } from '@zhin.js/core';
10
- import type { ConversationMemory } from '../conversation-memory.js';
10
+ import type { ConversationMemory } from '@zhin.js/ai';
11
11
  import type { UserProfileStore } from '../user-profile.js';
12
12
  import type { FollowUpManager } from '../follow-up.js';
13
13
  import type { SubagentManager, SubagentOrigin } from '../subagent.js';
@@ -2,8 +2,8 @@
2
2
  * ZhinAgent 配置、常量、类型定义
3
3
  */
4
4
 
5
- import type { RateLimitConfig } from '../rate-limiter.js';
6
- import { DEFAULT_CONTEXT_TOKENS } from '../compaction.js';
5
+ import type { RateLimitConfig } from '@zhin.js/ai';
6
+ import { DEFAULT_CONTEXT_TOKENS } from '@zhin.js/ai';
7
7
 
8
8
  export type ModelSizeHint = 'small' | 'medium' | 'large';
9
9
 
@@ -56,6 +56,14 @@ export const PERM_MAP: Record<string, number> = {
56
56
 
57
57
  export type OnChunkCallback = (chunk: string, full: string) => void;
58
58
 
59
+ /** 上下文感知内置工具的关键词触发正则 */
60
+ export const KEYWORD_TRIGGERS = {
61
+ chatHistory: /之前|上次|历史|回忆|聊过|记录|还记得|曾经/i,
62
+ userProfile: /偏好|设置|配置|档案|资料|时区|timezone|profile|喜好|我叫|叫我|记住我/i,
63
+ scheduleFollowUp: /提醒|定时|过一会|跟进|别忘|取消提醒|reminder|分钟后|小时后/i,
64
+ spawnTask: /后台|子任务|spawn|异步|background|并行|独立处理/i,
65
+ } as const;
66
+
59
67
  export interface ZhinAgentConfig {
60
68
  persona?: string;
61
69
  maxIterations?: number;
@@ -18,21 +18,21 @@
18
18
 
19
19
  import { Logger } from '@zhin.js/core';
20
20
  import type { Tool, ToolContext, SkillFeature, AIProvider, AgentTool, ChatMessage, ContentPart } from '@zhin.js/core';
21
- import { createAgent } from '../agent.js';
22
- import { SessionManager, createMemorySessionManager } from '../session.js';
23
- import type { ContextManager } from '../context-manager.js';
24
- import { ConversationMemory } from '../conversation-memory.js';
25
- import type { OutputElement } from '../output.js';
26
- import { parseOutput } from '../output.js';
21
+ import { createAgent } from '@zhin.js/ai';
22
+ import { SessionManager, createMemorySessionManager } from '@zhin.js/ai';
23
+ import type { ContextManager } from '@zhin.js/ai';
24
+ import { ConversationMemory } from '@zhin.js/ai';
25
+ import type { OutputElement } from '@zhin.js/ai';
26
+ import { parseOutput } from '@zhin.js/ai';
27
27
  import { UserProfileStore } from '../user-profile.js';
28
- import { RateLimiter } from '../rate-limiter.js';
29
- import { detectTone } from '../tone-detector.js';
28
+ import { RateLimiter } from '@zhin.js/ai';
29
+ import { detectTone } from '@zhin.js/ai';
30
30
  import { FollowUpManager, type FollowUpSender } from '../follow-up.js';
31
31
  import { SubagentManager, type SubagentResultSender } from '../subagent.js';
32
32
  import {
33
33
  pruneHistoryForContext,
34
34
  DEFAULT_CONTEXT_TOKENS,
35
- } from '../compaction.js';
35
+ } from '@zhin.js/ai';
36
36
  import { triggerAIHook, createAIHookEvent } from '../hooks.js';
37
37
 
38
38
  // ── Sub-modules ─────────────────────────────────────────────────────
@@ -40,6 +40,7 @@ import {
40
40
  type ZhinAgentConfig,
41
41
  type OnChunkCallback,
42
42
  DEFAULT_CONFIG,
43
+ KEYWORD_TRIGGERS,
43
44
  } from './config.js';
44
45
  import { applyExecPolicyToTools } from './exec-policy.js';
45
46
  import { collectRelevantTools } from './tool-collector.js';
@@ -62,6 +63,11 @@ export type { ZhinAgentConfig, OnChunkCallback } from './config.js';
62
63
  const logger = new Logger(null, 'ZhinAgent');
63
64
  const now = () => performance.now();
64
65
 
66
+ /** Strip `<think>…</think>` blocks that some reasoning models embed in content. */
67
+ function stripThinkBlocks(text: string): string {
68
+ return text.replace(/<think>[\s\S]*?<\/think>\s*/g, '').trim();
69
+ }
70
+
65
71
  // ============================================================================
66
72
  // ZhinAgent
67
73
  // ============================================================================
@@ -212,16 +218,16 @@ export class ZhinAgent {
212
218
  });
213
219
 
214
220
  // Inject context-aware built-in tools on keyword match
215
- if (/之前|上次|历史|回忆|聊过|记录|还记得|曾经/i.test(content)) {
221
+ if (KEYWORD_TRIGGERS.chatHistory.test(content)) {
216
222
  allTools.push(createChatHistoryTool(sessionId, this.memory));
217
223
  }
218
- if (/偏好|设置|配置|档案|资料|时区|timezone|profile|喜好|我叫|叫我|记住我/i.test(content)) {
224
+ if (KEYWORD_TRIGGERS.userProfile.test(content)) {
219
225
  allTools.push(createUserProfileTool(userId, this.userProfiles));
220
226
  }
221
- if (/提醒|定时|过一会|跟进|别忘|取消提醒|reminder|分钟后|小时后/i.test(content)) {
227
+ if (KEYWORD_TRIGGERS.scheduleFollowUp.test(content)) {
222
228
  allTools.push(createScheduleFollowUpTool(sessionId, context, this.followUps));
223
229
  }
224
- if (this.subagentManager && /后台|子任务|spawn|异步|background|并行|独立处理/i.test(content)) {
230
+ if (this.subagentManager && KEYWORD_TRIGGERS.spawnTask.test(content)) {
225
231
  allTools.push(createSpawnTaskTool(context, this.subagentManager));
226
232
  }
227
233
 
@@ -352,7 +358,7 @@ ${preData ? `\nPre-fetched data:\n${preData}\n` : ''}`;
352
358
 
353
359
  const userMessageWithHistory = buildUserMessageWithHistory(historyMessages, content);
354
360
  const result = await agent.run(userMessageWithHistory, []);
355
- reply = result.content || this.fallbackFormat(result.toolCalls);
361
+ reply = stripThinkBlocks(result.content) || this.fallbackFormat(result.toolCalls);
356
362
  logger.info(`[Agent 路径] 过滤=${filterMs}ms, 记忆=${memMs}ms, Agent=${(now() - tAgent).toFixed(0)}ms, 总=${(now() - t0).toFixed(0)}ms`);
357
363
  }
358
364
 
@@ -433,16 +439,25 @@ ${preData ? `\nPre-fetched data:\n${preData}\n` : ''}`;
433
439
  let reply = '';
434
440
  try {
435
441
  for await (const chunk of this.provider.chatStream({ model: visionModel, messages })) {
436
- const delta = chunk.choices?.[0]?.delta?.content;
437
- if (delta && typeof delta === 'string') {
438
- reply += delta;
439
- if (onChunk) onChunk(delta, reply);
442
+ const delta = chunk.choices?.[0]?.delta;
443
+ if (!delta) continue;
444
+ const text = typeof delta.content === 'string' ? delta.content : '';
445
+ if (text) {
446
+ reply += text;
447
+ if (onChunk) onChunk(text, reply);
440
448
  }
441
449
  }
450
+ reply = stripThinkBlocks(reply);
451
+ if (!reply) {
452
+ logger.warn('[processMultimodal] 流式响应内容为空,尝试非流式回退');
453
+ const response = await this.provider.chat({ model: visionModel, messages });
454
+ const msg = response.choices[0]?.message?.content;
455
+ reply = stripThinkBlocks(typeof msg === 'string' ? msg : '');
456
+ }
442
457
  } catch {
443
458
  const response = await this.provider.chat({ model: visionModel, messages });
444
459
  const msg = response.choices[0]?.message?.content;
445
- reply = typeof msg === 'string' ? msg : '';
460
+ reply = stripThinkBlocks(typeof msg === 'string' ? msg : '');
446
461
  }
447
462
 
448
463
  if (!reply) reply = '抱歉,我无法理解这条消息。';
@@ -474,20 +489,27 @@ ${preData ? `\nPre-fetched data:\n${preData}\n` : ''}`;
474
489
  try {
475
490
  let result = '';
476
491
  for await (const chunk of this.provider.chatStream({ model, messages })) {
477
- const delta = chunk.choices?.[0]?.delta?.content;
478
- if (delta && typeof delta === 'string') {
479
- result += delta;
480
- if (onChunk) onChunk(delta, result);
492
+ const delta = chunk.choices?.[0]?.delta;
493
+ if (!delta) continue;
494
+ const text = typeof delta.content === 'string' ? delta.content : '';
495
+ if (text) {
496
+ result += text;
497
+ if (onChunk) onChunk(text, result);
481
498
  }
482
499
  }
483
- return result;
500
+ result = stripThinkBlocks(result);
501
+ if (result) return result;
502
+ // Streaming returned empty content — fall back to non-streaming
503
+ logger.warn('[streamChat] 流式响应内容为空,尝试非流式回退');
484
504
  } catch {
485
- const response = await this.provider.chat({ model, messages });
486
- const msg = response.choices[0]?.message?.content;
487
- const result = typeof msg === 'string' ? msg : '';
488
- if (onChunk && result) onChunk(result, result);
489
- return result;
505
+ // Stream failed fall back to non-streaming
490
506
  }
507
+ const response = await this.provider.chat({ model, messages });
508
+ const msg = response.choices[0]?.message?.content;
509
+ let result = typeof msg === 'string' ? msg : '';
510
+ result = stripThinkBlocks(result);
511
+ if (onChunk && result) onChunk(result, result);
512
+ return result;
491
513
  }
492
514
 
493
515
  private async saveToSession(
@@ -5,7 +5,7 @@
5
5
  import { Logger } from '@zhin.js/core';
6
6
  import type { Tool, ToolContext, SkillFeature } from '@zhin.js/core';
7
7
  import type { AgentTool } from '@zhin.js/core';
8
- import { Agent } from '../agent.js';
8
+ import { Agent } from '@zhin.js/ai';
9
9
  import type { ZhinAgentConfig } from './config.js';
10
10
  import { PERM_MAP } from './config.js';
11
11