@sunnoy/wecom 1.7.0 → 1.7.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 CHANGED
@@ -20,6 +20,7 @@
20
20
  - [方式三:配置群机器人 (Webhook 模式)](#方式三配置群机器人-webhook-模式)
21
21
 
22
22
  ### 能力与路由
23
+ - [三种模式消息能力对比](#三种模式消息能力对比)
23
24
  - [支持的消息类型](#支持的消息类型)
24
25
  - [流式回复能力](#流式回复能力)
25
26
  - [管理员用户](#管理员用户)
@@ -322,6 +323,131 @@ Webhook Bot 用于向群聊发送通知消息。
322
323
  - 每个群聊可添加多个机器人
323
324
  - Webhook 地址请妥善保管,避免泄露
324
325
 
326
+ ## 三种模式消息能力对比
327
+
328
+ 企业微信提供了三种不同的接入方式,每种方式在私聊和群聊场景下的消息收发能力不同:
329
+
330
+ ### 能力矩阵
331
+
332
+ | 能力 | Bot 模式 (AI 机器人) | Agent 模式 (自建应用) | Webhook 模式 (群机器人) |
333
+ |------|---------------------|---------------------|----------------------|
334
+ | **私聊接收** | ✅ JSON 回调 | ✅ XML 回调 | ❌ 不支持 |
335
+ | **私聊被动回复** | ✅ 流式 stream | ✅ 同步回复 | ❌ 不支持 |
336
+ | **私聊主动发送** | ❌ 不支持 | ✅ 应用消息 API | ❌ 不支持 |
337
+ | **群聊接收** | ✅ @提及 JSON 回调 | ✅ @提及 XML 回调 | ❌ 不支持 |
338
+ | **群聊被动回复** | ✅ 流式 stream | ✅ 同步回复 | ❌ 不支持 |
339
+ | **群聊主动发送** | ❌ 不支持 | ✅ 应用消息 API | ✅ Webhook URL |
340
+ | **流式回复** | ✅ 打字机效果 | ❌ 仅完整消息 | ❌ 仅完整消息 |
341
+ | **思考过程展示** | ✅ thinking_content | ❌ | ❌ |
342
+ | **媒体发送** | ✅ msg_item (图片) | ✅ API 上传 (图片/文件) | ✅ base64/upload |
343
+ | **Markdown** | ✅ stream content | ✅ Markdown 消息类型 | ✅ Markdown 消息类型 |
344
+
345
+ ### 各模式详细说明
346
+
347
+ #### Bot 模式 (AI 机器人)
348
+
349
+ > 📖 [企业微信 AI 机器人开发指南](https://developer.work.weixin.qq.com/document/path/101039)
350
+
351
+ **消息接收机制**:企业微信将用户消息以 **JSON 格式**通过 HTTP POST 回调到配置的 URL。支持私聊消息和群聊中 @提及机器人的消息。
352
+
353
+ **消息回复机制**:采用**流式分片(streaming)**回复。收到回调后立即返回 `stream_id`,后续通过 `stream_refresh` 轮询接口推送增量内容。客户端展示打字机效果。
354
+
355
+ - **被动回复**:用户发消息 → 回调触发 → 流式回复(支持文本、Markdown、图片、思考过程)
356
+ - **主动发送**:❌ 不支持。AI 机器人没有主动发送 API,只能在收到消息后回复
357
+ - **适用场景**:实时对话、问答,流式体验好
358
+
359
+ #### Agent 模式 (自建应用)
360
+
361
+ > 📖 [企业微信自建应用开发指南](https://developer.work.weixin.qq.com/document/path/90226)
362
+ > 📖 [应用消息发送 API](https://developer.work.weixin.qq.com/document/path/90236)
363
+
364
+ **消息接收机制**:企业微信将用户消息以 **XML 格式**通过 HTTP POST 回调到配置的 URL。支持私聊和群聊消息,以及图片、语音、文件等多种消息类型。
365
+
366
+ **消息回复机制**:
367
+ - **被动回复**:在回调响应中直接返回 XML 格式回复(需在 5 秒内响应)
368
+ - **主动发送**:通过[应用消息 API](https://developer.work.weixin.qq.com/document/path/90236) 可主动向用户发送文本、图片、文件、Markdown 等消息。支持指定 `touser`(用户)、`toparty`(部门)、`totag`(标签)
369
+
370
+ - **适用场景**:需要主动推送的场景(异步任务完成通知、定时报告),需要收发文件的场景
371
+
372
+ #### Webhook 模式 (群机器人)
373
+
374
+ > 📖 [企业微信群机器人配置说明](https://developer.work.weixin.qq.com/document/path/99110)
375
+
376
+ **消息发送机制**:通过 HTTP POST 请求向 Webhook URL 发送消息。支持文本、Markdown、图片(base64)、文件(需先上传获取 media_id)。
377
+
378
+ - **接收消息**:❌ 不支持。Webhook 仅为单向发送通道
379
+ - **主动发送**:✅ 向 Webhook URL POST 即可发送到群聊
380
+ - **适用场景**:单向通知(告警、日报)、定时推送
381
+
382
+ ### Webhook 消息发送方式
383
+
384
+ Webhook 配置好后(见[方式三](#方式三配置群机器人-webhook-模式)),有以下方式发送消息:
385
+
386
+ #### CLI 直接发送
387
+
388
+ ```bash
389
+ openclaw message send --channel wecom --to "webhook:ops-group" "服务已恢复正常"
390
+ ```
391
+
392
+ #### Agent 处理后投递到群
393
+
394
+ 让 agent 处理消息后将回复发到 webhook 群:
395
+
396
+ ```bash
397
+ openclaw agent --agent myagent \
398
+ --message "帮我总结今天的监控告警" \
399
+ --deliver \
400
+ --reply-channel wecom \
401
+ --reply-to "webhook:ops-group"
402
+ ```
403
+
404
+ #### Heartbeat 定时推送(推荐)
405
+
406
+ 在 agent 配置中添加 heartbeat,自动定时触发并将回复发到 webhook 群:
407
+
408
+ ```json
409
+ {
410
+ "id": "report-agent",
411
+ "heartbeat": {
412
+ "every": "1h",
413
+ "target": "webhook:ops-group",
414
+ "prompt": "请总结最新的系统监控状态",
415
+ "activeHours": {
416
+ "start": "09:00",
417
+ "end": "18:00",
418
+ "timezone": "Asia/Shanghai"
419
+ }
420
+ }
421
+ }
422
+ ```
423
+
424
+ - `every` — 触发间隔(如 `30m`, `1h`, `6h`)
425
+ - `target` — 回复目标,`webhook:` 前缀加配置中的 webhook 名称
426
+ - `prompt` — 每次触发时给 agent 的提示语
427
+ - `activeHours` — 可选,限制只在工作时间段内触发
428
+
429
+ #### 系统 Crontab 定时发送
430
+
431
+ ```bash
432
+ # crontab -e
433
+ # 每天早上9点发送日报
434
+ 0 9 * * * openclaw agent --agent report-agent --message "生成今日晨报" --deliver --reply-channel wecom --reply-to "webhook:ops-group"
435
+
436
+ # 每小时发送监控摘要
437
+ 0 * * * * openclaw message send --channel wecom --to "webhook:monitor-group" "$(curl -s http://localhost:9090/api/v1/alerts | jq -r '.data.alerts | length') 条活跃告警"
438
+ ```
439
+
440
+ ### 模式选择建议
441
+
442
+ | 需求 | 推荐模式 |
443
+ |------|---------|
444
+ | 实时对话,流式打字机体验 | **Bot 模式** |
445
+ | 双向对话 + 主动推送 + 文件处理 | **Agent 模式** |
446
+ | 仅需向群聊推送通知 | **Webhook 模式** |
447
+ | 同时需要对话和群通知 | **Bot/Agent 模式 + Webhook 模式** 组合使用 |
448
+
449
+ > 💡 **三种模式可以同时启用**。例如:Bot 模式处理日常对话,Webhook 模式负责定时推送通知到群。配置时在同一个 `channels.wecom` 下同时填写 `token`/`encodingAesKey`(Bot)、`agent`(Agent)和 `webhooks`(Webhook)即可。
450
+
325
451
  ## 支持的消息类型
326
452
 
327
453
  | 类型 | 方向 | 说明 |
@@ -841,176 +967,6 @@ openclaw-plugin-wecom/
841
967
 
842
968
  本项目采用 [ISC License](./LICENSE) 协议。
843
969
 
844
- ## 配置示例参考
845
-
846
- 以下是一个生产环境的脱敏配置示例,供参考:
847
-
848
- ```json
849
- {
850
- "meta": {
851
- "lastTouchedVersion": "2026.2.25",
852
- "lastTouchedAt": "2026-02-28T03:14:11.564Z"
853
- },
854
- "wizard": {
855
- "lastRunAt": "2026-02-26T09:29:04.028Z",
856
- "lastRunVersion": "2026.2.25",
857
- "lastRunCommand": "onboard",
858
- "lastRunMode": "local"
859
- },
860
- "logging": {
861
- "level": "info",
862
- "consoleLevel": "debug",
863
- "consoleStyle": "pretty"
864
- },
865
- "models": {
866
- "mode": "merge",
867
- "providers": {
868
- "bailian": {
869
- "baseUrl": "https://coding.dashscope.aliyuncs.com/v1",
870
- "apiKey": "sk-xxxxxxxxxxxxxxxxxxxxxx",
871
- "api": "openai-completions",
872
- "models": [
873
- { "id": "qwen3.5-plus", "name": "qwen3.5-plus", "reasoning": false, "input": ["text", "image"], "contextWindow": 1000000, "maxTokens": 65536 },
874
- { "id": "MiniMax-M2.5", "name": "MiniMax-M2.5", "reasoning": false, "input": ["text"], "contextWindow": 1000000, "maxTokens": 65536 },
875
- { "id": "glm-5", "name": "glm-5", "reasoning": false, "input": ["text"], "contextWindow": 202752, "maxTokens": 16384 },
876
- { "id": "glm-4.7", "name": "glm-4.7", "reasoning": false, "input": ["text"], "contextWindow": 202752, "maxTokens": 16384 },
877
- { "id": "kimi-k2.5", "name": "kimi-k2.5", "reasoning": false, "input": ["text", "image"], "contextWindow": 262144, "maxTokens": 32768 }
878
- ]
879
- }
880
- }
881
- },
882
- "agents": {
883
- "defaults": {
884
- "model": { "primary": "bailian/kimi-k2.5" },
885
- "models": {
886
- "bailian/qwen3.5-plus": {},
887
- "bailian/MiniMax-M2.5": {},
888
- "bailian/glm-5": {},
889
- "bailian/glm-4.7": {},
890
- "bailian/kimi-k2.5": {}
891
- },
892
- "workspace": "/path/to/workspace",
893
- "userTimezone": "Asia/Shanghai",
894
- "timeFormat": "24",
895
- "compaction": {
896
- "mode": "safeguard",
897
- "reserveTokensFloor": 20000,
898
- "memoryFlush": {
899
- "enabled": true,
900
- "softThresholdTokens": 4000
901
- }
902
- },
903
- "thinkingDefault": "medium",
904
- "verboseDefault": "on",
905
- "heartbeat": {
906
- "every": "10m",
907
- "target": "last",
908
- "directPolicy": "allow"
909
- },
910
- "sandbox": {
911
- "mode": "all",
912
- "workspaceAccess": "rw",
913
- "scope": "agent",
914
- "docker": {
915
- "image": "your-registry.com/openclaw-agent:v2026.x.x",
916
- "readOnlyRoot": false,
917
- "network": "bridge",
918
- "extraHosts": [
919
- "your-domain.internal:xxx.xxx.xxx.xxx"
920
- ],
921
- "binds": [
922
- "/path/to/skills:/workspace/skills:ro"
923
- ],
924
- "dangerouslyAllowReservedContainerTargets": true,
925
- "dangerouslyAllowExternalBindSources": true
926
- },
927
- "prune": {
928
- "idleHours": 87600,
929
- "maxAgeDays": 3650
930
- }
931
- }
932
- },
933
- "list": [
934
- { "id": "main" },
935
- { "id": "wecom-dm-xxxxxx" }
936
- ]
937
- },
938
- "commands": {
939
- "native": "auto",
940
- "nativeSkills": "auto",
941
- "restart": true,
942
- "ownerDisplay": "raw"
943
- },
944
- "session": {
945
- "dmScope": "per-channel-peer"
946
- },
947
- "hooks": {
948
- "internal": {
949
- "enabled": true,
950
- "entries": {
951
- "boot-md": { "enabled": true },
952
- "command-logger": { "enabled": true },
953
- "session-memory": { "enabled": true },
954
- "bootstrap-extra-files": { "enabled": true }
955
- }
956
- }
957
- },
958
- "channels": {
959
- "wecom": {
960
- "enabled": true,
961
- "token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
962
- "encodingAesKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
963
- "commands": {
964
- "enabled": true,
965
- "allowlist": ["/help", "/commands", "/status", "/context", "/whoami", "/new", "/compact", "/stop", "/reset", "/usage", "/think", "/thinking", "/t", "/verbose", "/v", "/reasoning", "/reason", "/model", "/models", "/skill"]
966
- },
967
- "dynamicAgents": { "enabled": true },
968
- "dm": { "createAgentOnFirstMessage": true },
969
- "groupChat": { "enabled": true, "requireMention": true },
970
- "adminUsers": ["admin_userid"],
971
- "workspaceTemplate": "/path/to/workspace-template",
972
- "agent": {
973
- "corpId": "wwxxxxxxxxxxxxxxxx",
974
- "corpSecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
975
- "agentId": 1000002,
976
- "token": "xxxxxxxxxxxxxxx",
977
- "encodingAesKey": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
978
- }
979
- }
980
- },
981
- "gateway": {
982
- "port": 18789,
983
- "mode": "local",
984
- "bind": "lan",
985
- "controlUi": {
986
- "dangerouslyAllowHostHeaderOriginFallback": true,
987
- "allowInsecureAuth": true
988
- },
989
- "auth": {
990
- "mode": "token",
991
- "token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
992
- },
993
- "tailscale": {
994
- "mode": "off",
995
- "resetOnExit": false
996
- }
997
- },
998
- "skills": {
999
- "allowBundled": ["_none_"],
1000
- "load": {
1001
- "extraDirs": ["/path/to/skills"],
1002
- "watch": true,
1003
- "watchDebounceMs": 250
1004
- },
1005
- "install": { "nodeManager": "npm" }
1006
- },
1007
- "plugins": {
1008
- "allow": ["wecom"],
1009
- "entries": { "wecom": { "enabled": true } }
1010
- }
1011
- }
1012
- ```
1013
-
1014
970
  ## 自定义 Skills 配合沙箱使用实践
1015
971
 
1016
972
  OpenClaw 支持自定义 Skills 并通过沙箱(Docker)隔离执行,以下是生产环境的实践配置:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sunnoy/wecom",
3
- "version": "1.7.0",
3
+ "version": "1.7.1",
4
4
  "description": "Enterprise WeChat AI Bot channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -482,14 +482,6 @@ export async function processInboundMessage({
482
482
  });
483
483
  }
484
484
 
485
- // Mark stream meta when main response is done.
486
- if (streamId && (info.kind === "final" || info.kind === "block")) {
487
- streamMeta.set(streamId, {
488
- mainResponseDone: true,
489
- doneAt: Date.now(),
490
- });
491
- }
492
-
493
485
  // Schedule / reset stream close timer if dispatch already returned.
494
486
  if (streamId && dispatchDone) {
495
487
  scheduleStreamClose();
@@ -503,10 +495,17 @@ export async function processInboundMessage({
503
495
  });
504
496
  });
505
497
 
506
- // Dispatch returned.
498
+ // Dispatch returned — the entire LLM turn (all blocks, tool calls,
499
+ // and final payloads) is complete. Mark mainResponseDone now so the
500
+ // idle-close timer in http-handler only starts after there is truly
501
+ // no more content to deliver.
507
502
  dispatchDone = true;
508
503
 
509
504
  if (streamId) {
505
+ streamMeta.set(streamId, {
506
+ mainResponseDone: true,
507
+ doneAt: Date.now(),
508
+ });
510
509
  const stream = streamManager.getStream(streamId);
511
510
  if (!stream || stream.finished) {
512
511
  unregisterActiveStream(streamKey, streamId);