hyacinth-ai 0.9.20 → 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.
@@ -0,0 +1,44 @@
1
+ import type { Tool } from '../tools/interface.js';
2
+ import type { ChannelManager } from './manager.js';
3
+ /**
4
+ * MessageDispatcher — 跨渠道统一分发层。
5
+ *
6
+ * 持有 ChannelManager 引用,按渠道 ID 查找目标渠道并调用其 send() 方法。
7
+ * 本身无状态——每次调用都是独立的、一次性的。
8
+ */
9
+ export declare class MessageDispatcher {
10
+ private channelManager;
11
+ constructor(channelManager: ChannelManager);
12
+ /**
13
+ * 借用指定渠道发送消息。
14
+ *
15
+ * @returns 发送结果描述字符串,失败时包含错误原因(不抛异常,始终返回字符串)
16
+ */
17
+ send(args: {
18
+ /** 目标渠道 ID(如 'feishu'、'clawbot') */
19
+ channel: string;
20
+ /** 目标用户 ID 或群聊 ID */
21
+ to: string;
22
+ /** 目标类型,默认 user */
23
+ targetType?: 'user' | 'chat';
24
+ /** 文本内容(可选,与 images 至少有一项有意义) */
25
+ text?: string;
26
+ /** 图片列表(base64 + MIME 类型) */
27
+ images?: Array<{
28
+ data: string;
29
+ media_type: string;
30
+ }>;
31
+ }): Promise<string>;
32
+ }
33
+ /**
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() 之后注册,确保所有渠道已启动。
42
+ */
43
+ export declare function createSendChannelMessageTool(dispatcher: MessageDispatcher): Tool;
44
+ //# sourceMappingURL=dispatcher.d.ts.map
@@ -0,0 +1,135 @@
1
+ // ============================================================
2
+ // MessageDispatcher — 跨渠道消息分发层
3
+ // ============================================================
4
+ //
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() 的渠道返回 "不支持主动发送",不影响其他功能。
30
+ // ============================================================
31
+ /**
32
+ * MessageDispatcher — 跨渠道统一分发层。
33
+ *
34
+ * 持有 ChannelManager 引用,按渠道 ID 查找目标渠道并调用其 send() 方法。
35
+ * 本身无状态——每次调用都是独立的、一次性的。
36
+ */
37
+ export class MessageDispatcher {
38
+ channelManager;
39
+ constructor(channelManager) {
40
+ this.channelManager = channelManager;
41
+ }
42
+ /**
43
+ * 借用指定渠道发送消息。
44
+ *
45
+ * @returns 发送结果描述字符串,失败时包含错误原因(不抛异常,始终返回字符串)
46
+ */
47
+ async send(args) {
48
+ // ① 查找目标渠道
49
+ const state = this.channelManager.get(args.channel);
50
+ if (!state || state.status !== 'active') {
51
+ return `渠道 "${args.channel}" 未连接或未启动。可用渠道: ${this.channelManager.getStatusSummary().map(s => s.id).join(', ') || '无'}`;
52
+ }
53
+ const handler = state.handler;
54
+ // ② 检查渠道是否支持主动发送
55
+ if (!handler.send) {
56
+ return `渠道 "${args.channel}" (${handler.name}) 不支持主动发送。`;
57
+ }
58
+ // ③ 构造标准目标并委托给渠道的 send()
59
+ const target = {
60
+ type: args.targetType ?? 'user',
61
+ id: args.to,
62
+ };
63
+ try {
64
+ return await handler.send(target, {
65
+ content: args.text ?? '',
66
+ images: args.images,
67
+ });
68
+ }
69
+ catch (err) {
70
+ // 异常兜底——渠道 send() 应该自己 catch,这里防止未预期的异常泄漏
71
+ return `发送失败: ${err instanceof Error ? err.message : String(err)}`;
72
+ }
73
+ }
74
+ }
75
+ /**
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() 之后注册,确保所有渠道已启动。
84
+ */
85
+ export function createSendChannelMessageTool(dispatcher) {
86
+ return {
87
+ name: 'send_channel_message',
88
+ description: '借用指定渠道发送消息到目标用户或群聊。纯借用渠道能力——不创建会话、不写入对话记录、不影响其他渠道。' +
89
+ '适用于从当前渠道(如 TUI)通过飞书/微信等渠道向用户发送通知、图片或文件。',
90
+ inputSchema: {
91
+ type: 'object',
92
+ properties: {
93
+ channel: {
94
+ type: 'string',
95
+ description: '目标渠道 ID。可用 channel_info 查看已连接渠道。如 "feishu"、"clawbot"。',
96
+ },
97
+ to: {
98
+ type: 'string',
99
+ description: '目标用户 ID 或群聊 ID。飞书为 open_id 或 chat_id,微信为 wxid。',
100
+ },
101
+ target_type: {
102
+ type: 'string',
103
+ enum: ['user', 'chat'],
104
+ description: '目标类型。user=私聊,chat=群聊。默认 user。',
105
+ },
106
+ text: {
107
+ type: 'string',
108
+ description: '要发送的文本内容。',
109
+ },
110
+ images: {
111
+ type: 'array',
112
+ items: {
113
+ type: 'object',
114
+ properties: {
115
+ data: { type: 'string', description: '图片的 base64 数据' },
116
+ media_type: { type: 'string', description: 'MIME 类型,如 image/png' },
117
+ },
118
+ },
119
+ description: '要发送的图片列表(base64 + MIME 类型)。',
120
+ },
121
+ },
122
+ required: ['channel', 'to'],
123
+ },
124
+ async execute(args) {
125
+ return dispatcher.send({
126
+ channel: args.channel,
127
+ to: args.to,
128
+ targetType: args.target_type ?? 'user',
129
+ text: args.text || undefined,
130
+ images: args.images,
131
+ });
132
+ },
133
+ };
134
+ }
135
+ //# sourceMappingURL=dispatcher.js.map
@@ -45,6 +45,13 @@ export interface ChannelReply {
45
45
  }>;
46
46
  metadata?: Record<string, unknown>;
47
47
  }
48
+ /** 跨渠道主动发送的目标接收方 */
49
+ export interface ChannelTarget {
50
+ /** 目标类型:user=私聊用户,chat=群聊 */
51
+ type: 'user' | 'chat';
52
+ /** 目标 ID(格式由各渠道自行定义:飞书 ou_/oc_,微信 wxid 等) */
53
+ id: string;
54
+ }
48
55
  /** 输出处理器(渠道消息收集用) */
49
56
  export interface ChannelOutputHandler {
50
57
  onText?(content: string): void;
@@ -137,6 +144,27 @@ export interface ChannelHandler {
137
144
  * 获取渠道状态
138
145
  */
139
146
  getStatus(): ChannelStatus;
147
+ /**
148
+ * 主动发送消息(跨渠道借用能力入口)。
149
+ *
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 发送结果描述(成功或失败原因),不抛异常
166
+ */
167
+ send?(target: ChannelTarget, content: ChannelReply): Promise<string>;
140
168
  }
141
169
  export type ChannelStatus = 'registered' | 'starting' | 'active' | 'stopped' | 'error';
142
170
  export interface ChannelState {
@@ -1,4 +1,4 @@
1
- import type { ChannelHandler, ChannelEvent, ChannelReply, ChannelConfig, ChannelStatus, ChannelMessageEvent, AgentFactory, ReplyFn } from '../../interface.js';
1
+ import type { ChannelHandler, ChannelEvent, ChannelReply, ChannelTarget, ChannelConfig, ChannelStatus, ChannelMessageEvent, AgentFactory, ReplyFn } from '../../interface.js';
2
2
  export declare class FeishuChannel implements ChannelHandler {
3
3
  readonly id = "feishu";
4
4
  readonly name = "Feishu (\u98DE\u4E66)";
@@ -32,6 +32,8 @@ export declare class FeishuChannel implements ChannelHandler {
32
32
  onEvent(handler: (event: ChannelEvent) => Promise<void>): void;
33
33
  reply(sessionId: string, reply: ChannelReply): Promise<void>;
34
34
  getStatus(): ChannelStatus;
35
+ send(target: ChannelTarget, content: ChannelReply): Promise<string>;
36
+ private uploadImage;
35
37
  /** 获取 tenant access token(缓存,提前 60s 刷新,token 有效期 2h) */
36
38
  private getTenantAccessToken;
37
39
  /**
@@ -14,7 +14,7 @@
14
14
  import { resolveFeishuConfig, validateFeishuConfig, } from './feishu-config.js';
15
15
  import { FeishuTransport } from './feishu-transport.js';
16
16
  import { parseFeishuMessageEvent, FeishuDedupeStore, checkDmAccess, checkGroupAccess, } from './feishu-event.js';
17
- import { sendText, sendCard, getSenderInfo } from './feishu-send.js';
17
+ import { sendText, sendCard, sendImage, getSenderInfo } from './feishu-send.js';
18
18
  import { ChannelSessionPool, createCollectHandler } from './feishu-session.js';
19
19
  import { FeishuMessageQueue } from './feishu-message-queue.js';
20
20
  import { generateSessionId } from '../../../memory/session.js';
@@ -176,6 +176,109 @@ export class FeishuChannel {
176
176
  getStatus() {
177
177
  return this.status;
178
178
  }
179
+ // ================================================================
180
+ // send() — 跨渠道借用能力入口
181
+ // ================================================================
182
+ //
183
+ // 设计意图:
184
+ // 每个渠道有自己独特的发送能力(飞书能发卡片和图片、微信能发文件、
185
+ // TUI 只能显示文本)。send() 将这些能力暴露给外部,使得其他渠道
186
+ // 的 Agent 可以借用本渠道的能力发送消息。
187
+ //
188
+ // 行为保证("纯借用"语义):
189
+ // - 不创建 session —— 消息是一次性的,没有对应的 AgentLoop
190
+ // - 不写 conversation.jsonl —— 不污染任何渠道的对话历史
191
+ // - 不影响 ChannelHandler 内部状态 —— sessionMap、消息队列等完全不变
192
+ // - 被借用方对"谁借的"完全无感 —— 不需要知道调用方是哪个渠道
193
+ //
194
+ // 调用链:
195
+ // Agent → send_channel_message 工具 → MessageDispatcher
196
+ // → ChannelManager.get('feishu') → FeishuChannel.send()
197
+ //
198
+ // 支持的消息类型(按优先级):
199
+ // 1. 卡片消息(content.metadata.card 存在时)—— 飞书交互式卡片
200
+ // 2. 文本消息(默认)—— Post 格式,支持 Markdown 子集
201
+ // 3. 图片消息(content.images 存在时)—— 先上传获取 image_key,再发送
202
+ // ↑ 图片在文本/卡片之后独立发送,失败不影响前面的消息
203
+ //
204
+ // 注意:
205
+ // - 图片上传需要 tenant_access_token(通过 getTenantAccessToken 获取)
206
+ // - 每条图片独立上传 + 发送,某张失败不阻塞其他图片
207
+ // - 图片 base64 直接传给飞书 API,不在本地落盘
208
+ // ================================================================
209
+ async send(target, content) {
210
+ if (this.status !== 'active')
211
+ return '飞书渠道未连接,无法发送';
212
+ const config = this.config;
213
+ // 飞书 SDK 的 receive_id_type 由 ID 前缀推断(ou_→open_id, oc_→chat_id)
214
+ // sendText/sendCard/sendImage 内部调用 resolveSendTarget 自动处理
215
+ const to = target.type === 'chat' ? `chat:${target.id}` : `user:${target.id}`;
216
+ try {
217
+ // ① 主消息:卡片优先,否则文本
218
+ if (content.metadata?.card) {
219
+ await sendCard(config, { to, card: content.metadata.card });
220
+ }
221
+ else {
222
+ await sendText(config, { to, text: content.content });
223
+ }
224
+ // ② 图片:在文本/卡片之后独立发送
225
+ // 飞书的图片消息和文本消息是两条独立的消息,不支持图文混排
226
+ if (content.images && content.images.length > 0) {
227
+ const token = await this.getTenantAccessToken();
228
+ if (token) {
229
+ for (const img of content.images) {
230
+ try {
231
+ // 飞书图片发送两步:上传拿 image_key → 调用 im.message.create 发送
232
+ const imageKey = await this.uploadImage(token, img.data);
233
+ await sendImage(config, { to, imageKey });
234
+ }
235
+ catch {
236
+ // 某张图片失败不阻塞其他图片和整体流程
237
+ }
238
+ }
239
+ }
240
+ }
241
+ return `已通过飞书发送到 ${to}`;
242
+ }
243
+ catch (err) {
244
+ return `飞书发送失败: ${err instanceof Error ? err.message : String(err)}`;
245
+ }
246
+ }
247
+ // ================================================================
248
+ // uploadImage() — 飞书图片上传
249
+ // ================================================================
250
+ //
251
+ // 飞书发送图片必须先上传到飞书服务器获取 image_key,
252
+ // 再用 image_key 调用 im.message.create(msg_type='image')发送。
253
+ //
254
+ // API: POST https://open.feishu.cn/open-apis/im/v1/images
255
+ // 参数: image_type='message'(消息用图,非头像),image=base64(不含 data:xxx;base64, 前缀)
256
+ // 返回: { code: 0, data: { image_key: 'img_xxx' } }
257
+ //
258
+ // 注意:
259
+ // - token 由调用方传入(来自 getTenantAccessToken 的缓存结果)
260
+ // - mediaType 参数保留供未来扩展(如格式校验),当前仅透传 base64
261
+ // - 飞书 image_key 有时效性(约 2 小时),不持久化缓存
262
+ // ================================================================
263
+ async uploadImage(token, base64Data, _mediaType) {
264
+ const domain = this.config?.domain ?? 'https://open.feishu.cn';
265
+ const resp = await fetch(`${domain}/open-apis/im/v1/images`, {
266
+ method: 'POST',
267
+ headers: {
268
+ 'Authorization': `Bearer ${token}`,
269
+ 'Content-Type': 'application/json',
270
+ },
271
+ body: JSON.stringify({
272
+ image_type: 'message',
273
+ image: base64Data,
274
+ }),
275
+ });
276
+ const json = await resp.json();
277
+ if (json.code !== 0 || !json.data?.image_key) {
278
+ throw new Error(`飞书图片上传失败: code=${json.code}`);
279
+ }
280
+ return json.data.image_key;
281
+ }
179
282
  /** 获取 tenant access token(缓存,提前 60s 刷新,token 有效期 2h) */
180
283
  async getTenantAccessToken() {
181
284
  if (this.cachedToken && Date.now() < this.tokenExpiresAt - 60_000) {
@@ -41,4 +41,12 @@ export declare function getSenderInfo(config: FeishuChannelConfig, userId: strin
41
41
  name?: string;
42
42
  avatar?: string;
43
43
  } | null>;
44
+ /**
45
+ * 发送图片消息。
46
+ * @param imageKey 飞书上传 API 返回的 image_key
47
+ */
48
+ export declare function sendImage(config: FeishuChannelConfig, params: {
49
+ to: string;
50
+ imageKey: string;
51
+ }): Promise<FeishuSendResult>;
44
52
  //# sourceMappingURL=feishu-send.d.ts.map
@@ -229,4 +229,44 @@ 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
+ // ================================================================
248
+ /**
249
+ * 发送图片消息。
250
+ * @param imageKey 飞书上传 API 返回的 image_key
251
+ */
252
+ export async function sendImage(config, params) {
253
+ const client = await createFeishuClient(config);
254
+ const { receiveId, receiveIdType } = resolveSendTarget(params.to);
255
+ const content = JSON.stringify({ image_key: params.imageKey });
256
+ const res = await client.im.message.create({
257
+ params: { receive_id_type: receiveIdType },
258
+ data: {
259
+ receive_id: receiveId,
260
+ content,
261
+ msg_type: 'image',
262
+ },
263
+ });
264
+ if (res.code !== 0) {
265
+ throw new Error(`飞书图片发送失败: ${res.msg || `code ${res.code}`}`);
266
+ }
267
+ return {
268
+ messageId: res.data?.message_id ?? '',
269
+ chatId: receiveId,
270
+ };
271
+ }
232
272
  //# sourceMappingURL=feishu-send.js.map
@@ -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,6 +970,16 @@ export async function runTui(provider, sessionId, shouldContinue, maxTurns, maxC
970
970
  }
971
971
  // 启动所有渠道(每个渠道自行处理消息)
972
972
  await channelManager.startAll(agentFactory);
973
+ // ── 跨渠道消息发送工具 ────────────────────────────────────
974
+ //
975
+ // 在 channelManager.startAll() 之后注册,此时所有渠道已启动。
976
+ // 工具内通过 ChannelManager.get() 查找目标渠道并调用其 send()。
977
+ // 仅在本地 TUI 模式下注册——远程模式(remoteWs)下工具由服务端提供。
978
+ if (agent) {
979
+ const { MessageDispatcher, createSendChannelMessageTool } = await import('../channels/dispatcher.js');
980
+ const dispatcher = new MessageDispatcher(channelManager);
981
+ agent.toolRegistry.register(createSendChannelMessageTool(dispatcher));
982
+ }
973
983
  // ── ASCII Art loader ──────────────────────────────────────────────
974
984
  async function loadAsciiArt(maxWidth = 54) {
975
985
  const asciiDir = path.join(os.homedir(), '.agent', 'ascii');
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.20",
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
+ }