Commit 4657b3d3 authored by nanahira's avatar nanahira

add menu support

parent 0a55fbb8
......@@ -21,6 +21,28 @@ npm install koishi-plugin-adapter-wecom
corpId: 'corpId', // 企业 ID,在企业信息 https://work.weixin.qq.com/wework_admin/frame#profile 中查看
agentId: 'agentId', // 应用 ID,在应用管理中查看
secret: 'secret', // 应用密钥,在应用管理中查看
menus: [
{
type: 'view', // 打开网页
name: 'MyCard', // 按钮名称
url: 'https://mycard.moe', // 网页地址
},
{
type: 'click', // 运行指令
name: '看图', // 按钮名称
command: 'media -c 10000000', // 指令
},
{
type: 'parent', // 子指令
name: '更多', // 按钮名称
children: [
{ type: 'click', name: '点击事件', command: 'markdown' },
{ type: 'scancode_push', name: '扫一扫' }, // 扫一扫并打开网页。
{ type: 'scancode_waitmsg', name: '扫一扫2', command: 'scan' }, // 扫一扫并运行指令。
{ type: 'location_select', name: '地理位置', command: 'location' }, // 选择地理位置并运行指令。
],
},
],
},
],
path: '/wecom', // 回调 API 路径
......@@ -29,3 +51,6 @@ npm install koishi-plugin-adapter-wecom
}
```
## 事件
* `wecom/{Event}`:企业微信事件,以 `wecom/` 开头,参考 [企业微信文档](https://developer.work.weixin.qq.com/document/path/90240)
......@@ -19,6 +19,28 @@ app.plugin(adapterPlugin, {
corpId: 'corpId', // 企业 ID,在企业信息 https://work.weixin.qq.com/wework_admin/frame#profile 中查看
agentId: 'agentId', // 应用 ID,在应用管理中查看
secret: 'secret', // 应用密钥,在应用管理中查看
menus: [
{
type: 'view', // 打开网页
name: 'MyCard', // 按钮名称
url: 'https://mycard.moe', // 网页地址
},
{
type: 'click', // 运行指令
name: '看图', // 按钮名称
command: 'media -c 10000000', // 指令
},
{
type: 'parent', // 子指令
name: '更多', // 按钮名称
children: [
{ type: 'click', name: '点击事件', command: 'markdown' },
{ type: 'scancode_push', name: '扫一扫' }, // 扫一扫并打开网页。
{ type: 'scancode_waitmsg', name: '扫一扫2', command: 'scan' }, // 扫一扫并运行指令。
{ type: 'location_select', name: '地理位置', command: 'location' }, // 选择地理位置并运行指令。
],
},
],
},
],
path: '/wecom', // 回调 API 路径
......@@ -32,6 +54,10 @@ app.on('wecom/LOCATION', (session) => {
);
});
app.on('wecom/click', (session) => {
console.log(`${session.userId} clicked ${session.wecom.EventKey}`);
});
app.on('message', (session) => {
console.log(`Got message ${session.content}`);
});
......@@ -46,12 +72,24 @@ app.command('markdown').action(async () => {
return `${segment('markdown')}# 女装\n> 今天梦梦女装了吗?`;
});
app
.command('scan')
.action(
(argv) => `扫出来了: ${argv.session.wecom['ScanCodeInfo']['ScanResult']}`,
);
app.command('location').action(async (argv) => {
const { session } = argv;
return `你的位置是 ${session.wecom['SendLocationInfo']['Label']},经纬度是 ${session.wecom['SendLocationInfo']['Location_X']} ${session.wecom['SendLocationInfo']['Location_Y']}`;
});
app
.command('media')
.option('type', '-t, <type:string>', { fallback: 'image' })
.option('type', '-t <type:string>', { fallback: 'image' })
.option('cardcode', '-c <cardcode:string>', { fallback: '10000' })
.action(async (argv) =>
segment(argv.options.type, {
url: 'https://cdn02.moecube.com:444/images/ygopro-images-zh-CN/10000.jpg',
url: `https://cdn02.moecube.com:444/images/ygopro-images-zh-CN/${argv.options.cardcode}.jpg`,
}),
);
......
import { Adapter, Bot, Dict, omit, Quester, Schema, segment } from 'koishi';
import {
Adapter,
Bot,
Dict,
omit,
Quester,
Schema,
segment,
Session,
} from 'koishi';
import moment, { Moment } from 'moment';
import {
CommonOutMessage,
......@@ -11,6 +20,8 @@ import {
WecomSendMessageResponse,
WeComUser,
WeComAgentInfo,
WecomMenuDef,
adaptMenu,
} from './def';
import { AdapterConfig, adaptUser } from './utils';
import FormData from 'form-data';
......@@ -22,6 +33,7 @@ export interface BotConfig extends Bot.BaseConfig, Quester.Config {
corpId?: string;
agentId?: string;
secret?: string;
menus?: WecomMenuDef[];
}
export const BotConfig: Schema<BotConfig> = Schema.object({
......@@ -31,6 +43,7 @@ export const BotConfig: Schema<BotConfig> = Schema.object({
.role('secret')
.required()
.description('企业应用密钥。'),
menus: Schema.array(WecomMenuDef).description('企业微信菜单项。'),
...omit(Quester.Config.dict, ['endpoint']),
});
......@@ -40,6 +53,7 @@ export class WeComBot extends Bot<BotConfig> {
http: Quester;
private accessToken: string;
private accessTokenUntil: Moment;
private buttonKeyMap = new Map<string, string>();
constructor(adapter: Adapter, config: BotConfig) {
super(adapter, config);
......@@ -47,13 +61,60 @@ export class WeComBot extends Bot<BotConfig> {
this.selfId = config.agentId;
}
private async initializeMenu() {
if (!this.config.menus?.length) return true;
const button = this.config.menus.map((menu) =>
adaptMenu(menu, this.buttonKeyMap),
);
const buttonData = { button };
const data = await this.http.post<WecomSendMessageResponse>(
'https://qyapi.weixin.qq.com/cgi-bin/menu/create',
buttonData,
{
params: {
access_token: await this.getToken(),
agentid: this.config.agentId,
},
},
);
if (data.errcode) {
this.logger.error(
`Failed to initialize menu ${JSON.stringify(buttonData)}: ${
data.errmsg
}`,
);
return false;
}
return true;
}
async handleMenuEvent(event: string, session: Session) {
if (!this.app.isActive) return;
const eventKey = session.wecom['EventKey'] as string;
const command = this.buttonKeyMap.get(eventKey);
console.log(eventKey, command);
if (command) {
const result = await session.execute(command);
if (result) {
await this.sendPrivateMessage(session.userId, result);
}
}
}
async initialize() {
try {
const self = await this.getSelf();
const [self, menuResult] = await Promise.all([
this.getSelf(),
this.initializeMenu(),
]);
if (!self) {
this.reject(new Error('Invalid credentials.'));
return;
}
if (!menuResult) {
this.reject(new Error('Failed to initialize menu.'));
return;
}
Object.assign(this, self);
this.resolve();
} catch (e) {
......
......@@ -4,3 +4,4 @@ export * from './specific/WeComAgentInfo';
export * from './specific/ApprovalInfo';
export * from './specific/ChangeContact';
export * from './events';
export * from './menu';
import { createHash } from 'crypto';
import { Schema } from 'koishi';
export type WecomButtonTypes =
| 'click'
| 'view'
| 'scancode_push'
| 'scancode_waitmsg'
| 'pic_sysphoto'
| 'pic_photo_or_album'
| 'pic_weixin'
| 'location_select'
| 'view_miniprogram';
export interface WecomButton {
type: WecomButtonTypes;
name: string;
key?: string;
url?: string;
pagepath?: string;
appid?: string;
}
export interface WecomParentButton {
name: string;
sub_button: WecomButton[];
}
export interface WecomMenuDef {
type: WecomButtonTypes | 'parent';
name: string;
command?: string;
children?: (Omit<WecomMenuDef, 'children'> & { type: WecomButtonTypes })[];
url?: string;
pagePath?: string;
mediaId?: string;
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
export const WecomMenuDef: Schema<WecomMenuDef> = Schema.object({
type: Schema.union([
'parent',
'click',
'view',
'scancode_push',
'scancode_waitmsg',
'pic_sysphoto',
'pic_photo_or_album',
'pic_weixin',
'location_select',
'view_miniprogram',
])
.required()
.description('菜单的响应动作类型。'),
name: Schema.string().description('菜单标题。').required(),
command: Schema.string().description('菜单的响应动作指令。'),
children: Schema.array(
Schema.object({
type: Schema.union([
'click',
'view',
'scancode_push',
'scancode_waitmsg',
'pic_sysphoto',
'pic_photo_or_album',
'pic_weixin',
'location_select',
'view_miniprogram',
])
.required()
.description('菜单的响应动作类型。'),
name: Schema.string().description('菜单标题。').required(),
command: Schema.string().description('菜单的响应动作指令。'),
url: Schema.string().description(
'网页链接,成员点击菜单可打开链接,不超过1024字节。',
),
pagePath: Schema.string().description('小程序的页面路径。'),
appId: Schema.string().description('小程序的appid。'),
}),
).description('子菜单。'),
url: Schema.string().description(
'网页链接,成员点击菜单可打开链接,不超过1024字节。',
),
pagePath: Schema.string().description('小程序的页面路径。'),
appId: Schema.string().description('小程序的appid。'),
});
export const adaptMenu = (
menu: WecomMenuDef,
registerMap?: Map<string, string>,
): WecomButton | WecomParentButton => {
if (!menu.type && !menu.children) {
throw new Error('Menu type or children must be set');
}
if (menu.type !== 'parent') {
const key = createHash('sha512')
.update(`${menu.type}${menu.name}${menu.command || ''}`)
.digest('hex');
if (menu.command) {
registerMap?.set(key, menu.command);
}
return {
type: menu.type as WecomButtonTypes,
name: menu.name,
key,
url: menu.url,
pagepath: menu.pagePath,
appid: menu.mediaId,
};
} else {
return {
name: menu.name,
sub_button: menu.children.map((c) => adaptMenu(c, registerMap)),
} as WecomParentButton;
}
};
export interface WecomMenu {
button: WecomButton[];
}
......@@ -38,7 +38,7 @@ declare module 'koishi' {
}>;
'wecom/click': WecomEventFunction<EventKeyBody>;
'wecom/view': WecomEventFunction<EventKeyBody>;
'wecom/scan': WecomEventFunction<ScanCodeEventBody>;
'wecom/scancode_push': WecomEventFunction<ScanCodeEventBody>;
'wecom/scancode_waitmsg': WecomEventFunction<ScanCodeEventBody>;
'wecom/pic_sysphoto': WecomEventFunction<PhotoEventBody>;
'wecom/pic_photo_or_album': WecomEventFunction<PhotoEventBody>;
......
......@@ -111,4 +111,19 @@ export function dispatchSession(bot: WeComBot, message: WecomEventResponse) {
const session = new Session(bot, payload);
session.wecom = message.body;
bot.adapter.dispatch(session);
if (
message.body.Event &&
[
'click',
'view',
'scan',
'scancode_waitmsg',
'pic_sysphoto',
'pic_photo_or_album',
'pic_weixin',
'location_select',
].includes(message.body.Event)
) {
bot.handleMenuEvent(message.body.Event, session).then();
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment