DESKTOP-53URE31\USER 4 mesiacov pred
commit
c01efcc97c
13 zmenil súbory, kde vykonal 816 pridanie a 0 odobranie
  1. 6 0
      .gitignore
  2. 38 0
      common/log.py
  3. 11 0
      config.json
  4. 192 0
      config.py
  5. 186 0
      main.py
  6. 55 0
      main.spec
  7. 200 0
      service/robot.py
  8. 128 0
      service/ui.py
  9. BIN
      wcferry/sdk.dll
  10. BIN
      wcferry/spy.dll
  11. BIN
      wcferry/spy_debug.dll
  12. BIN
      wcferry/spy_dev.dll
  13. BIN
      wechat.ico

+ 6 - 0
.gitignore

@@ -0,0 +1,6 @@
+.idea
+__pycache__
+build
+dist
+logs
+*.log

+ 38 - 0
common/log.py

@@ -0,0 +1,38 @@
+import logging
+import sys
+
+
+def _reset_logger(log):
+    for handler in log.handlers:
+        handler.close()
+        log.removeHandler(handler)
+        del handler
+    log.handlers.clear()
+    log.propagate = False
+    console_handle = logging.StreamHandler(sys.stdout)
+    console_handle.setFormatter(
+        logging.Formatter(
+            "[%(levelname)s][%(asctime)s][%(filename)s:%(lineno)d] - %(message)s",
+            datefmt="%Y-%m-%d %H:%M:%S",
+        )
+    )
+    file_handle = logging.FileHandler("run.log", encoding="utf-8")
+    file_handle.setFormatter(
+        logging.Formatter(
+            "[%(levelname)s][%(asctime)s][%(filename)s:%(lineno)d] - %(message)s",
+            datefmt="%Y-%m-%d %H:%M:%S",
+        )
+    )
+    log.addHandler(file_handle)
+    log.addHandler(console_handle)
+
+
+def _get_logger():
+    log = logging.getLogger("log")
+    _reset_logger(log)
+    log.setLevel(logging.INFO)
+    return log
+
+
+# 日志句柄
+logger = _get_logger()

+ 11 - 0
config.json

@@ -0,0 +1,11 @@
+{
+    "debug": true,
+    "api_base": "https://fastgpt.gkscrm.com/api/v1",
+    "api_key": "fastgpt-j5EzfAPJ9YXjEb30BbeMnzgaTWCTK5m9OcMbjtX6E6HGx4NiR2l8KnTS5",
+    "token": "请填入获得的token",
+    "contacts_white_list": [
+        "ALL",
+        "ALL_GROUPS",
+        "ALL_CONTACTS"
+    ]
+}

+ 192 - 0
config.py

@@ -0,0 +1,192 @@
+# encoding:utf-8
+import argparse
+import json
+import logging
+import os
+import pickle
+
+from common.log import logger
+
+# 将所有可用的配置项写在字典里, 请使用小写字母
+# 此处的配置值无实际意义,程序不会读取此处的配置,仅用于提示格式,请将配置加入到config.json中
+available_setting = {
+    "debug":False,
+    "api_base": "http://fastgpt.ascrm.cn/api/v1",
+    "api_key": "fastgpt-sKABkv3PTHxlFZYPn9Mo35HHsZSdzdFNBH4XeWIRn5CwdkG7aXqEDmXwDwK",
+    "token":"",
+    "open_ai_model": "gpt-4o",
+    "open_ai_temperature": 0.7,
+    "open_ai_max_tokens": 1024,
+    "open_ai_top_p": 1,
+    "open_ai_frequency_penalty": 0,
+    "open_ai_presence_penalty": 0,
+    "open_ai_stop": ["<|im_end|>"],
+    "open_ai_stream": True,
+    "contacts_white_list": [],
+    "appdata_dir":"."
+}
+
+
+class Config(dict):
+    def __init__(self, d=None):
+        super().__init__()
+        if d is None:
+            d = {}
+        for k, v in d.items():
+            self[k] = v
+        # user_datas: 用户数据,key为用户名,value为用户数据,也是dict
+        self.user_datas = {}
+
+    def __getitem__(self, key):
+        if key not in available_setting:
+            raise Exception("key {} not in available_setting".format(key))
+        return super().__getitem__(key)
+
+    def __setitem__(self, key, value):
+        if key not in available_setting:
+            raise Exception("key {} not in available_setting".format(key))
+        return super().__setitem__(key, value)
+
+    def get(self, key, default=None):
+        try:
+            return self[key]
+        except KeyError as e:
+            return default
+        except Exception as e:
+            raise e
+
+    # Make sure to return a dictionary to ensure atomic
+    def get_user_data(self, user) -> dict:
+        if self.user_datas.get(user) is None:
+            self.user_datas[user] = {}
+        return self.user_datas[user]
+
+    def load_user_datas(self):
+        try:
+            with open(os.path.join(get_appdata_dir(), "user_datas.pkl"), "rb") as f:
+                self.user_datas = pickle.load(f)
+                logger.info("[Config] User datas loaded.")
+        except FileNotFoundError as e:
+            logger.info("[Config] User datas file not found, ignore.")
+        except Exception as e:
+            logger.info("[Config] User datas error: {}".format(e))
+            self.user_datas = {}
+
+    def save_user_datas(self):
+        try:
+            with open(os.path.join(get_appdata_dir(), "user_datas.pkl"), "wb") as f:
+                pickle.dump(self.user_datas, f)
+                logger.info("[Config] User datas saved.")
+        except Exception as e:
+            logger.info("[Config] User datas error: {}".format(e))
+
+
+config = Config()
+# parser = argparse.ArgumentParser(description='消息中间处理程序')
+# parser.add_argument('-c', '--config', help='设置配置文件,默认值是 ./config.json')
+
+
+def load_config():
+    global config, parser
+
+    # args = parser.parse_args()
+
+    # parser.print_help()
+
+    # if args.config:
+    #     config_path = args.config
+    # else:
+    config_path = "./config.json"
+    if not os.path.exists(config_path):
+        logger.info("配置文件不存在,将使用config-template.json模板")
+        config_path = "./config-template.json"
+
+    config_str = read_file(config_path)
+    logger.debug("[INIT] config str: {}".format(config_str))
+
+    # 将json字符串反序列化为dict类型
+    config = Config(json.loads(config_str))
+
+    # override config with environment variables.
+    # Some online deployment platforms (e.g. Railway) deploy project from github directly. So you shouldn't put your secrets like api key in a config file, instead use environment variables to override the default config.
+    for name, value in os.environ.items():
+        name = name.lower()
+        if name in available_setting:
+            logger.info("[INIT] override config by environ args: {}={}".format(name, value))
+            try:
+                config[name] = eval(value)
+            except:
+                if value == "false":
+                    config[name] = False
+                elif value == "true":
+                    config[name] = True
+                else:
+                    config[name] = value
+
+    if config.get("debug", False):
+        logger.setLevel(logging.DEBUG)
+        logger.debug("[INIT] set log level to DEBUG")
+
+    logger.info("[INIT] load config: {}".format(config))
+
+    # config.load_user_datas()
+    #
+    # logger.info("[INIT] load user datas: {}".format(config.get_user_data("api_base")))
+
+
+
+
+def get_root():
+    return os.path.dirname(os.path.abspath(__file__))
+
+
+def read_file(path):
+    with open(path, mode="r", encoding="utf-8") as f:
+        return f.read()
+
+
+def conf():
+    return config
+
+
+def get_appdata_dir():
+    data_path = os.path.join(get_root(), conf().get("appdata_dir", ""))
+    if not os.path.exists(data_path):
+        logger.info("[INIT] data path not exists, create it: {}".format(data_path))
+        os.makedirs(data_path)
+    return data_path
+
+
+def subscribe_msg():
+    trigger_prefix = conf().get("single_chat_prefix", [""])[0]
+    msg = conf().get("subscribe_msg", "")
+    return msg.format(trigger_prefix=trigger_prefix)
+
+
+# global plugin config
+plugin_config = {}
+
+
+def write_plugin_config(pconf: dict):
+    """
+    写入插件全局配置
+    :param pconf: 全量插件配置
+    """
+    global plugin_config
+    for k in pconf:
+        plugin_config[k.lower()] = pconf[k]
+
+
+def pconf(plugin_name: str) -> dict:
+    """
+    根据插件名称获取配置
+    :param plugin_name: 插件名称
+    :return: 该插件的配置项
+    """
+    return plugin_config.get(plugin_name.lower())
+
+
+# 全局配置,用于存放全局生效的状态
+global_config = {
+    "admin_users": []
+}

+ 186 - 0
main.py

@@ -0,0 +1,186 @@
+import hashlib
+import json
+import signal
+import time
+import tkinter
+import uuid
+from time import sleep
+from tkinter import messagebox
+
+import requests
+
+from service.ui import WinGUI
+from wcferry import Wcf
+
+from config import load_config, conf
+from service.robot import Robot
+
+
+
+class Win(WinGUI):
+    def __init__(self):
+        super().__init__()
+        self.__event_bind()
+        self.__style_config()
+        self.robot = None
+
+    def __event_bind(self):
+        self.tk_button_save.bind('<Button-1>', self.save_event)
+        self.tk_button_start.bind('<Button-1>', self.start_event)
+        self.tk_button_pause.bind('<Button-1>', self.stop_event)
+        self.tk_button_version.bind('<Button-1>', self.version_event)
+        pass
+
+    def __style_config(self):
+        pass
+
+    def version_event(self, event):
+        messagebox.showinfo('版本信息', '当前版本:v1.0.0')
+
+    def stop_event(self, event):
+
+        if self.tk_button_pause.cget('state') == tkinter.DISABLED:
+            return
+
+        self.robot.wcf.cleanup()
+        self.robot = None
+        self.tk_button_start.config(state=tkinter.NORMAL)
+        self.tk_button_pause.config(state=tkinter.DISABLED)
+        messagebox.showinfo('提示', '助手已停止运行!')
+
+    @staticmethod
+    def check_token():
+        # 获取当前网卡mac地址
+        mac = uuid.getnode()
+        url = "https://wxadminapi.gkscrm.com/wechat-api/token/check"
+
+        token = str(conf().get("token"))
+
+        payload = json.dumps({
+            "token": token,
+            "mac": str(mac)
+        })
+
+
+        headers = {
+            'Content-Type': 'application/json'
+        }
+
+        response = requests.request("POST", url, headers=headers, data=payload)
+
+
+        if response.status_code == 200:
+            resp = json.loads(response.text)
+
+            if resp['valid'] == True:
+                sign = resp['sign']
+                timestamp = resp['timestamp']
+                # 获取当前unix时间戳
+                current_timestamp = int(time.time())
+
+                if abs(current_timestamp - int(timestamp)) > 180:
+                    messagebox.showerror('错误', '验证失败!请重试')
+                    return False
+                # 创建一个MD5哈希对象
+                md5_hash = hashlib.md5()
+                # 更新哈希对象
+                md5_hash.update(token.encode('utf-8'))
+                # 获取十六进制格式的MD5哈希值
+                md5_token = md5_hash.hexdigest()
+
+                md5_hash = hashlib.md5()
+                md5_hash.update(str(mac).encode('utf-8'))
+                md5_mac = md5_hash.hexdigest()
+
+
+                md5_hash = hashlib.md5()
+                md5_hash.update(str(timestamp).encode('utf-8'))
+                md5_timestamp = md5_hash.hexdigest()
+
+                md5_hash = hashlib.md5()
+                md5_hash.update((md5_token+md5_mac+md5_timestamp).encode('utf-8'))
+                md5_sign = md5_hash.hexdigest()
+
+
+                if md5_sign != sign:
+                    messagebox.showerror('错误', 'token验证失败!')
+                    return False
+
+                return True
+
+            else:
+                messagebox.showerror('错误', 'token已失效 或 与当前设备未绑定,每个token只能绑定一台设备!请填入和当前设备绑定的token,或者获取新的token!')
+                return False
+        else:
+            messagebox.showerror('错误', '您的网络状态异常!请稍候重试')
+            return False
+
+    def start_event(self, event):
+
+        if self.tk_button_start.cget('state') == tkinter.DISABLED:
+            return
+
+        if not self.check_token():
+            return
+
+        wcf = Wcf(debug=conf().get("debug", False))
+        def handler(sig, frame):
+            wcf.cleanup()  # 退出前清理环境
+            exit(0)
+        signal.signal(signal.SIGINT, handler)
+
+        self.robot = Robot(conf(), wcf)
+
+        # 接收消息
+        # robot.enableRecvMsg()     # 可能会丢消息?
+        self.robot.enableReceivingMsg()  # 加队列
+
+        if self.robot.wcf.is_login():
+            self.tk_button_start.config(state='disabled')
+            self.tk_button_pause.config(state='normal')
+            messagebox.showinfo('提示', '助手开始运行!')
+
+        # robot.keepRunningAndBlockProcess()
+
+    def save_event(self, event):
+        conf().update({
+            "api_base":self.tk_input_api_base.get(),
+            "api_key":self.tk_input_api_key.get(),
+            "token":self.tk_input_token.get()
+        })
+        # 将字典写入 JSON 文件
+        with open('config.json', 'w') as json_file:
+            json.dump(conf(), json_file, indent=4)
+
+        messagebox.showinfo('提示', '保存文件成功!')
+
+def main():
+    load_config()
+
+    win = Win()
+    win.mainloop()
+
+
+
+    # wcf = Wcf(debug=conf().get("debug", False))
+    #
+    # def handler(sig, frame):
+    #     wcf.cleanup()  # 退出前清理环境
+    #     exit(0)
+    #
+    # signal.signal(signal.SIGINT, handler)
+    #
+    # robot = Robot(conf(), wcf)
+    #
+    # # 接收消息
+    # # robot.enableRecvMsg()     # 可能会丢消息?
+    # robot.enableReceivingMsg()  # 加队列
+    #
+    #
+    # robot.keepRunningAndBlockProcess()
+
+
+
+# Press the green button in the gutter to run the script.
+if __name__ == '__main__':
+    main()

+ 55 - 0
main.spec

@@ -0,0 +1,55 @@
+# -*- mode: python ; coding: utf-8 -*-
+
+
+a = Analysis(
+    ['main.py'],
+    pathex=[],
+    binaries=[('config.json','config.json')],
+    datas=[('wcferry', 'wcferry')],
+    hiddenimports=['_cffi_backend'],
+    hookspath=[],
+    hooksconfig={},
+    runtime_hooks=[],
+    excludes=[],
+    noarchive=False,
+    optimize=0,
+)
+pyz = PYZ(a.pure)
+
+exe = EXE(
+    pyz,
+    a.scripts,
+    a.binaries,
+    a.datas,
+    [],
+    name='轻马AI智能助手',
+    debug=False,
+    bootloader_ignore_signals=False,
+    strip=False,
+    upx=True,
+    upx_exclude=[],
+    runtime_tmpdir=None,
+    console=False,
+    disable_windowed_traceback=False,
+    argv_emulation=False,
+    target_arch=None,
+    codesign_identity=None,
+    entitlements_file=None,
+    icon=['wechat.ico'],
+)
+
+import shutil
+import os
+
+def create_logs_directory():
+    logs_path = os.path.join('dist','logs')
+    if not os.path.exists(logs_path):
+        os.makedirs(logs_path)
+
+def copy_file():
+    shutil.copy("./config.json", "./dist/config.json")
+
+# Call the function to create the logs directory
+create_logs_directory()
+
+copy_file()

+ 200 - 0
service/robot.py

@@ -0,0 +1,200 @@
+import json
+import logging
+from queue import Empty
+from threading import Thread
+
+import openai
+from openai import OpenAI, api_key, base_url, AuthenticationError, APIConnectionError, APIError
+from wcferry import Wcf, WxMsg
+
+from common.log import logger
+from config import Config
+
+
+class Robot():
+    def __init__(self, config: Config, wcf: Wcf) -> None:
+        self.wcf = wcf
+        self.config = config
+        self.LOG = logger
+        self.wxid = self.wcf.get_self_wxid()
+        self.user = self.wcf.get_user_info()
+        self.allContacts = self.getAllContacts()
+        self.aiClient = OpenAI(api_key=self.config.get("api_key"), base_url=self.config.get("api_base"))
+
+
+        self.LOG.info(f"{self.user} 登录成功")
+
+    def enableRecvMsg(self) -> None:
+        """
+        打开消息通知,可能会丢消息
+        :return:
+        """
+        self.wcf.enable_recv_msg(self.onMsg)
+
+    def enableReceivingMsg(self) -> None:
+        """
+        打开消息通知,使用消息队列的方式获取
+        :return:
+        """
+        def innerProcessMsg(wcf: Wcf):
+            while wcf.is_receiving_msg():
+                try:
+                    msg = wcf.get_msg()
+                    # self.LOG.info(msg)
+                    # self.processMsg(msg)
+                    self.onMsg(msg)
+                except Empty:
+                    continue  # Empty message
+                except Exception as e:
+                    self.LOG.error(f"Receiving message error: {e}")
+
+        self.wcf.enable_receiving_msg()
+        Thread(target=innerProcessMsg, name="GetMessage", args=(self.wcf,), daemon=True).start()
+
+    def onMsg(self, msg: WxMsg) -> int:
+        """
+        消息处理
+        :param msg:
+        :return:
+        """
+        # 判断 self.config.get("api_base") 是否包含 gkscrm.com 域
+        if "gkscrm.com" not in self.config.get("api_base"):
+            return 0
+
+
+
+        try:
+            self.LOG.info(f"Received message: {msg}") # 打印信息
+            self.processMsg(msg)
+        except Exception as e:
+            self.LOG.error(e)
+
+        return 0
+
+    def processMsg(self, msg: WxMsg) -> None:
+        """当接收到消息的时候,会调用本方法。如果不实现本方法,则打印原始消息。
+        此处可进行自定义发送的内容,如通过 msg.content 关键字自动获取当前天气信息,并发送到对应的群组@发送者
+        群号:msg.roomid  微信ID:msg.sender  消息内容:msg.content
+        content = "xx天气信息为:"
+        receivers = msg.roomid
+        self.sendTextMsg(content, receivers, msg.sender)
+        """
+        rsp = ""
+        if msg.from_self():
+            return
+        elif msg.is_text():
+            if msg.from_group():
+                if "ALL" not in self.config.get("contacts_white_list") and "ALL_GROUPS" not in self.config.get("contacts_white_list") and msg.roomid not in self.config.get("contacts_white_list") and self.allContacts.get(msg.roomid) not in self.config.get("contacts_white_list"):
+                    return
+                if msg.is_at(self.wxid) is False and "@"+self.user.get("name") not in msg.content:
+                    return
+                rsp = self.get_answer(msg)  # 闲聊
+            else:
+                if "ALL" not in self.config.get("contacts_white_list") and "ALL_CONTACTS" not in self.config.get("contacts_white_list") and msg.sender not in self.config.get("contacts_white_list") and self.allContacts.get(msg.sender) not in self.config.get("contacts_white_list"):
+                    return
+                rsp = self.get_answer(msg)  # 闲聊
+
+        if rsp != '':
+            try:
+                json_object = json.loads(rsp)
+                if "type" in json_object[0] and "content" in json_object[0]:
+                    for item in json_object:
+                        if item["content"] == "":
+                            continue
+                        if item["type"] == "TEXT":
+                            if msg.from_group():
+                                self.sendTextMsg(item["content"], msg.roomid, msg.sender)
+                            else:
+                                self.sendTextMsg(item["content"], msg.sender)
+                        elif item["type"] == "IMAGE_URL" or item["type"] == "IMAGE":
+                            if msg.from_group():
+                                self.wcf.send_image(item["content"], msg.roomid)
+                            else:
+                                self.wcf.send_image(item["content"], msg.sender)
+                        elif item["type"] == "FILE" or item["type"] == "FILE_URL" or item["type"] == "VIDEO_URL":
+                            if msg.from_group():
+                                self.wcf.send_file(item["content"], msg.roomid)
+                            else:
+                                self.wcf.send_file(item["content"], msg.sender)
+            except json.JSONDecodeError:
+                if msg.from_group():
+                    self.sendTextMsg(rsp, msg.roomid, msg.sender)
+                else:
+                    self.sendTextMsg(rsp, msg.sender)
+
+    def get_answer(self, msg: WxMsg) -> str:
+        rsp = ""
+        try:
+            self.aiClient.api_key = self.config.get("api_key")
+            self.aiClient.base_url = self.config.get("api_base")
+
+            # 在fastgpt的时候增加chatId字段
+            if "fastgpt" in self.config.get("api_base"):
+                extra_body = {
+                    "chatId": "chatId-"+msg.sender
+                }
+            else:
+                extra_body = {}
+
+            ret = self.aiClient.chat.completions.create(
+                model=self.config.get("open_ai_model", "gpt-3.5-turbo"),
+                max_tokens=self.config.get("open_ai_max_tokens",8192),
+                temperature=self.config.get("open_ai_temperature",0.7),
+                top_p=self.config.get("open_ai_top_p",1),
+                extra_body=extra_body,
+                messages=[
+                    {"role": "user", "content": msg.content}
+                ]
+            )
+            rsp = ret.choices[0].message.content
+            rsp = rsp[2:] if rsp.startswith("\n\n") else rsp
+            rsp = rsp.replace("\n\n", "\n")
+            self.LOG.info(rsp)
+        except AuthenticationError:
+            self.LOG.error("OpenAI API 认证失败,请检查 API 密钥是否正确")
+        except APIConnectionError:
+            self.LOG.error("无法连接到 OpenAI API,请检查网络连接")
+        except APIError as e1:
+            self.LOG.error(f"OpenAI API 返回了错误:{str(e1)}")
+        except Exception as e0:
+            self.LOG.error(f"发生未知错误:{str(e0)}")
+        return rsp
+
+    def sendTextMsg(self, msg: str, receiver: str, at_list: str = "") -> None:
+        """ 发送消息
+        :param msg: 消息字符串
+        :param receiver: 接收人wxid或者群id
+        :param at_list: 要@的wxid, @所有人的wxid为:notify@all
+        """
+        # msg 中需要有 @ 名单中一样数量的 @
+        ats = ""
+        if at_list:
+            if at_list == "notify@all":  # @所有人
+                ats = " @所有人"
+            else:
+                wxids = at_list.split(",")
+                for wxid in wxids:
+                    # 根据 wxid 查找群昵称
+                    ats += f" @{self.wcf.get_alias_in_chatroom(wxid, receiver)}"
+
+        # {msg}{ats} 表示要发送的消息内容后面紧跟@,例如 北京天气情况为:xxx @张三
+        if ats == "":
+            self.LOG.info(f"To {receiver}: {msg}")
+            self.wcf.send_text(f"{msg}", receiver, at_list)
+        else:
+            self.LOG.info(f"To {receiver}: {ats}\r{msg}")
+            self.wcf.send_text(f"{ats}\n\n{msg}", receiver, at_list)
+
+    def getAllContacts(self) -> dict:
+        """
+        获取联系人(包括好友、公众号、服务号、群成员……)
+        格式: {"wxid": "NickName"}
+        """
+        contacts = self.wcf.query_sql("MicroMsg.db", "SELECT UserName, NickName FROM Contact;")
+        return {contact["UserName"]: contact["NickName"] for contact in contacts}
+
+    def keepRunningAndBlockProcess(self) -> None:
+        """
+        保持机器人运行,不让进程退出
+        """
+        self.wcf.keep_running()

+ 128 - 0
service/ui.py

@@ -0,0 +1,128 @@
+import random
+import tkinter
+from tkinter import *
+from tkinter.ttk import *
+
+from config import conf
+
+
+class WinGUI(Tk):
+    def __init__(self):
+        super().__init__()
+        self.__win()
+        self.tk_label_lab_api_base = self.__tk_label_lab_api_base(self)
+        self.tk_label_lab_api_key = self.__tk_label_lab_api_key(self)
+        self.tk_input_api_base = self.__tk_input_api_base(self)
+        self.tk_input_api_key = self.__tk_input_api_key(self)
+        self.tk_input_token = self.__tk_input_token(self)
+        self.tk_label_lab_token = self.__tk_label_lab_token(self)
+        self.tk_button_save = self.__tk_button_save(self)
+        self.tk_button_start = self.__tk_button_start(self)
+        self.tk_button_pause = self.__tk_button_pause(self)
+        self.tk_button_version = self.__tk_button_version(self)
+
+    def __win(self):
+        self.title("轻马私域微信机器人助手")
+        # 设置窗口大小、居中
+        width = 670
+        height = 250
+        screenwidth = self.winfo_screenwidth()
+        screenheight = self.winfo_screenheight()
+        geometry = '%dx%d+%d+%d' % (width, height, (screenwidth - width) / 2, (screenheight - height) / 2)
+        self.geometry(geometry)
+
+        self.resizable(width=False, height=False)
+
+    def scrollbar_autohide(self, vbar, hbar, widget):
+        """自动隐藏滚动条"""
+
+        def show():
+            if vbar: vbar.lift(widget)
+            if hbar: hbar.lift(widget)
+
+        def hide():
+            if vbar: vbar.lower(widget)
+            if hbar: hbar.lower(widget)
+
+        hide()
+        widget.bind("<Enter>", lambda e: show())
+        if vbar: vbar.bind("<Enter>", lambda e: show())
+        if vbar: vbar.bind("<Leave>", lambda e: hide())
+        if hbar: hbar.bind("<Enter>", lambda e: show())
+        if hbar: hbar.bind("<Leave>", lambda e: hide())
+        widget.bind("<Leave>", lambda e: hide())
+
+    def v_scrollbar(self, vbar, widget, x, y, w, h, pw, ph):
+        widget.configure(yscrollcommand=vbar.set)
+        vbar.config(command=widget.yview)
+        vbar.place(relx=(w + x) / pw, rely=y / ph, relheight=h / ph, anchor='ne')
+
+    def h_scrollbar(self, hbar, widget, x, y, w, h, pw, ph):
+        widget.configure(xscrollcommand=hbar.set)
+        hbar.config(command=widget.xview)
+        hbar.place(relx=x / pw, rely=(y + h) / ph, relwidth=w / pw, anchor='sw')
+
+    def create_bar(self, master, widget, is_vbar, is_hbar, x, y, w, h, pw, ph):
+        vbar, hbar = None, None
+        if is_vbar:
+            vbar = Scrollbar(master)
+            self.v_scrollbar(vbar, widget, x, y, w, h, pw, ph)
+        if is_hbar:
+            hbar = Scrollbar(master, orient="horizontal")
+            self.h_scrollbar(hbar, widget, x, y, w, h, pw, ph)
+        self.scrollbar_autohide(vbar, hbar, widget)
+
+    def __tk_label_lab_api_base(self, parent):
+        label = Label(parent, text="API_BASE", anchor="center", )
+        label.place(x=30, y=30, width=60, height=30)
+        return label
+
+    def __tk_label_lab_api_key(self, parent):
+        label = Label(parent, text="API_KEY", anchor="center", )
+        label.place(x=30, y=80, width=60, height=30)
+        return label
+
+    def __tk_input_api_base(self, parent):
+        ipt = Entry(parent, )
+        ipt.insert(0, conf().get("api_base"))
+        ipt.place(x=100, y=30, width=520, height=30)
+        return ipt
+
+    def __tk_input_api_key(self, parent):
+        ipt = Entry(parent, )
+        ipt.insert(0, conf().get("api_key"))
+        ipt.place(x=100, y=80, width=520, height=30)
+        return ipt
+
+    def __tk_input_token(self, parent):
+        ipt = Entry(parent, )
+        ipt.insert(0, conf().get("token"))
+        ipt.place(x=100, y=130, width=520, height=30)
+        return ipt
+
+    def __tk_label_lab_token(self, parent):
+        label = Label(parent, text="TOKEN", anchor="center", )
+        label.place(x=30, y=130, width=60, height=30)
+        return label
+
+    def __tk_button_save(self, parent):
+        btn = Button(parent, text="保存", takefocus=False, )
+        btn.place(x=30, y=190, width=80, height=30)
+        return btn
+
+    def __tk_button_start(self, parent):
+        btn = Button(parent, text="启动", takefocus=False, )
+        btn.place(x=130, y=190, width=80, height=30)
+        return btn
+
+    def __tk_button_pause(self, parent):
+        btn = Button(parent, text="暂停", takefocus=False, state=tkinter.DISABLED )
+        btn.place(x=230, y=190, width=50, height=30)
+        return btn
+
+    def __tk_button_version(self, parent):
+        btn = Button(parent, text="版本", takefocus=False )
+        btn.place(x=530,y=190, width=50, height=30)
+        return btn
+
+

BIN
wcferry/sdk.dll


BIN
wcferry/spy.dll


BIN
wcferry/spy_debug.dll


BIN
wcferry/spy_dev.dll


BIN
wechat.ico