fix: rename plugin id to wechat-access-unqclawed, add login/logout commands, update README with enable instructions
This commit is contained in:
16
README.md
16
README.md
@@ -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 | 跳过邀请码验证 |
|
||||||
|
|||||||
@@ -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 处理消息所需的所有信息
|
||||||
|
|||||||
@@ -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",
|
||||||
});
|
});
|
||||||
|
|||||||
63
index.ts
63
index.ts
@@ -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] 腾讯通路插件已注册");
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
4
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user