From 94784ee1ec84fdbcf366801cd6401e8fdaa7f983 Mon Sep 17 00:00:00 2001 From: b31in Date: Mon, 12 Jan 2026 18:01:04 +0800 Subject: [PATCH] wip --- .env.example | 22 ++ .gitignore | 3 + .vscode/launch.json | 16 ++ README.md | 237 +++++++++++++++++ dist/common/wechat-error.d.ts | 9 + dist/common/wechat-error.d.ts.map | 1 + dist/common/wechat-error.js | 13 + dist/common/wechat-error.js.map | 1 + dist/main.d.ts | 2 + dist/main.d.ts.map | 1 + dist/main.js | 9 + dist/main.js.map | 1 + dist/server.d.ts | 2 + dist/server.d.ts.map | 1 + dist/server.js | 95 +++++++ dist/server.js.map | 1 + dist/types/wechat.d.ts | 343 ++++++++++++++++++++++++ dist/types/wechat.d.ts.map | 1 + dist/types/wechat.js | 5 + dist/types/wechat.js.map | 1 + dist/wechat-client.d.ts | 93 +++++++ dist/wechat-client.d.ts.map | 1 + dist/wechat-client.js | 287 +++++++++++++++++++++ dist/wechat-client.js.map | 1 + dist/wecomp.d.ts | 2 + dist/wecomp.d.ts.map | 1 + dist/wecomp.js | 54 ++++ dist/wecomp.js.map | 1 + docs/prd/prd-wip.md | 179 +++++++++++++ package.json | 28 ++ pnpm-lock.yaml | 415 ++++++++++++++++++++++++++++++ pnpm-workspace.yaml | 2 + src/common/wechat-error.ts | 11 + src/main.ts | 13 + src/server.ts | 134 ++++++++++ src/types/wechat.ts | 355 +++++++++++++++++++++++++ src/wechat-client.ts | 395 ++++++++++++++++++++++++++++ src/wecomp.ts | 62 +++++ tsconfig.json | 49 ++++ 39 files changed, 2847 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 README.md create mode 100644 dist/common/wechat-error.d.ts create mode 100644 dist/common/wechat-error.d.ts.map create mode 100644 dist/common/wechat-error.js create mode 100644 dist/common/wechat-error.js.map create mode 100644 dist/main.d.ts create mode 100644 dist/main.d.ts.map create mode 100644 dist/main.js create mode 100644 dist/main.js.map create mode 100644 dist/server.d.ts create mode 100644 dist/server.d.ts.map create mode 100644 dist/server.js create mode 100644 dist/server.js.map create mode 100644 dist/types/wechat.d.ts create mode 100644 dist/types/wechat.d.ts.map create mode 100644 dist/types/wechat.js create mode 100644 dist/types/wechat.js.map create mode 100644 dist/wechat-client.d.ts create mode 100644 dist/wechat-client.d.ts.map create mode 100644 dist/wechat-client.js create mode 100644 dist/wechat-client.js.map create mode 100644 dist/wecomp.d.ts create mode 100644 dist/wecomp.d.ts.map create mode 100644 dist/wecomp.js create mode 100644 dist/wecomp.js.map create mode 100644 docs/prd/prd-wip.md create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 src/common/wechat-error.ts create mode 100644 src/main.ts create mode 100644 src/server.ts create mode 100644 src/types/wechat.ts create mode 100644 src/wechat-client.ts create mode 100644 src/wecomp.ts create mode 100644 tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..74db07c --- /dev/null +++ b/.env.example @@ -0,0 +1,22 @@ +# 企业微信API配置示例 +# 复制此文件为 .env 并填入实际的值 + +# 企业ID +WEIXIN_CORP_ID=${WEIXIN_CORP_ID} + +# 应用的Secret +WEIXIN_CORP_SECRET=${WEIXIN_CORP_SECRET} + +# API基础URL(可选,默认为 https://qyapi.weixin.qq.com) +WEIXIN_CORP_URL=https://qyapi.weixin.qq.com + +# 企业成员userid(用于获取客户列表) +# 注意:这个成员需要配置了客户联系功能 +WEIXIN_USER_ID=zhangsan + +# 外部联系人ID(用于测试获取客户详情) +# 注意:这个ID需要通过其他接口(如获取客户列表)获取 +# WEIXIN_EXTERNAL_USER_ID='' + +WEIXIN_EVENT_TOKEN=${WEIXIN_EVENT_TOKEN}; +WEIXIN_EVENT_ENCODING_AES_KEY=${WEIXIN_EVENT_ENCODING_AES_KEY}; \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..95cdce3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules + +.environment/* \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..64843b5 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "▶️:dev", + "request": "launch", + "type": "node", + "runtimeExecutable": "pnpm", + "args": [ + "start", + ], + "console": "integratedTerminal", + "envFile": "${workspaceFolder}/.environment/.env" + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..3240cc1 --- /dev/null +++ b/README.md @@ -0,0 +1,237 @@ +# 企业微信客户联系API Demo + +这是一个使用企业微信客户联系API的TypeScript示例项目,演示如何获取客户列表和客户详情。 + +## 功能特性 + +- ✅ 获取access_token(带自动缓存和过期管理) +- ✅ 获取配置了客户联系功能的成员列表 +- ✅ 获取客户列表 +- ✅ 获取客户详情 +- ✅ 批量获取客户详情 +- ✅ 支持分页获取(跟进人超过500人时) +- ✅ 完整的TypeScript类型定义 +- ✅ 错误处理和响应验证 +- ✅ 自动使用第一个成员获取客户列表 + +## 项目结构 + +``` +pg-demo/ +├── src/ +│ ├── main.ts # 主程序入口 +│ ├── wechat-client.ts # 企业微信API客户端 +│ └── types/ +│ └── wechat.ts # TypeScript类型定义 +├── .env.example # 环境变量配置示例 +├── package.json +└── tsconfig.json +``` + +## 快速开始 + +### 1. 安装依赖 + +```bash +pnpm install +``` + +### 2. 配置环境变量 + +复制 `.env.example` 为 `.env` 并填入实际的值: + +```bash +cp .env.example .env +``` + +编辑 `.env` 文件: + +```env +WEIXIN_CORP_ID=your_corp_id +WEIXIN_CORP_SECRET=your_corp_secret +WEIXIN_EXTERNAL_USER_ID=your_external_user_id +``` + +### 3. 运行程序 + +```bash +# 自动使用第一个配置了客户联系功能的成员 +pnpm start + +# 或者指定特定的企业成员userid +pnpm start +``` + +**示例**: +```bash +# 自动使用第一个成员 +pnpm start + +# 指定特定成员 +pnpm start zhangsan +``` + +程序会依次执行以下操作: +1. 获取access_token +2. 获取配置了客户联系功能的成员列表 +3. 获取指定成员(或第一个成员)的客户列表 +4. 获取第一个客户的详情 +5. 批量获取所有客户详情 + +## 使用示例 + +### 获取配置了客户联系功能的成员列表 + +```typescript +import { WeChatClient } from './wechat-client'; + +// 创建客户端 +const client = new WeChatClient({ + corpId: process.env.WEIXIN_CORP_ID!, + corpSecret: process.env.WEIXIN_CORP_SECRET!, +}); + +// 获取配置了客户联系功能的成员列表 +const followUserList = await client.getFollowUserList(); + +console.log('成员数量:', followUserList.follow_user?.length || 0); +console.log('成员ID列表:', followUserList.follow_user); + +// 使用第一个成员获取客户列表 +if (followUserList.follow_user && followUserList.follow_user.length > 0) { + const firstUserId = followUserList.follow_user[0]; + const contactList = await client.getExternalContactList(firstUserId); + console.log('客户数量:', contactList.external_userid?.length || 0); +} +``` + +### 获取客户列表 + +```typescript +// 获取指定成员的客户列表 +const contactList = await client.getExternalContactList('zhangsan'); + +console.log('客户数量:', contactList.external_userid?.length || 0); +console.log('客户ID列表:', contactList.external_userid); +``` + +### 获取客户详情 + +```typescript +// 获取单个客户的详细信息 +const contactDetail = await client.getExternalContact('external_user_id'); + +console.log('客户姓名:', contactDetail.external_contact.name); +console.log('客户类型:', contactDetail.external_contact.type === 1 ? '微信用户' : '企业微信用户'); +console.log('跟进人数量:', contactDetail.follow_user.length); +``` + +### 批量获取客户详情 + +```typescript +// 批量获取客户详情(支持分页) +const batchContacts = await client.batchGetExternalContact('zhangsan', undefined, 100); + +console.log('本次返回客户数量:', batchContacts.external_contact_list?.length || 0); +``` + +### 自动分页获取所有客户 + +```typescript +// 自动分页获取所有客户详情 +const allContacts = await client.batchGetExternalContactFull('zhangsan'); + +console.log('总客户数量:', allContacts.length); +allContacts.forEach((contact, index) => { + console.log(`${index + 1}. ${contact.name} (${contact.external_userid})`); +}); +``` + +### 获取客户详情(支持跟进人分页) + +当跟进人超过500人时,使用 `getExternalContactFull` 方法自动获取所有跟进人: + +```typescript +const fullDetail = await client.getExternalContactFull('external_user_id'); +console.log('总跟进人数量:', fullDetail.follow_user.length); +``` + +## API说明 + +### 获取配置了客户联系功能的成员列表 + +**接口地址**: `https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get_follow_user_list` + +**请求参数**: +- `access_token`: 调用接口凭证 + +**响应字段**: +- `follow_user`: 配置了客户联系功能的成员userid列表 + +### 获取客户列表 + +**接口地址**: `https://qyapi.weixin.qq.com/cgi-bin/externalcontact/list` + +**请求参数**: +- `access_token`: 调用接口凭证 +- `userid`: 企业成员的userid + +**响应字段**: +- `external_userid`: 外部联系人的userid列表 + +### 获取客户详情 + +**接口地址**: `https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get` + +**请求参数**: +- `access_token`: 调用接口凭证 +- `external_userid`: 外部联系人的userid +- `cursor`: (可选)上次请求返回的next_cursor,用于分页 + +**响应字段**: +- `external_contact`: 外部联系人详情 + - `name`: 名称 + - `type`: 1-微信用户,2-企业微信用户 + - `gender`: 0-未知,1-男性,2-女性 + - `position`: 职位(仅企业微信用户) + - `corp_name`: 企业简称(仅企业微信用户) +- `follow_user`: 添加了此外部联系人的企业成员列表 + - `userid`: 成员userid + - `remark`: 备注 + - `description`: 描述 + - `createtime`: 添加时间 + - `tags`: 标签列表 + +### 批量获取客户详情 + +**接口地址**: `https://qyapi.weixin.qq.com/cgi-bin/externalcontact/batch/get_by_user` + +**请求参数**: +- `access_token`: 调用接口凭证 +- `userid`: 企业成员的userid +- `cursor`: (可选)上次请求返回的next_cursor,用于分页 +- `limit`: 返回的最大记录数,最大值1000,默认值为100 + +**响应字段**: +- `external_contact_list`: 外部联系人详情列表 +- `next_cursor`: 下一次分页的起始位置 + +## 权限要求 + +应用需要配置到「客户联系 > 可调用接口的应用」中,并具有「客户基础信息」权限。 + +## 注意事项 + +1. **access_token有效期**: 7200秒,客户端会自动缓存并提前5分钟刷新 +2. **分页限制**: 当跟进人超过500人时,必须使用cursor参数分页获取 +3. **字段限制**: 第三方应用无法获取头像、性别、备注手机号、unionid等字段 +4. **external_userid**: 需要通过其他接口(如获取客户列表)获取 + +## 相关文档 + +- [企业微信API文档 - 获取客户详情](https://developer.work.weixin.qq.com/document/path/101215) +- [企业微信API文档 - 获取access_token](https://developer.work.weixin.qq.com/document/path/91039) + +## 许可证 + +MIT diff --git a/dist/common/wechat-error.d.ts b/dist/common/wechat-error.d.ts new file mode 100644 index 0000000..5b943b4 --- /dev/null +++ b/dist/common/wechat-error.d.ts @@ -0,0 +1,9 @@ +/** + * API错误 + */ +export declare class WeChatError extends Error { + errcode: number; + errmsg: string; + constructor(errcode: number, errmsg: string); +} +//# sourceMappingURL=wechat-error.d.ts.map \ No newline at end of file diff --git a/dist/common/wechat-error.d.ts.map b/dist/common/wechat-error.d.ts.map new file mode 100644 index 0000000..3c9093c --- /dev/null +++ b/dist/common/wechat-error.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"wechat-error.d.ts","sourceRoot":"","sources":["../../src/common/wechat-error.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,qBAAa,WAAY,SAAQ,KAAK;IAEvB,OAAO,EAAE,MAAM;IACf,MAAM,EAAE,MAAM;gBADd,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM;CAI5B"} \ No newline at end of file diff --git a/dist/common/wechat-error.js b/dist/common/wechat-error.js new file mode 100644 index 0000000..54277c8 --- /dev/null +++ b/dist/common/wechat-error.js @@ -0,0 +1,13 @@ +/** + * API错误 + */ +export class WeChatError extends Error { + errcode; + errmsg; + constructor(errcode, errmsg) { + super(`企业微信API错误 [${errcode}]: ${errmsg}`); + this.errcode = errcode; + this.errmsg = errmsg; + } +} +//# sourceMappingURL=wechat-error.js.map \ No newline at end of file diff --git a/dist/common/wechat-error.js.map b/dist/common/wechat-error.js.map new file mode 100644 index 0000000..7a6b565 --- /dev/null +++ b/dist/common/wechat-error.js.map @@ -0,0 +1 @@ +{"version":3,"file":"wechat-error.js","sourceRoot":"","sources":["../../src/common/wechat-error.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,OAAO,WAAY,SAAQ,KAAK;IAEvB;IACA;IAFX,YACW,OAAe,EACf,MAAc;QAErB,KAAK,CAAC,cAAc,OAAO,MAAM,MAAM,EAAE,CAAC,CAAC;QAHpC,YAAO,GAAP,OAAO,CAAQ;QACf,WAAM,GAAN,MAAM,CAAQ;IAGzB,CAAC;CACJ"} \ No newline at end of file diff --git a/dist/main.d.ts b/dist/main.d.ts new file mode 100644 index 0000000..371115b --- /dev/null +++ b/dist/main.d.ts @@ -0,0 +1,2 @@ +export {}; +//# sourceMappingURL=main.d.ts.map \ No newline at end of file diff --git a/dist/main.d.ts.map b/dist/main.d.ts.map new file mode 100644 index 0000000..28b87d8 --- /dev/null +++ b/dist/main.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/dist/main.js b/dist/main.js new file mode 100644 index 0000000..3697943 --- /dev/null +++ b/dist/main.js @@ -0,0 +1,9 @@ +import { handleServer } from './server.js'; +/** + * 主函数:演示如何使用企业微信客户联系API + */ +async function main() { + await handleServer(); +} +main().catch(console.error); +//# sourceMappingURL=main.js.map \ No newline at end of file diff --git a/dist/main.js.map b/dist/main.js.map new file mode 100644 index 0000000..e279fc2 --- /dev/null +++ b/dist/main.js.map @@ -0,0 +1 @@ +{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAI3C;;GAEG;AACH,KAAK,UAAU,IAAI;IACf,MAAM,YAAY,EAAE,CAAC;AACzB,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC"} \ No newline at end of file diff --git a/dist/server.d.ts b/dist/server.d.ts new file mode 100644 index 0000000..e4837fe --- /dev/null +++ b/dist/server.d.ts @@ -0,0 +1,2 @@ +export declare function handleServer(): Promise; +//# sourceMappingURL=server.d.ts.map \ No newline at end of file diff --git a/dist/server.d.ts.map b/dist/server.d.ts.map new file mode 100644 index 0000000..40ebeb8 --- /dev/null +++ b/dist/server.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAeA,wBAAsB,YAAY,kBAsHjC"} \ No newline at end of file diff --git a/dist/server.js b/dist/server.js new file mode 100644 index 0000000..6c0851f --- /dev/null +++ b/dist/server.js @@ -0,0 +1,95 @@ +import { serve } from "@hono/node-server"; +import { Hono } from "hono"; +import { getSignature, decrypt, encrypt } from '@wecom/crypto'; +import { parseStringPromise } from 'xml2js'; +function validateSignature(msgSignature, timestamp, nonce, encrypt) { + const { WEIXIN_EVENT_TOKEN, } = process.env; + if (!WEIXIN_EVENT_TOKEN) + throw new Error('static param error'); + const devMsgSignature = getSignature(WEIXIN_EVENT_TOKEN, timestamp, nonce, encrypt); + return devMsgSignature === msgSignature; +} +export async function handleServer() { + const app = new Hono(); + app.get('/we-comp/event', ({ req, ...args }) => { + try { + const { 'msg_signature': msgSignature, timestamp, nonce, echostr } = req.query(); + const { WEIXIN_EVENT_TOKEN, WEIXIN_EVENT_ENCODING_AES_KEY, } = process.env; + if (!WEIXIN_EVENT_TOKEN + || !WEIXIN_EVENT_ENCODING_AES_KEY + || !msgSignature + || !timestamp + || !nonce + || !echostr) { + throw new Error('static param error'); + } + if (!validateSignature(msgSignature, timestamp, nonce, echostr)) { + throw new Error('validate error'); + } + const { message, id } = decrypt(WEIXIN_EVENT_ENCODING_AES_KEY, echostr); + console.log('URL验证成功,解密后的消息:', message); + console.log('接收者ID:', id); + } + catch (e) { + console.error(e); + } + return args.text(""); + }); + app.post('/we-comp/event', async ({ req, ...args }) => { + const { 'msg_signature': msgSignature, timestamp, nonce } = req.query(); + try { + const result = await parseStringPromise(await req.text()); + const encrypt = result.xml.Encrypt.at(0); + const { WEIXIN_EVENT_TOKEN, WEIXIN_EVENT_ENCODING_AES_KEY, } = process.env; + if (!WEIXIN_EVENT_TOKEN + || !WEIXIN_EVENT_ENCODING_AES_KEY + || !msgSignature + || !timestamp + || !nonce) { + throw new Error('static param error'); + } + // 2. 验证签名 + if (!validateSignature(msgSignature, timestamp, nonce, encrypt)) { + throw new Error('签名验证失败'); + } + const { message, id } = decrypt(WEIXIN_EVENT_ENCODING_AES_KEY, encrypt); + console.log('解密后的消息:', message); + console.log('接收者ID:', id); + // 5. 解析明文XML + const decryptedXml = await parseStringPromise(message); + const msgData = decryptedXml.xml; + const msgType = msgData.MsgType?.[0]; + const fromUserName = msgData.FromUserName?.[0]; + const toUserName = msgData.ToUserName?.[0]; + const createTime = msgData.CreateTime?.[0]; + console.log('========== 消息详情 =========='); + console.log('消息类型:', msgType); + console.log('发送者:', fromUserName); + console.log('接收者:', toUserName); + console.log('创建时间:', createTime); + console.log('============================'); + } + catch (e) { + console.error(e); + } + return args.text(''); + }); + const server = serve({ + fetch: app.fetch, + port: 3080, + }); + process.on('SIGINT', () => { + server.close(); + process.exit(0); + }); + process.on('SIGTERM', () => { + server.close((err) => { + if (err) { + console.error(err); + process.exit(1); + } + process.exit(0); + }); + }); +} +//# sourceMappingURL=server.js.map \ No newline at end of file diff --git a/dist/server.js.map b/dist/server.js.map new file mode 100644 index 0000000..732b157 --- /dev/null +++ b/dist/server.js.map @@ -0,0 +1 @@ +{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/types/wechat.d.ts b/dist/types/wechat.d.ts new file mode 100644 index 0000000..93a1cf1 --- /dev/null +++ b/dist/types/wechat.d.ts @@ -0,0 +1,343 @@ +/** + * 企业微信客户联系 API 类型定义 + */ +/** + * 外部联系人详情 + */ +export interface ExternalContact { + /** 外部联系人userid */ + external_userid: string; + /** 名称(微信用户返回昵称,企业微信用户返回对外展示名称) */ + name: string; + /** 头像URL(部分应用不可获取) */ + avatar?: string; + /** 1-微信用户,2-企业微信用户 */ + type: 1 | 2; + /** 0-未知,1-男性,2-女性 */ + gender: 0 | 1 | 2; + /** 微信unionid(需绑定微信开发者ID) */ + unionid?: string; + /** 职位(仅企业微信用户) */ + position?: string; + /** 企业简称(仅企业微信用户) */ + corp_name?: string; + /** 企业主体名称(仅企业微信用户) */ + corp_full_name?: string; + /** 自定义展示信息(仅企业微信用户) */ + external_profile?: ExternalProfile; +} +/** + * 外部联系人自定义展示信息 + */ +export interface ExternalProfile { + /** 自定义展示字段列表 */ + external_attr: ExternalAttr[]; +} +/** + * 自定义展示字段 + */ +export interface ExternalAttr { + /** 0-文本,1-网页,2-小程序 */ + type: number; + /** 属性名称 */ + name: string; + /** 文本内容(type=0时) */ + text?: { + value: string; + }; + /** 网页内容(type=1时) */ + web?: { + url: string; + title: string; + }; + /** 小程序内容(type=2时) */ + miniprogram?: { + appid: string; + pagepath: string; + title: string; + }; +} +/** + * 标签信息 + */ +export interface Tag { + /** 标签分组名称 */ + group_name: string; + /** 标签名称 */ + tag_name: string; + /** 企业标签ID(用户自定义标签不返回) */ + tag_id?: string; + /** 1-企业设置,2-用户自定义,3-规则组标签 */ + type: 1 | 2 | 3; +} +/** + * 视频号信息 + */ +export interface WechatChannels { + /** 视频号名称 */ + nickname: string; + /** 0-未知,1-主页,2-直播间,3-留资服务 */ + source: 0 | 1 | 2 | 3; +} +/** + * 跟进用户信息 + */ +export interface FollowUser { + /** 成员userid */ + userid: string; + /** 该成员对客户的备注 */ + remark: string; + /** 该成员对客户的描述 */ + description: string; + /** 添加时间(Unix时间戳) */ + createtime: number; + /** 标签列表 */ + tags: Tag[]; + /** 备注的企业名称 */ + remark_corp_name: string; + /** 备注的手机号(部分应用不可获取) */ + remark_mobiles?: string[]; + /** 添加来源 */ + add_way: number; + /** 视频号信息(add_way=10时有此字段) */ + wechat_channels?: WechatChannels; + /** 发起添加的userid */ + oper_userid: string; + /** 自定义state参数(联系我/获客链接) */ + state?: string; +} +/** + * 获取客户详情API响应 + */ +export interface GetExternalContactResponse { + /** 返回码 */ + errcode: number; + /** 返回信息 */ + errmsg: string; + /** 外部联系人详情 */ + external_contact: ExternalContact; + /** 添加了此外部联系人的企业成员列表 */ + follow_user: FollowUser[]; + /** 分页cursor(跟进人>500时返回) */ + next_cursor?: string; +} +/** + * 获取access_token API响应 + */ +export interface AccessTokenResponse { + /** 返回码 */ + errcode: number; + /** 返回信息 */ + errmsg: string; + /** 获取到的凭证 */ + access_token?: string; + /** 凭证有效期(秒) */ + expires_in?: number; +} +/** + * 获取配置了客户联系功能的成员列表API响应 + */ +export interface GetFollowUserListResponse { + /** 返回码 */ + errcode: number; + /** 返回信息 */ + errmsg: string; + /** 配置了客户联系功能的成员userid列表 */ + follow_user?: string[]; +} +/** + * 获取客户列表API响应 + */ +export interface GetExternalContactListResponse { + /** 返回码 */ + errcode: number; + /** 返回信息 */ + errmsg: string; + /** 外部联系人的userid列表 */ + external_userid?: string[]; +} +/** + * 批量获取客户详情API响应 + */ +export interface BatchGetExternalContactResponse { + /** 返回码 */ + errcode: number; + /** 返回信息 */ + errmsg: string; + /** 外部联系人详情列表 */ + external_contact_list?: ExternalContact[]; + /** 下一次分页的起始位置 */ + next_cursor?: string; +} +/** + * 结束语文本内容 + */ +export interface ConclusionText { + /** 文本内容 */ + content: string; +} +/** + * 结束语 + */ +export interface Conclusion { + /** 文本结束语 */ + text?: ConclusionText; +} +/** + * 添加「联系我」方式请求参数 + */ +export interface AddContactWayRequest { + /** 联系方式类型,1-单人,2-多人 */ + type: 1 | 2; + /** 场景,1-在小程序中联系,2-通过二维码联系 */ + scene: 1 | 2; + /** 在小程序中联系时使用的控件样式 */ + style?: number; + /** 联系方式的备注信息,用于助记,不超过30个字符 */ + remark?: string; + /** 外部客户添加时是否无需验证,默认为true */ + skip_verify?: boolean; + /** 企业自定义的state参数,用于区分不同的添加渠道 */ + state?: string; + /** 使用该联系方式的用户userID列表,在type为1时为必填,且只能有一个 */ + user?: string[]; + /** 使用该联系方式的部门id列表,只在type为2时有效 */ + party?: number[]; + /** 是否临时会话模式 */ + is_temp?: boolean; + /** 临时会话二维码有效期,以秒为单位 */ + expires_in?: number; + /** 临时会话有效期,以秒为单位 */ + chat_expires_in?: number; + /** 可进行临时会话的客户unionid */ + unionid?: string; + /** 是否开启同一外部企业客户只能添加同一个员工 */ + is_exclusive?: boolean; + /** 是否标记客户添加来源 */ + mark_source?: boolean; + /** 结束语 */ + conclusions?: Conclusion; +} +/** + * 添加「联系我」方式响应 + */ +export interface AddContactWayResponse { + /** 返回码 */ + errcode: number; + /** 返回信息 */ + errmsg: string; + /** 联系方式的配置id */ + config_id?: string; + /** 联系方式二维码的URL,开发者需要自行将此二维码转换为图片 */ + qr_code?: string; +} +/** + * 获取「联系我」方式请求参数 + */ +export interface GetContactWayRequest { + /** 联系方式的配置id */ + config_id: string; +} +/** + * 获取「联系我」方式响应 + */ +export interface GetContactWayResponse { + /** 返回码 */ + errcode: number; + /** 返回信息 */ + errmsg: string; + /** 联系方式配置信息 */ + contact_way?: ContactWayInfo; +} +/** + * 联系方式配置信息 + */ +export interface ContactWayInfo { + /** 联系方式配置id */ + config_id: string; + /** 联系方式类型 */ + type: 1 | 2; + /** 场景 */ + scene: 1 | 2; + /** 在小程序中联系时使用的控件样式 */ + style?: number; + /** 联系方式的备注信息 */ + remark?: string; + /** 是否无需验证 */ + skip_verify: boolean; + /** state参数 */ + state?: string; + /** 使用该联系方式的用户userID列表 */ + user?: string[]; + /** 使用该联系方式的部门id列表 */ + party?: number[]; + /** 是否临时会话模式 */ + is_temp: boolean; + /** 临时会话二维码有效期 */ + expires_in?: number; + /** 临时会话有效期 */ + chat_expires_in?: number; + /** 临时会话指定的客户unionid */ + unionid?: string; + /** 是否开启同一外部企业客户只能添加同一个员工 */ + is_exclusive: boolean; + /** 结束语 */ + conclusions?: Conclusion; + /** 二维码的URL */ + qr_code?: string; +} +/** + * 更新「联系我」方式请求参数 + */ +export interface UpdateContactWayRequest { + /** 联系方式的配置id */ + config_id: string; + /** 在小程序中联系时使用的控件样式 */ + style?: number; + /** 联系方式的备注信息 */ + remark?: string; + /** 是否无需验证 */ + skip_verify?: boolean; + /** 使用该联系方式的用户userID列表 */ + user?: string[]; + /** 使用该联系方式的部门id列表 */ + party?: number[]; + /** 是否临时会话模式 */ + is_temp?: boolean; + /** 临时会话二维码有效期 */ + expires_in?: number; + /** 临时会话有效期 */ + chat_expires_in?: number; + /** 临时会话指定的客户unionid */ + unionid?: string; + /** 是否开启同一外部企业客户只能添加同一个员工 */ + is_exclusive?: boolean; + /** 结束语 */ + conclusions?: Conclusion; +} +/** + * 更新「联系我」方式响应 + */ +export interface UpdateContactWayResponse { + /** 返回码 */ + errcode: number; + /** 返回信息 */ + errmsg: string; +} +/** + * 删除「联系我」方式请求参数 + */ +export interface DeleteContactWayRequest { + /** 联系方式的配置id */ + config_id: string; +} +/** + * 删除「联系我」方式响应 + */ +export interface DeleteContactWayResponse { + /** 返回码 */ + errcode: number; + /** 返回信息 */ + errmsg: string; +} +//# sourceMappingURL=wechat.d.ts.map \ No newline at end of file diff --git a/dist/types/wechat.d.ts.map b/dist/types/wechat.d.ts.map new file mode 100644 index 0000000..e562f66 --- /dev/null +++ b/dist/types/wechat.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"wechat.d.ts","sourceRoot":"","sources":["../../src/types/wechat.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/types/wechat.js b/dist/types/wechat.js new file mode 100644 index 0000000..db4bced --- /dev/null +++ b/dist/types/wechat.js @@ -0,0 +1,5 @@ +/** + * 企业微信客户联系 API 类型定义 + */ +export {}; +//# sourceMappingURL=wechat.js.map \ No newline at end of file diff --git a/dist/types/wechat.js.map b/dist/types/wechat.js.map new file mode 100644 index 0000000..3b3a965 --- /dev/null +++ b/dist/types/wechat.js.map @@ -0,0 +1 @@ +{"version":3,"file":"wechat.js","sourceRoot":"","sources":["../../src/types/wechat.ts"],"names":[],"mappings":"AAAA;;GAEG"} \ No newline at end of file diff --git a/dist/wechat-client.d.ts b/dist/wechat-client.d.ts new file mode 100644 index 0000000..8373a29 --- /dev/null +++ b/dist/wechat-client.d.ts @@ -0,0 +1,93 @@ +import type { GetExternalContactResponse, GetExternalContactListResponse, GetFollowUserListResponse, BatchGetExternalContactResponse, ExternalContact, AddContactWayRequest, AddContactWayResponse, GetContactWayRequest, GetContactWayResponse, UpdateContactWayRequest, UpdateContactWayResponse, DeleteContactWayRequest, DeleteContactWayResponse } from './types/wechat.js'; +/** + * 企业微信API客户端配置 + */ +export interface WeChatClientConfig { + /** 企业ID */ + corpId: string; + /** 应用的secret */ + corpSecret: string; + /** API基础URL */ + baseUrl?: string; +} +/** + * 企业微信API客户端 + */ +export declare class WeChatClient { + private config; + private accessToken; + private tokenExpireTime; + constructor(config: WeChatClientConfig); + /** + * 获取access_token + * @returns access_token + */ + getAccessToken(): Promise; + /** + * 获取客户详情 + * @param externalUserId 外部联系人的userid + * @param cursor 上次请求返回的next_cursor,用于分页 + * @returns 客户详情 + */ + getExternalContact(externalUserId: string, cursor?: string): Promise; + /** + * 获取客户详情(支持分页,自动获取所有跟进人) + * @param externalUserId 外部联系人的userid + * @returns 客户详情(包含所有跟进人) + */ + getExternalContactFull(externalUserId: string): Promise; + /** + * 获取配置了客户联系功能的成员列表 + * @returns 配置了客户联系功能的成员userid列表 + */ + getFollowUserList(): Promise; + /** + * 获取客户列表 + * @param userId 企业成员的userid + * @returns 外部联系人的userid列表 + */ + getExternalContactList(userId: string): Promise; + /** + * 批量获取客户详情 + * @param userId 企业成员的userid + * @param cursor 上次请求返回的next_cursor,用于分页 + * @param limit 返回的最大记录数,整型,最大值1000,默认值为100 + * @returns 客户详情列表 + */ + batchGetExternalContact(userId: string, cursor?: string, limit?: number): Promise; + /** + * 批量获取客户详情(自动分页,获取所有客户) + * @param userId 企业成员的userid + * @returns 所有客户详情列表 + */ + batchGetExternalContactFull(userId: string): Promise; + /** + * 添加「联系我」方式 + * @param params 联系方式配置参数 + * @returns 联系方式配置信息(包含config_id和qr_code) + */ + addContactWay(params: AddContactWayRequest): Promise; + /** + * 获取「联系我」方式 + * @param params 查询参数(包含config_id) + * @returns 联系方式配置信息 + */ + getContactWay(params: GetContactWayRequest): Promise; + /** + * 更新「联系我」方式 + * @param params 更新参数(包含config_id和要更新的字段) + * @returns 更新结果 + */ + updateContactWay(params: UpdateContactWayRequest): Promise; + /** + * 删除「联系我」方式 + * @param params 删除参数(包含config_id) + * @returns 删除结果 + */ + deleteContactWay(params: DeleteContactWayRequest): Promise; + /** + * 重置access_token(强制重新获取) + */ + resetAccessToken(): void; +} +//# sourceMappingURL=wechat-client.d.ts.map \ No newline at end of file diff --git a/dist/wechat-client.d.ts.map b/dist/wechat-client.d.ts.map new file mode 100644 index 0000000..2328f2d --- /dev/null +++ b/dist/wechat-client.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"wechat-client.d.ts","sourceRoot":"","sources":["../src/wechat-client.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAEV,0BAA0B,EAC1B,8BAA8B,EAC9B,yBAAyB,EACzB,+BAA+B,EAC/B,eAAe,EACf,oBAAoB,EACpB,qBAAqB,EACrB,oBAAoB,EACpB,qBAAqB,EACrB,uBAAuB,EACvB,wBAAwB,EACxB,uBAAuB,EACvB,wBAAwB,EACzB,MAAM,mBAAmB,CAAC;AAE3B;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,WAAW;IACX,MAAM,EAAE,MAAM,CAAC;IACf,gBAAgB;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,MAAM,CAA+B;IAC7C,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,eAAe,CAAqB;gBAEhC,MAAM,EAAE,kBAAkB;IAOtC;;;OAGG;IACG,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC;IAwBvC;;;;;OAKG;IACG,kBAAkB,CACtB,cAAc,EAAE,MAAM,EACtB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,0BAA0B,CAAC;IAwBtC;;;;OAIG;IACG,sBAAsB,CAC1B,cAAc,EAAE,MAAM,GACrB,OAAO,CAAC,0BAA0B,CAAC;IAyBtC;;;OAGG;IACG,iBAAiB,IAAI,OAAO,CAAC,yBAAyB,CAAC;IAoB7D;;;;OAIG;IACG,sBAAsB,CAC1B,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,8BAA8B,CAAC;IAqB1C;;;;;;OAMG;IACG,uBAAuB,CAC3B,MAAM,EAAE,MAAM,EACd,MAAM,CAAC,EAAE,MAAM,EACf,KAAK,GAAE,MAAY,GAClB,OAAO,CAAC,+BAA+B,CAAC;IAyB3C;;;;OAIG;IACG,2BAA2B,CAC/B,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,eAAe,EAAE,CAAC;IAe7B;;;;OAIG;IACG,aAAa,CACjB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,qBAAqB,CAAC;IA2BjC;;;;OAIG;IACG,aAAa,CACjB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,qBAAqB,CAAC;IA2BjC;;;;OAIG;IACG,gBAAgB,CACpB,MAAM,EAAE,uBAAuB,GAC9B,OAAO,CAAC,wBAAwB,CAAC;IA2BpC;;;;OAIG;IACG,gBAAgB,CACpB,MAAM,EAAE,uBAAuB,GAC9B,OAAO,CAAC,wBAAwB,CAAC;IA2BpC;;OAEG;IACH,gBAAgB,IAAI,IAAI;CAIzB"} \ No newline at end of file diff --git a/dist/wechat-client.js b/dist/wechat-client.js new file mode 100644 index 0000000..84849fd --- /dev/null +++ b/dist/wechat-client.js @@ -0,0 +1,287 @@ +/** + * 企业微信 API 客户端 + */ +import { WeChatError } from './common/wechat-error.js'; +/** + * 企业微信API客户端 + */ +export class WeChatClient { + config; + accessToken; + tokenExpireTime; + constructor(config) { + this.config = { + baseUrl: process.env.WEIXIN_CORP_URL ?? 'https://qyapi.weixin.qq.com', + ...config, + }; + } + /** + * 获取access_token + * @returns access_token + */ + async getAccessToken() { + // 检查token是否有效 + if (this.accessToken && this.tokenExpireTime && Date.now() < this.tokenExpireTime) { + return this.accessToken; + } + const url = new URL('/cgi-bin/gettoken', this.config.baseUrl); + url.searchParams.set('corpid', this.config.corpId); + url.searchParams.set('corpsecret', this.config.corpSecret); + const response = await fetch(url); + const data = await response.json(); + if (data.errcode !== 0 || !data.access_token) { + throw new Error(`获取access_token失败: ${data.errmsg}`); + } + this.accessToken = data.access_token; + // 提前5分钟过期,避免临界点问题 + this.tokenExpireTime = Date.now() + (data.expires_in || 7200) * 1000 - 5 * 60 * 1000; + return this.accessToken; + } + /** + * 获取客户详情 + * @param externalUserId 外部联系人的userid + * @param cursor 上次请求返回的next_cursor,用于分页 + * @returns 客户详情 + */ + async getExternalContact(externalUserId, cursor) { + const token = await this.getAccessToken(); + const url = new URL('/cgi-bin/externalcontact/get', this.config.baseUrl); + url.searchParams.set('access_token', token); + url.searchParams.set('external_userid', externalUserId); + if (cursor) { + url.searchParams.set('cursor', cursor); + } + const response = await fetch(url); + const data = await response.json(); + // 检查API错误 + if (data.errcode !== 0) { + const error = new WeChatError(data.errcode, data.errmsg); + error.errcode = data.errcode; + error.errmsg = data.errmsg; + throw error; + } + return data; + } + /** + * 获取客户详情(支持分页,自动获取所有跟进人) + * @param externalUserId 外部联系人的userid + * @returns 客户详情(包含所有跟进人) + */ + async getExternalContactFull(externalUserId) { + let result = null; + let cursor; + do { + const data = await this.getExternalContact(externalUserId, cursor); + if (!result) { + // 第一次请求,直接使用返回的数据 + result = data; + } + else { + // 后续请求,合并follow_user列表 + result.follow_user.push(...data.follow_user); + // 更新next_cursor + if (data.next_cursor !== undefined) { + result.next_cursor = data.next_cursor; + } + } + cursor = data.next_cursor; + } while (cursor); + return result; + } + /** + * 获取配置了客户联系功能的成员列表 + * @returns 配置了客户联系功能的成员userid列表 + */ + async getFollowUserList() { + const token = await this.getAccessToken(); + const url = new URL('/cgi-bin/externalcontact/get_follow_user_list', this.config.baseUrl); + url.searchParams.set('access_token', token); + const response = await fetch(url); + const data = await response.json(); + // 检查API错误 + if (data.errcode !== 0) { + const error = new WeChatError(data.errcode, data.errmsg); + error.errcode = data.errcode; + error.errmsg = data.errmsg; + throw error; + } + return data; + } + /** + * 获取客户列表 + * @param userId 企业成员的userid + * @returns 外部联系人的userid列表 + */ + async getExternalContactList(userId) { + const token = await this.getAccessToken(); + const url = new URL('/cgi-bin/externalcontact/list', this.config.baseUrl); + url.searchParams.set('access_token', token); + url.searchParams.set('userid', userId); + const response = await fetch(url); + const data = await response.json(); + // 检查API错误 + if (data.errcode !== 0) { + const error = new WeChatError(data.errcode, data.errmsg); + error.errcode = data.errcode; + error.errmsg = data.errmsg; + throw error; + } + return data; + } + /** + * 批量获取客户详情 + * @param userId 企业成员的userid + * @param cursor 上次请求返回的next_cursor,用于分页 + * @param limit 返回的最大记录数,整型,最大值1000,默认值为100 + * @returns 客户详情列表 + */ + async batchGetExternalContact(userId, cursor, limit = 100) { + const token = await this.getAccessToken(); + const url = new URL('/cgi-bin/externalcontact/batch/get_by_user', this.config.baseUrl); + url.searchParams.set('access_token', token); + url.searchParams.set('userid', userId); + if (cursor) { + url.searchParams.set('cursor', cursor); + } + url.searchParams.set('limit', limit.toString()); + const response = await fetch(url); + const data = await response.json(); + // 检查API错误 + if (data.errcode !== 0) { + const error = new WeChatError(data.errcode, data.errmsg); + error.errcode = data.errcode; + error.errmsg = data.errmsg; + throw error; + } + return data; + } + /** + * 批量获取客户详情(自动分页,获取所有客户) + * @param userId 企业成员的userid + * @returns 所有客户详情列表 + */ + async batchGetExternalContactFull(userId) { + const allContacts = []; + let cursor; + do { + const data = await this.batchGetExternalContact(userId, cursor); + if (data.external_contact_list) { + allContacts.push(...data.external_contact_list); + } + cursor = data.next_cursor; + } while (cursor); + return allContacts; + } + /** + * 添加「联系我」方式 + * @param params 联系方式配置参数 + * @returns 联系方式配置信息(包含config_id和qr_code) + */ + async addContactWay(params) { + const token = await this.getAccessToken(); + const url = new URL('/cgi-bin/externalcontact/add_contact_way', this.config.baseUrl); + url.searchParams.set('access_token', token); + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }); + const data = await response.json(); + // 检查API错误 + if (data.errcode !== 0) { + const error = new WeChatError(data.errcode, data.errmsg); + error.errcode = data.errcode; + error.errmsg = data.errmsg; + throw error; + } + return data; + } + /** + * 获取「联系我」方式 + * @param params 查询参数(包含config_id) + * @returns 联系方式配置信息 + */ + async getContactWay(params) { + const token = await this.getAccessToken(); + const url = new URL('/cgi-bin/externalcontact/get_contact_way', this.config.baseUrl); + url.searchParams.set('access_token', token); + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }); + const data = await response.json(); + // 检查API错误 + if (data.errcode !== 0) { + const error = new WeChatError(data.errcode, data.errmsg); + error.errcode = data.errcode; + error.errmsg = data.errmsg; + throw error; + } + return data; + } + /** + * 更新「联系我」方式 + * @param params 更新参数(包含config_id和要更新的字段) + * @returns 更新结果 + */ + async updateContactWay(params) { + const token = await this.getAccessToken(); + const url = new URL('/cgi-bin/externalcontact/update_contact_way', this.config.baseUrl); + url.searchParams.set('access_token', token); + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }); + const data = await response.json(); + // 检查API错误 + if (data.errcode !== 0) { + const error = new WeChatError(data.errcode, data.errmsg); + error.errcode = data.errcode; + error.errmsg = data.errmsg; + throw error; + } + return data; + } + /** + * 删除「联系我」方式 + * @param params 删除参数(包含config_id) + * @returns 删除结果 + */ + async deleteContactWay(params) { + const token = await this.getAccessToken(); + const url = new URL('/cgi-bin/externalcontact/del_contact_way', this.config.baseUrl); + url.searchParams.set('access_token', token); + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }); + const data = await response.json(); + // 检查API错误 + if (data.errcode !== 0) { + const error = new WeChatError(data.errcode, data.errmsg); + error.errcode = data.errcode; + error.errmsg = data.errmsg; + throw error; + } + return data; + } + /** + * 重置access_token(强制重新获取) + */ + resetAccessToken() { + this.accessToken = undefined; + this.tokenExpireTime = undefined; + } +} +//# sourceMappingURL=wechat-client.js.map \ No newline at end of file diff --git a/dist/wechat-client.js.map b/dist/wechat-client.js.map new file mode 100644 index 0000000..c581cfb --- /dev/null +++ b/dist/wechat-client.js.map @@ -0,0 +1 @@ +{"version":3,"file":"wechat-client.js","sourceRoot":"","sources":["../src/wechat-client.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/dist/wecomp.d.ts b/dist/wecomp.d.ts new file mode 100644 index 0000000..f03a252 --- /dev/null +++ b/dist/wecomp.d.ts @@ -0,0 +1,2 @@ +export declare function handleWecomp(): Promise; +//# sourceMappingURL=wecomp.d.ts.map \ No newline at end of file diff --git a/dist/wecomp.d.ts.map b/dist/wecomp.d.ts.map new file mode 100644 index 0000000..48f1d5d --- /dev/null +++ b/dist/wecomp.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"wecomp.d.ts","sourceRoot":"","sources":["../src/wecomp.ts"],"names":[],"mappings":"AAEA,wBAAsB,YAAY,kBA2DjC"} \ No newline at end of file diff --git a/dist/wecomp.js b/dist/wecomp.js new file mode 100644 index 0000000..e282340 --- /dev/null +++ b/dist/wecomp.js @@ -0,0 +1,54 @@ +import { WeChatClient } from "./wechat-client.js"; +export async function handleWecomp() { + // 检查环境变量 + if (process.env.WEIXIN_CORP_ID == null || + process.env.WEIXIN_CORP_SECRET == null) { + throw new Error('环境变量错误:请设置 WEIXIN_CORP_ID 和 WEIXIN_CORP_SECRET'); + } + // 创建企业微信客户端 + const client = new WeChatClient({ + corpId: process.env.WEIXIN_CORP_ID, + corpSecret: process.env.WEIXIN_CORP_SECRET, + baseUrl: process.env.WEIXIN_CORP_URL || 'https://qyapi.weixin.qq.com', + }); + try { + // 示例:获取配置了客户联系功能的成员列表 + console.log('\n=== 获取配置了客户联系功能的成员列表 ==='); + const followUserList = await client.getFollowUserList(); + console.log('\n--- 成员列表 ---'); + console.log('成员数量:', followUserList.follow_user?.length || 0); + if (!followUserList.follow_user || followUserList.follow_user.length === 0) { + console.log('没有配置了客户联系功能的成员'); + return; + } + console.log('成员ID列表:'); + followUserList.follow_user.forEach((id, index) => { + console.log(` ${index + 1}. ${id}`); + }); + let userId = 'YeDeLiang'; + if (!userId) { + console.log('错误:无法获取有效的成员userid'); + return; + } + const res = await client.getContactWay({ + config_id: 'd5990127d833c10d9db8ba7f192e7b19' + }); + console.log(res); + } + catch (error) { + if (error instanceof Error) { + console.error('发生错误:', error.message); + const err = error; + if (err.errcode !== undefined) { + console.error('错误码:', err.errcode); + } + if (err.errmsg !== undefined) { + console.error('错误信息:', err.errmsg); + } + } + else { + throw error; + } + } +} +//# sourceMappingURL=wecomp.js.map \ No newline at end of file diff --git a/dist/wecomp.js.map b/dist/wecomp.js.map new file mode 100644 index 0000000..eac48df --- /dev/null +++ b/dist/wecomp.js.map @@ -0,0 +1 @@ +{"version":3,"file":"wecomp.js","sourceRoot":"","sources":["../src/wecomp.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,MAAM,CAAC,KAAK,UAAU,YAAY;IAC9B,SAAS;IACT,IAAI,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,IAAI;QAClC,OAAO,CAAC,GAAG,CAAC,kBAAkB,IAAI,IAAI,EACxC,CAAC;QACC,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;IACtE,CAAC;IAED,YAAY;IACZ,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC;QAC5B,MAAM,EAAE,OAAO,CAAC,GAAG,CAAC,cAAc;QAClC,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,kBAAkB;QAC1C,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,eAAe,IAAI,6BAA6B;KACxE,CAAC,CAAC;IAEH,IAAI,CAAC;QACD,sBAAsB;QACtB,OAAO,CAAC,GAAG,CAAC,4BAA4B,CAAC,CAAC;QAC1C,MAAM,cAAc,GAAG,MAAM,MAAM,CAAC,iBAAiB,EAAE,CAAC;QAExD,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;QAC9B,OAAO,CAAC,GAAG,CAAC,OAAO,EAAE,cAAc,CAAC,WAAW,EAAE,MAAM,IAAI,CAAC,CAAC,CAAC;QAE9D,IAAI,CAAC,cAAc,CAAC,WAAW,IAAI,cAAc,CAAC,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzE,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;YAC9B,OAAO;QACX,CAAC;QAED,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QACvB,cAAc,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,KAAK,EAAE,EAAE;YAC7C,OAAO,CAAC,GAAG,CAAC,KAAK,KAAK,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QACzC,CAAC,CAAC,CAAC;QAEH,IAAI,MAAM,GAAG,WAAW,CAAC;QAEzB,IAAI,CAAC,MAAM,EAAE,CAAC;YACV,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;YAClC,OAAO;QACX,CAAC;QAED,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC;YACnC,SAAS,EAAE,kCAAkC;SAChD,CAAC,CAAC;QAEH,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACrB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACb,IAAI,KAAK,YAAY,KAAK,EAAE,CAAC;YACzB,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;YACtC,MAAM,GAAG,GAAG,KAAyD,CAAC;YACtE,IAAI,GAAG,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;gBAC5B,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;YACvC,CAAC;YACD,IAAI,GAAG,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;gBAC3B,OAAO,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;YACvC,CAAC;QACL,CAAC;aAAM,CAAC;YACJ,MAAM,KAAK,CAAC;QAChB,CAAC;IACL,CAAC;AACL,CAAC"} \ No newline at end of file diff --git a/docs/prd/prd-wip.md b/docs/prd/prd-wip.md new file mode 100644 index 0000000..f88c9e6 --- /dev/null +++ b/docs/prd/prd-wip.md @@ -0,0 +1,179 @@ + +# 企业微信外部客户引流与识别系统 PRD + +## 需求概述 + +实现一个基于企业微信的外部客户引流与识别系统,根据用户与企业的关系状态(是否已添加为外部联系人),引导用户进入不同的业务流程。 + +## 核心需求点 + +1. 用户的入口(二维码) 二维码是哪里生成的? 需要带什么参数? +2. 用户扫码后, 判断当前用户是否已添加该企业微信的外部用户 +3. 如果是该企业的外部客户, 跳转到该企业的具体应用页面, 如果不是该企业的外部客户则生成添加企业的二维码页面(这里的逻辑该如何处理) + +## 技术方案 + +### 1. 二维码生成方案 + +#### 1.1 使用企业微信「联系我」接口 + +api doc: [客户联系「联系我」管理](https://developer.work.weixin.qq.com/document/path/92228) + +**接口地址**: `POST https://qyapi.weixin.qq.com/cgi-bin/externalcontact/add_contact_way` + +**推荐配置**(临时会话模式): +```javascript +{ + type: 1, // 单人联系 + scene: 2, // 通过二维码联系 + user: ["service_staff_id"], // 接待人员UserID + is_temp: true, // 开启临时会话 + expires_in: 86400, // 二维码有效期(秒) + chat_expires_in: 86400, // 会话有效期(秒) + state: "channel_source_001", // 渠道标识参数 + skip_verify: true, // 无需验证直接添加 + conclusions: { + text: { + content: "感谢咨询,如需长期服务请添加我们" + } + } +} +``` + +| 参数 | 必须 | 说明 | +| --------------- | ---- | ------------------------------------------------------------------------------------------------------ | +| access_token | 是 | 调用接口凭证 | +| type | 是 | 联系方式类型,1-单人,2-多人 | +| scene | 是 | 场景,1-在小程序中联系,2-通过二维码联系 | +| style | 否 | 在小程序中联系时使用的控件样式,详见附表 | +| remark | 否 | 联系方式的备注信息,用于助记,不超过30个字符 | +| skip_verify | 否 | 外部客户添加时是否无需验证,默认为true | +| state | 否 | 企业自定义的state参数,用于区分不同的添加渠道,在调用"获取客户详情"时会返回该参数值,不超过30个字符 | +| user | 否 | 使用该联系方式的用户userID列表,在type为1时为必填,且只能有一个 | +| party | 否 | 使用该联系方式的部门id列表,只在type为2时有效 | +| is_temp | 否 | 是否临时会话模式,true表示使用临时会话模式,默认为false | +| expires_in | 否 | 临时会话二维码有效期,以秒为单位。该参数仅在is_temp为true时有效,默认7天,最多为14天 | +| chat_expires_in | 否 | 临时会话有效期,以秒为单位。该参数仅在is_temp为true时有效,默认为添加好友后24小时,最多为14天 | +| unionid | 否 | 可进行临时会话的客户unionid,该参数仅在is_temp为true时有效,如不指定则不进行限制 | +| is_exclusive | 否 | 是否开启同一外部企业客户只能添加同一个员工,默认为否,开启后,同一个企业的客户会优先添加到同一个跟进人 | +| mark_source | 否 | 是否标记客户添加来源为该应用创建的「联系我」,默认为true;仅对「营销获客」应用生效 | +| conclusions | 否 | 结束语,会话结束时自动发送给客户,可参考"结束语定义",仅在is_temp为true时有效 | + +**关键参数说明**: +- `state`: 自定义状态参数,用于识别不同引流渠道,建议格式:`channel_{渠道ID}_{场景ID}` +- `is_temp`: 临时会话模式,不占用企业「联系我」配额,适合首次接触 +- `user`: 指定接待人员,可以是固定客服或根据业务规则分配 + +#### 1.2 二维码参数设计 + +**必需参数**: +- `state`: 渠道标识(如:`channel_website_homepage`) +- `user`: 接待人员ID(可配置轮询规则) + +**可选参数**: +- `expires_in`: 二维码有效期(建议7天内) +- `chat_expires_in`: 会话有效期(建议24小时) +- `conclusions`: 会话结束语(引导正式添加) + +### 2. 客户识别与分流逻辑 + +#### 2.1 客户状态判断流程 + +```javascript +// 用户扫码后的处理流程 +async function handleCustomerScan(qrCodeState, externalUserId) { + // Step 1: 解析渠道信息 + const channelInfo = parseChannelState(qrCodeState); + + // Step 2: 获取客户详情 + const customerDetail = await getExternalContactDetail(externalUserId); + + // Step 3: 判断客户状态 + if (customerDetail.external_contact) { + // 已是外部客户 -> 跳转应用 + return { + action: "redirect_to_app", + url: `https://your-app.com/dashboard?channel=${channelInfo.source}&user=${externalUserId}` + }; + } else { + // 非外部客户 -> 生成添加二维码 + const addContactWay = await generatePermanentContactWay(channelInfo); + return { + action: "show_add_qrcode", + qrCode: addContactWay.qr_code, + message: "请添加我们以获得完整服务", + expiresIn: addContactWay.expires_in + }; + } +} +``` + +#### 2.2 相关接口 + +**获取客户详情**: +- 接口: `POST https://qyapi.weixin.qq.com/cgi-bin/externalcontact/get` +- 参数: `external_userid`(从扫码回调中获取) +- 返回: 客户基本信息、添加状态、标签等 + +**批量获取客户详情**: +- 接口: `POST https://qyapi.weixin.qq.com/cgi-bin/externalcontact/batch/get_by_user` +- 用于验证客户是否已添加 + +### 3. 分流处理方案 + +#### 3.1 已添加客户流程 + +1. **触发条件**: 客户已在企业外部联系人列表中 +2. **处理动作**: + - 直接跳转到企业应用页面 + - 传递客户标识和渠道参数 + - 可根据客户标签进行个性化页面展示 +3. **示例URL**: `https://app.yourcompany.com/home?external_userid=xxx&source=channel_001` + +#### 3.2 未添加客户流程 + +1. **触发条件**: 客户不在企业外部联系人列表中 +2. **处理动作**: + - 生成永久「联系我」二维码(非临时会话) + - 展示引导页面,说明添加后的服务内容 + - 记录引流渠道,用于后续数据分析 +3. **二维码配置**: +```javascript +{ + type: 1, + scene: 2, + user: ["service_staff_id"], + is_temp: false, // 永久添加 + state: "permanent_add_xxx", // 记录来源 + skip_verify: true, + remark: "官网引流转化" +} +``` + +### 4. 接口调用流程 + +#### 4.1 初始化阶段 + +1. 调用 `add_contact_way` 接口生成引流二维码 +2. 保存返回的 `config_id` 和 `qr_code` URL +3. 将二维码部署到各引流渠道 + +#### 4.2 用户扫码阶段 + +1. 用户扫码后,企业微信推送事件到回调URL +2. 回调事件包含 `external_userid` 和 `state` 参数 +3. 后端接收事件,调用 `get_external_contact` 获取客户详情 +4. 根据客户状态执行分流逻辑 + +#### 4.3 事件回调处理 + +**回调事件类型**: +- `change_external_contact`: 外部联系人变更事件 +- `add_external_contact`: 添加外部联系人事件 +- `del_external_contact`: 删除外部联系人事件 + +**关键参数**: +- `UserID`: 接待人员的企业微信ID +- `ExternalUserID`: 外部联系人ID +- `State`: 二维码中的state参数 + diff --git a/package.json b/package.json new file mode 100644 index 0000000..3ce9962 --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "pg-demo", + "version": "1.0.0", + "description": "", + "main": "index.js", + "type": "module", + "scripts": { + "build": "tsc", + "start": "tsx ./src/main.ts" + }, + "keywords": [], + "author": "", + "license": "ISC", + "packageManager": "pnpm@10.27.0", + "devDependencies": { + "@types/node": "^22.19.5", + "@types/xml2js": "^0.4.14", + "tsx": "latest", + "typescript": "^5.9.3" + }, + "dependencies": { + "@hono/node-server": "^1.19.8", + "@wecom/crypto": "^1.0.1", + "@wecom/jssdk": "^2.3.3", + "hono": "^4.11.3", + "xml2js": "^0.6.2" + } +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..38ca61b --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,415 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@hono/node-server': + specifier: ^1.19.8 + version: 1.19.8(hono@4.11.3) + '@wecom/crypto': + specifier: ^1.0.1 + version: 1.0.1 + '@wecom/jssdk': + specifier: ^2.3.3 + version: 2.3.3 + hono: + specifier: ^4.11.3 + version: 4.11.3 + xml2js: + specifier: ^0.6.2 + version: 0.6.2 + devDependencies: + '@types/node': + specifier: ^22.19.5 + version: 22.19.5 + '@types/xml2js': + specifier: ^0.4.14 + version: 0.4.14 + tsx: + specifier: latest + version: 4.21.0 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + +packages: + + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@hono/node-server@1.19.8': + resolution: {integrity: sha512-0/g2lIOPzX8f3vzW1ggQgvG5mjtFBDBHFAzI5SFAi2DzSqS9luJwqg9T6O/gKYLi+inS7eNxBeIFkkghIPvrMA==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@types/node@22.19.5': + resolution: {integrity: sha512-HfF8+mYcHPcPypui3w3mvzuIErlNOh2OAG+BCeBZCEwyiD5ls2SiCwEyT47OELtf7M3nHxBdu0FsmzdKxkN52Q==} + + '@types/xml2js@0.4.14': + resolution: {integrity: sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==} + + '@wecom/crypto@1.0.1': + resolution: {integrity: sha512-K4Ilkl1l64ceJDbj/kflx8ND/J88pcl8tKx4Ivp7IiCrshRJU+Uo5uWCjAa+PjUiLIdcQSZ4m4d0t1npMPCX5A==} + + '@wecom/jssdk@2.3.3': + resolution: {integrity: sha512-bazYSD39MoiaUV1xMVxrOPsTFa37ARY0xXAqDgth4kU/xk8n61o1YiAcdn4dPM+kBnJKOfukFYPjOPQv8K5l+w==} + + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + engines: {node: '>=18'} + hasBin: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + + hono@4.11.3: + resolution: {integrity: sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w==} + engines: {node: '>=16.9.0'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + sax@1.4.4: + resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==} + engines: {node: '>=11.0.0'} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + xml2js@0.6.2: + resolution: {integrity: sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + +snapshots: + + '@esbuild/aix-ppc64@0.27.2': + optional: true + + '@esbuild/android-arm64@0.27.2': + optional: true + + '@esbuild/android-arm@0.27.2': + optional: true + + '@esbuild/android-x64@0.27.2': + optional: true + + '@esbuild/darwin-arm64@0.27.2': + optional: true + + '@esbuild/darwin-x64@0.27.2': + optional: true + + '@esbuild/freebsd-arm64@0.27.2': + optional: true + + '@esbuild/freebsd-x64@0.27.2': + optional: true + + '@esbuild/linux-arm64@0.27.2': + optional: true + + '@esbuild/linux-arm@0.27.2': + optional: true + + '@esbuild/linux-ia32@0.27.2': + optional: true + + '@esbuild/linux-loong64@0.27.2': + optional: true + + '@esbuild/linux-mips64el@0.27.2': + optional: true + + '@esbuild/linux-ppc64@0.27.2': + optional: true + + '@esbuild/linux-riscv64@0.27.2': + optional: true + + '@esbuild/linux-s390x@0.27.2': + optional: true + + '@esbuild/linux-x64@0.27.2': + optional: true + + '@esbuild/netbsd-arm64@0.27.2': + optional: true + + '@esbuild/netbsd-x64@0.27.2': + optional: true + + '@esbuild/openbsd-arm64@0.27.2': + optional: true + + '@esbuild/openbsd-x64@0.27.2': + optional: true + + '@esbuild/openharmony-arm64@0.27.2': + optional: true + + '@esbuild/sunos-x64@0.27.2': + optional: true + + '@esbuild/win32-arm64@0.27.2': + optional: true + + '@esbuild/win32-ia32@0.27.2': + optional: true + + '@esbuild/win32-x64@0.27.2': + optional: true + + '@hono/node-server@1.19.8(hono@4.11.3)': + dependencies: + hono: 4.11.3 + + '@types/node@22.19.5': + dependencies: + undici-types: 6.21.0 + + '@types/xml2js@0.4.14': + dependencies: + '@types/node': 22.19.5 + + '@wecom/crypto@1.0.1': {} + + '@wecom/jssdk@2.3.3': {} + + esbuild@0.27.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 + + fsevents@2.3.3: + optional: true + + get-tsconfig@4.13.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + hono@4.11.3: {} + + resolve-pkg-maps@1.0.0: {} + + sax@1.4.4: {} + + tsx@4.21.0: + dependencies: + esbuild: 0.27.2 + get-tsconfig: 4.13.0 + optionalDependencies: + fsevents: 2.3.3 + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + xml2js@0.6.2: + dependencies: + sax: 1.4.4 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..efc037a --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +onlyBuiltDependencies: + - esbuild diff --git a/src/common/wechat-error.ts b/src/common/wechat-error.ts new file mode 100644 index 0000000..e6db6bb --- /dev/null +++ b/src/common/wechat-error.ts @@ -0,0 +1,11 @@ +/** + * API错误 + */ +export class WeChatError extends Error { + constructor( + public errcode: number, + public errmsg: string + ) { + super(`企业微信API错误 [${errcode}]: ${errmsg}`); + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..e7b93c2 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,13 @@ + +import { handleServer } from './server.js'; + + + +/** + * 主函数:演示如何使用企业微信客户联系API + */ +async function main() { + await handleServer(); +} + +main().catch(console.error); \ No newline at end of file diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..2baae65 --- /dev/null +++ b/src/server.ts @@ -0,0 +1,134 @@ +import { serve } from "@hono/node-server"; +import { Hono } from "hono"; +import { getSignature, decrypt, encrypt } from '@wecom/crypto'; +import { parseStringPromise } from 'xml2js'; + + +function validateSignature(msgSignature: string, timestamp: string, nonce: string, encrypt: string) { + const { + WEIXIN_EVENT_TOKEN, + } = process.env; + if (!WEIXIN_EVENT_TOKEN) throw new Error('static param error'); + const devMsgSignature = getSignature(WEIXIN_EVENT_TOKEN, timestamp, nonce, encrypt); + return devMsgSignature === msgSignature; +} + +export async function handleServer() { + + const app = new Hono(); + + app.get('/we-comp/event', ({ req, ...args }) => { + + try { + const { 'msg_signature': msgSignature, timestamp, nonce, echostr } = req.query(); + + const { + WEIXIN_EVENT_TOKEN, + WEIXIN_EVENT_ENCODING_AES_KEY, + } = process.env; + + if ( + !WEIXIN_EVENT_TOKEN + || !WEIXIN_EVENT_ENCODING_AES_KEY + || !msgSignature + || !timestamp + || !nonce + || !echostr + ) { + throw new Error('static param error'); + } + + if (!validateSignature(msgSignature, timestamp, nonce, echostr)) { + throw new Error('validate error'); + } + + const { message, id } = decrypt(WEIXIN_EVENT_ENCODING_AES_KEY, echostr); + + console.log('URL验证成功,解密后的消息:', message); + console.log('接收者ID:', id); + } catch (e) { + console.error(e); + } + return args.text(""); + }); + + app.post('/we-comp/event', async ({ req, ...args }) => { + + const { + 'msg_signature': msgSignature, + timestamp, + nonce + } = req.query(); + + try { + const result = await parseStringPromise(await req.text()); + const encrypt = result.xml.Encrypt.at(0); + + const { + WEIXIN_EVENT_TOKEN, + WEIXIN_EVENT_ENCODING_AES_KEY, + } = process.env; + + if ( + !WEIXIN_EVENT_TOKEN + || !WEIXIN_EVENT_ENCODING_AES_KEY + || !msgSignature + || !timestamp + || !nonce + ) { + throw new Error('static param error'); + } + + // 2. 验证签名 + if (!validateSignature(msgSignature, timestamp, nonce, encrypt)) { + throw new Error('签名验证失败'); + } + + const { message, id } = decrypt(WEIXIN_EVENT_ENCODING_AES_KEY, encrypt); + console.log('解密后的消息:', message); + console.log('接收者ID:', id); + + + // 5. 解析明文XML + const decryptedXml = await parseStringPromise(message); + const msgData = decryptedXml.xml; + + const msgType = msgData.MsgType?.[0]; + const fromUserName = msgData.FromUserName?.[0]; + const toUserName = msgData.ToUserName?.[0]; + const createTime = msgData.CreateTime?.[0]; + + console.log('========== 消息详情 =========='); + console.log('消息类型:', msgType); + console.log('发送者:', fromUserName); + console.log('接收者:', toUserName); + console.log('创建时间:', createTime); + console.log('============================'); + + } catch (e) { + console.error(e); + } + + + return args.text(''); + }) + + const server = serve({ + fetch: app.fetch, + port: 3080, + }); + process.on('SIGINT', () => { + server.close() + process.exit(0) + }) + process.on('SIGTERM', () => { + server.close((err) => { + if (err) { + console.error(err) + process.exit(1) + } + process.exit(0) + }) + }) + +} \ No newline at end of file diff --git a/src/types/wechat.ts b/src/types/wechat.ts new file mode 100644 index 0000000..a9c9330 --- /dev/null +++ b/src/types/wechat.ts @@ -0,0 +1,355 @@ +/** + * 企业微信客户联系 API 类型定义 + */ + +/** + * 外部联系人详情 + */ +export interface ExternalContact { + /** 外部联系人userid */ + external_userid: string; + /** 名称(微信用户返回昵称,企业微信用户返回对外展示名称) */ + name: string; + /** 头像URL(部分应用不可获取) */ + avatar?: string; + /** 1-微信用户,2-企业微信用户 */ + type: 1 | 2; + /** 0-未知,1-男性,2-女性 */ + gender: 0 | 1 | 2; + /** 微信unionid(需绑定微信开发者ID) */ + unionid?: string; + /** 职位(仅企业微信用户) */ + position?: string; + /** 企业简称(仅企业微信用户) */ + corp_name?: string; + /** 企业主体名称(仅企业微信用户) */ + corp_full_name?: string; + /** 自定义展示信息(仅企业微信用户) */ + external_profile?: ExternalProfile; +} + +/** + * 外部联系人自定义展示信息 + */ +export interface ExternalProfile { + /** 自定义展示字段列表 */ + external_attr: ExternalAttr[]; +} + +/** + * 自定义展示字段 + */ +export interface ExternalAttr { + /** 0-文本,1-网页,2-小程序 */ + type: number; + /** 属性名称 */ + name: string; + /** 文本内容(type=0时) */ + text?: { value: string }; + /** 网页内容(type=1时) */ + web?: { url: string; title: string }; + /** 小程序内容(type=2时) */ + miniprogram?: { appid: string; pagepath: string; title: string }; +} + +/** + * 标签信息 + */ +export interface Tag { + /** 标签分组名称 */ + group_name: string; + /** 标签名称 */ + tag_name: string; + /** 企业标签ID(用户自定义标签不返回) */ + tag_id?: string; + /** 1-企业设置,2-用户自定义,3-规则组标签 */ + type: 1 | 2 | 3; +} + +/** + * 视频号信息 + */ +export interface WechatChannels { + /** 视频号名称 */ + nickname: string; + /** 0-未知,1-主页,2-直播间,3-留资服务 */ + source: 0 | 1 | 2 | 3; +} + +/** + * 跟进用户信息 + */ +export interface FollowUser { + /** 成员userid */ + userid: string; + /** 该成员对客户的备注 */ + remark: string; + /** 该成员对客户的描述 */ + description: string; + /** 添加时间(Unix时间戳) */ + createtime: number; + /** 标签列表 */ + tags: Tag[]; + /** 备注的企业名称 */ + remark_corp_name: string; + /** 备注的手机号(部分应用不可获取) */ + remark_mobiles?: string[]; + /** 添加来源 */ + add_way: number; + /** 视频号信息(add_way=10时有此字段) */ + wechat_channels?: WechatChannels; + /** 发起添加的userid */ + oper_userid: string; + /** 自定义state参数(联系我/获客链接) */ + state?: string; +} + +/** + * 获取客户详情API响应 + */ +export interface GetExternalContactResponse { + /** 返回码 */ + errcode: number; + /** 返回信息 */ + errmsg: string; + /** 外部联系人详情 */ + external_contact: ExternalContact; + /** 添加了此外部联系人的企业成员列表 */ + follow_user: FollowUser[]; + /** 分页cursor(跟进人>500时返回) */ + next_cursor?: string; +} + +/** + * 获取access_token API响应 + */ +export interface AccessTokenResponse { + /** 返回码 */ + errcode: number; + /** 返回信息 */ + errmsg: string; + /** 获取到的凭证 */ + access_token?: string; + /** 凭证有效期(秒) */ + expires_in?: number; +} + +/** + * 获取配置了客户联系功能的成员列表API响应 + */ +export interface GetFollowUserListResponse { + /** 返回码 */ + errcode: number; + /** 返回信息 */ + errmsg: string; + /** 配置了客户联系功能的成员userid列表 */ + follow_user?: string[]; +} + +/** + * 获取客户列表API响应 + */ +export interface GetExternalContactListResponse { + /** 返回码 */ + errcode: number; + /** 返回信息 */ + errmsg: string; + /** 外部联系人的userid列表 */ + external_userid?: string[]; +} + +/** + * 批量获取客户详情API响应 + */ +export interface BatchGetExternalContactResponse { + /** 返回码 */ + errcode: number; + /** 返回信息 */ + errmsg: string; + /** 外部联系人详情列表 */ + external_contact_list?: ExternalContact[]; + /** 下一次分页的起始位置 */ + next_cursor?: string; +} + +/** + * 结束语文本内容 + */ +export interface ConclusionText { + /** 文本内容 */ + content: string; +} + +/** + * 结束语 + */ +export interface Conclusion { + /** 文本结束语 */ + text?: ConclusionText; +} + +/** + * 添加「联系我」方式请求参数 + */ +export interface AddContactWayRequest { + /** 联系方式类型,1-单人,2-多人 */ + type: 1 | 2; + /** 场景,1-在小程序中联系,2-通过二维码联系 */ + scene: 1 | 2; + /** 在小程序中联系时使用的控件样式 */ + style?: number; + /** 联系方式的备注信息,用于助记,不超过30个字符 */ + remark?: string; + /** 外部客户添加时是否无需验证,默认为true */ + skip_verify?: boolean; + /** 企业自定义的state参数,用于区分不同的添加渠道 */ + state?: string; + /** 使用该联系方式的用户userID列表,在type为1时为必填,且只能有一个 */ + user?: string[]; + /** 使用该联系方式的部门id列表,只在type为2时有效 */ + party?: number[]; + /** 是否临时会话模式 */ + is_temp?: boolean; + /** 临时会话二维码有效期,以秒为单位 */ + expires_in?: number; + /** 临时会话有效期,以秒为单位 */ + chat_expires_in?: number; + /** 可进行临时会话的客户unionid */ + unionid?: string; + /** 是否开启同一外部企业客户只能添加同一个员工 */ + is_exclusive?: boolean; + /** 是否标记客户添加来源 */ + mark_source?: boolean; + /** 结束语 */ + conclusions?: Conclusion; +} + +/** + * 添加「联系我」方式响应 + */ +export interface AddContactWayResponse { + /** 返回码 */ + errcode: number; + /** 返回信息 */ + errmsg: string; + /** 联系方式的配置id */ + config_id?: string; + /** 联系方式二维码的URL,开发者需要自行将此二维码转换为图片 */ + qr_code?: string; +} + +/** + * 获取「联系我」方式请求参数 + */ +export interface GetContactWayRequest { + /** 联系方式的配置id */ + config_id: string; +} + +/** + * 获取「联系我」方式响应 + */ +export interface GetContactWayResponse { + /** 返回码 */ + errcode: number; + /** 返回信息 */ + errmsg: string; + /** 联系方式配置信息 */ + contact_way?: ContactWayInfo; +} + +/** + * 联系方式配置信息 + */ +export interface ContactWayInfo { + /** 联系方式配置id */ + config_id: string; + /** 联系方式类型 */ + type: 1 | 2; + /** 场景 */ + scene: 1 | 2; + /** 在小程序中联系时使用的控件样式 */ + style?: number; + /** 联系方式的备注信息 */ + remark?: string; + /** 是否无需验证 */ + skip_verify: boolean; + /** state参数 */ + state?: string; + /** 使用该联系方式的用户userID列表 */ + user?: string[]; + /** 使用该联系方式的部门id列表 */ + party?: number[]; + /** 是否临时会话模式 */ + is_temp: boolean; + /** 临时会话二维码有效期 */ + expires_in?: number; + /** 临时会话有效期 */ + chat_expires_in?: number; + /** 临时会话指定的客户unionid */ + unionid?: string; + /** 是否开启同一外部企业客户只能添加同一个员工 */ + is_exclusive: boolean; + /** 结束语 */ + conclusions?: Conclusion; + /** 二维码的URL */ + qr_code?: string; +} + +/** + * 更新「联系我」方式请求参数 + */ +export interface UpdateContactWayRequest { + /** 联系方式的配置id */ + config_id: string; + /** 在小程序中联系时使用的控件样式 */ + style?: number; + /** 联系方式的备注信息 */ + remark?: string; + /** 是否无需验证 */ + skip_verify?: boolean; + /** 使用该联系方式的用户userID列表 */ + user?: string[]; + /** 使用该联系方式的部门id列表 */ + party?: number[]; + /** 是否临时会话模式 */ + is_temp?: boolean; + /** 临时会话二维码有效期 */ + expires_in?: number; + /** 临时会话有效期 */ + chat_expires_in?: number; + /** 临时会话指定的客户unionid */ + unionid?: string; + /** 是否开启同一外部企业客户只能添加同一个员工 */ + is_exclusive?: boolean; + /** 结束语 */ + conclusions?: Conclusion; +} + +/** + * 更新「联系我」方式响应 + */ +export interface UpdateContactWayResponse { + /** 返回码 */ + errcode: number; + /** 返回信息 */ + errmsg: string; +} + +/** + * 删除「联系我」方式请求参数 + */ +export interface DeleteContactWayRequest { + /** 联系方式的配置id */ + config_id: string; +} + +/** + * 删除「联系我」方式响应 + */ +export interface DeleteContactWayResponse { + /** 返回码 */ + errcode: number; + /** 返回信息 */ + errmsg: string; +} \ No newline at end of file diff --git a/src/wechat-client.ts b/src/wechat-client.ts new file mode 100644 index 0000000..e304b26 --- /dev/null +++ b/src/wechat-client.ts @@ -0,0 +1,395 @@ +/** + * 企业微信 API 客户端 + */ +import { WeChatError } from './common/wechat-error.js'; +import type { + AccessTokenResponse, + GetExternalContactResponse, + GetExternalContactListResponse, + GetFollowUserListResponse, + BatchGetExternalContactResponse, + ExternalContact, + AddContactWayRequest, + AddContactWayResponse, + GetContactWayRequest, + GetContactWayResponse, + UpdateContactWayRequest, + UpdateContactWayResponse, + DeleteContactWayRequest, + DeleteContactWayResponse, +} from './types/wechat.js'; + +/** + * 企业微信API客户端配置 + */ +export interface WeChatClientConfig { + /** 企业ID */ + corpId: string; + /** 应用的secret */ + corpSecret: string; + /** API基础URL */ + baseUrl?: string; +} + +/** + * 企业微信API客户端 + */ +export class WeChatClient { + private config: Required; + private accessToken: string | undefined; + private tokenExpireTime: number | undefined; + + constructor(config: WeChatClientConfig) { + this.config = { + baseUrl: process.env.WEIXIN_CORP_URL ?? 'https://qyapi.weixin.qq.com', + ...config, + }; + } + + /** + * 获取access_token + * @returns access_token + */ + async getAccessToken(): Promise { + // 检查token是否有效 + if (this.accessToken && this.tokenExpireTime && Date.now() < this.tokenExpireTime) { + return this.accessToken as string; + } + + const url = new URL('/cgi-bin/gettoken', this.config.baseUrl); + url.searchParams.set('corpid', this.config.corpId); + url.searchParams.set('corpsecret', this.config.corpSecret); + + const response = await fetch(url); + const data = await response.json() as AccessTokenResponse; + + if (data.errcode !== 0 || !data.access_token) { + throw new Error(`获取access_token失败: ${data.errmsg}`); + } + + this.accessToken = data.access_token; + // 提前5分钟过期,避免临界点问题 + this.tokenExpireTime = Date.now() + (data.expires_in || 7200) * 1000 - 5 * 60 * 1000; + + return this.accessToken; + } + + /** + * 获取客户详情 + * @param externalUserId 外部联系人的userid + * @param cursor 上次请求返回的next_cursor,用于分页 + * @returns 客户详情 + */ + async getExternalContact( + externalUserId: string, + cursor?: string + ): Promise { + const token = await this.getAccessToken(); + + const url = new URL('/cgi-bin/externalcontact/get', this.config.baseUrl); + url.searchParams.set('access_token', token); + url.searchParams.set('external_userid', externalUserId); + if (cursor) { + url.searchParams.set('cursor', cursor); + } + + const response = await fetch(url); + const data = await response.json() as GetExternalContactResponse; + + // 检查API错误 + if (data.errcode !== 0) { + const error = new WeChatError(data.errcode, data.errmsg); + error.errcode = data.errcode; + error.errmsg = data.errmsg; + throw error; + } + + return data; + } + + /** + * 获取客户详情(支持分页,自动获取所有跟进人) + * @param externalUserId 外部联系人的userid + * @returns 客户详情(包含所有跟进人) + */ + async getExternalContactFull( + externalUserId: string + ): Promise { + let result: GetExternalContactResponse | null = null; + let cursor: string | undefined; + + do { + const data = await this.getExternalContact(externalUserId, cursor); + + if (!result) { + // 第一次请求,直接使用返回的数据 + result = data; + } else { + // 后续请求,合并follow_user列表 + result.follow_user.push(...data.follow_user); + // 更新next_cursor + if (data.next_cursor !== undefined) { + result.next_cursor = data.next_cursor; + } + } + + cursor = data.next_cursor; + } while (cursor); + + return result!; + } + + /** + * 获取配置了客户联系功能的成员列表 + * @returns 配置了客户联系功能的成员userid列表 + */ + async getFollowUserList(): Promise { + const token = await this.getAccessToken(); + + const url = new URL('/cgi-bin/externalcontact/get_follow_user_list', this.config.baseUrl); + url.searchParams.set('access_token', token); + + const response = await fetch(url); + const data = await response.json() as GetFollowUserListResponse; + + // 检查API错误 + if (data.errcode !== 0) { + const error = new WeChatError(data.errcode, data.errmsg); + error.errcode = data.errcode; + error.errmsg = data.errmsg; + throw error; + } + + return data; + } + + /** + * 获取客户列表 + * @param userId 企业成员的userid + * @returns 外部联系人的userid列表 + */ + async getExternalContactList( + userId: string + ): Promise { + const token = await this.getAccessToken(); + + const url = new URL('/cgi-bin/externalcontact/list', this.config.baseUrl); + url.searchParams.set('access_token', token); + url.searchParams.set('userid', userId); + + const response = await fetch(url); + const data = await response.json() as GetExternalContactListResponse; + + // 检查API错误 + if (data.errcode !== 0) { + const error = new WeChatError(data.errcode, data.errmsg); + error.errcode = data.errcode; + error.errmsg = data.errmsg; + throw error; + } + + return data; + } + + /** + * 批量获取客户详情 + * @param userId 企业成员的userid + * @param cursor 上次请求返回的next_cursor,用于分页 + * @param limit 返回的最大记录数,整型,最大值1000,默认值为100 + * @returns 客户详情列表 + */ + async batchGetExternalContact( + userId: string, + cursor?: string, + limit: number = 100 + ): Promise { + const token = await this.getAccessToken(); + + const url = new URL('/cgi-bin/externalcontact/batch/get_by_user', this.config.baseUrl); + url.searchParams.set('access_token', token); + url.searchParams.set('userid', userId); + if (cursor) { + url.searchParams.set('cursor', cursor); + } + url.searchParams.set('limit', limit.toString()); + + const response = await fetch(url); + const data = await response.json() as BatchGetExternalContactResponse; + + // 检查API错误 + if (data.errcode !== 0) { + const error = new WeChatError(data.errcode, data.errmsg); + error.errcode = data.errcode; + error.errmsg = data.errmsg; + throw error; + } + + return data; + } + + /** + * 批量获取客户详情(自动分页,获取所有客户) + * @param userId 企业成员的userid + * @returns 所有客户详情列表 + */ + async batchGetExternalContactFull( + userId: string + ): Promise { + const allContacts: ExternalContact[] = []; + let cursor: string | undefined; + + do { + const data = await this.batchGetExternalContact(userId, cursor); + if (data.external_contact_list) { + allContacts.push(...data.external_contact_list); + } + cursor = data.next_cursor; + } while (cursor); + + return allContacts; + } + + /** + * 添加「联系我」方式 + * @param params 联系方式配置参数 + * @returns 联系方式配置信息(包含config_id和qr_code) + */ + async addContactWay( + params: AddContactWayRequest + ): Promise { + const token = await this.getAccessToken(); + + const url = new URL('/cgi-bin/externalcontact/add_contact_way', this.config.baseUrl); + url.searchParams.set('access_token', token); + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }); + + const data = await response.json() as AddContactWayResponse; + + // 检查API错误 + if (data.errcode !== 0) { + const error = new WeChatError(data.errcode, data.errmsg); + error.errcode = data.errcode; + error.errmsg = data.errmsg; + throw error; + } + + return data; + } + + /** + * 获取「联系我」方式 + * @param params 查询参数(包含config_id) + * @returns 联系方式配置信息 + */ + async getContactWay( + params: GetContactWayRequest + ): Promise { + const token = await this.getAccessToken(); + + const url = new URL('/cgi-bin/externalcontact/get_contact_way', this.config.baseUrl); + url.searchParams.set('access_token', token); + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }); + + const data = await response.json() as GetContactWayResponse; + + // 检查API错误 + if (data.errcode !== 0) { + const error = new WeChatError(data.errcode, data.errmsg); + error.errcode = data.errcode; + error.errmsg = data.errmsg; + throw error; + } + + return data; + } + + /** + * 更新「联系我」方式 + * @param params 更新参数(包含config_id和要更新的字段) + * @returns 更新结果 + */ + async updateContactWay( + params: UpdateContactWayRequest + ): Promise { + const token = await this.getAccessToken(); + + const url = new URL('/cgi-bin/externalcontact/update_contact_way', this.config.baseUrl); + url.searchParams.set('access_token', token); + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }); + + const data = await response.json() as UpdateContactWayResponse; + + // 检查API错误 + if (data.errcode !== 0) { + const error = new WeChatError(data.errcode, data.errmsg); + error.errcode = data.errcode; + error.errmsg = data.errmsg; + throw error; + } + + return data; + } + + /** + * 删除「联系我」方式 + * @param params 删除参数(包含config_id) + * @returns 删除结果 + */ + async deleteContactWay( + params: DeleteContactWayRequest + ): Promise { + const token = await this.getAccessToken(); + + const url = new URL('/cgi-bin/externalcontact/del_contact_way', this.config.baseUrl); + url.searchParams.set('access_token', token); + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(params), + }); + + const data = await response.json() as DeleteContactWayResponse; + + // 检查API错误 + if (data.errcode !== 0) { + const error = new WeChatError(data.errcode, data.errmsg); + error.errcode = data.errcode; + error.errmsg = data.errmsg; + throw error; + } + + return data; + } + + /** + * 重置access_token(强制重新获取) + */ + resetAccessToken(): void { + this.accessToken = undefined as string | undefined; + this.tokenExpireTime = undefined as number | undefined; + } +} diff --git a/src/wecomp.ts b/src/wecomp.ts new file mode 100644 index 0000000..2a9ed7a --- /dev/null +++ b/src/wecomp.ts @@ -0,0 +1,62 @@ +import { WeChatClient } from "./wechat-client.js"; + +export async function handleWecomp() { + // 检查环境变量 + if (process.env.WEIXIN_CORP_ID == null || + process.env.WEIXIN_CORP_SECRET == null + ) { + throw new Error('环境变量错误:请设置 WEIXIN_CORP_ID 和 WEIXIN_CORP_SECRET'); + } + + // 创建企业微信客户端 + const client = new WeChatClient({ + corpId: process.env.WEIXIN_CORP_ID, + corpSecret: process.env.WEIXIN_CORP_SECRET, + baseUrl: process.env.WEIXIN_CORP_URL || 'https://qyapi.weixin.qq.com', + }); + + try { + // 示例:获取配置了客户联系功能的成员列表 + console.log('\n=== 获取配置了客户联系功能的成员列表 ==='); + const followUserList = await client.getFollowUserList(); + + console.log('\n--- 成员列表 ---'); + console.log('成员数量:', followUserList.follow_user?.length || 0); + + if (!followUserList.follow_user || followUserList.follow_user.length === 0) { + console.log('没有配置了客户联系功能的成员'); + return; + } + + console.log('成员ID列表:'); + followUserList.follow_user.forEach((id, index) => { + console.log(` ${index + 1}. ${id}`); + }); + + let userId = 'YeDeLiang'; + + if (!userId) { + console.log('错误:无法获取有效的成员userid'); + return; + } + + const res = await client.getContactWay({ + config_id: 'd5990127d833c10d9db8ba7f192e7b19' + }); + + console.log(res); + } catch (error) { + if (error instanceof Error) { + console.error('发生错误:', error.message); + const err = error as unknown as { errcode?: number; errmsg?: string }; + if (err.errcode !== undefined) { + console.error('错误码:', err.errcode); + } + if (err.errmsg !== undefined) { + console.error('错误信息:', err.errmsg); + } + } else { + throw error; + } + } +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6fc65c9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,49 @@ +{ + // Visit https://aka.ms/tsconfig to read more about this file + "compilerOptions": { + // File Layout + // "rootDir": "./src", + // "outDir": "./dist", + // Environment Settings + // See also https://aka.ms/tsconfig/module + "module": "nodenext", + "target": "esnext", + // "types": [], + // For nodejs: + "lib": [ + "esnext" + ], + // and npm install -D @types/node + // Other Outputs + "sourceMap": true, + "declaration": true, + "declarationMap": true, + // Stricter Typechecking Options + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + // Style Options + // "noImplicitReturns": true, + // "noImplicitOverride": true, + // "noUnusedLocals": true, + // "noUnusedParameters": true, + // "noFallthroughCasesInSwitch": true, + // "noPropertyAccessFromIndexSignature": true, + // Recommended Options + "strict": true, + "jsx": "react-jsx", + "verbatimModuleSyntax": true, + "isolatedModules": true, + "noUncheckedSideEffectImports": true, + "moduleDetection": "force", + "skipLibCheck": true, + "outDir": "./dist", + "paths": { + "~/*": [ + "./*" + ], + "@/*": [ + "./src/*" + ] + } + } +} \ No newline at end of file