hyacinth-ai 0.9.21 → 0.9.22

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.
@@ -1,17 +1,29 @@
1
1
  import type { Tool } from '../tools/interface.js';
2
2
  import type { ChannelManager } from './manager.js';
3
3
  /**
4
- * MessageDispatcher — 统一分发层。
5
- * 查目标渠道 → 调其 send() → 返回结果。
4
+ * MessageDispatcher — 跨渠道统一分发层。
5
+ *
6
+ * 持有 ChannelManager 引用,按渠道 ID 查找目标渠道并调用其 send() 方法。
7
+ * 本身无状态——每次调用都是独立的、一次性的。
6
8
  */
7
9
  export declare class MessageDispatcher {
8
10
  private channelManager;
9
11
  constructor(channelManager: ChannelManager);
12
+ /**
13
+ * 借用指定渠道发送消息。
14
+ *
15
+ * @returns 发送结果描述字符串,失败时包含错误原因(不抛异常,始终返回字符串)
16
+ */
10
17
  send(args: {
18
+ /** 目标渠道 ID(如 'feishu'、'clawbot') */
11
19
  channel: string;
20
+ /** 目标用户 ID 或群聊 ID */
12
21
  to: string;
22
+ /** 目标类型,默认 user */
13
23
  targetType?: 'user' | 'chat';
24
+ /** 文本内容(可选,与 images 至少有一项有意义) */
14
25
  text?: string;
26
+ /** 图片列表(base64 + MIME 类型) */
15
27
  images?: Array<{
16
28
  data: string;
17
29
  media_type: string;
@@ -19,7 +31,14 @@ export declare class MessageDispatcher {
19
31
  }): Promise<string>;
20
32
  }
21
33
  /**
22
- * send_channel_message 工具——暴露给 Agent 的跨渠道发送能力。
34
+ * send_channel_message 工具 —— 暴露给 Agent 的跨渠道发送入口。
35
+ *
36
+ * Agent 在任何渠道的任意会话中都可以调用此工具,借用其他已连接渠道
37
+ * 的发送能力。典型场景:TUI 会话中的 Agent 通过飞书发图片给用户,
38
+ * 或者定时任务触发后通过微信推送通知。
39
+ *
40
+ * 此工具注册在 TUI/Server 的网关层(gateway/tui.ts、gateway/server.ts),
41
+ * 在 channelManager.startAll() 之后注册,确保所有渠道已启动。
23
42
  */
24
43
  export declare function createSendChannelMessageTool(dispatcher: MessageDispatcher): Tool;
25
44
  //# sourceMappingURL=dispatcher.d.ts.map
@@ -2,46 +2,85 @@
2
2
  // MessageDispatcher — 跨渠道消息分发层
3
3
  // ============================================================
4
4
  //
5
- // 允许 Agent 从任意会话借用其他渠道的发送能力。
6
- // 消息是一次性的——不创建 session、不写 conversation、不影响任何渠道的对话状态。
7
- // 被借用的渠道对"谁借了道"完全无感。
5
+ // 设计意图:
6
+ // Hyacinth 的渠道系统原本是"请求-回复"模式——用户在某个渠道发消息,
7
+ // Agent 通过同一个渠道的 reply() 回复。但这限制了 Agent 的能力:
8
+ // TUI 会话中的 Agent 无法通过飞书发图片,飞书会话中的 Agent 也无法
9
+ // 通过微信传文件。每个渠道的发送能力被封闭在自己的生命周期里。
10
+ //
11
+ // MessageDispatcher 打破了这个封闭——它让 Agent 可以从任意会话
12
+ // 借用任意已连接渠道的发送能力。这是"跨渠道"而非"跨会话"——
13
+ // 借用的是渠道的 API 能力,不影响任何渠道的对话状态。
14
+ //
15
+ // 核心语义("纯借用"):
16
+ // 1. 不创建 session —— 消息是一次性的,不关联 AgentLoop
17
+ // 2. 不写 conversation.jsonl —— 不污染任何渠道的对话历史
18
+ // 3. 不影响 ChannelHandler 内部状态 —— sessionMap、消息队列等完全不变
19
+ // 4. 被借用渠道对"谁借的"完全无感 —— 不记录调用来源
20
+ //
21
+ // 调用链:
22
+ // Agent → send_channel_message({channel:'feishu', to:'ou_xxx', text:'...'})
23
+ // → MessageDispatcher.send()
24
+ // → ChannelManager.get('feishu') → handler.send(target, content)
25
+ // → 渠道 API → 用户收到消息
26
+ //
27
+ // 扩展新渠道时:
28
+ // 只需实现 ChannelHandler.send?() 方法(可选),MessageDispatcher 自动发现。
29
+ // 不实现 send() 的渠道返回 "不支持主动发送",不影响其他功能。
8
30
  // ============================================================
9
31
  /**
10
- * MessageDispatcher — 统一分发层。
11
- * 查目标渠道 → 调其 send() → 返回结果。
32
+ * MessageDispatcher — 跨渠道统一分发层。
33
+ *
34
+ * 持有 ChannelManager 引用,按渠道 ID 查找目标渠道并调用其 send() 方法。
35
+ * 本身无状态——每次调用都是独立的、一次性的。
12
36
  */
13
37
  export class MessageDispatcher {
14
38
  channelManager;
15
39
  constructor(channelManager) {
16
40
  this.channelManager = channelManager;
17
41
  }
42
+ /**
43
+ * 借用指定渠道发送消息。
44
+ *
45
+ * @returns 发送结果描述字符串,失败时包含错误原因(不抛异常,始终返回字符串)
46
+ */
18
47
  async send(args) {
48
+ // ① 查找目标渠道
19
49
  const state = this.channelManager.get(args.channel);
20
50
  if (!state || state.status !== 'active') {
21
51
  return `渠道 "${args.channel}" 未连接或未启动。可用渠道: ${this.channelManager.getStatusSummary().map(s => s.id).join(', ') || '无'}`;
22
52
  }
23
53
  const handler = state.handler;
54
+ // ② 检查渠道是否支持主动发送
24
55
  if (!handler.send) {
25
56
  return `渠道 "${args.channel}" (${handler.name}) 不支持主动发送。`;
26
57
  }
58
+ // ③ 构造标准目标并委托给渠道的 send()
27
59
  const target = {
28
60
  type: args.targetType ?? 'user',
29
61
  id: args.to,
30
62
  };
31
63
  try {
32
- const result = await handler.send(target, {
64
+ return await handler.send(target, {
33
65
  content: args.text ?? '',
34
66
  images: args.images,
35
67
  });
36
- return result;
37
68
  }
38
69
  catch (err) {
70
+ // 异常兜底——渠道 send() 应该自己 catch,这里防止未预期的异常泄漏
39
71
  return `发送失败: ${err instanceof Error ? err.message : String(err)}`;
40
72
  }
41
73
  }
42
74
  }
43
75
  /**
44
- * send_channel_message 工具——暴露给 Agent 的跨渠道发送能力。
76
+ * send_channel_message 工具 —— 暴露给 Agent 的跨渠道发送入口。
77
+ *
78
+ * Agent 在任何渠道的任意会话中都可以调用此工具,借用其他已连接渠道
79
+ * 的发送能力。典型场景:TUI 会话中的 Agent 通过飞书发图片给用户,
80
+ * 或者定时任务触发后通过微信推送通知。
81
+ *
82
+ * 此工具注册在 TUI/Server 的网关层(gateway/tui.ts、gateway/server.ts),
83
+ * 在 channelManager.startAll() 之后注册,确保所有渠道已启动。
45
84
  */
46
85
  export function createSendChannelMessageTool(dispatcher) {
47
86
  return {
@@ -45,11 +45,11 @@ export interface ChannelReply {
45
45
  }>;
46
46
  metadata?: Record<string, unknown>;
47
47
  }
48
- /** 主动发送的目标(跨渠道借用能力时指定接收方) */
48
+ /** 跨渠道主动发送的目标接收方 */
49
49
  export interface ChannelTarget {
50
- /** 目标类型 */
50
+ /** 目标类型:user=私聊用户,chat=群聊 */
51
51
  type: 'user' | 'chat';
52
- /** 用户 ID 或群聊 ID */
52
+ /** 目标 ID(格式由各渠道自行定义:飞书 ou_/oc_,微信 wxid 等) */
53
53
  id: string;
54
54
  }
55
55
  /** 输出处理器(渠道消息收集用) */
@@ -145,13 +145,24 @@ export interface ChannelHandler {
145
145
  */
146
146
  getStatus(): ChannelStatus;
147
147
  /**
148
- * 主动发送消息到渠道(跨 session,不关联任何 AgentLoop、不落盘)。
149
- * 用于从其他渠道借用本渠道的发送能力(如 TUI 中的 Agent 通过飞书发消息)。
150
- * 未实现 = 该渠道不支持被外部借用。
148
+ * 主动发送消息(跨渠道借用能力入口)。
151
149
  *
152
- * @param target 目标用户或群聊
153
- * @param content 消息内容
154
- * @returns 发送结果描述
150
+ * 设计意图:reply() 是"回复"——在 handleMessage 生命周期内,将 Agent
151
+ * 的响应发回给当前会话的用户。send() 是"借用"——任何渠道的 Agent
152
+ * 都可以调用其他渠道的 send() 来借用其发送能力,不依赖 handleMessage 生命周期。
153
+ *
154
+ * 行为保证("纯借用"语义):
155
+ * - 不创建 session —— 消息是一次性的,不关联 AgentLoop
156
+ * - 不写 conversation —— 不污染对话历史
157
+ * - 不影响渠道内部状态 —— sessionMap、消息队列等完全不变
158
+ *
159
+ * 调用来源:MessageDispatcher → send_channel_message 工具 → Agent
160
+ *
161
+ * 未实现 = 该渠道不支持被外部借用(返回 undefined,MessageDispatcher 会给出友好提示)。
162
+ *
163
+ * @param target 目标接收方(用户或群聊),各渠道自行解析 ID 格式
164
+ * @param content 消息内容(文本 + 可选图片),复用 ChannelReply 避免定义新类型
165
+ * @returns 发送结果描述(成功或失败原因),不抛异常
155
166
  */
156
167
  send?(target: ChannelTarget, content: ChannelReply): Promise<string>;
157
168
  }
@@ -42,7 +42,8 @@ export declare function getSenderInfo(config: FeishuChannelConfig, userId: strin
42
42
  avatar?: string;
43
43
  } | null>;
44
44
  /**
45
- * 发送图片消息(需先通过上传 API 获取 image_key)。
45
+ * 发送图片消息。
46
+ * @param imageKey 飞书上传 API 返回的 image_key
46
47
  */
47
48
  export declare function sendImage(config: FeishuChannelConfig, params: {
48
49
  to: string;
@@ -229,8 +229,25 @@ export async function getSenderInfo(config, userId, userIdType = 'open_id') {
229
229
  return null;
230
230
  }
231
231
  }
232
+ // ================================================================
233
+ // 图片消息发送
234
+ // ================================================================
235
+ //
236
+ // 飞书发送图片是两步操作:
237
+ // 1. 上传图片到飞书服务器 → 获取 image_key
238
+ // POST /open-apis/im/v1/images { image_type: 'message', image: base64 }
239
+ // 2. 用 image_key 发送图片消息
240
+ // POST /open-apis/im/v1/messages { msg_type: 'image', content: '{"image_key":"img_xxx"}' }
241
+ //
242
+ // 注意:
243
+ // - 图片上传走二进制 base64,不走 multipart/form-data
244
+ // - image_key 有时效性(约 2 小时),不持久化
245
+ // - 图片消息和文本消息是两条独立的飞书消息,不支持单条图文混排
246
+ // - 调用方(FeishuChannel.send())负责先上传拿到 image_key 再调用本函数
247
+ // ================================================================
232
248
  /**
233
- * 发送图片消息(需先通过上传 API 获取 image_key)。
249
+ * 发送图片消息。
250
+ * @param imageKey 飞书上传 API 返回的 image_key
234
251
  */
235
252
  export async function sendImage(config, params) {
236
253
  const client = await createFeishuClient(config);
@@ -570,13 +570,13 @@ export async function createAgent(options, supervisor) {
570
570
  if (sessionType === 'companion' && companionCharName) {
571
571
  bypassManager.activateForMode('companion').catch(() => { });
572
572
  }
573
- // 普通模式:orchestrator 已暂停,不再激活
574
- // if (sessionType === 'normal') {
575
- // const orchestratorEnabled = configCenter.get<boolean>('bypass.orchestratorEnabled') ?? true;
576
- // if (orchestratorEnabled) {
577
- // bypassManager.activateAgent('orchestrator').catch(() => {});
578
- // }
579
- // }
573
+ // 普通模式:按配置决定是否激活 orchestrator
574
+ if (sessionType === 'normal') {
575
+ const orchestratorEnabled = configCenter.get('bypass.orchestratorEnabled') ?? false;
576
+ if (orchestratorEnabled) {
577
+ bypassManager.activateAgent('orchestrator').catch(() => { });
578
+ }
579
+ }
580
580
  // ── Read 工具的图片处理器 — 将读到的图片注入 ImageStore ──
581
581
  const readTool = toolRegistry.get('read');
582
582
  if (readTool && typeof readTool.setImageHandler === 'function') {
@@ -970,7 +970,11 @@ export async function runTui(provider, sessionId, shouldContinue, maxTurns, maxC
970
970
  }
971
971
  // 启动所有渠道(每个渠道自行处理消息)
972
972
  await channelManager.startAll(agentFactory);
973
- // 注册跨渠道发送工具(TUI 模式)
973
+ // ── 跨渠道消息发送工具 ────────────────────────────────────
974
+ //
975
+ // 在 channelManager.startAll() 之后注册,此时所有渠道已启动。
976
+ // 工具内通过 ChannelManager.get() 查找目标渠道并调用其 send()。
977
+ // 仅在本地 TUI 模式下注册——远程模式(remoteWs)下工具由服务端提供。
974
978
  if (agent) {
975
979
  const { MessageDispatcher, createSendChannelMessageTool } = await import('../channels/dispatcher.js');
976
980
  const dispatcher = new MessageDispatcher(channelManager);
package/dist/index.js CHANGED
File without changes
@@ -78,7 +78,6 @@ export async function compressImageIfLarge(buf, mime) {
78
78
  return { buffer: buf, mime, compressed: false };
79
79
  let sharp;
80
80
  try {
81
- // @ts-expect-error — sharp 0.35 types incompatible with pnpm exports
82
81
  sharp = (await import('sharp')).default;
83
82
  }
84
83
  catch {
@@ -93,7 +93,6 @@ function ansiBg(r, g, b) { return `\x1b[48;2;${r};${g};${b}m`; }
93
93
  export async function imageToAscii(imagePath, maxWidth = 60) {
94
94
  let sharp;
95
95
  try {
96
- // @ts-expect-error — sharp 0.35 的 types 与 pnpm exports 解析不兼容
97
96
  sharp = (await import('sharp')).default;
98
97
  }
99
98
  catch {
@@ -144,7 +144,7 @@ tools: tool1,tool2
144
144
 
145
145
  | 需求 | 文件 | 写法 | 生效方式 |
146
146
  |------|------|------|----------|
147
- | 渠道(飞书/Lark 等) | `.agent/config.json` | 修改根对象的 `channels` 字段 | **需重启**:调用 `restart` 工具 |
147
+ | 渠道(飞书/Lark/微信 ClawBot 等) | `.agent/config.json` | 修改根对象的 `channels` 字段 | **需重启**:调用 `restart` 工具 |
148
148
  | MCP Server | `.agent/mcp.json` | 添加或更新 server 定义 | **热加载**:保存后自动检测,无需重启 |
149
149
  | 工具包 | `.agent/tool-bundles.json` | 优先用 `list_bundles` / `activate_bundle` / `create_bundle` 等工具 | **热加载**:即时生效 |
150
150
  | Provider 元数据 | `.agent/providers.json` | 修改 provider 的 baseUrl、defaultModel、envKey、maxTokens | **热加载**:即时生效 |
@@ -159,7 +159,9 @@ tools: tool1,tool2
159
159
 
160
160
  ### 渠道配置
161
161
 
162
- 渠道配置写在项目级 `.agent/config.json` 的 `channels` 字段中。配置飞书或 Lark 时,读取并合并完整 JSON,例如:
162
+ 渠道配置写在项目级 `.agent/config.json` 的 `channels` 字段中。配置飞书、Lark 或微信 ClawBot 时,读取并合并完整 JSON
163
+
164
+ **飞书/Lark 示例:**
163
165
 
164
166
  ```json
165
167
  {
@@ -176,6 +178,20 @@ tools: tool1,tool2
176
178
  }
177
179
  ```
178
180
 
181
+ **微信 ClawBot 示例:**
182
+
183
+ ```json
184
+ {
185
+ "channels": {
186
+ "clawbot": {
187
+ "enabled": true
188
+ }
189
+ }
190
+ }
191
+ ```
192
+
193
+ ClawBot 是微信官方 AI 助手连接插件,支持扫码授权。最简单的配置只需 `{ "enabled": true }`,首次启动自动生成二维码,扫码后即可使用。不需要 appId/appSecret。
194
+
179
195
  配置渠道时:
180
196
  1. 优先修改当前工作区的 `.agent/config.json`
181
197
  2. 先读取现有 JSON,保留 provider、session、safety 等已有配置
@@ -135,7 +135,7 @@ export function getDefaultConfig() {
135
135
  defaultMode: 'normal',
136
136
  },
137
137
  bypass: {
138
- orchestratorEnabled: true,
138
+ orchestratorEnabled: false,
139
139
  },
140
140
  diagnostics: {
141
141
  enabled: true,
@@ -21,8 +21,8 @@ import { getLocalProviderConfigLoader } from '../provider/local-config.js';
21
21
  import { DEFAULT_PROVIDERS } from '../provider/config.js';
22
22
  import { getDefaultConfig } from '../runtime/defaults.js';
23
23
  const DEFAULT_CONFIG = {
24
- provider: 'anthropic',
25
- model: DEFAULT_PROVIDERS.providers.anthropic?.defaultModel ?? 'unknown',
24
+ provider: '',
25
+ model: '',
26
26
  maxTurns: getDefaultConfig().session.maxTurns,
27
27
  maxContext: 200000,
28
28
  safety: {
@@ -143,12 +143,12 @@ export class SetupWizard {
143
143
  }
144
144
  /** Step 1: Provider */
145
145
  async stepProvider(defaultProvider) {
146
- const initialValue = defaultProvider ?? 'anthropic';
146
+ const initialValue = defaultProvider || PROVIDERS[0].value;
147
147
  return p.select({
148
148
  message: '选择 AI 提供商',
149
149
  options: PROVIDERS.map(p => ({
150
150
  ...p,
151
- hint: p.value === initialValue ? `${p.hint} (当前)` : p.hint,
151
+ hint: p.value === defaultProvider ? `${p.hint} (当前)` : p.hint,
152
152
  })),
153
153
  initialValue,
154
154
  });
package/package.json CHANGED
@@ -1,12 +1,21 @@
1
1
  {
2
2
  "name": "hyacinth-ai",
3
- "version": "0.9.21",
3
+ "version": "0.9.22",
4
4
  "description": "Hyacinth 🪻 — multi-provider AI agent with tool system and context management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
8
8
  "hyacinth": "dist/index.js"
9
9
  },
10
+ "scripts": {
11
+ "build": "tsc && node scripts/copy-prompts.cjs && node scripts/clean-tests.cjs && node scripts/clean-maps.cjs",
12
+ "dev": "tsc --watch",
13
+ "start": "node dist/index.js",
14
+ "start:win": "chcp 65001 > nul && node dist/index.js",
15
+ "test": "vitest run",
16
+ "test:watch": "vitest",
17
+ "setup:llamacpp": "powershell -ExecutionPolicy Bypass -File scripts/setup-llamacpp.ps1"
18
+ },
10
19
  "keywords": [
11
20
  "ai",
12
21
  "agent",
@@ -19,6 +28,7 @@
19
28
  ],
20
29
  "author": "孑遗",
21
30
  "license": "MIT",
31
+ "packageManager": "pnpm@10.25.0",
22
32
  "dependencies": {
23
33
  "@anthropic-ai/sdk": "^0.98.0",
24
34
  "@clack/prompts": "^1.4.0",
@@ -52,14 +62,5 @@
52
62
  "typescript": "^6.0.3",
53
63
  "undici-types": "^7.24.6",
54
64
  "vitest": "^4.1.7"
55
- },
56
- "scripts": {
57
- "build": "tsc && node scripts/copy-prompts.cjs && node scripts/clean-tests.cjs && node scripts/clean-maps.cjs",
58
- "dev": "tsc --watch",
59
- "start": "node dist/index.js",
60
- "start:win": "chcp 65001 > nul && node dist/index.js",
61
- "test": "vitest run",
62
- "test:watch": "vitest",
63
- "setup:llamacpp": "powershell -ExecutionPolicy Bypass -File scripts/setup-llamacpp.ps1"
64
65
  }
65
- }
66
+ }