@yanhaidao/wecom 2.3.160 → 2.3.190
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 +294 -379
- package/SKILLS_CAL.md +895 -0
- package/SKILLS_DOC.md +2288 -0
- package/changelog/v2.3.18.md +22 -0
- package/changelog/v2.3.19.md +73 -0
- package/index.ts +39 -3
- package/package.json +2 -3
- package/src/agent/handler.event-filter.test.ts +11 -0
- package/src/agent/handler.ts +732 -643
- package/src/app/account-runtime.ts +46 -20
- package/src/app/index.ts +20 -1
- package/src/capability/bot/stream-orchestrator.ts +1 -1
- package/src/capability/calendar/SKILLS_CHECKLIST.md +251 -0
- package/src/capability/calendar/client.ts +815 -0
- package/src/capability/calendar/index.ts +3 -0
- package/src/capability/calendar/schema.ts +417 -0
- package/src/capability/calendar/tool.ts +417 -0
- package/src/capability/calendar/types.ts +309 -0
- package/src/capability/doc/client.ts +788 -64
- package/src/capability/doc/schema.ts +419 -318
- package/src/capability/doc/tool.ts +1517 -1178
- package/src/capability/doc/types.ts +130 -14
- package/src/capability/mcp/index.ts +10 -0
- package/src/capability/mcp/schema.ts +107 -0
- package/src/capability/mcp/tool.ts +170 -0
- package/src/capability/mcp/transport.ts +394 -0
- package/src/channel.ts +70 -28
- package/src/config/index.ts +7 -1
- package/src/config/media.test.ts +113 -0
- package/src/config/media.ts +133 -6
- package/src/config/schema.ts +74 -102
- package/src/outbound.test.ts +250 -15
- package/src/outbound.ts +155 -30
- package/src/runtime/reply-orchestrator.test.ts +35 -2
- package/src/runtime/reply-orchestrator.ts +14 -2
- package/src/runtime/routing-bridge.test.ts +115 -0
- package/src/runtime/routing-bridge.ts +26 -1
- package/src/runtime/session-manager.ts +20 -6
- package/src/runtime/source-registry.ts +165 -0
- package/src/transport/bot-webhook/inbound-normalizer.ts +4 -4
- package/src/transport/bot-ws/media.test.ts +44 -0
- package/src/transport/bot-ws/media.ts +272 -0
- package/src/transport/bot-ws/reply.test.ts +216 -18
- package/src/transport/bot-ws/reply.ts +116 -21
- package/src/transport/bot-ws/sdk-adapter.test.ts +64 -1
- package/src/transport/bot-ws/sdk-adapter.ts +89 -12
- package/src/types/config.ts +3 -0
- package/.claude/settings.local.json +0 -11
- package/docs/update-content-fix.md +0 -135
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import AiBot, {
|
|
3
|
+
generateReqId,
|
|
4
|
+
type BaseMessage,
|
|
5
|
+
type EventMessage,
|
|
6
|
+
type WsFrame,
|
|
7
|
+
} from "@wecom/aibot-node-sdk";
|
|
8
|
+
import type { WecomAccountRuntime } from "../../app/account-runtime.js";
|
|
6
9
|
import { registerBotWsPushHandle, unregisterBotWsPushHandle } from "../../app/index.js";
|
|
10
|
+
import { clearWecomMcpAccountCache } from "../../capability/mcp/index.js";
|
|
11
|
+
import type { RuntimeLogSink } from "../../types/index.js";
|
|
7
12
|
import { mapBotWsFrameToInboundEvent } from "./inbound.js";
|
|
13
|
+
import { uploadAndSendBotWsMedia } from "./media.js";
|
|
8
14
|
import { createBotWsReplyHandle } from "./reply.js";
|
|
9
15
|
import { createBotWsSessionSnapshot } from "./session.js";
|
|
10
|
-
import type { WecomAccountRuntime } from "../../app/account-runtime.js";
|
|
11
16
|
|
|
12
17
|
export class BotWsSdkAdapter {
|
|
13
18
|
private client?: AiBot.WSClient;
|
|
@@ -32,15 +37,35 @@ export class BotWsSdkAdapter {
|
|
|
32
37
|
botId: bot.ws.botId,
|
|
33
38
|
secret: bot.ws.secret,
|
|
34
39
|
logger: {
|
|
35
|
-
debug: (message, ...args) =>
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
debug: (message, ...args) =>
|
|
41
|
+
this.log.info?.(`[wecom-ws] ${message} ${args.join(" ")}`.trim()),
|
|
42
|
+
info: (message, ...args) =>
|
|
43
|
+
this.log.info?.(`[wecom-ws] ${message} ${args.join(" ")}`.trim()),
|
|
44
|
+
warn: (message, ...args) =>
|
|
45
|
+
this.log.warn?.(`[wecom-ws] ${message} ${args.join(" ")}`.trim()),
|
|
46
|
+
error: (message, ...args) =>
|
|
47
|
+
this.log.error?.(`[wecom-ws] ${message} ${args.join(" ")}`.trim()),
|
|
39
48
|
},
|
|
40
49
|
});
|
|
41
50
|
this.client = client;
|
|
42
51
|
registerBotWsPushHandle(this.runtime.account.accountId, {
|
|
43
52
|
isConnected: () => client.isConnected,
|
|
53
|
+
replyCommand: async ({ cmd, body, headers }) => {
|
|
54
|
+
const result = await client.reply(
|
|
55
|
+
{ headers: headers ?? { req_id: generateReqId("wecom_ws") } },
|
|
56
|
+
body ?? {},
|
|
57
|
+
cmd,
|
|
58
|
+
);
|
|
59
|
+
this.runtime.touchTransportSession("bot-ws", {
|
|
60
|
+
ownerId: this.ownerId,
|
|
61
|
+
running: true,
|
|
62
|
+
connected: client.isConnected,
|
|
63
|
+
authenticated: client.isConnected,
|
|
64
|
+
lastOutboundAt: Date.now(),
|
|
65
|
+
lastError: undefined,
|
|
66
|
+
});
|
|
67
|
+
return result as Record<string, unknown>;
|
|
68
|
+
},
|
|
44
69
|
sendMarkdown: async (chatId, content) => {
|
|
45
70
|
await client.sendMessage(chatId, {
|
|
46
71
|
msgtype: "markdown",
|
|
@@ -55,6 +80,30 @@ export class BotWsSdkAdapter {
|
|
|
55
80
|
lastError: undefined,
|
|
56
81
|
});
|
|
57
82
|
},
|
|
83
|
+
sendMedia: async ({ chatId, mediaUrl, text, mediaLocalRoots, maxBytes }) => {
|
|
84
|
+
const result = await uploadAndSendBotWsMedia({
|
|
85
|
+
wsClient: client,
|
|
86
|
+
chatId,
|
|
87
|
+
mediaUrl,
|
|
88
|
+
mediaLocalRoots,
|
|
89
|
+
maxBytes,
|
|
90
|
+
});
|
|
91
|
+
if (result.ok && text?.trim()) {
|
|
92
|
+
await client.sendMessage(chatId, {
|
|
93
|
+
msgtype: "markdown",
|
|
94
|
+
markdown: { content: text.trim() },
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
this.runtime.touchTransportSession("bot-ws", {
|
|
98
|
+
ownerId: this.ownerId,
|
|
99
|
+
running: true,
|
|
100
|
+
connected: client.isConnected,
|
|
101
|
+
authenticated: client.isConnected,
|
|
102
|
+
lastOutboundAt: Date.now(),
|
|
103
|
+
lastError: result.ok ? undefined : result.error,
|
|
104
|
+
});
|
|
105
|
+
return result;
|
|
106
|
+
},
|
|
58
107
|
});
|
|
59
108
|
|
|
60
109
|
client.on("connected", () => {
|
|
@@ -82,8 +131,12 @@ export class BotWsSdkAdapter {
|
|
|
82
131
|
});
|
|
83
132
|
|
|
84
133
|
client.on("disconnected", (reason) => {
|
|
134
|
+
clearWecomMcpAccountCache(this.runtime.account.accountId);
|
|
85
135
|
const normalizedReason = String(reason ?? "").toLowerCase();
|
|
86
|
-
const kicked =
|
|
136
|
+
const kicked =
|
|
137
|
+
normalizedReason.includes("kick") ||
|
|
138
|
+
normalizedReason.includes("owner") ||
|
|
139
|
+
normalizedReason.includes("replaced");
|
|
87
140
|
this.log.warn?.(
|
|
88
141
|
`[wecom-ws] disconnected account=${this.runtime.account.accountId} kicked=${String(kicked)} reason=${reason ?? "unknown"}`,
|
|
89
142
|
);
|
|
@@ -109,11 +162,15 @@ export class BotWsSdkAdapter {
|
|
|
109
162
|
});
|
|
110
163
|
|
|
111
164
|
client.on("reconnecting", (attempt) => {
|
|
112
|
-
this.log.warn?.(
|
|
165
|
+
this.log.warn?.(
|
|
166
|
+
`[wecom-ws] reconnecting account=${this.runtime.account.accountId} attempt=${attempt}`,
|
|
167
|
+
);
|
|
113
168
|
});
|
|
114
169
|
|
|
115
170
|
client.on("error", (error) => {
|
|
116
|
-
this.log.error?.(
|
|
171
|
+
this.log.error?.(
|
|
172
|
+
`[wecom-ws] error account=${this.runtime.account.accountId} message=${error.message}`,
|
|
173
|
+
);
|
|
117
174
|
this.runtime.updateTransportSession(
|
|
118
175
|
createBotWsSessionSnapshot({
|
|
119
176
|
accountId: this.runtime.account.accountId,
|
|
@@ -176,6 +233,25 @@ export class BotWsSdkAdapter {
|
|
|
176
233
|
});
|
|
177
234
|
},
|
|
178
235
|
});
|
|
236
|
+
|
|
237
|
+
const staticWelcomeText =
|
|
238
|
+
event.inboundKind === "welcome" ? botAccount.config.welcomeText?.trim() : undefined;
|
|
239
|
+
if (staticWelcomeText) {
|
|
240
|
+
this.log.info?.(
|
|
241
|
+
`[wecom-ws] static welcome reply account=${this.runtime.account.accountId} messageId=${event.messageId} peer=${event.conversation.peerKind}:${event.conversation.peerId} len=${staticWelcomeText.length}`,
|
|
242
|
+
);
|
|
243
|
+
await replyHandle.deliver(
|
|
244
|
+
{
|
|
245
|
+
text: staticWelcomeText,
|
|
246
|
+
},
|
|
247
|
+
{ kind: "final" },
|
|
248
|
+
);
|
|
249
|
+
this.log.info?.(
|
|
250
|
+
`[wecom-ws] static welcome delivered account=${this.runtime.account.accountId} messageId=${event.messageId}`,
|
|
251
|
+
);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
179
255
|
await this.runtime.handleEvent(event, replyHandle);
|
|
180
256
|
};
|
|
181
257
|
|
|
@@ -221,6 +297,7 @@ export class BotWsSdkAdapter {
|
|
|
221
297
|
|
|
222
298
|
stop(): void {
|
|
223
299
|
this.log.info?.(`[wecom-ws] stop account=${this.runtime.account.accountId}`);
|
|
300
|
+
clearWecomMcpAccountCache(this.runtime.account.accountId);
|
|
224
301
|
unregisterBotWsPushHandle(this.runtime.account.accountId);
|
|
225
302
|
this.runtime.updateTransportSession(
|
|
226
303
|
createBotWsSessionSnapshot({
|
package/src/types/config.ts
CHANGED
|
@@ -11,6 +11,7 @@ export type WecomMediaConfig = {
|
|
|
11
11
|
retentionHours?: number;
|
|
12
12
|
cleanupOnStart?: boolean;
|
|
13
13
|
maxBytes?: number;
|
|
14
|
+
localRoots?: string[];
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
export type WecomNetworkConfig = {
|
|
@@ -72,12 +73,14 @@ export type WecomDynamicAgentsConfig = {
|
|
|
72
73
|
export type WecomAccountConfig = {
|
|
73
74
|
enabled?: boolean;
|
|
74
75
|
name?: string;
|
|
76
|
+
mediaMaxMb?: number;
|
|
75
77
|
bot?: WecomBotConfig;
|
|
76
78
|
agent?: WecomAgentConfig;
|
|
77
79
|
};
|
|
78
80
|
|
|
79
81
|
export type WecomConfig = {
|
|
80
82
|
enabled?: boolean;
|
|
83
|
+
mediaMaxMb?: number;
|
|
81
84
|
bot?: WecomBotConfig;
|
|
82
85
|
agent?: WecomAgentConfig;
|
|
83
86
|
accounts?: Record<string, WecomAccountConfig>;
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
# 企微文档批量更新修复指南
|
|
2
|
-
|
|
3
|
-
## 问题背景
|
|
4
|
-
|
|
5
|
-
企业微信文档 API 的 `batch_update` 接口存在以下限制:
|
|
6
|
-
|
|
7
|
-
1. **索引基于快照**:所有操作的索引基于请求发送时的文档快照
|
|
8
|
-
2. **原子性**:批量操作中一个失败则全部回滚
|
|
9
|
-
3. **段落验证**:`insert_text` 必须指向已有 Run 元素,不能在空段落执行
|
|
10
|
-
4. **版本控制**:`version` 参数用于并发控制(与最新版本差≤100)
|
|
11
|
-
|
|
12
|
-
## 常见问题
|
|
13
|
-
|
|
14
|
-
### ParagraphValidator cannot find p's parent
|
|
15
|
-
|
|
16
|
-
**原因**:在空段落或无效位置插入内容
|
|
17
|
-
|
|
18
|
-
**解决方案**:
|
|
19
|
-
```typescript
|
|
20
|
-
// 错误:直接在空位置插入文本
|
|
21
|
-
requests: [
|
|
22
|
-
{ insert_paragraph: { location: { index: 1 } } },
|
|
23
|
-
{ insert_text: { location: { index: 1 }, text: "内容" } } // ❌ 索引错误
|
|
24
|
-
]
|
|
25
|
-
|
|
26
|
-
// 正确:使用顺序模式或正确计算索引
|
|
27
|
-
requests: [
|
|
28
|
-
{ insert_paragraph: { location: { index: 1 } } },
|
|
29
|
-
{ insert_text: { location: { index: 2 }, text: "内容" } } // ✅ index+1
|
|
30
|
-
]
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
### TextValidator cannot find p parent
|
|
34
|
-
|
|
35
|
-
**原因**:`insert_text` 操作的目标段落在文档快照中不存在
|
|
36
|
-
|
|
37
|
-
**解决方案**:每次操作前获取最新文档结构
|
|
38
|
-
|
|
39
|
-
## 推荐方案
|
|
40
|
-
|
|
41
|
-
### 方案 A:顺序模式(推荐,默认)
|
|
42
|
-
|
|
43
|
-
```typescript
|
|
44
|
-
await docClient.updateDocContent({
|
|
45
|
-
agent, docId,
|
|
46
|
-
requests: [...],
|
|
47
|
-
batchMode: false // 默认:顺序执行,每次获取最新版本
|
|
48
|
-
});
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
**优点**:
|
|
52
|
-
- 可靠性高
|
|
53
|
-
- 自动处理版本和索引
|
|
54
|
-
- 适合复杂场景
|
|
55
|
-
|
|
56
|
-
**缺点**:
|
|
57
|
-
- API 调用次数较多
|
|
58
|
-
|
|
59
|
-
### 方案 B:批量模式(高性能场景)
|
|
60
|
-
|
|
61
|
-
```typescript
|
|
62
|
-
await docClient.updateDocContent({
|
|
63
|
-
agent, docId,
|
|
64
|
-
requests: [
|
|
65
|
-
{ insert_paragraph: { location: { index: 1 } } },
|
|
66
|
-
{ insert_text: { location: { index: 2 }, text: "内容" } }
|
|
67
|
-
],
|
|
68
|
-
batchMode: true, // 批量执行,基于同一快照
|
|
69
|
-
version: 123 // 必须提供版本号
|
|
70
|
-
});
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
**优点**:
|
|
74
|
-
- API 调用次数少
|
|
75
|
-
- 性能更好
|
|
76
|
-
|
|
77
|
-
**缺点**:
|
|
78
|
-
- 需要精确计算索引
|
|
79
|
-
- 所有操作基于同一快照
|
|
80
|
-
|
|
81
|
-
### 方案 C:使用 init_content(创建文档)
|
|
82
|
-
|
|
83
|
-
```typescript
|
|
84
|
-
await docClient.createDoc({
|
|
85
|
-
agent,
|
|
86
|
-
docName: "新文档",
|
|
87
|
-
docType: "doc",
|
|
88
|
-
init_content: [
|
|
89
|
-
"# 标题",
|
|
90
|
-
"第一段内容",
|
|
91
|
-
{ type: "image", url: "https://..." },
|
|
92
|
-
"第二段内容"
|
|
93
|
-
]
|
|
94
|
-
});
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
**优点**:
|
|
98
|
-
- 最简单可靠
|
|
99
|
-
- 自动处理分段
|
|
100
|
-
|
|
101
|
-
**缺点**:
|
|
102
|
-
- 仅适用于创建文档
|
|
103
|
-
|
|
104
|
-
## API 限制速查
|
|
105
|
-
|
|
106
|
-
| 操作 | 限制 |
|
|
107
|
-
|------|------|
|
|
108
|
-
| 批量操作数 | 1-30 个 requests |
|
|
109
|
-
| 版本差 | ≤100 |
|
|
110
|
-
| 新增行 | ≤1000 |
|
|
111
|
-
| 新增列 | ≤200 |
|
|
112
|
-
| 单元格总数 | ≤10000 |
|
|
113
|
-
| 查询范围 | 行≤1000, 列≤200 |
|
|
114
|
-
|
|
115
|
-
## 错误码
|
|
116
|
-
|
|
117
|
-
| 错误 | 原因 | 解决方案 |
|
|
118
|
-
|------|------|----------|
|
|
119
|
-
| ParagraphValidator | 段落位置无效 | 使用正确索引或顺序模式 |
|
|
120
|
-
| TextValidator | 目标段落不存在 | 先创建段落或获取最新结构 |
|
|
121
|
-
| DrawingValidator | 图片位置无效 | 在有效段落插入 |
|
|
122
|
-
| version mismatch | 版本过旧 | 重新获取最新文档 |
|
|
123
|
-
|
|
124
|
-
## 最佳实践
|
|
125
|
-
|
|
126
|
-
1. **创建文档**:使用 `init_content`(最可靠)
|
|
127
|
-
2. **更新文档**:使用顺序模式(`batchMode: false`,默认)
|
|
128
|
-
3. **批量追加**:简单场景可用批量模式(需精确计算索引)
|
|
129
|
-
4. **并发控制**:始终传递 `version` 参数
|
|
130
|
-
5. **错误处理**:捕获异常并重试(最多 3 次)
|
|
131
|
-
|
|
132
|
-
## 相关文档
|
|
133
|
-
|
|
134
|
-
- [API 使用指南](./api-usage-guide.md)
|
|
135
|
-
- [示例代码](./examples.md)
|