ylib-syim 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,376 @@
1
+ # @ylib/syim
2
+
3
+ 多 IM(钉钉、飞书等)/ 多 Agent 的会话路由与上下文管理,支持 `/new`、`/reset` 重开上下文。可作为库接入现有项目,或配合 OpenClaw 使用。
4
+
5
+ 本仓库为**独立项目**(已从 OpenClaw monorepo 拆出),依赖用 **npm** 管理。
6
+
7
+ ## 钉钉桥接(连接远程 yuce-gpt Gateway)
8
+
9
+ 本地调试:
10
+
11
+ ```bash
12
+ npm install
13
+ npx tsx scripts/dingtalk-stdio-bridge.ts
14
+ ```
15
+
16
+ **打包成单文件 JS(仍需本机 Node,分发更方便)**:
17
+
18
+ ```bash
19
+ npm run build:bridge
20
+ node dist/dingtalk-stdio-bridge.cjs
21
+ ```
22
+
23
+ `build:bridge` 外置上述重型依赖并 **默认压缩**,产物约 **200KB+**(未压缩调试用 `npm run build:bridge:debug`)。运行前项目根 **`npm install`**;仅拷贝 cjs 见 `scripts/bridge-runtime-package.json`。全量单文件:`npm run build:bridge:all`。体积构成与继续缩小思路见 **[docs/packaging-二进制与分发.md](./docs/packaging-二进制与分发.md)**(文内「主库还能再缩小吗」)。
24
+
25
+ 说明与「真·无 Node 二进制」的可行性见 **[docs/packaging-二进制与分发.md](./docs/packaging-二进制与分发.md)**。
26
+
27
+ 一键安装配置 + 后台服务 + `im-agent-hub-ctl` 管理命令:见 **[scripts/install/README.md](./scripts/install/README.md)**。日常升级代码与依赖可执行 **`im-agent-hub-ctl update`**(或 `update-help` / `version` 查看说明与当前状态)。若使用 bundle,`update` 后请再执行 **`npm run build:bridge`**。
28
+
29
+ 仓库内默认 **`openclaw.json` 可为精简结构**:仅包含 `channels.dingtalk-connector`(钉钉插件 / `@dingtalk-real-ai/dingtalk-connector` 所需字段)。安装脚本只合并该通道,不会自动写入 `bindings` 等其它顶级键。
30
+
31
+ ## 直接使用方式
32
+
33
+ im-agent-hub 是**库**:在你的代码里创建 Hub、配置 Router、Agents、Adapters,然后把收到的 IM 消息转成 `InboundMessage` 调用 `hub.handleInbound(input)`,回复会通过对应 adapter 发回 IM。
34
+
35
+ ```ts
36
+ import {
37
+ createHub,
38
+ createDingTalkAdapter,
39
+ createRuleBasedRouter,
40
+ createInMemorySessionStore,
41
+ createLangChainLikeAgent,
42
+ } from "@ylib/syim";
43
+
44
+ const hub = createHub({
45
+ router: createRuleBasedRouter({ defaultAgentId: "main" }),
46
+ agents: [
47
+ createLangChainLikeAgent({
48
+ id: "main",
49
+ systemPrompt: "你是助手。",
50
+ runnable: async ({ messages }) => {
51
+ const last = messages[messages.length - 1];
52
+ return { content: `回复:${last?.content ?? ""}` };
53
+ },
54
+ }),
55
+ ],
56
+ adapters: [
57
+ createDingTalkAdapter({
58
+ config: { clientId: "钉钉 AppKey", clientSecret: "钉钉 AppSecret" },
59
+ }),
60
+ ],
61
+ sessions: createInMemorySessionStore(),
62
+ });
63
+
64
+ // 当从钉钉收到消息时,把事件转成 InboundMessage 再调用:
65
+ await hub.handleInbound({
66
+ provider: "dingtalk",
67
+ peer: { kind: "direct", id: "用户 staffId" }, // 或 kind: "group", id: "群 openConversationId"
68
+ sender: { id: "用户 id", name: "用户名" },
69
+ messageId: "消息 id",
70
+ timestamp: Date.now(),
71
+ text: "用户发的文字",
72
+ });
73
+ ```
74
+
75
+ ## 多 Agent 与 Bindings(与主项目一致)
76
+
77
+ 主项目通过 `agents.list` + `bindings` 配置多个 agent,并按 channel/accountId/peer 匹配路由。im-agent-hub 提供相同语义:
78
+
79
+ - **agents**:多个 `Agent`(每个有 `id`、`run`,可选 `meta` 做 per-agent 配置)。
80
+ - **bindings**:`AgentRouteBinding[]`,每项为 `{ agentId, match }`。`match` 支持:`channel`、`accountId?`、`peer?`、`guildId?`(Discord)、`teamId?`(Slack)、`roles?`(角色列表)。
81
+ - **匹配顺序**(与主项目一致):**peer 精确** → **guildId + roles** → **guildId** → **teamId** → **account** → **channel** → **defaultAgentId**。
82
+
83
+ **方式一:用 createRouterFromBindings 自己组 Hub**
84
+
85
+ ```ts
86
+ import {
87
+ createHub,
88
+ createRouterFromBindings,
89
+ createDingTalkAdapter,
90
+ createInMemorySessionStore,
91
+ createLangChainLikeAgent,
92
+ } from "@ylib/syim";
93
+
94
+ const agents = [
95
+ createLangChainLikeAgent({ id: "main", runnable: async ({ messages }) => ({ content: "主助手回复" }) }),
96
+ createLangChainLikeAgent({ id: "support", runnable: async ({ messages }) => ({ content: "客服回复" }) }),
97
+ ];
98
+
99
+ const hub = createHub({
100
+ router: createRouterFromBindings({
101
+ defaultAgentId: "main",
102
+ bindings: [
103
+ { agentId: "support", match: { channel: "dingtalk", peer: { kind: "group", id: "群openConversationId" } } },
104
+ { agentId: "main", match: { channel: "dingtalk" } },
105
+ ],
106
+ }),
107
+ agents,
108
+ adapters: [createDingTalkAdapter({ config: { clientId: "...", clientSecret: "..." } })],
109
+ sessions: createInMemorySessionStore(),
110
+ });
111
+ ```
112
+
113
+ **方式二:用 createHubFromConfig 一次传入配置**
114
+
115
+ ```ts
116
+ import { createHubFromConfig, createDingTalkAdapter, createInMemorySessionStore, createLangChainLikeAgent } from "@ylib/syim";
117
+
118
+ const hub = createHubFromConfig({
119
+ defaultAgentId: "main",
120
+ agents: [
121
+ createLangChainLikeAgent({ id: "main", runnable: async ({ messages }) => ({ content: "主助手" }) }),
122
+ createLangChainLikeAgent({ id: "support", runnable: async ({ messages }) => ({ content: "客服" }) }),
123
+ ],
124
+ adapters: [createDingTalkAdapter({ config: { clientId: "...", clientSecret: "..." } })],
125
+ bindings: [
126
+ { agentId: "support", match: { channel: "dingtalk", peer: { kind: "group", id: "某群ID" } } },
127
+ { agentId: "main", match: { channel: "dingtalk" } },
128
+ ],
129
+ sessions: createInMemorySessionStore(),
130
+ });
131
+ ```
132
+
133
+ 同一钉钉账号下:该群消息走 `support`,其他单聊/群走 `main`。
134
+
135
+ ## Session 持久化与 HTTP 入口(与主项目能力对齐)
136
+
137
+ - **Session 持久化**:`createFileSessionStore({ dir? })` 将会话落盘(默认 `~/.openclaw/im-agent-hub/sessions`),pm2 重启后会话可恢复。`pnpm run dev` 时设置 `PERSIST_SESSIONS=1` 或 `SESSION_DIR=/path` 即可启用。
138
+ - **HTTP 入口**:`createInboundServer({ hub, healthPath?, auth? })` / `startInboundServer({ hub, port })` 提供 `GET /health`、`POST /inbound`;可选 `auth: { type: 'bearer' | 'api-key', token }`。
139
+
140
+ ## 媒体、记忆与工具(与主项目能力对齐)
141
+
142
+ - **入站附件**:`downloadInboundAttachments(input, { dir?, timeoutMs? })` 将 `input.attachments` 中带 `url` 的项下载到本地并填回 `path`,再交 `hub.handleInbound`。
143
+ - **出站附件**:`OutboundMessage.attachments` 可带 `{ path, contentType?, fileName? }`,钉钉 adapter 会上传后发文件消息。
144
+ - **记忆**:`HubConfig.memory` 传入 `MemoryStore`(如 `createInMemoryMemoryStore()`),hub 会注入到 `agent.run({ memory })`。
145
+ - **工具**:`HubConfig.tools` 为 `Record<string, (args) => Promise<string>>`;agent 返回 `toolCalls` 时 hub 执行并拼入回复。
146
+ - **白名单**:`HubConfig.allowFrom?: (input: InboundMessage) => boolean`,仅当返回 true 时处理消息。
147
+
148
+ ## 如何测试(单 agent 收所有 IM)
149
+
150
+ 用「一个简单 agent 接收所有 IM 消息」来验证 Hub 与路由是否正常。
151
+
152
+ ### 1. 启动示例服务
153
+
154
+ 在 `packages/im-agent-hub` 下执行:
155
+
156
+ ```bash
157
+ cd packages/im-agent-hub
158
+ pnpm install
159
+ # 可选:配置钉钉后回复会真实发到钉钉;不配置则仅 HTTP 测试,回复在控制台打印
160
+ DINGTALK_CLIENT_ID=你的AppKey DINGTALK_CLIENT_SECRET=你的AppSecret pnpm run example:single-agent
161
+ ```
162
+
163
+ 服务监听 `http://localhost:3100`(可用 `PORT=3000` 改端口)。
164
+
165
+ ### 2. 用 curl 模拟一条 IM 消息
166
+
167
+ 任意一条「入站消息」都会路由到同一个 agent(`main`),并得到回复 `[测试 agent] 收到:xxx`。
168
+
169
+ 单聊示例(把 `YOUR_STAFF_ID` 换成你的钉钉 userId):
170
+
171
+ ```bash
172
+ curl -X POST http://localhost:3100/inbound -H "Content-Type: application/json" -d '{
173
+ "provider": "dingtalk",
174
+ "peer": { "kind": "direct", "id": "YOUR_STAFF_ID" },
175
+ "sender": { "id": "YOUR_STAFF_ID", "name": "测试" },
176
+ "messageId": "test-1",
177
+ "timestamp": 1700000000000,
178
+ "text": "你好"
179
+ }'
180
+ ```
181
+
182
+ 成功时返回 `{"ok":true,"text":"你好","agentId":"main"}`。若已配置钉钉,钉钉端会收到 `[测试 agent] 收到:你好`。
183
+
184
+ ### 3. 测试 /new、/reset
185
+
186
+ 再发一条,内容为 `/new` 或 `/reset`:
187
+
188
+ ```bash
189
+ curl -X POST http://localhost:3100/inbound -H "Content-Type: application/json" -d '{"provider":"dingtalk","peer":{"kind":"direct","id":"YOUR_STAFF_ID"},"sender":{"id":"YOUR_STAFF_ID","name":"测试"},"messageId":"test-2","timestamp":1700000000000,"text":"/new"}'
190
+ ```
191
+
192
+ 会返回成功,且若配置了钉钉会收到「已开启新的上下文。」。
193
+
194
+ ### 4. 用真实钉钉收消息
195
+
196
+ 若要让「钉钉里发消息 → 同一 agent 回复」,可再开一个终端跑 Stream 收消息(与上面示例共用同一套 agent 逻辑):
197
+
198
+ ```bash
199
+ DINGTALK_CLIENT_ID=xxx DINGTALK_CLIENT_SECRET=yyy pnpm run start:dingtalk
200
+ ```
201
+
202
+ 钉钉里给机器人发消息,会由 `start:dingtalk` 里的 Hub 处理并回发(该脚本内也是单 agent,逻辑与 example-single-agent 一致)。
203
+
204
+ ## 使用 iflow 接口作为 Agent
205
+
206
+ 若后端是 **iflow(OpenAI 兼容)** 接口,可用内置的 `createIflowAgent`,让所有 IM 消息对接该 agent。
207
+
208
+ ### 环境变量
209
+
210
+ | 变量 | 必填 | 说明 |
211
+ |------|------|------|
212
+ | `IFLOW_API_KEY` | 是 | API Key,**请勿写入代码**,仅通过环境变量传入 |
213
+ | `IFLOW_BASE_URL` | 否 | 默认 `http://92.113.117.22:10068/proxy/iflow` |
214
+
215
+ ### 一键跑示例(HTTP + 可选钉钉回发)
216
+
217
+ ```bash
218
+ cd packages/im-agent-hub
219
+ export IFLOW_API_KEY=sk-你的key
220
+ # 可选:钉钉回发
221
+ export DINGTALK_CLIENT_ID=xxx
222
+ export DINGTALK_CLIENT_SECRET=yyy
223
+ pnpm run example:iflow-agent
224
+ ```
225
+
226
+ 然后 `curl -X POST http://localhost:3100/inbound ...` 或钉钉内发消息,回复由 iflow 接口生成。
227
+
228
+ ### 在代码中使用
229
+
230
+ ```ts
231
+ import { createHubFromConfig, createIflowAgent, createDingTalkAdapter, createInMemorySessionStore } from "@ylib/syim";
232
+
233
+ const hub = createHubFromConfig({
234
+ defaultAgentId: "main",
235
+ agents: [
236
+ createIflowAgent({
237
+ id: "main",
238
+ systemPrompt: "你是助手。",
239
+ historyLimit: 20,
240
+ // baseUrl / apiKey 不传则从环境变量 IFLOW_BASE_URL、IFLOW_API_KEY 读取
241
+ }),
242
+ ],
243
+ adapters: [createDingTalkAdapter({ config: { clientId: "...", clientSecret: "..." } })],
244
+ bindings: [{ agentId: "main", match: { channel: "dingtalk" } }],
245
+ sessions: createInMemorySessionStore(),
246
+ });
247
+ ```
248
+
249
+ ## 本地测试钉钉 IM
250
+
251
+ 不跑完整 OpenClaw 和钉钉 connector 时,可以用包里的**开发服务器**只测「Hub + 钉钉发送」是否正常。
252
+
253
+ ### 1. 安装依赖
254
+
255
+ 在 openclaw 仓库根目录执行:
256
+
257
+ ```bash
258
+ pnpm install
259
+ ```
260
+
261
+ ### 2. 配置钉钉应用
262
+
263
+ - 在 [钉钉开放平台](https://open.dingtalk.com/) 创建企业内部应用,拿到 **AppKey**(clientId)和 **AppSecret**(clientSecret)。
264
+ - 开通机器人能力、单聊/群聊发送权限(与 [dingtalk-openclaw-connector](https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector) 文档一致)。
265
+
266
+ ### 3. 启动开发服务器
267
+
268
+ 在 `packages/im-agent-hub` 下执行(需先 `pnpm install` 以安装 tsx):
269
+
270
+ ```bash
271
+ cd packages/im-agent-hub
272
+ DINGTALK_CLIENT_ID=你的AppKey DINGTALK_CLIENT_SECRET=你的AppSecret pnpm run dev
273
+ ```
274
+
275
+ 或先导出环境变量再启动:
276
+
277
+ ```bash
278
+ export DINGTALK_CLIENT_ID=你的AppKey
279
+ export DINGTALK_CLIENT_SECRET=你的AppSecret
280
+ pnpm run dev
281
+ ```
282
+
283
+ 服务会在 `http://localhost:3100` 启动(可通过 `PORT=3000 pnpm run dev` 改端口)。
284
+
285
+ ### 4. 发送测试消息
286
+
287
+ - **单聊**:`peer.id` 填你的钉钉 **staffId**(或 unionId,视应用权限而定)。
288
+ - **群聊**:`peer` 填 `{ "kind": "group", "id": "群的 openConversationId" }`。
289
+
290
+ 示例(单聊,把 `YOUR_STAFF_ID` 换成真实 userId):
291
+
292
+ ```bash
293
+ curl -X POST http://localhost:3100/inbound \
294
+ -H "Content-Type: application/json" \
295
+ -d '{
296
+ "provider": "dingtalk",
297
+ "peer": { "kind": "direct", "id": "YOUR_STAFF_ID" },
298
+ "sender": { "id": "YOUR_STAFF_ID", "name": "测试" },
299
+ "messageId": "test-1",
300
+ "timestamp": 1700000000000,
301
+ "text": "你好"
302
+ }'
303
+ ```
304
+
305
+ 成功时接口返回 `{"ok":true,"text":"你好"}`,钉钉端会收到机器人回复(当前 dev-server 为 echo 风格:「收到:你好」)。
306
+
307
+ ### 5. 测试 /new、/reset
308
+
309
+ 对同一 peer 再发一条:
310
+
311
+ ```bash
312
+ curl -X POST http://localhost:3100/inbound \
313
+ -H "Content-Type: application/json" \
314
+ -d '{"provider":"dingtalk","peer":{"kind":"direct","id":"YOUR_STAFF_ID"},"sender":{"id":"YOUR_STAFF_ID","name":"测试"},"messageId":"test-2","timestamp":1700000000000,"text":"/new"}'
315
+ ```
316
+
317
+ 钉钉会收到「已开启新的上下文。」,后续消息会使用新的会话上下文。
318
+
319
+ ## 钉钉完整收发流程(Stream 收消息 + Hub 回发)
320
+
321
+ 在**不启动 OpenClaw** 的情况下,用与官方 [dingtalk-openclaw-connector](https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector) 相同的 **Stream 模式**收钉钉消息,由 im-agent-hub 处理并回发。
322
+
323
+ ### 1. 钉钉应用配置
324
+
325
+ - 在 [钉钉开放平台](https://open.dingtalk.com/) 创建**企业内部应用**,开启机器人能力。
326
+ - 消息接收方式选择 **Stream 模式**(与官方 connector 一致),并保存 **AppKey**、**AppSecret**。
327
+ - 为应用开通「机器人发送消息到群聊」「机器人发送单聊消息」等权限。
328
+
329
+ ### 2. 启动 Stream 收消息服务
330
+
331
+ 在 `packages/im-agent-hub` 下执行:
332
+
333
+ ```bash
334
+ cd packages/im-agent-hub
335
+ pnpm install
336
+ DINGTALK_CLIENT_ID=你的AppKey DINGTALK_CLIENT_SECRET=你的AppSecret pnpm run start:dingtalk
337
+ ```
338
+
339
+ 可选:`DEBUG=1` 可打开 dingtalk-stream 调试日志。
340
+
341
+ ### 3. 在钉钉里发消息
342
+
343
+ - **单聊**:在钉钉里找到你的机器人,发「你好」等文字,机器人会经 Hub 回复(当前为 echo:「收到:你好」)。
344
+ - **群聊**:把机器人拉进群,在群里 @机器人 或发消息(视钉钉机器人设置),同样会走 Hub 回发。
345
+ - 发送 **/new** 或 **/reset** 可重开会话上下文。
346
+
347
+ ### 4. 自定义 Agent(如接 LLM)
348
+
349
+ 当前 `scripts/dingtalk-stream-server.ts` 里使用的是内置 echo 风格 agent。要接真实 LLM,可复制该脚本到自己的项目,或修改脚本中的 `createLangChainLikeAgent` 的 `runnable`,改为调用你的 LangChain / OpenAI 等接口。
350
+
351
+ ## Connector 模式(多账号、项目配置)
352
+
353
+ `pnpm run start:connector` 使用官方 dingtalk-openclaw-connector,支持多企业钉钉部署。配置读取优先级:
354
+
355
+ 1. **项目目录** `openclaw.json`(与 package.json 同目录)
356
+ 2. `~/.openclaw/openclaw.json`
357
+ 3. 环境变量 `DINGTALK_CLIENT_ID` / `DINGTALK_CLIENT_SECRET`(单账号)
358
+
359
+ 在项目根目录创建或复制 `openclaw.json`:至少配置 `channels.dingtalk-connector`。**`dingtalk-stdio-bridge`** 与精简 `openclaw.json`(仅 `channels`)即可工作;若使用 **`start:connector`** 且需多 Agent 路由,再按需增加 `bindings` 等键。
360
+
361
+ ## 将 /v1/chat/completions 代理到 yuce-gpt(可选)
362
+
363
+ 若希望 **对话能力由 yuce-gpt 提供**(复用其 Agent、技能、记忆等),可将 im-agent-hub 的 `POST /v1/chat/completions` 配置为**反向代理**到 yuce-gpt:
364
+
365
+ - **yuce-gpt 端点**:`POST https://<yuce-gpt-host>/api/v1/yucegpt/v1/chat/completions`
366
+ - **鉴权**:在代理请求头中增加 `Authorization: Bearer <OPENCLAW_GATEWAY_API_KEY>`(yuce-gpt 侧需配置同名环境变量 `OPENCLAW_GATEWAY_API_KEY` 及 `OPENCLAW_GATEWAY_TENANT_ID` / `USER_ID` / `USER_NAME`)。
367
+ - **透传**:将 `X-OpenClaw-Agent-Id`、`X-OpenClaw-Session-Key` 及 body 原样转发。
368
+
369
+ 实现方式:在 `createInboundServer` 中若启用 `enableChatCompletions`,可改为不调用本地 `createChatCompletionsHandler({ hub })`,而是将请求 proxy 到上述 yuce-gpt URL(需自行用 fetch/axios 或 nginx 做反向代理)。详见 yuce-gpt 仓库文档:`doc/OpenClaw与ACP会话形式对接说明.md`。
370
+
371
+ ## 与 OpenClaw / 钉钉 connector 的关系
372
+
373
+ - **im-agent-hub**:提供「会话路由 + 上下文 + 多 adapter 发送」;钉钉发送逻辑复制自官方 connector 的 OpenAPI。
374
+ - **完整收发**可选两种方式:
375
+ 1. **本包内置**:`pnpm run start:dingtalk` 使用 dingtalk-stream 收消息,交给 hub 处理并用本包钉钉 adapter 回发(无需 OpenClaw)。
376
+ 2. **Connector 模式**:`pnpm run start:connector` 加载 dingtalk-openclaw-connector,支持多账号,配置从项目 `openclaw.json` 读取。
package/bin/syim.mjs ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "node:child_process";
4
+ import { createRequire } from "node:module";
5
+ import { fileURLToPath } from "node:url";
6
+ import path from "node:path";
7
+
8
+ const major = Number.parseInt(process.versions.node.split(".")[0] || "0", 10);
9
+ if (major < 22) {
10
+ console.error(`[syim] Node.js >= 22 is required, current: v${process.versions.node}`);
11
+ console.error("[syim] please run: nvm use 22");
12
+ process.exit(1);
13
+ }
14
+
15
+ const thisFile = fileURLToPath(import.meta.url);
16
+ const pkgRoot = path.resolve(path.dirname(thisFile), "..");
17
+ const entry = path.join(pkgRoot, "bridges", "main.ts");
18
+ const require = createRequire(import.meta.url);
19
+ const tsxPkgJson = require.resolve("tsx/package.json", { paths: [pkgRoot] });
20
+ const tsxCli = path.join(path.dirname(tsxPkgJson), "dist", "cli.mjs");
21
+
22
+ const child = spawn(process.execPath, [tsxCli, entry, ...process.argv.slice(2)], {
23
+ stdio: "inherit",
24
+ env: process.env,
25
+ });
26
+
27
+ child.on("exit", (code, signal) => {
28
+ if (signal) {
29
+ process.kill(process.pid, signal);
30
+ return;
31
+ }
32
+ process.exit(code ?? 0);
33
+ });
34
+
@@ -0,0 +1,10 @@
1
+ /**
2
+ * 主目录入口:钉钉 stdio bridge。
3
+ * 真实实现暂保留在 scripts,避免一次性迁移带来路径风险。
4
+ */
5
+ import { setupBridgeLogger } from "./logger.ts";
6
+
7
+ const logFilePath = setupBridgeLogger("dingtalk-bridge");
8
+ console.error("[dingtalk-bridge] log file:", logFilePath);
9
+
10
+ import "../scripts/dingtalk-stdio-bridge.ts";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * 主目录入口:飞书 stdio bridge。
3
+ * 真实实现暂保留在 scripts,避免一次性迁移带来路径风险。
4
+ */
5
+ import { setupBridgeLogger } from "./logger.ts";
6
+
7
+ const logFilePath = setupBridgeLogger("lark-bridge");
8
+ console.error("[lark-bridge] log file:", logFilePath);
9
+
10
+ import "../scripts/lark-stdio-bridge.ts";
@@ -0,0 +1,47 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { format } from "node:util";
4
+
5
+ type ConsoleMethod = (...args: unknown[]) => void;
6
+
7
+ type BridgeLoggerState = {
8
+ enabled: boolean;
9
+ logFilePath: string;
10
+ };
11
+
12
+ declare global {
13
+ // eslint-disable-next-line no-var
14
+ var __imAgentHubBridgeLogger: BridgeLoggerState | undefined;
15
+ }
16
+
17
+ function buildLogFilePath(prefix: string): string {
18
+ const logsDir = path.join(process.cwd(), "logs");
19
+ fs.mkdirSync(logsDir, { recursive: true });
20
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
21
+ return path.join(logsDir, `${prefix}-${stamp}-${process.pid}.log`);
22
+ }
23
+
24
+ function patchConsole(writeLine: (line: string) => void): void {
25
+ const methods: Array<keyof Console> = ["log", "info", "warn", "error", "debug"];
26
+ for (const method of methods) {
27
+ const original = console[method] as ConsoleMethod;
28
+ console[method] = (...args: unknown[]) => {
29
+ const line = format(...args);
30
+ writeLine(`[${new Date().toISOString()}] [${method}] ${line}`);
31
+ original(...args);
32
+ };
33
+ }
34
+ }
35
+
36
+ export function setupBridgeLogger(prefix: string): string {
37
+ const exists = globalThis.__imAgentHubBridgeLogger;
38
+ if (exists?.enabled) return exists.logFilePath;
39
+
40
+ const logFilePath = buildLogFilePath(prefix);
41
+ const stream = fs.createWriteStream(logFilePath, { flags: "a" });
42
+ stream.write(`[${new Date().toISOString()}] [system] bridge logger started\n`);
43
+
44
+ patchConsole((line) => stream.write(`${line}\n`));
45
+ globalThis.__imAgentHubBridgeLogger = { enabled: true, logFilePath };
46
+ return logFilePath;
47
+ }
@@ -0,0 +1,126 @@
1
+ import { setupBridgeLogger } from "./logger.ts";
2
+ import fs from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const bridgeDir = path.dirname(fileURLToPath(import.meta.url));
8
+ const packageRoot = path.resolve(bridgeDir, "..");
9
+
10
+ /**
11
+ * 统一桥接入口。
12
+ *
13
+ * 默认同时启动钉钉和飞书两个 bridge。
14
+ * 可通过参数控制:
15
+ * --only=dingtalk
16
+ * --only=lark
17
+ * --only=all
18
+ */
19
+ function parseOnlyArg(): "all" | "dingtalk" | "lark" {
20
+ const onlyArg = process.argv.find((arg) => arg.startsWith("--only="));
21
+ const onlyValue = (onlyArg?.slice("--only=".length) || "all").toLowerCase();
22
+ if (onlyValue === "dingtalk" || onlyValue === "lark" || onlyValue === "all") {
23
+ return onlyValue;
24
+ }
25
+ console.warn(`[bridges/main] unknown --only value "${onlyValue}", fallback to "all"`);
26
+ return "all";
27
+ }
28
+
29
+ function loadOpenClawConfig(): { configPath: string; config: Record<string, unknown> } | null {
30
+ const configPaths = [path.join(process.cwd(), "syim.json"), path.join(os.homedir(), ".syim", "syim.json")];
31
+ for (const configPath of configPaths) {
32
+ if (!fs.existsSync(configPath)) continue;
33
+ try {
34
+ const content = fs.readFileSync(configPath, "utf-8");
35
+ const config = JSON.parse(content) as Record<string, unknown>;
36
+ return { configPath, config };
37
+ } catch (err) {
38
+ console.error("[bridges/main] found config but failed to parse:", configPath, (err as Error).message);
39
+ process.exit(1);
40
+ }
41
+ }
42
+ return null;
43
+ }
44
+
45
+ function printConfigBootstrapGuide(): void {
46
+ const homeOpenClawDir = path.join(os.homedir(), ".syim");
47
+ const homeConfigPath = path.join(homeOpenClawDir, "syim.json");
48
+ const templatePath = path.join(packageRoot, "syim.json.bak");
49
+ const template = fs.existsSync(templatePath)
50
+ ? fs.readFileSync(templatePath, "utf-8")
51
+ : "{\n \"error\": \"syim.json.bak not found\"\n}";
52
+
53
+ console.error("\n[syim] 未检测到配置文件。");
54
+ console.error("[syim] 已检查路径:");
55
+ console.error(` - ${path.join(process.cwd(), "syim.json")}`);
56
+ console.error(` - ${homeConfigPath}`);
57
+
58
+ console.error("\n[syim] 请执行以下命令在 Home 目录创建配置:");
59
+ console.error(` mkdir -p "${homeOpenClawDir}"`);
60
+ console.error(` cp "${templatePath}" "${homeConfigPath}"`);
61
+
62
+ console.error("\n[syim] 打开配置文件进行编辑(macOS):");
63
+ console.error(` open -a TextEdit "${homeConfigPath}"`);
64
+ console.error("\n[syim] 或使用命令行编辑器:");
65
+ console.error(` nano "${homeConfigPath}"`);
66
+ console.error("\n[syim] 或使用 VS Code:");
67
+ console.error(` code "${homeConfigPath}"`);
68
+
69
+ console.error("\n[syim] 配置模板(来自 syim.json.bak):\n");
70
+ console.error(template);
71
+
72
+ console.error("\n[syim] 字段说明:");
73
+ console.error("- channels: 渠道配置集合。");
74
+ console.error("- channels.feishu.enabled: 是否启用飞书渠道。");
75
+ console.error("- channels.feishu.accounts.<id>.appId/appSecret: 飞书应用凭据。");
76
+ console.error("- channels.feishu.accounts.<id>.gatewayBaseUrl: 网关地址。");
77
+ console.error("- channels.feishu.accounts.<id>.gatewayToken: 网关鉴权 token。");
78
+ console.error("- channels.feishu.accounts.<id>.dmPolicy/allowFrom: 私聊与来源策略。");
79
+ console.error("- channels.feishu.accounts.<id>.separateSessionByConversation/groupSessionScope: 会话隔离策略。");
80
+ console.error("- channels.feishu.accounts.<id>.streaming: 是否流式回复。");
81
+ console.error("- channels.feishu.accounts.<id>.uploadHost: 上传服务地址。");
82
+ console.error("- channels.dingtalk-connector.enabled: 是否启用钉钉渠道。");
83
+ console.error("- channels.dingtalk-connector.accounts.<id>.clientId/clientSecret: 钉钉应用凭据。");
84
+ console.error("- channels.dingtalk-connector.accounts.<id>.gatewayBaseUrl/gatewayToken: 网关地址与鉴权。");
85
+ console.error("- channels.dingtalk-connector.accounts.<id>.modelName: 模型标识。");
86
+ console.error("- channels.dingtalk-connector.accounts.<id>.asyncMode: 是否异步模式。");
87
+ console.error("- channels.dingtalk-connector.accounts.<id>.gatewayPort: 本地网关端口(部分模式使用)。");
88
+ console.error("- channels.dingtalk-connector.accounts.<id>.uploadHost: 上传服务地址。");
89
+ console.error("- bindings: 渠道路由规则。");
90
+ console.error("- bindings[].agentId: 目标 Agent ID。");
91
+ console.error("- bindings[].match.channel: 命中的渠道。");
92
+ }
93
+
94
+ async function main(): Promise<void> {
95
+ const logFilePath = setupBridgeLogger("bridges-main");
96
+ console.error("[bridges/main] log file:", logFilePath);
97
+
98
+ const loaded = loadOpenClawConfig();
99
+ if (!loaded) {
100
+ printConfigBootstrapGuide();
101
+ process.exit(1);
102
+ }
103
+ console.error("[bridges/main] config loaded:", loaded.configPath);
104
+
105
+ const only = parseOnlyArg();
106
+ const tasks: Array<Promise<unknown>> = [];
107
+
108
+ if (only === "all" || only === "dingtalk") {
109
+ console.error("[bridges/main] starting dingtalk bridge...");
110
+ tasks.push(import("./dingtalk-stdio-bridge.ts"));
111
+ }
112
+
113
+ if (only === "all" || only === "lark") {
114
+ console.error("[bridges/main] starting lark bridge...");
115
+ tasks.push(import("./lark-stdio-bridge.ts"));
116
+ }
117
+
118
+ // 两个模块内部都以长驻服务运行;这里仅确保入口导入完成。
119
+ await Promise.all(tasks);
120
+ console.error(`[bridges/main] started: ${only}`);
121
+ }
122
+
123
+ main().catch((err) => {
124
+ console.error("[bridges/main]", err);
125
+ process.exit(1);
126
+ });
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "ylib-syim",
3
+ "version": "0.0.1",
4
+ "description": "多 IM / 多 Agent 的会话路由与上下文管理(支持 /new)",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./src/index.ts"
8
+ },
9
+ "bin": {
10
+ "syim": "./bin/syim.mjs"
11
+ },
12
+ "engines": {
13
+ "node": ">=22"
14
+ },
15
+ "files": [
16
+ "bin",
17
+ "bridges",
18
+ "scripts",
19
+ "README.md",
20
+ "syim.json.bak"
21
+ ],
22
+ "sideEffects": false,
23
+ "scripts": {
24
+ "dev": "tsx scripts/dev-server.ts",
25
+ "start": "tsx src/cli.ts start",
26
+ "start:dingtalk": "tsx scripts/dingtalk-stream-server.ts",
27
+ "example:single-agent": "tsx scripts/example-single-agent.ts",
28
+ "example:iflow-agent": "tsx scripts/example-iflow-agent.ts",
29
+ "example:feishu-webhook": "tsx scripts/example-feishu-webhook.ts",
30
+ "start:connector": "tsx src/connector-host.ts",
31
+ "start:stdio-bridge": "tsx bridges/dingtalk-stdio-bridge.ts",
32
+ "start:lark-bridge": "tsx bridges/lark-stdio-bridge.ts",
33
+ "start:bridges": "tsx bridges/main.ts",
34
+ "start:feishu-bridge": "tsx scripts/feishu-yuce-bridge.ts",
35
+ "build:bridge": "node scripts/build-bridge.mjs",
36
+ "build:bridge:all": "node scripts/build-bridge.mjs --all",
37
+ "build:bridge:debug": "node scripts/build-bridge.mjs --no-minify",
38
+ "start:stdio-bridge:bundle": "node dist/dingtalk-stdio-bridge.cjs"
39
+ },
40
+ "dependencies": {
41
+ "@ffmpeg-installer/ffmpeg": "^1.1.0",
42
+ "ylib-dingtalk-connector": "0.7.10-beata.1",
43
+ "ylib-openclaw-lark": "2026.3.17-beata.1",
44
+ "axios": "^1.6.0",
45
+ "dingtalk-stream": "^2.1.4",
46
+ "fluent-ffmpeg": "^2.1.3",
47
+ "form-data": "^4.0.0",
48
+ "mammoth": "^1.8.0",
49
+ "openclaw": "2026.2.26",
50
+ "pdf-parse": "^1.1.1",
51
+ "tsx": "^4.21.0"
52
+ },
53
+ "devDependencies": {
54
+ "esbuild": "^0.27.4"
55
+ }
56
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "im-agent-hub-dingtalk-bridge-runtime",
3
+ "private": true,
4
+ "description": "仅拷贝 dist/dingtalk-stdio-bridge.cjs 部署时:与本文件同目录 npm install,提供 build:bridge 未打入的运行时依赖(与 im-agent-hub package.json 保持一致)",
5
+ "dependencies": {
6
+ "@dingtalk-real-ai/dingtalk-connector": "^0.7.10",
7
+ "@ffmpeg-installer/ffmpeg": "^1.1.0",
8
+ "axios": "^1.6.0",
9
+ "dingtalk-stream": "^2.1.4",
10
+ "fluent-ffmpeg": "^2.1.3",
11
+ "mammoth": "^1.8.0",
12
+ "pdf-parse": "^1.1.1"
13
+ }
14
+ }