fix: rename plugin id to wechat-access-unqclawed, add login/logout commands, update README with enable instructions

This commit is contained in:
HenryXiaoYang
2026-03-10 03:11:39 +08:00
parent 11bb1b94eb
commit 56f91381dd
8 changed files with 85 additions and 32 deletions

View File

@@ -8,7 +8,13 @@ OpenClaw 微信通路插件 — 通过 WeChat OAuth 扫码登录获取 token
openclaw plugins install @henryxiaoyang/wechat-access-unqclawed openclaw plugins install @henryxiaoyang/wechat-access-unqclawed
``` ```
重启 Gateway 后生效。 安装后启用渠道:
```bash
openclaw config set channels.wechat-access-unqclawed.enabled true
```
重启 Gateway首次启动会在终端显示微信扫码登录二维码。
## 功能 ## 功能
@@ -17,15 +23,18 @@ openclaw plugins install @henryxiaoyang/wechat-access-unqclawed
- AGP 协议 WebSocket 双向通信(流式文本、工具调用) - AGP 协议 WebSocket 双向通信(流式文本、工具调用)
- 邀请码验证(可配置跳过) - 邀请码验证(可配置跳过)
- 支持生产/测试环境切换 - 支持生产/测试环境切换
- `/wechat-login` 命令手动触发扫码登录
- `/wechat-logout` 命令清除已保存的登录态
## 配置 ## 配置
在 OpenClaw 配置文件的 `channels.wechat-access` 下: 在 OpenClaw 配置文件的 `channels.wechat-access-unqclawed` 下:
```json ```json
{ {
"channels": { "channels": {
"wechat-access": { "wechat-access-unqclawed": {
"enabled": true,
"token": "", "token": "",
"wsUrl": "", "wsUrl": "",
"bypassInvite": false, "bypassInvite": false,
@@ -37,6 +46,7 @@ openclaw plugins install @henryxiaoyang/wechat-access-unqclawed
| 字段 | 类型 | 说明 | | 字段 | 类型 | 说明 |
|------|------|------| |------|------|------|
| `enabled` | boolean | 启用渠道(必须设为 `true` |
| `token` | string | 手动指定 channel token留空则走扫码登录 | | `token` | string | 手动指定 channel token留空则走扫码登录 |
| `wsUrl` | string | WebSocket 网关地址(留空使用环境默认值) | | `wsUrl` | string | WebSocket 网关地址(留空使用环境默认值) |
| `bypassInvite` | boolean | 跳过邀请码验证 | | `bypassInvite` | boolean | 跳过邀请码验证 |

View File

@@ -82,7 +82,7 @@ export const buildMessageContext = (message: FuwuhaoMessage): MessageContext =>
// 根据频道、账号、对话类型等信息,决定使用哪个 Agent 处理消息 // 根据频道、账号、对话类型等信息,决定使用哪个 Agent 处理消息
const frameworkRoute = runtime.channel.routing.resolveAgentRoute({ const frameworkRoute = runtime.channel.routing.resolveAgentRoute({
cfg, // 全局配置 cfg, // 全局配置
channel: "wechat-access", // 频道标识 channel: "wechat-access-unqclawed", // 频道标识
accountId: "default", // 账号 ID支持多账号场景 accountId: "default", // 账号 ID支持多账号场景
peer: { peer: {
kind: "dm", // 对话类型dm=私聊group=群聊 kind: "dm", // 对话类型dm=私聊group=群聊
@@ -132,7 +132,7 @@ export const buildMessageContext = (message: FuwuhaoMessage): MessageContext =>
// runtime.channel.reply.formatInboundEnvelope 将原始消息格式化为标准格式 // runtime.channel.reply.formatInboundEnvelope 将原始消息格式化为标准格式
// 添加时间戳、发送者信息、格式化选项等 // 添加时间戳、发送者信息、格式化选项等
const body = runtime.channel.reply.formatInboundEnvelope({ const body = runtime.channel.reply.formatInboundEnvelope({
channel: "wechat-access", // 频道标识 channel: "wechat-access-unqclawed", // 频道标识
from: userId, // 发送者 ID from: userId, // 发送者 ID
timestamp, // 消息时间戳 timestamp, // 消息时间戳
body: content, // 消息内容 body: content, // 消息内容
@@ -161,11 +161,11 @@ export const buildMessageContext = (message: FuwuhaoMessage): MessageContext =>
ChatType: "direct" as const, // 对话类型 ChatType: "direct" as const, // 对话类型
ChannelSource: WECHAT_CHANNEL_LABELS.serviceAccount, // 渠道来源标识(用于 UI 侧区分消息来源) ChannelSource: WECHAT_CHANNEL_LABELS.serviceAccount, // 渠道来源标识(用于 UI 侧区分消息来源)
SenderId: userId, // 发送者 ID SenderId: userId, // 发送者 ID
Provider: "wechat-access", // 提供商标识 Provider: "wechat-access-unqclawed", // 提供商标识
Surface: "wechat-access", // 界面标识 Surface: "wechat-access-unqclawed", // 界面标识
MessageSid: messageId, // 消息唯一标识 MessageSid: messageId, // 消息唯一标识
Timestamp: timestamp, // 时间戳 Timestamp: timestamp, // 时间戳
OriginatingChannel: "wechat-access" as const, // 原始频道 OriginatingChannel: "wechat-access-unqclawed" as const, // 原始频道
OriginatingTo: `wechat-access:${userId}`, // 原始接收者 OriginatingTo: `wechat-access:${userId}`, // 原始接收者
}); });
// ctx 包含了 Agent 处理消息所需的所有信息 // ctx 包含了 Agent 处理消息所需的所有信息

View File

@@ -103,7 +103,7 @@ export const handleMessage = async (message: FuwuhaoMessage): Promise<string | n
// runtime.channel.activity.record 记录频道的活动统计 // runtime.channel.activity.record 记录频道的活动统计
// 用于监控、分析、计费等场景 // 用于监控、分析、计费等场景
runtime.channel.activity.record({ runtime.channel.activity.record({
channel: "wechat-access", // 频道标识 channel: "wechat-access-unqclawed", // 频道标识
accountId: "default", // 账号 ID accountId: "default", // 账号 ID
direction: "inbound", // 方向inbound=入站用户发送outbound=出站Bot 回复) direction: "inbound", // 方向inbound=入站用户发送outbound=出站Bot 回复)
}); });
@@ -196,7 +196,7 @@ export const handleMessage = async (message: FuwuhaoMessage): Promise<string | n
// 记录出站活动统计Bot 回复) // 记录出站活动统计Bot 回复)
runtime.channel.activity.record({ runtime.channel.activity.record({
channel: "wechat-access", channel: "wechat-access-unqclawed",
accountId: "default", accountId: "default",
direction: "outbound", // 出站Bot 发送给用户 direction: "outbound", // 出站Bot 发送给用户
}); });
@@ -322,7 +322,7 @@ export const handleMessageStream = async (
// 4. 记录频道活动统计 // 4. 记录频道活动统计
// ============================================ // ============================================
runtime.channel.activity.record({ runtime.channel.activity.record({
channel: "wechat-access", channel: "wechat-access-unqclawed",
accountId: "default", accountId: "default",
direction: "inbound", direction: "inbound",
}); });
@@ -519,7 +519,7 @@ const unsubscribeAgentEvents = onAgentEvent((evt: AgentEventPayload) => {
// 记录出站活动 // 记录出站活动
runtime.channel.activity.record({ runtime.channel.activity.record({
channel: "wechat-access", channel: "wechat-access-unqclawed",
accountId: "default", accountId: "default",
direction: "outbound", direction: "outbound",
}); });

View File

@@ -13,14 +13,14 @@ const wsClients = new Map<string, WechatAccessWebSocketClient>();
// 渠道元数据 // 渠道元数据
const meta = { const meta = {
id: "wechat-access", id: "wechat-access-unqclawed",
label: "腾讯通路", label: "腾讯通路",
/** 选择时的显示文本 */ /** 选择时的显示文本 */
selectionLabel: "腾讯通路", selectionLabel: "腾讯通路",
detailLabel: "腾讯通路", detailLabel: "腾讯通路",
/** 文档路径 */ /** 文档路径 */
docsPath: "/channels/wechat-access", docsPath: "/channels/wechat-access",
docsLabel: "wechat-access", docsLabel: "wechat-access-unqclawed",
/** 简介 */ /** 简介 */
blurb: "通用通路", blurb: "通用通路",
/** 图标 */ /** 图标 */
@@ -31,7 +31,7 @@ const meta = {
// 渠道插件 // 渠道插件
const tencentAccessPlugin = { const tencentAccessPlugin = {
id: "wechat-access", id: "wechat-access-unqclawed",
meta, meta,
// 能力声明 // 能力声明
@@ -46,13 +46,13 @@ const tencentAccessPlugin = {
// 热重载token 或 wsUrl 变更时触发 gateway 重启 // 热重载token 或 wsUrl 变更时触发 gateway 重启
reload: { reload: {
configPrefixes: ["channels.wechat-access.token", "channels.wechat-access.wsUrl"], configPrefixes: ["channels.wechat-access-unqclawed.token", "channels.wechat-access-unqclawed.wsUrl"],
}, },
// 配置适配器(必需) // 配置适配器(必需)
config: { config: {
listAccountIds: (cfg: any) => { listAccountIds: (cfg: any) => {
const accounts = cfg.channels?.["wechat-access"]?.accounts; const accounts = cfg.channels?.["wechat-access-unqclawed"]?.accounts;
if (accounts && typeof accounts === "object") { if (accounts && typeof accounts === "object") {
return Object.keys(accounts); return Object.keys(accounts);
} }
@@ -60,7 +60,7 @@ const tencentAccessPlugin = {
return ["default"]; return ["default"];
}, },
resolveAccount: (cfg: any, accountId: string) => { resolveAccount: (cfg: any, accountId: string) => {
const accounts = cfg.channels?.["wechat-access"]?.accounts; const accounts = cfg.channels?.["wechat-access-unqclawed"]?.accounts;
const account = accounts?.[accountId ?? "default"]; const account = accounts?.[accountId ?? "default"];
return account ?? { accountId: accountId ?? "default" }; return account ?? { accountId: accountId ?? "default" };
}, },
@@ -86,7 +86,7 @@ const tencentAccessPlugin = {
startAccount: async (ctx: any) => { startAccount: async (ctx: any) => {
const { cfg, accountId, abortSignal, log } = ctx; const { cfg, accountId, abortSignal, log } = ctx;
const tencentAccessConfig = cfg?.channels?.["wechat-access"]; const tencentAccessConfig = cfg?.channels?.["wechat-access-unqclawed"];
let token = tencentAccessConfig?.token ? String(tencentAccessConfig.token) : ""; let token = tencentAccessConfig?.token ? String(tencentAccessConfig.token) : "";
const configWsUrl = tencentAccessConfig?.wsUrl ? String(tencentAccessConfig.wsUrl) : ""; const configWsUrl = tencentAccessConfig?.wsUrl ? String(tencentAccessConfig.wsUrl) : "";
const bypassInvite = tencentAccessConfig?.bypassInvite === true; const bypassInvite = tencentAccessConfig?.bypassInvite === true;
@@ -211,7 +211,7 @@ const tencentAccessPlugin = {
}; };
const index = { const index = {
id: "wechat-access", id: "wechat-access-unqclawed",
name: "通用通路插件", name: "通用通路插件",
description: "腾讯通用通路插件", description: "腾讯通用通路插件",
configSchema: emptyPluginConfigSchema(), configSchema: emptyPluginConfigSchema(),
@@ -226,8 +226,51 @@ const index = {
// 2. 注册渠道插件 // 2. 注册渠道插件
api.registerChannel({ plugin: tencentAccessPlugin as any }); api.registerChannel({ plugin: tencentAccessPlugin as any });
// 3. 注册 HTTP 处理器(如需要 // 3. 注册 /wechat-login 命令(手动触发扫码登录
// api.registerHttpHandler(handleSimpleWecomWebhook); api.registerCommand?.({
command: "wechat-login",
description: "手动执行微信扫码登录,获取 channel token",
handler: async ({ cfg, reply }) => {
const channelCfg = cfg?.channels?.["wechat-access-unqclawed"];
const bypassInvite = channelCfg?.bypassInvite === true;
const authStatePath = channelCfg?.authStatePath
? String(channelCfg.authStatePath)
: undefined;
const envName = channelCfg?.environment
? String(channelCfg.environment)
: "production";
const env = getEnvironment(envName);
const guid = getDeviceGuid();
try {
reply("正在启动微信扫码登录,请查看终端...");
const credentials = await performLogin({
guid,
env,
bypassInvite,
authStatePath,
});
reply(`登录成功! token: ${credentials.channelToken.substring(0, 6)}... (已保存,重启 Gateway 生效)`);
} catch (err) {
reply(`登录失败: ${err instanceof Error ? err.message : String(err)}`);
}
},
});
// 4. 注册 /wechat-logout 命令(清除已保存的登录态)
api.registerCommand?.({
command: "wechat-logout",
description: "清除已保存的微信登录态",
handler: async ({ cfg, reply }) => {
const channelCfg = cfg?.channels?.["wechat-access-unqclawed"];
const authStatePath = channelCfg?.authStatePath
? String(channelCfg.authStatePath)
: undefined;
clearState(authStatePath);
reply("已清除登录态,下次启动将重新扫码登录。");
},
});
console.log("[wechat-access] 腾讯通路插件已注册"); console.log("[wechat-access] 腾讯通路插件已注册");
}, },

View File

@@ -1,9 +1,9 @@
{ {
"id": "wechat-access", "id": "wechat-access-unqclawed",
"name": "WeChat Access", "name": "WeChat Access",
"description": "微信通路插件 — 扫码登录 + AGP WebSocket 双向通信", "description": "微信通路插件 — 扫码登录 + AGP WebSocket 双向通信",
"version": "1.0.0", "version": "1.0.0",
"channels": ["wechat-access"], "channels": ["wechat-access-unqclawed"],
"configSchema": { "configSchema": {
"type": "object", "type": "object",
"properties": { "properties": {

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "wechat-access", "name": "wechat-access",
"version": "1.0.0", "version": "1.0.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "wechat-access", "name": "wechat-access",
"version": "1.0.0", "version": "1.0.1",
"dependencies": { "dependencies": {
"fast-xml-parser": "^5.4.1", "fast-xml-parser": "^5.4.1",
"qrcode-terminal": "^0.12.0", "qrcode-terminal": "^0.12.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "@henryxiaoyang/wechat-access-unqclawed", "name": "@henryxiaoyang/wechat-access-unqclawed",
"version": "1.0.0", "version": "1.0.1",
"type": "module", "type": "module",
"description": "OpenClaw 微信通路插件 — 扫码登录 + AGP WebSocket 双向通信", "description": "OpenClaw 微信通路插件 — 扫码登录 + AGP WebSocket 双向通信",
"author": "HenryXiaoYang", "author": "HenryXiaoYang",
@@ -24,12 +24,12 @@
"./index.ts" "./index.ts"
], ],
"channel": { "channel": {
"id": "wechat-access", "id": "wechat-access-unqclawed",
"label": "wechat-access", "label": "wechat-access-unqclawed",
"selectionLabel": "WeCom (plugin)", "selectionLabel": "WeCom (plugin)",
"detailLabel": "WeCom Bot", "detailLabel": "WeCom Bot",
"docsPath": "/channels/wechat-access", "docsPath": "/channels/wechat-access",
"docsLabel": "wechat-access", "docsLabel": "wechat-access-unqclawed",
"blurb": "Enterprise WeCom intelligent bot (API mode) via encrypted webhooks + passive replies.", "blurb": "Enterprise WeCom intelligent bot (API mode) via encrypted webhooks + passive replies.",
"aliases": [ "aliases": [
"wechatwork", "wechatwork",

View File

@@ -210,7 +210,7 @@ export const handlePrompt = async (
* 这些统计数据用于 OpenClaw 控制台的活动监控面板。 * 这些统计数据用于 OpenClaw 控制台的活动监控面板。
*/ */
runtime.channel.activity.record({ runtime.channel.activity.record({
channel: "wechat-access", channel: "wechat-access-unqclawed",
accountId: route.accountId ?? "default", accountId: route.accountId ?? "default",
direction: "inbound", direction: "inbound",
}); });
@@ -449,7 +449,7 @@ export const handlePrompt = async (
// 记录出站活动统计(每次 deliver 都算一次出站) // 记录出站活动统计(每次 deliver 都算一次出站)
runtime.channel.activity.record({ runtime.channel.activity.record({
channel: "wechat-access", channel: "wechat-access-unqclawed",
accountId: route.accountId ?? "default", accountId: route.accountId ?? "default",
direction: "outbound", direction: "outbound",
}); });