{"id":13435910,"url":"https://github.com/Eric-Guo/wechat","last_synced_at":"2025-03-18T12:30:30.573Z","repository":{"id":15476368,"uuid":"18209875","full_name":"Eric-Guo/wechat","owner":"Eric-Guo","description":"API, command and message handling for WeChat in Rails","archived":false,"fork":false,"pushed_at":"2025-03-01T18:10:19.000Z","size":1590,"stargazers_count":1320,"open_issues_count":12,"forks_count":372,"subscribers_count":65,"default_branch":"main","last_synced_at":"2025-03-15T01:51:05.762Z","etag":null,"topics":["wechat","wechat-framework","wechat-oauth","wechat-sdk"],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":"zpydr/gnome-shell-extension-taskbar","license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Eric-Guo.png","metadata":{"files":{"readme":"README-CN.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2014-03-28T11:23:11.000Z","updated_at":"2025-03-01T18:09:52.000Z","dependencies_parsed_at":"2024-06-03T01:57:41.721Z","dependency_job_id":"aa74fb1a-dee2-44f3-9b56-f3f219bc2b1b","html_url":"https://github.com/Eric-Guo/wechat","commit_stats":{"total_commits":1015,"total_committers":78,"mean_commits":"13.012820512820513","dds":0.5605911330049261,"last_synced_commit":"8eef832e7087995e15020918b89a05c039fe955e"},"previous_names":["skinnyworm/wechat-rails"],"tags_count":94,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Eric-Guo%2Fwechat","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Eric-Guo%2Fwechat/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Eric-Guo%2Fwechat/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Eric-Guo%2Fwechat/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Eric-Guo","download_url":"https://codeload.github.com/Eric-Guo/wechat/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":244222054,"owners_count":20418433,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["wechat","wechat-framework","wechat-oauth","wechat-sdk"],"created_at":"2024-07-31T03:00:40.647Z","updated_at":"2025-03-18T12:30:30.566Z","avatar_url":"https://github.com/Eric-Guo.png","language":"Ruby","funding_links":[],"categories":["Ruby"],"sub_categories":[],"readme":"WeChat [![Gem Version](https://badge.fury.io/rb/wechat.svg)](https://rubygems.org/gems/wechat) [![Build Status](https://mixtint.semaphoreci.com/badges/wechat/branches/main.svg?style=shields)](https://mixtint.semaphoreci.com/projects/wechat) [![Maintainability](https://api.codeclimate.com/v1/badges/12885358487c13e91e00/maintainability)](https://codeclimate.com/github/Eric-Guo/wechat/maintainability) [![Test Coverage](https://api.codeclimate.com/v1/badges/12885358487c13e91e00/test_coverage)](https://codeclimate.com/github/Eric-Guo/wechat/test_coverage)\n======\n\n[![Join the chat](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Eric-Guo/wechat?utm_source=badge\u0026utm_medium=badge\u0026utm_campaign=pr-badge\u0026utm_content=badge)\n\nWeChat Gem 帮助开发者方便地在 Rails 环境中集成[微信公众平台](https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html)、[企业微信](https://work.weixin.qq.com/api/doc)和[小程序](https://developers.weixin.qq.com/miniprogram/dev/framework/)，包括功能：\n\n- 微信公众平台/企业微信[发送消息](http://qydev.weixin.qq.com/wiki/index.php?title=%E5%8F%91%E9%80%81%E6%B6%88%E6%81%AF) API（命令行和 Web 环境都可以使用）\n- [接收消息](http://qydev.weixin.qq.com/wiki/index.php?title=%E6%8E%A5%E6%94%B6%E6%B6%88%E6%81%AF%E4%B8%8E%E4%BA%8B%E4%BB%B6)（必须运行 Web 服务器）\n- [微信 JS-SDK](http://qydev.weixin.qq.com/wiki/index.php?title=%E5%BE%AE%E4%BF%A1JS%E6%8E%A5%E5%8F%A3) config 接口注入权限验证\n- OAuth 2.0 认证机制\n- 接收消息会话 ( Session ) 记录机制（可选）\n\n命令行工具 `wechat` 可以调用各种无需 Web 环境的 API，同时也提供了 Rails Controller 的 Responder DSL 。可以帮助开发者方便地在 Rails 应用中集成微信的消息处理，包括主动推送的和被动响应的消息。\n\n如果您的 App 还需要集成微信 OAuth2.0 除了简便的 `wechat_oauth2` 指令，也可以考虑 [omniauth-wechat-oauth2](https://github.com/skinnyworm/omniauth-wechat-oauth2) 以便和 [devise](https://github.com/heartcombo/devise) 集成提供完整的用户认证。\n\n如果您对如何制作微信网页 UI 没有灵感，可以参考官方的 [weui](https://github.com/weui/weui) 。针对 Rails 的 Gem 是 [weui-rails](https://github.com/Eric-Guo/weui-rails)。\n\n主页型应用请使用 [`wechat_api`](#wechat_api---rails-controller-wechat-api)，传统消息型应用请使用 [`wechat_responder`](#wechat_responder---rails-responder-controller-dsl)。\n\n如果您想从一个稍微完整一些的示例开始微信开发，可以参考 [wechat-starter](https://github.com/goofansu/wechat-starter)，这个示例甚至包括了微信支付的内容。\n\n## 安装\n\n使用 `gem install`\n\n```\ngem install \"wechat\"\n# 如果使用 Ruby 版本小于 2.6\n# gem install wechat -v 0.12.4\n```\n\n或者添加下面这行到 `Gemfile`:\n\n```\ngem 'wechat'\n# 如果使用 Rails 版本小于 6\n# gem 'wechat', '~\u003e 0.12.4'\n```\n\n运行下面这行代码来安装：\n\n```console\nbundle install\n```\n\n运行下面这行代码来生成必要文件：\n\n```console\nrails generate wechat:install\n```\n\n运行 `rails g wechat:install` 后会自动生成 wechat.yml 配置，还有 wechats_controller 及相关路由配置到当前 Rails 项目。\n\n启用 Session 会话记录：\n\n```console\nrails g wechat:session\nrake db:migrate\n```\n\n运行后会自动启用回调消息会话 (Session) 记录，`wechat` Gem 会在 Rails 项目中生成两个文件，用户可以在 *wechat_session* 表中添加更多字段或者声明一些关联关系。使用已有的 **hash_store** 直接保存也是可以的，但对于 PostgreSQL 用户，使用 [hstore](http://guides.rubyonrails.org/active_record_postgresql.html#hstore) 或者 json 格式可能更佳，当然最佳方案仍然是添加新字段记录数据。\n\n启用 Redis 存贮 Token 和 Ticket:\n\n```console\nrails g wechat:redis_store\n```\n\nRedis 存贮相比默认的文件存贮，可以允许 Rails 应用运行在多台服务器中。如果只有一台服务器，仍然推荐使用默认的文件存贮，另外命令行不会读取 Redis 存贮的 Token 或者 Ticket。\n\n启用数据库配置微信账户：\n\n```console\nrails g wechat:config\nrake db:migrate\n```\n\n运行后会在数据库中创建  `wechat_configs`  表用来记录不同微信账户的配置。\n\n## 配置\n\n#### 微信的第一次配置\n\n请先确保在服务器上配置成功，再到微信官网提交链接。否则微信会提示错误。\n\n默认通过 `rails g wechat:install` 生成的 URL 是： `http://your-server.com/wechat`\n\nappid/corpid 以及 secret 的配置请阅读下一节\n\n#### 数据配置模式\n需要保证传入的 `record` 属性中包含 `access_token`, `token_expires_in`, `got_token_at`。\n```ruby\ndef client\n  @client ||= Wechat::Api.new(app_id, app_secret, token_file, network_setting, jsapi_ticket_file, record)\nend\n```\n\n#### 命令行程序的配置\n\n要使用命令行程序，需要在 home 目录中创建一个 `~/.wechat.yml`，包含以下内容。其中 `access_token` 是存放 access_token 的文件位置。\n\n```\nappid: \"my_appid\"\nsecret: \"my_secret\"\naccess_token: \"/var/tmp/wechat_access_token\"\n```\n\nWindows 或者使用企业号需要存放在 `C:/Users/[user_name]/` 下，其中 corpid 和 corpsecret 可以从企业号管理界面的设置 -\u003e 权限管理，通过新建任意一个管理组后获取。\n\n```\ncorpid: \"my_appid\"\ncorpsecret: \"my_secret\"\nagentid: 1 # 企业应用的 id 整型可在应用的设置页面查看\naccess_token: \"C:/Users/[user_name]/wechat_access_token\"\n```\n\n#### Rails 全局配置\nRails 应用程序中，需要将配置文件放在 `config/wechat.yml`，可以为不同 environment 创建不同的配置。\n\n微信公众平台配置示例：\n\n```\ndefault: \u0026default\n  appid: \"app_id\"\n  secret: \"app_secret\"\n  token:  \"app_token\"\n  access_token: \"/var/tmp/wechat_access_token\"\n  jsapi_ticket: \"/var/tmp/wechat_jsapi_ticket\"\n\nproduction:\n  appid: \u003c%= ENV['WECHAT_APPID'] %\u003e\n  secret: \u003c%= ENV['WECHAT_APP_SECRET'] %\u003e\n  token:   \u003c%= ENV['WECHAT_TOKEN'] %\u003e\n  access_token: \u003c%= ENV['WECHAT_ACCESS_TOKEN'] %\u003e\n  jsapi_ticket: \u003c%= ENV['WECHAT_JSAPI_TICKET'] %\u003e\n  oauth2_cookie_duration: \u003c%= ENV['WECHAT_OAUTH2_COOKIE_DURATION'] %\u003e # seconds\n\ndevelopment:\n  \u003c\u003c: *default\n  trusted_domain_fullname: \"http://your_dev.proxy.qqbrowser.cc\"\n\ntest:\n  \u003c\u003c: *default\n```\n\n微信公众平台可选安全模式（加密模式），通过添加如下配置可开启加密模式。\n\n```\ndefault: \u0026default\n  encrypt_mode: true\n  encoding_aes_key:  \"my_encoding_aes_key\"\n```\n\n企业微信配置下必须使用加密模式，其中 token 和 encoding_aes_key 可以从企业号管理界面的应用中心 -\u003e 某个应用 -\u003e 模式选择，选择回调模式后获得。\n\n```\ndefault: \u0026default\n  corpid: \"corpid\"\n  corpsecret: \"corpsecret\"\n  agentid:  1\n  access_token: \"C:/Users/[user_name]/wechat_access_token\"\n  token:    \"\"\n  encoding_aes_key:  \"\"\n  jsapi_ticket: \"C:/Users/[user_name]/wechat_jsapi_ticket\"\n\nproduction:\n  corpid:     \u003c%= ENV['WECHAT_CORPID'] %\u003e\n  corpsecret: \u003c%= ENV['WECHAT_CORPSECRET'] %\u003e\n  agentid:    \u003c%= ENV['WECHAT_AGENTID'] %\u003e\n  access_token:  \u003c%= ENV['WECHAT_ACCESS_TOKEN'] %\u003e\n  token:      \u003c%= ENV['WECHAT_TOKEN'] %\u003e\n  timeout:    30,\n  skip_verify_ssl: true\n  encoding_aes_key:  \u003c%= ENV['WECHAT_ENCODING_AES_KEY'] %\u003e\n  jsapi_ticket: \u003c%= ENV['WECHAT_JSAPI_TICKET'] %\u003e\n  oauth2_cookie_duration: \u003c%= ENV['WECHAT_OAUTH2_COOKIE_DURATION'] %\u003e\n\ndevelopment:\n  \u003c\u003c: *default\n  trusted_domain_fullname: \"http://your_dev.proxy.qqbrowser.cc\"\n\ntest:\n  \u003c\u003c: *default\n\n # Multiple Accounts\n #\n # wx2_development:\n #  \u003c\u003c: *default\n #  appid: \"my_appid\"\n #  secret: \"my_secret\"\n #  access_token: \"tmp/wechat_access_token2\"\n #  jsapi_ticket: \"tmp/wechat_jsapi_ticket2\"\n #\n # wx2_test:\n #  \u003c\u003c: *default\n #  appid: \"my_appid\"\n #  secret: \"my_secret\"\n #\n # wx2_production:\n #  \u003c\u003c: *default\n #  appid: \"my_appid\"\n #  secret: \"my_secret\"\n```\n\n支持微信公众平台 / 企业微信多账号的注意点 ( 例如：增加账号 `wx2` ):\n\n* 配置文件可增加多个微信公众平台 ( 企业微信 ) 配置，用法类似 Rails 中 `config/database.yml` 多数据库配置的处理。 `development`,  `test`,  `production` 是默认账号的配置段，要想增加账号 `wx2`，你需要增加配置段 `wx2_development`,  `wx2_test`,  `wx2_production`。\n\n* 声明账号 `wx2 `的 `wechat_responder`:\n  ```ruby\n  wechat_responder account: :wx2\n  ```\n\n* `Wechat.api(:wx2)`  表示使用账号 `wx2` 的 Wechat api，而 `Wechat.api` 或 `Wechat.api(:default)` 则表示默认账号的 wechat api。\n\n* 在 wechat 命令行中，通过增加可选参数 `-a, [--account=ACCOUNT]` 来表示使用其他账号，例如 `wechat users -a wx2` 表示列举 `wx2` 这个账号的粉丝列表\n\n进一步的多账号支持参见 [PR 150](https://github.com/Eric-Guo/wechat/pull/150)。\n\n#### 数据库微信账户配置\n启用数据库微信配置之后，会生成如下数据表：\n\n属性 | 类型 |  备注\n---- | ---- | ----\nenvironment | 字串 | 必填。配置对应的运行环境，一般有：`production`、`development`、`test`。比如  `production`  配置仅在生产环境有效。默认为  `development`。 \naccount | 字串 | 必填。自定义的微信账户名称。同一  `environment`  下，账户名称不允许重复。 \nenabled | 布尔 | 必填。配置是否生效。默认  `true`。 \nappid | 字串 | 公众号 id ，此字段和  `corpid`  两者必填其一。 \nsecret | 字串 | 公众号相关配置。当公众号  `appid`  存在时必填。 \ncorpid | 字串 | 企业号 id。此字段和 `appid` 两者必填其一。\ncorpsecret | 字串 | 企业号相关配置。当企业号 `corpid` 存在时必填。\nagentid | 整数 | 企业号相关配置。当企业号 `corpid` 存在时必填。\nencrypt_mode | 布尔 |\nencoding_aes_key | 字串 | 当 `encrypt_mode` 为 `true` 时必填。\ntoken | 字串 | 必填。\naccess_token | 字串 | 必填。存储 `access token` 文件的路径。\njsapi_ticket | 字串 | 必填。存储 `jsapi ticket` 文件的路径。\nskip_verify_ssl | 布尔 |\ntimeout | 整数 | 默认值是 20。\ntrusted_domain_fullname | 字串 |\n\n数据库配置更新后，需要重启服务器或者调用 `Wechat.reload_config!` 载入更新，否则更新不会生效。\n\n##### 配置优先级\n\n注意在 Rails 项目根目录下运行 `wechat` 命令行工具会优先使用 `config/wechat.yml `中的 `default`配置，如果失败则使用 `~\\.wechat.yml` 中的配置，以便于在生产环境下管理多个微信账号应用。\n\n如果启用数据库账户配置，数据库中的账户信息在读入  `wechat.yml`  或环境变量之后被载入。当存在同名账户时，数据库中的配置会覆盖前两者。\n\n##### 配置微信服务器超时\n\n微信服务器有时请求会花很长时间，如果不配置默认为 20 秒，可视情况配置。\n\n##### 配置跳过 SSL 认证\n\nWechat 服务器有报道曾出现 [RestClient::SSLCertificateNotVerified](http://qydev.weixin.qq.com/qa/index.php?qa=11037) 错误，此时可以选择关闭 SSL 验证。`skip_verify_ssl: true`\n\n#### 为每个 Responder 配置不同的 appid 和 secret\n\n有些情况下，单个 Rails 应用可能需要处理来自多个微信公众号的消息，您可以通过在 `wechat_responder` 和 `wechat_api` 后配置多个相关参数来支持多账号。\n\n```ruby\nclass WechatFirstController \u003c ActionController::Base\n   wechat_responder account: :new_account, account_from_request: Proc.new{ |request| request.params[:wechat] }\n\n   on :text, with:\"help\", respond: \"help content\"\nend\n```\n\n或者直接完整配置\n\n```ruby\nclass WechatFirstController \u003c ActionController::Base\n   wechat_responder appid: \"app1\", secret: \"secret1\", token: \"token1\", access_token: Rails.root.join(\"tmp/access_token1\"),\n                    account_from_request: Proc.new{ |request| request.params[:wechat] }\n\n   on :text, with:\"help\", respond: \"help content\"\nend\n```\n\n其中 `account_from_request` 是一个 `Proc`，接受 `request` 作为唯一参数，返回相应的微信账户名称。以上示例中，`controller` 会根据 `request` 中传入的 `wechat` 参数选择微信账户。如果没有提供 `account_from_request` 或者 `Proc` 的结果是 `nil`，则使用 `account` 或者完整配置。\n\n#### JS-SDK 支持\n\n通过 JS-SDK 可以在 HTML 网页中控制微信客户端的行为，但必须先注入配置信息. Wechat gem 提供了帮助方法 `wechat_config_js` 使这个过程更简单：\n\n注意 wechat_config_js 指令依赖于 [`wechat_api`](#wechat_api---rails-controller-wechat-api)  或  [`wechat_responder`](#wechat_responder---rails-responder-controller-dsl) ，需要先在 controller 里面添加。\n\n```erb\n\u003cbody\u003e\n\u003c%= wechat_config_js debug: false, api: %w(hideMenuItems closeWindow) -%\u003e\n\u003cscript type=\"application/javascript\"\u003e\n  wx.ready(function() {\n      wx.hideOptionMenu();\n  });\n\u003c/script\u003e\n\u003ca href=\"javascript:wx.closeWindow();\"\u003eClose\u003c/a\u003e\n\u003c/body\u003e\n```\n\n在开发模式下，由于程序往往通过微信调试工具的服务器端调试工具反向代理被访问，此时需要配置 `trusted_domain_fullname` 以便 wechat gem 可以使用正确的域名做 JS-SDK 的权限签名。\n\n#### OAuth2.0 验证接口支持\n\n公众号可使用如下代码取得关注用户的相关信息。\n\n```ruby\nclass CartController \u003c ActionController::Base\n  wechat_api\n  def index\n    wechat_oauth2 do |openid|\n      @current_user = User.find_by(wechat_openid: openid)\n      @articles = @current_user.articles\n    end\n\n    # 指定 account_name 可以使用任意微信账户\n    # wechat_oauth2('snsapi_base', nil, account_name) do |openid|\n    #  ...\n    # end\n  end\nend\n```\n\n企业微信可使用如下代码取得企业用户的相关信息。\n\n```ruby\nclass WechatsController \u003c ActionController::Base\n  layout 'wechat'\n  wechat_responder\n  def apply_new\n    wechat_oauth2 do |userid|\n      @current_user = User.find_by(wechat_userid: userid)\n      @apply = Apply.new\n      @apply.user_id = @current_user.id\n    end\n  end\nend\n```\n\n`wechat_oauth2 `封装了 OAuth2.0 验证接口和 Cookies 处理逻辑，用户仅需提供业务代码块即可。userid 指的是微信企业成员 userid，openid 是关注该公众号的用户 openid。\n\n注意：\n* 如果使用 `wechat_responder`，请不要在 Controller 里定义 `show` 和 `create` 方法，否则会报错。\n* 如果遇到 “ redirect_uri 参数错误” 的错误信息，请登录服务号管理后台，查看 “开发者中心/网页服务/网页授权获取用户基本信息” 的授权回调页面域名已正确配置。\n\n## 关于接口权限\n\nWechat Gem 内部不会检查权限，但因公众号类型不同和微信服务器端通讯时，可能会被拒绝详细权限控制可参考[官方文档](https://mp.weixin.qq.com/wiki?t=resource/res_main\u0026id=mp1433401084)。\n\n## 使用命令行\n\n根据企业微信和微信公众平台配置不同 Wechat 提供了的命令行命令。\n\n#### 微信公众平台命令行\n\n```\n$ wechat\nWechat Public Account commands:\n  wechat addvoicetorecofortext [VOICE_ID]                       # AI 开放接口 - 提交语音\n  wechat callbackip                                             # 获取微信服务器 iP 地址\n  wechat clear_quota                                            # 接口调用次数清零\n  wechat custom_image [OPENID, IMAGE_PATH]                      # 发送图片客服消息\n  wechat custom_music [OPENID, THUMBNAIL_PATH, MUSIC_URL]       # 发送音乐客服消息\n  wechat custom_news [OPENID, NEWS_YAML_PATH]                   # 发送图文客服消息\n  wechat custom_text [OPENID, TEXT_MESSAGE]                     # 发送文字客服消息\n  wechat custom_video [OPENID, VIDEO_PATH]                      # 发送视频客服消息\n  wechat custom_voice [OPENID, VOICE_PATH]                      # 发送语音客服消息\n  wechat customservice_getonlinekflist                          # 获取在线客服接待信息\n  wechat group_create [GROUP_NAME]                              # 创建分组\n  wechat group_delete [GROUP_ID]                                # 删除分组\n  wechat group_update [GROUP_ID, NEW_GROUP_NAME]                # 修改分组名\n  wechat groups                                                 # 查询所有分组\n  wechat material_get [MEDIA_ID, PATH]                          # 永久媒体下载\n  wechat material_add [MEDIA_TYPE, PATH]                        # 永久媒体上传\n  wechat material_add_news [MPNEWS_YAML_PATH]                   # 永久图文素材上传\n  wechat material_count                                         # 获取永久素材总数\n  wechat material_delete [MEDIA_ID]                             # 删除永久素材\n  wechat material_list [TYPE, OFFSET, COUNT]                    # 获取永久素材列表\n  wechat media [MEDIA_ID, PATH]                                 # 媒体下载\n  wechat media_hq [MEDIA_ID, PATH]                              # 高清音频下载\n  wechat media_create [MEDIA_TYPE, PATH]                        # 媒体上传\n  wechat media_uploadimg [IMAGE_PATH]                           # 上传图文消息内的图片\n  wechat media_uploadnews [MPNEWS_YAML_PATH]                    # 上传图文消息素材\n  wechat menu                                                   # 当前菜单\n  wechat menu_addconditional [CONDITIONAL_MENU_YAML_PATH]       # 创建个性化菜单\n  wechat menu_create [MENU_YAML_PATH]                           # 创建菜单\n  wechat menu_delconditional [MENU_ID]                          # 删除个性化菜单\n  wechat menu_delete                                            # 删除菜单\n  wechat menu_trymatch [USER_ID]                                # 测试个性化菜单匹配结果\n  wechat message_mass_delete [MSG_ID]                           # 删除群发消息\n  wechat message_mass_get [MSG_ID]                              # 查询群发消息发送状态\n  wechat message_mass_preview [WX_NAME, MPNEWS_MEDIA_ID]        # 预览图文消息素材\n  wechat qrcode_create_limit_scene [SCENE_ID_OR_STR]            # 请求永久二维码\n  wechat qrcode_create_scene [SCENE_ID_OR_STR, EXPIRE_SECONDS]  # 请求临时二维码\n  wechat qrcode_download [TICKET, QR_CODE_PIC_PATH]             # 通过 ticket 下载二维码\n  wechat queryrecoresultfortext [VOICE_ID]                      # AI 开放接口 - 获取语音识别结果\n  wechat shorturl [LONG_URL]                                    # 长链接转短链接\n  wechat tag [TAGID]                                            # 获取标签下粉丝列表\n  wechat tag_add_user [TAG_ID, OPEN_IDS]                        # 批量为用户打标签\n  wechat tag_create [TAGNAME, TAG_ID]                           # 创建标签\n  wechat tag_del_user [TAG_ID, OPEN_IDS]                        # 批量为用户取消标签\n  wechat tag_delete [TAG_ID]                                    # 删除标签\n  wechat tag_update [TAG_ID, TAGNAME]                           # 更新标签名字\n  wechat tags                                                   # 获取所有标签\n  wechat template_message [OPENID, TEMPLATE_YAML_PATH]          # 模板消息接口\n  wechat translatecontent [CONTENT]                             # AI 开放接口 - 微信翻译\n  wechat user [OPEN_ID]                                         # 获取用户基本信息\n  wechat user_batchget [OPEN_ID_LIST]                           # 批量获取用户基本信息\n  wechat user_change_group [OPEN_ID, TO_GROUP_ID]               # 移动用户分组\n  wechat user_group [OPEN_ID]                                   # 查询用户所在分组\n  wechat user_update_remark [OPEN_ID, REMARK]                   # 设置备注名\n  wechat users                                                  # 关注者列表\n  wechat wxa_msg_sec_check [CONTENT]                            # 检查一段文本是否含有违法违规内容。\n  wechat wxacode_download [WXA_CODE_PIC_PATH, PATH, WIDTH]      # 下载小程序码\n```\n\n#### 企业微信命令行\n```\n$ wechat\nWechat Enterprise Account commands:\n  wechat agent [AGENT_ID]                                  # 获取企业号应用详情\n  wechat agent_list                                        # 获取应用概况列表\n  wechat batch_job_result [JOB_ID]                         # 获取异步任务结果\n  wechat batch_replaceparty [BATCH_PARTY_CSV_MEDIA_ID]     # 全量覆盖部门\n  wechat batch_replaceuser [BATCH_USER_CSV_MEDIA_ID]       # 全量覆盖成员\n  wechat batch_syncuser [SYNC_USER_CSV_MEDIA_ID]           # 增量更新成员\n  wechat callbackip                                        # 获取微信服务器 iP 地址\n  wechat clear_quota                                       # 接口调用次数清零\n  wechat convert_to_openid [USER_ID]                       # userid 转换成 openid\n  wechat convert_to_userid [OPENID]                        # openid 转换成 userid\n  wechat custom_image [OPENID, IMAGE_PATH]                 # 发送图片客服消息\n  wechat custom_music [OPENID, THUMBNAIL_PATH, MUSIC_URL]  # 发送音乐客服消息\n  wechat custom_news [OPENID, NEWS_YAML_PATH]              # 发送图文客服消息\n  wechat custom_text [OPENID, TEXT_MESSAGE]                # 发送文字客服消息\n  wechat custom_video [OPENID, VIDEO_PATH]                 # 发送视频客服消息\n  wechat custom_voice [OPENID, VOICE_PATH]                 # 发送语音客服消息\n  wechat department [DEPARTMENT_ID]                        # 获取部门列表\n  wechat department_create [NAME, PARENT_ID]               # 创建部门\n  wechat department_delete [DEPARTMENT_ID]                 # 删除部门\n  wechat department_update [DEPARTMENT_ID, NAME]           # 更新部门\n  wechat getusercumulate [BEGIN_DATE, END_DATE]            # 获取累计用户数据\n  wechat getusersummary [BEGIN_DATE, END_DATE]             # 获取用户增减数据\n  wechat invite_user [USER_ID]                             # 邀请成员关注\n  wechat material [MEDIA_ID, PATH]                         # 永久媒体下载\n  wechat material_add [MEDIA_TYPE, PATH]                   # 永久媒体上传\n  wechat material_count                                    # 获取永久素材总数\n  wechat material_delete [MEDIA_ID]                        # 删除永久素材\n  wechat material_list [TYPE, OFFSET, COUNT]               # 获取永久素材列表\n  wechat media [MEDIA_ID, PATH]                            # 媒体下载\n  wechat media_create [MEDIA_TYPE, PATH]                   # 媒体上传\n  wechat media_hq [MEDIA_ID, PATH]                         # 高清音频媒体下载\n  wechat media_uploadimg [IMAGE_PATH]                      # 上传图文消息内的图片\n  wechat menu                                              # 当前菜单\n  wechat menu_addconditional [CONDITIONAL_MENU_YAML_PATH]  # 创建个性化菜单\n  wechat menu_create [MENU_YAML_PATH]                      # 创建菜单\n  wechat menu_delconditional [MENU_ID]                     # 删除个性化菜单\n  wechat menu_delete                                       # 删除菜单\n  wechat menu_trymatch [USER_ID]                           # 测试个性化菜单匹配结果\n  wechat message_send [OPENID, TEXT_MESSAGE]               # 发送文字消息\n  wechat qrcode_download [TICKET, QR_CODE_PIC_PATH]        # 通过 ticket 下载二维码\n  wechat tag [TAG_ID]                                      # 获取标签成员\n  wechat tag_add_department [TAG_ID, PARTY_IDS]            # 增加标签部门\n  wechat tag_add_user [TAG_ID, USER_IDS]                   # 增加标签成员\n  wechat tag_create [TAGNAME, TAG_ID]                      # 创建标签\n  wechat tag_del_department [TAG_ID, PARTY_IDS]            # 删除标签部门\n  wechat tag_del_user [TAG_ID, USER_IDS]                   # 删除标签成员\n  wechat tag_delete [TAG_ID]                               # 删除标签\n  wechat tag_update [TAG_ID, TAGNAME]                      # 更新标签名字\n  wechat tags                                              # 获取所有标签\n  wechat template_message [OPENID, TEMPLATE_YAML_PATH]     # 模板消息接口\n  wechat upload_replaceparty [BATCH_PARTY_CSV_PATH]        # 上传文件方式全量覆盖部门\n  wechat upload_replaceuser [BATCH_USER_CSV_PATH]          # 上传文件方式全量覆盖成员\n  wechat user [OPEN_ID]                                    # 获取用户基本信息\n  wechat user_batchdelete [USER_ID_LIST]                   # 批量删除成员\n  wechat user_create [USER_ID, NAME]                       # 创建成员\n  wechat user_delete [USER_ID]                             # 删除成员\n  wechat user_list [DEPARTMENT_ID]                         # 获取部门成员详情\n  wechat user_simplelist [DEPARTMENT_ID]                   # 获取部门成员\n  wechat user_update_remark [OPEN_ID, REMARK]              # 设置备注名\n```\n\n注意：replaceparty 全量覆盖部门只支持单个根节点作为部门，不支持平行多根节点。\n\n### 使用场景\n以下是几种典型场景的使用方法\n\n##### 获取所有用户的 openid\n\n```\n$ wechat users\n\n{\"total\"=\u003e4, \"count\"=\u003e4, \"data\"=\u003e{\"openid\"=\u003e[\"oCfEht9***********\", \"oCfEhtwqa***********\", \"oCfEht9oMCqGo***********\", \"oCfEht_81H5o2***********\"]}, \"next_openid\"=\u003e\"oCfEht_81H5o2***********\"}\n\n```\n\n##### 获取用户的信息\n\n```\n$ wechat user \"oCfEht9***********\"\n\n{\"subscribe\"=\u003e1, \"openid\"=\u003e\"oCfEht9***********\", \"nickname\"=\u003e\"Nickname\", \"sex\"=\u003e1, \"language\"=\u003e\"zh_CN\", \"city\"=\u003e\"徐汇\", \"province\"=\u003e\"上海\", \"country\"=\u003e\"中国\", \"headimgurl\"=\u003e\"http://wx.qlogo.cn/mmopen/ajNVdqHZLLBd0SG8NjV3UpXZuiaGGPDcaKHebTKiaTyof*********/0\", \"subscribe_time\"=\u003e1395715239}\n\n```\n\n##### 获取当前菜单\n```\n$ wechat menu\n\n{\"menu\"=\u003e{\"button\"=\u003e[{\"type\"=\u003e\"view\", \"name\"=\u003e\"保护的\", \"url\"=\u003e\"http://***/protected\", \"sub_button\"=\u003e[]}, {\"type\"=\u003e\"view\", \"name\"=\u003e\"公开的\", \"url\"=\u003e\"http://***\", \"sub_button\"=\u003e[]}]}}\n\n```\n\n##### 创建菜单\n\n\n通过运行`rails g wechat:menu`可以生成一个定义菜单内容的 yaml 文件菜单可以包含下列内容：\n\n```\nbutton:\n -\n  name: \"我要\"\n  sub_button:\n   -\n    type: \"scancode_waitmsg\"\n    name: \"绑定用餐二维码\"\n    key: \"BINDING_QR_CODE\"\n   -\n    type: \"click\"\n    name: \"预订午餐\"\n    key:  \"BOOK_LUNCH\"\n   -\n    type: \"miniprogram\"\n    name: \"小程序示例\"\n    url:  \"http://ericguo.com/\"\n    appid: \"wx1234567890\"\n    pagepath: \"pages/index\"\n -\n  name: \"查询\"\n  sub_button:\n   -\n    type: \"click\"\n    name: \"进出记录\"\n    key:  \"BADGE_IN_OUT\"\n   -\n    type: \"click\"\n    name: \"年假余额\"\n    key:  \"ANNUAL_LEAVE\"\n -\n  type: \"view\"\n  name: \"关于\"\n  url:  \"http://blog.cloud-mes.com/\"\n```\n\n下列命令行将上传自定义菜单：\n\n```\n$ wechat menu_create menu.yaml\n```\n\n需确保设置，权限管理中有对此应用的管理权限，否则会报 [60011](http://qydev.weixin.qq.com/wiki/index.php?title=%E5%85%A8%E5%B1%80%E8%BF%94%E5%9B%9E%E7%A0%81%E8%AF%B4%E6%98%8E) 错。\n\n##### 发送客服图文消息\n需定义一个图文消息内容的 yaml 文件，比如\narticles.yaml\n\n```\narticles:\n -\n  title: \"习近平在布鲁日欧洲学院演讲\"\n  description: \"新华网比利时布鲁日 4 月 1 日电 国家主席习近平 1 日在比利时布鲁日欧洲学院发表重要演讲\"\n  url: \"http://news.sina.com.cn/c/2014-04-01/232629843387.shtml\"\n  pic_url: \"http://i3.sinaimg.cn/dy/c/2014-04-01/1396366518_bYays1.jpg\"\n```\n\n然后执行命令行\n\n```\n$ wechat custom_news oCfEht9oM*********** articles.yml\n\n```\n\n##### 发送模板消息\n需定义一个模板消息内容的 yaml 文件，比如\ntemplate.yml\n\n```\ntemplate:\n  template_id: \"o64KQ62_xxxxxxxxxxxxxxx-Qz-MlNcRKteq8\"\n  url: \"http://weixin.qq.com/download\"\n  topcolor: \"#FF0000\"\n  data:\n    first:\n      value: \"您好，您已报名成功\"\n      color: \"#0A0A0A\"\n    keynote1:\n      value: \"XX 活动\"\n      color: \"#CCCCCC\"\n    keynote2:\n      value: \"2014 年 9 月 16 日\"\n      color: \"#CCCCCC\"\n    keynote3:\n      value: \"上海徐家汇 xxx 城\"\n      color: \"#CCCCCC\"\n    remark:\n      value: \"欢迎再次使用。\"\n      color: \"#173177\"\n\n```\n\n然后执行命令行\n\n```\n$ wechat template_message oCfEht9oM*********** template.yml\n```\n\n在代码中可以这样使用：\n\n```ruby\ntemplate = YAML.load(File.read(template_yaml_path))\nWechat.api.template_message_send Wechat::Message.to(openid).template(template['template'])\n```\n\n若在 Controller 中使用 wechat_api 或者 wechat_responder，可以使用 wechat：\n\n```ruby\ntemplate = YAML.load(File.read(template_yaml_path))\nwechat.template_message_send Wechat::Message.to(openid).template(template['template'])\n```\n\n## wechat_api - Rails Controller Wechat API\n\n虽然用户可以随时通过 `Wechat.api` 在任意代码中访问 Wechat 的 API 功能，但是更推荐的做法是仅在 Controller 中，通过引入 `wechat_api`，使用 `wechat` 调用 API 功能，不仅因为这样是支持多个微信公众平台账号的必然要求，而且也避免了在模型层内过多引入微信相关代码。\n\n```ruby\nclass WechatReportsController \u003c ApplicationController\n  wechat_api\n  layout 'wechat'\n\n  def index\n    @lots = Lot.with_preloading.wip_lot\n  end\nend\n```\n\n## 在 ActiveJob/Rake tasks 中调用有 wechat api\n\n可以通过 `Wechat.api` 在任意地方使用 Wechat Api 的功能。\n\n下面以通过 `rails console` 调用微信 Ai 开放接口的语音识别为例：\n\n```bash\n# Audio file with ID3 version 2.4.0, contains:MPEG ADTS, layer III, v2,  40 kbps, 16 kHz, Monaural\ntest_voice_file='test_voice.mp3'\nWechat.api.addvoicetorecofortext('test_voice_id', File.open(test_voice_file))\nWechat.api.queryrecoresultfortext 'test_voice_id'\n```\n\n## wechat_responder - Rails Responder Controller DSL\n\n为了在 Rails App 中响应用户的消息，开发者需要创建一个 wechat responder Controller 首先在 Router 中定义\n\n```ruby\n  resource :wechat, only:[:show, :create]\n```\n\n然后创建 Controller Class 例如\n\n```ruby\nclass WechatsController \u003c ActionController::Base\n  wechat_responder\n\n  # 默认文字信息 responder\n  on :text do |request, content|\n    request.reply.text \"echo: #{content}\" #Just echo\n  end\n\n  # 当请求的文字信息内容为 'help' 时，使用这个 responder 处理\n  on :text, with: 'help' do |request|\n    request.reply.text 'help content' #回复帮助信息\n  end\n\n  # 当请求的文字信息内容为'\u003cn\u003e条新闻'时，使用这个 responder 处理，并将 n 作为第二个参数\n  on :text, with: /^(\\d+) 条新闻$/ do |request, count|\n    # 微信最多显示 8 条新闻，大于 8 条将只取前 8 条\n    news = (1..count.to_i).each_with_object([]) { |n, memo| memo \u003c\u003c { title: '新闻标题', content: \"第#{n}条新闻的内容#{n.hash}\" } }\n    request.reply.news(news) do |article, n, index| # 回复\"articles\"\n      article.item title: \"#{index} #{n[:title]}\", description: n[:content], pic_url: 'http://www.baidu.com/img/bdlogo.gif', url: 'http://www.baidu.com/'\n    end\n  end\n\n  # 当用户加关注\n  on :event, with: 'subscribe' do |request|\n    request.reply.text \"User #{request[:FromUserName]} subscribe now\"\n  end\n\n  # 公众平台收到未关注用户扫描 qrscene_xxxxxx 二维码时。注意此次扫描事件将不再引发上条的用户加关注事件\n  on :scan, with: 'qrscene_xxxxxx' do |request, ticket|\n    request.reply.text \"Unsubscribe user #{request[:FromUserName]} Ticket #{ticket}\"\n  end\n\n  # 公众平台收到已关注用户扫描创建二维码的 scene_id 事件时\n  on :scan, with: 'scene_id' do |request, ticket|\n    request.reply.text \"Subscribe user #{request[:FromUserName]} Ticket #{ticket}\"\n  end\n\n  # 当没有任何 on :scan 事件处理已关注用户扫描的 scene_id 时\n  on :event, with: 'scan' do |request|\n    if request[:EventKey].present?\n      request.reply.text \"event scan got EventKey #{request[:EventKey]} Ticket #{request[:Ticket]}\"\n    end\n  end\n\n  # 企业微信收到 EventKey 为二维码扫描结果事件时\n  on :scan, with: 'BINDING_QR_CODE' do |request, scan_result, scan_type|\n    request.reply.text \"User #{request[:FromUserName]} ScanResult #{scan_result} ScanType #{scan_type}\"\n  end\n\n  # 企业微信收到 EventKey 为 CODE 39 码扫描结果事件时\n  on :scan, with: 'BINDING_BARCODE' do |message, scan_result|\n    if scan_result.start_with? 'CODE_39,'\n      message.reply.text \"User: #{message[:FromUserName]} scan barcode, result is #{scan_result.split(',')[1]}\"\n    end\n  end\n\n  # 当用户点击菜单时\n  on :click, with: 'BOOK_LUNCH' do |request, key|\n    request.reply.text \"User: #{request[:FromUserName]} click #{key}\"\n  end\n\n  # 当用户点击菜单时\n  on :view, with: 'http://wechat.somewhere.com/view_url' do |request, view|\n    request.reply.text \"#{request[:FromUserName]} view #{view}\"\n  end\n\n  # 处理图片信息\n  on :image do |request|\n    request.reply.image(request[:MediaId]) #直接将图片返回给用户\n  end\n\n  # 处理语音信息\n  on :voice do |request|\n    # 直接语音音返回给用户\n    # request.reply.voice(request[:MediaId])\n\n    voice_id = request[:MediaId]\n    # 开通语音识别后，用户每次发送语音给服务号时，微信会在推送的语音消息 XML 数据包中，增加一个 Recognition 字段\n    recognition = request[:Recognition]\n    request.reply.text \"#{voice_id} #{recognition}\"\n  end\n\n  # 处理视频信息\n  on :video do |request|\n    nickname = wechat.user(request[:FromUserName])['nickname'] #调用 api 获得发送者的 nickname\n    request.reply.video(request[:MediaId], title: '回声', description: \"#{nickname}发来的视频请求\") #直接视频返回给用户\n  end\n\n  # 处理地理位置消息\n  on :label_location do |request|\n    request.reply.text(\"Label: #{request[:Label]} Location_X: #{request[:Location_X]} Location_Y: #{request[:Location_Y]} Scale: #{request[:Scale]}\")\n  end\n\n  # 处理上报地理位置事件\n  on :location do |request|\n    request.reply.text(\"Latitude: #{request[:Latitude]} Longitude: #{request[:Longitude]} Precision: #{request[:Precision]}\")\n  end\n\n  # 当用户取消关注订阅\n  on :event, with: 'unsubscribe' do |request|\n    request.reply.success # user can not receive this message\n  end\n\n  # 成员进入应用的事件推送\n  on :event, with: 'enter_agent' do |request|\n    request.reply.text \"#{request[:FromUserName]} enter agent app now\"\n  end\n\n  # 当异步任务增量更新成员完成时推送\n  on :batch_job, with: 'sync_user' do |request, batch_job|\n    request.reply.text \"job #{batch_job[:JobId]} finished, return code #{batch_job[:ErrCode]}, return message #{batch_job[:ErrMsg]}\"\n  end\n\n  # 当异步任务全量覆盖成员完成时推送\n  on :batch_job, with: 'replace_user' do |request, batch_job|\n    request.reply.text \"job #{batch_job[:JobId]} finished, return code #{batch_job[:ErrCode]}, return message #{batch_job[:ErrMsg]}\"\n  end\n\n  # 当异步任务邀请成员关注完成时推送\n  on :batch_job, with: 'invite_user' do |request, batch_job|\n    request.reply.text \"job #{batch_job[:JobId]} finished, return code #{batch_job[:ErrCode]}, return message #{batch_job[:ErrMsg]}\"\n  end\n\n  # 当异步任务全量覆盖部门完成时推送\n  on :batch_job, with: 'replace_party' do |request, batch_job|\n    request.reply.text \"job #{batch_job[:JobId]} finished, return code #{batch_job[:ErrCode]}, return message #{batch_job[:ErrMsg]}\"\n  end\n\n  # 事件推送群发结果\n  on :event, with: 'masssendjobfinish' do |request|\n    # https://mp.weixin.qq.com/wiki?action=doc\u0026id=mp1481187827_i0l21\u0026t=0.03571905015619936#8\n    request.reply.success # request is XML result hash.\n  end\n\n  # 客户同意进行聊天内容存档事件回调\n  on :change_external_contact do |request|\n    # https://open.work.weixin.qq.com/api/doc/90000/90135/92005\n    request.reply.success # request is XML result hash.\n  end\n\n  # 会话事件回调\n  on :msgaudit_notify do |request|\n    # https://open.work.weixin.qq.com/api/doc/90000/90135/95039\n    request.reply.success # request is XML result hash.\n  end\n\n  # 当无任何 responder 处理用户信息时，使用这个 responder 处理\n  on :fallback, respond: 'fallback message'\nend\n```\n\n在 controller 中使用 `wechat_responder` 引入 Responder DSL，之后可以用\n\n```\non \u003cmessage_type\u003e do |message|\n message.reply.text \"some text\"\nend\n```\n\n来响应用户信息。\n\n目前支持的 message_type 有如下几种\n\n- :text 响应文字消息，可以用 `:with` 参数来匹配文本内容  `on(:text, with:'help'){|message, content| ...}`\n- :image 响应图片消息\n- :voice 响应语音消息\n- :shortvideo 响应短视频消息\n- :video 响应视频消息\n- :label_location 响应地理位置消息\n- :link 响应链接消息\n- :event 响应事件消息，可以用 `:with` 参数来匹配事件类型，同文字消息类似，支持正则表达式匹配\n- :click 虚拟响应事件消息，微信传入:event，但 gem 内部会单独处理\n- :view 虚拟响应事件消息，微信传入:event，但 gem 内部会单独处理\n- :scan  虚拟响应事件消息\n- :batch_job  虚拟响应事件消息\n- :location 虚拟响应上报地理位置事件消息\n- :fallback 默认响应，当收到的消息无法被其他 responder 响应时，会使用这个 responder.\n\n### 多客服消息转发\n\n```ruby\nclass WechatsController \u003c ActionController::Base\n  # 当无任何 responder 处理用户信息时，转发至客服处理。\n  on :fallback do |message|\n    message.reply.transfer_customer_service\n  end\nend\n```\n\n注意设置了[多客服消息转发](http://dkf.qq.com/)后，不能再添加默认文字信息 `responder`，否则文字消息将得不到转发。\n\n\n### 无法处理给一个默认回答，一周内不要重复\n\n```ruby\nclass WechatsController \u003c ActionController::Base\n  on :fallback do |message|\n    Rails.logger.debug \"YouApp_ToUserName: #{message.message_hash[\"FromUserName\"]}\"\n    session = WechatSession.find_by openid: message.message_hash[\"FromUserName\"]\n    return message.reply.success if session.present? \u0026\u0026 session.greating_time.present? \u0026\u0026 session.greating_time \u003e= 1.week.ago\n\n    images_path = Rails.root.join(\"public\", \"images\", \"default_help.jpg\")\n    media_id = Wechat.api.media_create(\"image\", images_path)[\"media_id\"]\n    if session.present? \u0026\u0026 session.reload\n      # generate migration: add_column :wechat_sessions, :greating_time, :datetime\n      session.update(greating_time: Time.current)\n    end\n    message.reply.image(media_id)\n  end\nend\n```\n\n### 通知\n\n现支持以下通知：\n\n* `wechat.responder.after_create` data 包含 request \u003cWechat::Message\u003e 和 response\u003cWechat::Message\u003e\n\n使用示例：\n\n```ruby\nActiveSupport::Notifications.subscribe('wechat.responder.after_create') do |name, started, finished, unique_id, data|\n  WechatLog.create request: data[:request], response: data[:response]\nend\n```\n\n## 已知问题\n\n* 企业微信接受菜单消息时，Wechat 腾讯服务器无法解析部分域名，请使用 iP 绑定回调 URL，用户的普通消息目前不受影响。\n* 企业微信全量覆盖成员使用的 CSV 通讯录格式，直接将下载的模板导入[是不工作的](http://qydev.weixin.qq.com/qa/index.php?qa=13978)，必须使用 Excel 打开，然后另存为 CSV 格式才会变成合法格式。\n* 如果使用 Nginx + Unicron 部署方案，并且使用了 Https，必须设置 `trusted_domain_fullname` 为 Https，否则会导致 JS-SDK 签名失效。\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FEric-Guo%2Fwechat","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FEric-Guo%2Fwechat","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FEric-Guo%2Fwechat/lists"}