@zhin.js/agent 0.0.16 → 0.0.18

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 (69) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/lib/builtin-tools.d.ts +7 -50
  3. package/lib/builtin-tools.d.ts.map +1 -1
  4. package/lib/builtin-tools.js +13 -377
  5. package/lib/builtin-tools.js.map +1 -1
  6. package/lib/discover-agents.d.ts +28 -0
  7. package/lib/discover-agents.d.ts.map +1 -0
  8. package/lib/discover-agents.js +116 -0
  9. package/lib/discover-agents.js.map +1 -0
  10. package/lib/discover-skills.d.ts +49 -0
  11. package/lib/discover-skills.d.ts.map +1 -0
  12. package/lib/discover-skills.js +297 -0
  13. package/lib/discover-skills.js.map +1 -0
  14. package/lib/discover-tools.d.ts +56 -0
  15. package/lib/discover-tools.d.ts.map +1 -0
  16. package/lib/discover-tools.js +263 -0
  17. package/lib/discover-tools.js.map +1 -0
  18. package/lib/discovery-utils.d.ts +27 -0
  19. package/lib/discovery-utils.d.ts.map +1 -0
  20. package/lib/discovery-utils.js +96 -0
  21. package/lib/discovery-utils.js.map +1 -0
  22. package/lib/init/create-zhin-agent.d.ts.map +1 -1
  23. package/lib/init/create-zhin-agent.js +2 -1
  24. package/lib/init/create-zhin-agent.js.map +1 -1
  25. package/lib/init/register-builtin-tools.d.ts.map +1 -1
  26. package/lib/init/register-builtin-tools.js +170 -38
  27. package/lib/init/register-builtin-tools.js.map +1 -1
  28. package/lib/init/register-db-models.js +3 -3
  29. package/lib/init/register-db-models.js.map +1 -1
  30. package/lib/init/register-db-upgrade.js +2 -2
  31. package/lib/init/register-db-upgrade.js.map +1 -1
  32. package/lib/init/register-management-tools.js +1 -1
  33. package/lib/init/register-management-tools.js.map +1 -1
  34. package/lib/service.d.ts +4 -8
  35. package/lib/service.d.ts.map +1 -1
  36. package/lib/service.js +23 -112
  37. package/lib/service.js.map +1 -1
  38. package/lib/subagent.js +1 -1
  39. package/lib/subagent.js.map +1 -1
  40. package/lib/zhin-agent/builtin-tools.d.ts +1 -1
  41. package/lib/zhin-agent/builtin-tools.d.ts.map +1 -1
  42. package/lib/zhin-agent/config.d.ts +8 -1
  43. package/lib/zhin-agent/config.d.ts.map +1 -1
  44. package/lib/zhin-agent/config.js +8 -1
  45. package/lib/zhin-agent/config.js.map +1 -1
  46. package/lib/zhin-agent/index.d.ts +3 -3
  47. package/lib/zhin-agent/index.d.ts.map +1 -1
  48. package/lib/zhin-agent/index.js +52 -29
  49. package/lib/zhin-agent/index.js.map +1 -1
  50. package/lib/zhin-agent/tool-collector.js +1 -1
  51. package/package.json +3 -3
  52. package/src/builtin-tools.ts +21 -404
  53. package/src/discover-agents.ts +138 -0
  54. package/src/discover-skills.ts +325 -0
  55. package/src/discover-tools.ts +302 -0
  56. package/src/discovery-utils.ts +96 -0
  57. package/src/init/create-zhin-agent.ts +2 -1
  58. package/src/init/register-ai-trigger.ts +1 -1
  59. package/src/init/register-builtin-tools.ts +167 -46
  60. package/src/init/register-db-models.ts +3 -3
  61. package/src/init/register-db-upgrade.ts +2 -2
  62. package/src/init/register-management-tools.ts +1 -1
  63. package/src/init/register-message-recorder.ts +1 -1
  64. package/src/service.ts +28 -132
  65. package/src/subagent.ts +1 -1
  66. package/src/zhin-agent/builtin-tools.ts +1 -1
  67. package/src/zhin-agent/config.ts +10 -2
  68. package/src/zhin-agent/index.ts +51 -29
  69. package/src/zhin-agent/tool-collector.ts +1 -1
@@ -0,0 +1,96 @@
1
+ /**
2
+ * 发现模块共用的工具函数
3
+ *
4
+ * 被 builtin-tools / discover-skills / discover-agents / discover-tools 共同依赖,
5
+ * 独立出来以避免循环导入。
6
+ */
7
+
8
+ import * as fs from 'fs';
9
+ import * as os from 'os';
10
+ import * as path from 'path';
11
+ import type { Plugin } from '@zhin.js/core';
12
+
13
+ /** 将 unknown 错误转为字符串 */
14
+ export function errMsg(e: unknown): string {
15
+ return e instanceof Error ? e.message : String(e);
16
+ }
17
+
18
+ /** 获取 data/ 目录路径,自动创建 */
19
+ export function getDataDir(): string {
20
+ const dir = path.join(process.cwd(), 'data');
21
+ fs.mkdirSync(dir, { recursive: true });
22
+ return dir;
23
+ }
24
+
25
+ /** 展开路径中的 ~ 为实际 home 目录 */
26
+ export function expandHome(p: string): string {
27
+ if (p === '~') return os.homedir();
28
+ if (p.startsWith('~/') || p.startsWith('~\\')) return path.join(os.homedir(), p.slice(2));
29
+ return p;
30
+ }
31
+
32
+ /** Workspace / ~/.zhin / data 下 skills 根目录(与 activate_skill 扫描顺序一致的前缀) */
33
+ export function buildStandardSkillDirs(): string[] {
34
+ return [
35
+ path.join(process.cwd(), 'skills'),
36
+ path.join(os.homedir(), '.zhin', 'skills'),
37
+ path.join(getDataDir(), 'skills'),
38
+ ];
39
+ }
40
+
41
+ /**
42
+ * 从根插件树收集:根插件与**直接子插件**包目录下的 `skills/`(其下为 `<name>/SKILL.md`)
43
+ */
44
+ export function collectPluginSkillSearchRoots(root: Plugin | null | undefined): string[] {
45
+ if (!root) return [];
46
+ const dirs: string[] = [];
47
+ const push = (d: string) => {
48
+ if (d && !dirs.includes(d)) dirs.push(d);
49
+ };
50
+ const fromPlugin = (p: Plugin) => {
51
+ if (!p?.filePath) return;
52
+ const dir = path.dirname(p.filePath);
53
+ push(path.join(dir, 'skills'));
54
+ // Also check package root when filePath is under src/ or lib/
55
+ const dirName = path.basename(dir);
56
+ if (dirName === 'src' || dirName === 'lib') {
57
+ push(path.join(path.dirname(dir), 'skills'));
58
+ }
59
+ };
60
+ fromPlugin(root);
61
+ for (const child of root.children || []) {
62
+ fromPlugin(child);
63
+ }
64
+ return dirs;
65
+ }
66
+
67
+ /**
68
+ * 技能发现与 activate_skill 查找共用:标准目录 + 已加载插件包 skills/
69
+ */
70
+ export function getSkillSearchDirectories(root?: Plugin | null): string[] {
71
+ const list = [...buildStandardSkillDirs()];
72
+ for (const d of collectPluginSkillSearchRoots(root ?? undefined)) {
73
+ if (!list.includes(d)) list.push(d);
74
+ }
75
+ return list;
76
+ }
77
+
78
+ export function mergeSkillDirsWithResolver(resolver?: () => string[]): string[] {
79
+ const list = [...buildStandardSkillDirs()];
80
+ for (const d of resolver?.() ?? []) {
81
+ if (d && !list.includes(d)) list.push(d);
82
+ }
83
+ return list;
84
+ }
85
+
86
+ /** 将 Node 文件错误转为 miniclawd 风格的结构化短句,便于模型区分并重试 */
87
+ export function nodeErrToFileMessage(err: unknown, filePath: string, kind: 'read' | 'write' | 'edit' | 'list'): string {
88
+ const e = err as NodeJS.ErrnoException;
89
+ if (e?.code === 'ENOENT') {
90
+ if (kind === 'list') return `Error: Directory not found: ${filePath}`;
91
+ return `Error: File not found: ${filePath}`;
92
+ }
93
+ if (e?.code === 'EACCES') return `Error: Permission denied: ${filePath}`;
94
+ const action = kind === 'read' ? 'reading file' : kind === 'write' ? 'writing file' : kind === 'edit' ? 'editing file' : 'listing directory';
95
+ return `Error ${action}: ${e?.message ?? String(err)}`;
96
+ }
@@ -5,7 +5,8 @@
5
5
  import * as path from 'path';
6
6
  import { getPlugin, Scheduler, getScheduler, setScheduler, type MessageType, type SendOptions } from '@zhin.js/core';
7
7
  import { ZhinAgent } from '../zhin-agent/index.js';
8
- import { collectPluginSkillSearchRoots, createBuiltinTools } from '../builtin-tools.js';
8
+ import { createBuiltinTools } from '../builtin-tools.js';
9
+ import { collectPluginSkillSearchRoots } from '../discovery-utils.js';
9
10
  import { resolveSkillInstructionMaxChars, DEFAULT_CONFIG } from '../zhin-agent/config.js';
10
11
  import { PersistentCronEngine, setCronManager } from '../cron-engine.js';
11
12
  import type { AIServiceRefs } from './shared-refs.js';
@@ -5,7 +5,7 @@ import './types.js';
5
5
  import { getPlugin, Message, shouldTriggerAI, inferSenderPermissions, parseRichMediaContent, mergeAITriggerConfig } from '@zhin.js/core';
6
6
  import type { Tool, ToolContext } from '@zhin.js/core';
7
7
  import type { ContentPart } from '@zhin.js/core';
8
- import type { OutputElement } from '../output.js';
8
+ import type { OutputElement } from '@zhin.js/ai';
9
9
  import type { AIServiceRefs } from './shared-refs.js';
10
10
 
11
11
  /**
@@ -5,14 +5,12 @@
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';
9
- import {
10
- collectPluginSkillSearchRoots,
11
- createBuiltinTools,
12
- discoverWorkspaceSkills,
13
- loadAlwaysSkillsContent,
14
- buildSkillsSummaryXML,
15
- } from '../builtin-tools.js';
8
+ import { getPlugin, type Tool, type SkillFeature, type AgentPresetFeature } from '@zhin.js/core';
9
+ import { createBuiltinTools } from '../builtin-tools.js';
10
+ import { collectPluginSkillSearchRoots } from '../discovery-utils.js';
11
+ import { discoverWorkspaceSkills, loadAlwaysSkillsContent, buildSkillsSummaryXML } from '../discover-skills.js';
12
+ import { discoverWorkspaceAgents } from '../discover-agents.js';
13
+ import { discoverWorkspaceTools, buildToolFromMeta } from '../discover-tools.js';
16
14
  import { resolveSkillInstructionMaxChars, DEFAULT_CONFIG } from '../zhin-agent/config.js';
17
15
  import { loadBootstrapFiles, buildContextFiles, buildBootstrapContextSection } from '../bootstrap.js';
18
16
  import { triggerAIHook, createAIHookEvent } from '../hooks.js';
@@ -33,6 +31,10 @@ export function registerBuiltinTools(refs: AIServiceRefs): void {
33
31
  const builtinTools = createBuiltinTools({
34
32
  skillInstructionMaxChars: resolveSkillInstructionMaxChars(fullCfg, modelName),
35
33
  pluginSkillRootsResolver: () => collectPluginSkillSearchRoots(root),
34
+ skillFileLookup: (name: string) => {
35
+ const skillFeature = root.inject?.('skill') as SkillFeature | undefined;
36
+ return skillFeature?.get(name)?.filePath;
37
+ },
36
38
  });
37
39
  const disposers: (() => void)[] = [];
38
40
  for (const tool of builtinTools) disposers.push(toolService.addTool(tool, root.name));
@@ -42,6 +44,7 @@ export function registerBuiltinTools(refs: AIServiceRefs): void {
42
44
 
43
45
  let skillWatchers: fs.FSWatcher[] = [];
44
46
  let skillReloadDebounce: ReturnType<typeof setTimeout> | null = null;
47
+ let toolReloadDebounce: ReturnType<typeof setTimeout> | null = null;
45
48
 
46
49
  async function syncWorkspaceSkills(): Promise<number> {
47
50
  const skillFeature = root.inject?.('skill') as SkillFeature | undefined;
@@ -49,31 +52,122 @@ export function registerBuiltinTools(refs: AIServiceRefs): void {
49
52
  const existing = skillFeature.getByPlugin(root.name);
50
53
  for (const s of existing) skillFeature.remove(s);
51
54
  const skills = await discoverWorkspaceSkills(root);
52
- if (skills.length === 0) return 0;
55
+ if (skills.length > 0) {
56
+ const allRegisteredTools = toolService.getAll();
57
+ const toolNameIndex = new Map<string, Tool>();
58
+ for (const t of allRegisteredTools) {
59
+ toolNameIndex.set(t.name, t);
60
+ const parts = t.name.split('_');
61
+ if (parts.length === 2) toolNameIndex.set(`${parts[1]}_${parts[0]}`, t);
62
+ }
63
+ for (const s of skills) {
64
+ const associatedTools: Tool[] = [];
65
+ const toolNames = s.toolNames || [];
66
+ for (const toolName of toolNames) {
67
+ let tool = toolService.get(toolName) || toolNameIndex.get(toolName);
68
+ if (tool) associatedTools.push(tool);
69
+ }
70
+ skillFeature.add({
71
+ name: s.name,
72
+ description: s.description,
73
+ tools: associatedTools,
74
+ keywords: s.keywords || [],
75
+ tags: s.tags || [],
76
+ pluginName: root.name,
77
+ filePath: s.filePath,
78
+ always: s.always,
79
+ }, root.name);
80
+ }
81
+ }
82
+ // Inject always-on skills content + XML summary into agent
83
+ if (refs.zhinAgent) {
84
+ const alwaysContent = await loadAlwaysSkillsContent(skills);
85
+ const skillsXml = buildSkillsSummaryXML(skills);
86
+ refs.zhinAgent.setActiveSkillsContext(alwaysContent);
87
+ refs.zhinAgent.setSkillsSummaryXML(skillsXml);
88
+ }
89
+ return skills.length;
90
+ }
91
+
92
+ // 文件化 Tool 的 disposer(用于热重载时移除旧 tool)
93
+ let toolFileDisposers: (() => void)[] = [];
94
+
95
+ /**
96
+ * Discover *.tool.md files and register them as tools.
97
+ */
98
+ async function syncWorkspaceTools(): Promise<number> {
99
+ // 移除之前文件化注册的 tool
100
+ for (const d of toolFileDisposers) d();
101
+ toolFileDisposers = [];
102
+
103
+ const toolMetas = await discoverWorkspaceTools(root);
104
+ if (toolMetas.length === 0) return 0;
105
+
106
+ let added = 0;
107
+ for (const meta of toolMetas) {
108
+ // 跳过已通过程序化方式注册的同名 tool
109
+ if (toolService.get(meta.name)) {
110
+ logger.debug(`Tool '${meta.name}' 已存在(程序化注册),跳过文件化版本`);
111
+ continue;
112
+ }
113
+ const tool = await buildToolFromMeta(meta);
114
+ if (!tool) continue;
115
+ const dispose = toolService.addTool(tool, root.name);
116
+ toolFileDisposers.push(dispose);
117
+ added++;
118
+ }
119
+ return added;
120
+ }
121
+
122
+ /**
123
+ * Discover *.agent.md files and register agent presets into AgentPresetFeature.
124
+ */
125
+ async function syncWorkspaceAgents(): Promise<number> {
126
+ const agentPresetFeature = root.inject?.('agentPreset') as AgentPresetFeature | undefined;
127
+ if (!agentPresetFeature) return 0;
128
+
129
+ // Remove previously discovered presets for this plugin
130
+ const existing = agentPresetFeature.getByPlugin(root.name);
131
+ for (const p of existing) agentPresetFeature.remove(p);
132
+
133
+ const agentMetas = await discoverWorkspaceAgents(root);
134
+ if (agentMetas.length === 0) return 0;
135
+
53
136
  const allRegisteredTools = toolService.getAll();
54
- const toolNameIndex = new Map<string, Tool>();
137
+ const toolNameIndex = new Map<string, import('@zhin.js/core').Tool>();
55
138
  for (const t of allRegisteredTools) {
56
139
  toolNameIndex.set(t.name, t);
57
- const parts = t.name.split('_');
58
- if (parts.length === 2) toolNameIndex.set(`${parts[1]}_${parts[0]}`, t);
59
140
  }
60
- for (const s of skills) {
61
- const associatedTools: Tool[] = [];
62
- const toolNames = s.toolNames || [];
63
- for (const toolName of toolNames) {
64
- let tool = toolService.get(toolName) || toolNameIndex.get(toolName);
141
+ let added = 0;
142
+ for (const meta of agentMetas) {
143
+ if (agentPresetFeature.get(meta.name)) continue;
144
+ const associatedTools: import('@zhin.js/core').Tool[] = [];
145
+ for (const toolName of meta.toolNames || []) {
146
+ const tool = toolService.get(toolName) || toolNameIndex.get(toolName);
65
147
  if (tool) associatedTools.push(tool);
66
148
  }
67
- skillFeature.add({
68
- name: s.name,
69
- description: s.description,
70
- tools: associatedTools,
71
- keywords: s.keywords || [],
72
- tags: s.tags || [],
73
- pluginName: root.name,
149
+ // Read body as systemPrompt
150
+ let systemPrompt: string | undefined;
151
+ try {
152
+ const content = await fs.promises.readFile(meta.filePath, 'utf-8');
153
+ const body = content.replace(/^---\s*\n[\s\S]*?\n---\s*(?:\n|$)/, '').trim();
154
+ if (body) systemPrompt = body;
155
+ } catch { /* ignore */ }
156
+ agentPresetFeature.add({
157
+ name: meta.name,
158
+ description: meta.description,
159
+ keywords: meta.keywords,
160
+ tags: meta.tags,
161
+ tools: associatedTools.length > 0 ? associatedTools : undefined,
162
+ systemPrompt,
163
+ model: meta.model,
164
+ provider: meta.provider,
165
+ maxIterations: meta.maxIterations,
166
+ filePath: meta.filePath,
74
167
  }, root.name);
168
+ added++;
75
169
  }
76
- return skills.length;
170
+ return added;
77
171
  }
78
172
 
79
173
  (async () => {
@@ -88,6 +182,26 @@ export function registerBuiltinTools(refs: AIServiceRefs): void {
88
182
  logger.warn(`Failed to discover workspace skills: ${e.message}`);
89
183
  }
90
184
 
185
+ // Step 1b: discover *.tool.md file-based tools
186
+ try {
187
+ const toolCount = await syncWorkspaceTools();
188
+ if (toolCount > 0) {
189
+ logger.info(`Registered ${toolCount} workspace file-based tools`);
190
+ }
191
+ } catch (e: any) {
192
+ logger.warn(`Failed to discover workspace tools: ${e.message}`);
193
+ }
194
+
195
+ // Step 1c: discover *.agent.md agent presets
196
+ try {
197
+ const agentCount = await syncWorkspaceAgents();
198
+ if (agentCount > 0) {
199
+ logger.info(`Registered ${agentCount} workspace agent presets`);
200
+ }
201
+ } catch (e: any) {
202
+ logger.debug(`Failed to discover workspace agents: ${e.message}`);
203
+ }
204
+
91
205
  // Step 2: load bootstrap files
92
206
  const loadedFiles: string[] = [];
93
207
  try {
@@ -118,19 +232,6 @@ export function registerBuiltinTools(refs: AIServiceRefs): void {
118
232
  logger.debug(`Bootstrap files not loaded: ${e.message}`);
119
233
  }
120
234
 
121
- // Step 3: inject always-on skills content + XML summary
122
- try {
123
- const skillsForContext = await discoverWorkspaceSkills(root);
124
- const alwaysContent = await loadAlwaysSkillsContent(skillsForContext);
125
- const skillsXml = buildSkillsSummaryXML(skillsForContext);
126
- if (refs.zhinAgent) {
127
- refs.zhinAgent.setActiveSkillsContext(alwaysContent);
128
- refs.zhinAgent.setSkillsSummaryXML(skillsXml);
129
- }
130
- } catch (e: unknown) {
131
- logger.debug(`Skills context not set: ${e instanceof Error ? e.message : String(e)}`);
132
- }
133
-
134
235
  // Trigger agent:bootstrap hook
135
236
  const skillFeature2 = root.inject('skill') as SkillFeature | undefined;
136
237
  await triggerAIHook(createAIHookEvent('agent', 'bootstrap', undefined, {
@@ -140,6 +241,30 @@ export function registerBuiltinTools(refs: AIServiceRefs): void {
140
241
  bootstrapFiles: loadedFiles,
141
242
  }));
142
243
 
244
+ // Hot-reload tool directories
245
+ const workspaceToolDir = path.join(process.cwd(), 'tools');
246
+ const onToolDirChange = () => {
247
+ if (toolReloadDebounce) clearTimeout(toolReloadDebounce);
248
+ toolReloadDebounce = setTimeout(async () => {
249
+ toolReloadDebounce = null;
250
+ try {
251
+ const count = await syncWorkspaceTools();
252
+ if (count >= 0) logger.info(`[Tool热重载] 已更新,工作区文件化Tool: ${count}`);
253
+ } catch (e: any) {
254
+ logger.warn(`[Tool热重载] 失败: ${e.message}`);
255
+ }
256
+ }, 400);
257
+ };
258
+ if (fs.existsSync(workspaceToolDir)) {
259
+ try {
260
+ const w = fs.watch(workspaceToolDir, { recursive: true }, onToolDirChange);
261
+ skillWatchers.push(w);
262
+ logger.debug(`[Tool热重载] 监听目录: ${workspaceToolDir}`);
263
+ } catch (e: any) {
264
+ logger.debug(`[Tool热重载] 无法监听 ${workspaceToolDir}: ${e.message}`);
265
+ }
266
+ }
267
+
143
268
  // Hot-reload skill directories
144
269
  const workspaceSkillDir = path.join(process.cwd(), 'skills');
145
270
  const localSkillDir = path.join(os.homedir(), '.zhin', 'skills');
@@ -149,15 +274,8 @@ export function registerBuiltinTools(refs: AIServiceRefs): void {
149
274
  skillReloadDebounce = null;
150
275
  try {
151
276
  const count = await syncWorkspaceSkills();
152
- const skillsForContext = await discoverWorkspaceSkills(root);
153
- const alwaysContent = await loadAlwaysSkillsContent(skillsForContext);
154
- const skillsXml = buildSkillsSummaryXML(skillsForContext);
155
- if (refs.zhinAgent) {
156
- refs.zhinAgent.setActiveSkillsContext(alwaysContent);
157
- refs.zhinAgent.setSkillsSummaryXML(skillsXml);
158
- }
159
277
  await triggerAIHook(createAIHookEvent('agent', 'skills-reloaded', undefined, { skillCount: count }));
160
- if (count >= 0) logger.info(`[技能热重载] 已更新,当前工作区技能数: ${count}`);
278
+ if (count >= 0) logger.info(`[技能热重载] 已更新,工作区技能: ${count}`);
161
279
  } catch (e: any) {
162
280
  logger.warn(`[技能热重载] 失败: ${e.message}`);
163
281
  }
@@ -178,9 +296,12 @@ export function registerBuiltinTools(refs: AIServiceRefs): void {
178
296
 
179
297
  return () => {
180
298
  disposers.forEach(d => d());
299
+ toolFileDisposers.forEach(d => d());
300
+ toolFileDisposers = [];
181
301
  skillWatchers.forEach(w => w.close());
182
302
  skillWatchers = [];
183
303
  if (skillReloadDebounce) clearTimeout(skillReloadDebounce);
304
+ if (toolReloadDebounce) clearTimeout(toolReloadDebounce);
184
305
  };
185
306
  });
186
307
  }
@@ -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