From ed4da64a7a9a8ac5c03d217ebf4d9733fa03a30a Mon Sep 17 00:00:00 2001 From: K2cr2O1 <2221577113@qq.com> Date: Sat, 28 Feb 2026 16:59:52 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(mysql):=20=E6=B7=BB=E5=8A=A0MySQL?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在requirements.txt中添加aiomysql依赖 - 在config.toml中添加MySQL配置块 - 新增MySQLModel配置模型 - 实现MySQLManager单例管理器 - 更新Config类以支持MySQL配置加载 - 在__init__.py中导出mysql_manager - 改进ConfigError异常处理 --- config.toml | 21 ++++- core/config_loader.py | 21 +++-- core/config_models.py | 13 +++ core/managers/__init__.py | 5 ++ core/managers/mysql_manager.py | 148 +++++++++++++++++++++++++++++++++ core/utils/exceptions.py | 7 +- requirements.txt | 1 + 7 files changed, 203 insertions(+), 13 deletions(-) create mode 100644 core/managers/mysql_manager.py diff --git a/config.toml b/config.toml index e7a7d12..d5943dc 100644 --- a/config.toml +++ b/config.toml @@ -21,13 +21,28 @@ permission_denied_message = "权限不足,需要 {permission_name} 权限" # Redis 配置 [redis] # Redis 主机地址 -host = "101.36.126.55" +host = "114.66.61.199" # Redis 端口 -port = 6379 +port = 37080 # Redis 数据库编号 db = 0 # Redis 密码 -password = "redis_5fCmnE" +password = "redis_n7Ke76" + +# MySQL 配置 +[mysql] +# MySQL 主机地址 +host = "114.66.61.199" +# MySQL 端口 +port = 42398 +# MySQL 用户名 +user = "neobot" +# MySQL 密码 +password = "neobot" +# MySQL 数据库名称 +db = "neobot" + + # Docker 配置 [docker] diff --git a/core/config_loader.py b/core/config_loader.py index 1e3450a..8910756 100644 --- a/core/config_loader.py +++ b/core/config_loader.py @@ -7,7 +7,7 @@ from pathlib import Path import tomllib from pydantic import ValidationError -from .config_models import ConfigModel, NapCatWSModel, BotModel, RedisModel, DockerModel, ImageManagerModel +from .config_models import ConfigModel, NapCatWSModel, BotModel, RedisModel, DockerModel, ImageManagerModel, MySQLModel from .utils.logger import ModuleLogger from .utils.exceptions import ConfigError, ConfigNotFoundError, ConfigValidationError @@ -59,9 +59,9 @@ class Config: error_details.append(error_msg) validation_error = ConfigValidationError( - message="配置验证失败", - original_error=e + message="配置验证失败" ) + validation_error.original_error = e self.logger.error("配置验证失败,请检查 `config.toml` 文件中的以下错误:") for detail in error_details: @@ -71,17 +71,17 @@ class Config: raise validation_error except tomllib.TOMLDecodeError as e: error = ConfigError( - message=f"TOML解析错误: {str(e)}", - original_error=e + message=f"TOML解析错误: {str(e)}" ) + error.original_error = e self.logger.error(f"加载配置文件时发生TOML解析错误: {error.message}") self.logger.log_custom_exception(error) raise error except Exception as e: error = ConfigError( - message=f"加载配置文件时发生未知错误: {str(e)}", - original_error=e + message=f"加载配置文件时发生未知错误: {str(e)}" ) + error.original_error = e self.logger.exception(f"加载配置文件时发生未知错误: {error.message}") self.logger.log_custom_exception(error) raise error @@ -107,6 +107,13 @@ class Config: 获取 Redis 配置 """ return self._model.redis + + @property + def mysql(self) -> MySQLModel: + """ + 获取 MySQL 配置 + """ + return self._model.mysql @property def docker(self) -> DockerModel: diff --git a/core/config_models.py b/core/config_models.py index 1b9bb2a..13c0d7b 100644 --- a/core/config_models.py +++ b/core/config_models.py @@ -36,6 +36,18 @@ class RedisModel(BaseModel): password: str +class MySQLModel(BaseModel): + """ + 对应 `config.toml` 中的 `[mysql]` 配置块。 + """ + host: str + port: int + user: str + password: str + db: str + charset: str = "utf8mb4" + + class DockerModel(BaseModel): """ 对应 `config.toml` 中的 `[docker]` 配置块。 @@ -64,6 +76,7 @@ class ConfigModel(BaseModel): napcat_ws: NapCatWSModel bot: BotModel redis: RedisModel + mysql: MySQLModel docker: DockerModel image_manager: ImageManagerModel diff --git a/core/managers/__init__.py b/core/managers/__init__.py index a30c6f5..a221ee3 100644 --- a/core/managers/__init__.py +++ b/core/managers/__init__.py @@ -8,6 +8,7 @@ from .command_manager import matcher as command_manager from .permission_manager import PermissionManager from .plugin_manager import PluginManager from .redis_manager import RedisManager +from .mysql_manager import MySQLManager from .browser_manager import BrowserManager from .image_manager import ImageManager @@ -26,6 +27,9 @@ plugin_manager = PluginManager(command_manager) # Redis 管理器 redis_manager = RedisManager() +# MySQL 管理器 +mysql_manager = MySQLManager() + # 浏览器管理器 browser_manager = BrowserManager() @@ -38,6 +42,7 @@ __all__ = [ "matcher", "plugin_manager", "redis_manager", + "mysql_manager", "browser_manager", "image_manager", ] diff --git a/core/managers/mysql_manager.py b/core/managers/mysql_manager.py new file mode 100644 index 0000000..136ac4b --- /dev/null +++ b/core/managers/mysql_manager.py @@ -0,0 +1,148 @@ +import aiomysql +from ..config_loader import global_config as config +from ..utils.logger import logger +from ..utils.singleton import Singleton + + +class MySQLManager(Singleton): + """ + MySQL 数据库连接管理器(异步单例) + """ + _pool = None + + def __init__(self): + """ + 初始化 MySQL 管理器 + """ + super().__init__() + + async def initialize(self): + """ + 异步初始化 MySQL 连接池并进行健康检查 + """ + if self._pool is None: + try: + mysql_config = config.mysql + host = mysql_config.host + port = mysql_config.port + user = mysql_config.user + password = mysql_config.password + db = mysql_config.db + charset = mysql_config.charset + + logger.info(f"正在尝试连接 MySQL: {host}:{port}, DB: {db}") + + self._pool = await aiomysql.create_pool( + host=host, + port=port, + user=user, + password=password, + db=db, + charset=charset, + autocommit=False, + maxsize=10, + minsize=1 + ) + + async with self._pool.acquire() as conn: + async with conn.cursor() as cur: + await cur.execute("SELECT 1") + result = await cur.fetchone() + if result and result[0] == 1: + logger.success("MySQL 连接成功!") + else: + logger.error("MySQL 连接失败: 健康检查失败") + except Exception as e: + logger.exception(f"MySQL 初始化时发生未知错误: {e}") + self._pool = None + + @property + def pool(self): + """ + 获取 MySQL 连接池实例 + """ + if self._pool is None: + raise ConnectionError("MySQL 未初始化或连接失败,请先调用 initialize()") + return self._pool + + async def execute(self, sql: str, args: tuple = None): + """ + 执行 SQL 语句(用于 INSERT、UPDATE、DELETE) + + Args: + sql: SQL 语句 + args: 参数元组 + + Returns: + 影响的行数 + """ + async with self._pool.acquire() as conn: + async with conn.cursor() as cur: + await cur.execute(sql, args) + await conn.commit() + return cur.rowcount + + async def fetchone(self, sql: str, args: tuple = None): + """ + 查询单条记录 + + Args: + sql: SQL 语句 + args: 参数元组 + + Returns: + 单条记录字典 + """ + async with self._pool.acquire() as conn: + async with conn.cursor(aiomysql.DictCursor) as cur: + await cur.execute(sql, args) + return await cur.fetchone() + + async def fetchall(self, sql: str, args: tuple = None): + """ + 查询多条记录 + + Args: + sql: SQL 语句 + args: 参数元组 + + Returns: + 记录列表 + """ + async with self._pool.acquire() as conn: + async with conn.cursor(aiomysql.DictCursor) as cur: + await cur.execute(sql, args) + return await cur.fetchall() + + async def begin_transaction(self): + """ + 开始事务 + + Returns: + 事务连接对象 + """ + conn = await self._pool.acquire() + return conn + + async def commit_transaction(self, conn): + """ + 提交事务 + + Args: + conn: 事务连接对象 + """ + await conn.commit() + await self._pool.release(conn) + + async def rollback_transaction(self, conn): + """ + 回滚事务 + + Args: + conn: 事务连接对象 + """ + await conn.rollback() + await self._pool.release(conn) + + +mysql_manager = MySQLManager() diff --git a/core/utils/exceptions.py b/core/utils/exceptions.py index acaf404..417b006 100644 --- a/core/utils/exceptions.py +++ b/core/utils/exceptions.py @@ -83,14 +83,15 @@ class ConfigError(Exception): 配置相关错误的基类。 Args: - section: 配置部分名称 - key: 配置项名称 - message: 错误消息 + section: 配置部分名称(可选) + key: 配置项名称(可选) + message: 错误消息(可选) """ def __init__(self, section=None, key=None, message=None): self.section = section self.key = key self.message = message + self.original_error = None if section and key and message: super().__init__(f"配置错误 [{section}.{key}]: {message}") diff --git a/requirements.txt b/requirements.txt index 18ad662..3e72413 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ aiohappyeyeballs==2.6.1 aiohttp==3.13.3 +aiomysql==0.2.0 aiosignal==1.4.0 annotated-types==0.7.0 anyio==4.12.1 From 5f0c837536de97d3d63d04f681f7521447d4eb51 Mon Sep 17 00:00:00 2001 From: baby-2016 <2185823427@qq.com> Date: Sat, 28 Feb 2026 20:57:00 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat(plugins):=20=E8=A1=A5=E5=85=A8?= =?UTF-8?q?=E5=9F=8E=E5=B8=82=E4=BB=A3=E7=A0=81=E6=98=A0=E5=B0=84=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/resource/city_code.py | 2573 +++++++++++++++++++++++++++++++++ plugins/weather.py | 42 +- 2 files changed, 2574 insertions(+), 41 deletions(-) create mode 100644 plugins/resource/city_code.py diff --git a/plugins/resource/city_code.py b/plugins/resource/city_code.py new file mode 100644 index 0000000..89932b3 --- /dev/null +++ b/plugins/resource/city_code.py @@ -0,0 +1,2573 @@ +CITY_CODES = { +"北京": "101010100", +"海淀": "101010200", +"朝阳": "101010300", +"顺义": "101010400", +"怀柔": "101010500", +"通州": "101010600", +"昌平": "101010700", +"延庆": "101010800", +"丰台": "101010900", +"石景山": "101011000", +"大兴": "101011100", +"房山": "101011200", +"密云": "101011300", +"门头沟": "101011400", +"平谷": "101011500", +"八达岭": "101011600", +"佛爷顶": "101011700", +"汤河口": "101011800", +"密云上甸子": "101011900", +"斋堂": "101012000", +"霞云岭": "101012100", +"上海": "101020100", +"闵行": "101020200", +"宝山": "101020300", +"川沙": "101020400", +"嘉定": "101020500", +"南汇": "101020600", +"金山": "101020700", +"青浦": "101020800", +"松江": "101020900", +"奉贤": "101021000", +"崇明": "101021100", +"陈家镇": "101021101", +"引水船": "101021102", +"徐家汇": "101021200", +"浦东": "101021300", +"天津": "101030100", +"武清": "101030200", +"宝坻": "101030300", +"东丽": "101030400", +"西青": "101030500", +"北辰": "101030600", +"宁河": "101030700", +"汉沽": "101030800", +"静海": "101030900", +"津南": "101031000", +"塘沽": "101031100", +"大港": "101031200", +"平台": "101031300", +"蓟县": "101031400", +"重庆": "101040100", +"永川": "101040200", +"合川": "101040300", +"南川": "101040400", +"江津": "101040500", +"万盛": "101040600", +"渝北": "101040700", +"北碚": "101040800", +"巴南": "101040900", +"长寿": "101041000", +"黔江": "101041100", +"万州天城": "101041200", +"万州龙宝": "101041300", +"涪陵": "101041400", +"开县": "101041500", +"城口": "101041600", +"云阳": "101041700", +"巫溪": "101041800", +"奉节": "101041900", +"巫山": "101042000", +"潼南": "101042100", +"垫江": "101042200", +"梁平": "101042300", +"忠县": "101042400", +"石柱": "101042500", +"大足": "101042600", +"荣昌": "101042700", +"铜梁": "101042800", +"璧山": "101042900", +"丰都": "101043000", +"武隆": "101043100", +"彭水": "101043200", +"綦江": "101043300", +"酉阳": "101043400", +"金佛山": "101043500", +"秀山": "101043600", +"沙坪坝": "101043700", +"哈尔滨": "101050101", +"双城": "101050102", +"呼兰": "101050103", +"阿城": "101050104", +"宾县": "101050105", +"依兰": "101050106", +"巴彦": "101050107", +"通河": "101050108", +"方正": "101050109", +"延寿": "101050110", +"尚志": "101050111", +"五常": "101050112", +"木兰": "101050113", +"齐齐哈尔": "101050201", +"讷河": "101050202", +"龙江": "101050203", +"甘南": "101050204", +"富裕": "101050205", +"依安": "101050206", +"拜泉": "101050207", +"克山": "101050208", +"克东": "101050209", +"泰来": "101050210", +"牡丹江": "101050301", +"海林": "101050302", +"穆棱": "101050303", +"林口": "101050304", +"绥芬河": "101050305", +"宁安": "101050306", +"东宁": "101050307", +"佳木斯": "101050401", +"汤原": "101050402", +"抚远": "101050403", +"桦川": "101050404", +"桦南": "101050405", +"同江": "101050406", +"富锦": "101050407", +"绥化": "101050501", +"肇东": "101050502", +"安达": "101050503", +"海伦": "101050504", +"明水": "101050505", +"望奎": "101050506", +"兰西": "101050507", +"青冈": "101050508", +"庆安": "101050509", +"绥棱": "101050510", +"黑河": "101050601", +"嫩江": "101050602", +"孙吴": "101050603", +"逊克": "101050604", +"五大连池": "101050605", +"北安": "101050606", +"大兴安岭": "101050701", +"塔河": "101050702", +"漠河": "101050703", +"呼玛": "101050704", +"呼中": "101050705", +"新林": "101050706", +"阿木尔": "101050707", +"加格达奇": "101050708", +"伊春": "101050801", +"乌伊岭": "101050802", +"五营": "101050803", +"铁力": "101050804", +"嘉荫": "101050805", +"大庆": "101050901", +"林甸": "101050902", +"肇州": "101050903", +"肇源": "101050904", +"杜蒙": "101050905", +"七台河": "101051002", +"勃利": "101051003", +"鸡西": "101051101", +"虎林": "101051102", +"密山": "101051103", +"鸡东": "101051104", +"鹤岗": "101051201", +"绥滨": "101051202", +"萝北": "101051203", +"双鸭山": "101051301", +"集贤": "101051302", +"宝清": "101051303", +"饶河": "101051304", +"长春": "101060101", +"农安": "101060102", +"德惠": "101060103", +"九台": "101060104", +"榆树": "101060105", +"双阳": "101060106", +"吉林": "101060201", +"舒兰": "101060202", +"永吉": "101060203", +"蛟河": "101060204", +"磐石": "101060205", +"桦甸": "101060206", +"烟筒山": "101060207", +"延吉": "101060301", +"敦化": "101060302", +"安图": "101060303", +"汪清": "101060304", +"和龙": "101060305", +"天池": "101060306", +"龙井": "101060307", +"珲春": "101060308", +"图们": "101060309", +"罗子沟": "101060311", +"延边": "101060312", +"四平": "101060401", +"双辽": "101060402", +"梨树": "101060403", +"公主岭": "101060404", +"伊通": "101060405", +"孤家子": "101060406", +"通化": "101060501", +"梅河口": "101060502", +"柳河": "101060503", +"辉南": "101060504", +"集安": "101060505", +"通化县": "101060506", +"白城": "101060601", +"洮南": "101060602", +"大安": "101060603", +"镇赉": "101060604", +"通榆": "101060605", +"辽源": "101060701", +"东丰": "101060702", +"松原": "101060801", +"乾安": "101060802", +"前郭": "101060803", +"长岭": "101060804", +"扶余": "101060805", +"白山": "101060901", +"靖宇": "101060902", +"临江": "101060903", +"东岗": "101060904", +"长白": "101060905", +"沈阳": "101070101", +"苏家屯": "101070102", +"辽中": "101070103", +"康平": "101070104", +"法库": "101070105", +"新民": "101070106", +"于洪": "101070107", +"新城子": "101070108", +"大连": "101070201", +"瓦房店": "101070202", +"金州": "101070203", +"普兰店": "101070204", +"旅顺": "101070205", +"长海": "101070206", +"庄河": "101070207", +"皮口": "101070208", +"海洋岛": "101070209", +"鞍山": "101070301", +"台安": "101070302", +"岫岩": "101070303", +"海城": "101070304", +"抚顺": "101070401", +"清原": "101070403", +"章党": "101070404", +"本溪": "101070501", +"本溪县": "101070502", +"草河口": "101070503", +"桓仁": "101070504", +"丹东": "101070601", +"凤城": "101070602", +"宽甸": "101070603", +"东港": "101070604", +"东沟": "101070605", +"锦州": "101070701", +"凌海": "101070702", +"北宁": "101070703", +"义县": "101070704", +"黑山": "101070705", +"北镇": "101070706", +"营口": "101070801", +"大石桥": "101070802", +"盖州": "101070803", +"阜新": "101070901", +"彰武": "101070902", +"辽阳": "101071001", +"辽阳县": "101071002", +"灯塔": "101071003", +"铁岭": "101071101", +"开原": "101071102", +"昌图": "101071103", +"西丰": "101071104", +"建平": "101071202", +"凌源": "101071203", +"喀左": "101071204", +"北票": "101071205", +"羊山": "101071206", +"建平县": "101071207", +"盘锦": "101071301", +"大洼": "101071302", +"盘山": "101071303", +"葫芦岛": "101071401", +"建昌": "101071402", +"绥中": "101071403", +"兴城": "101071404", +"呼和浩特": "101080101", +"土默特左旗": "101080102", +"托克托": "101080103", +"和林格尔": "101080104", +"清水河": "101080105", +"呼和浩特市郊区": "101080106", +"武川": "101080107", +"包头": "101080201", +"白云鄂博": "101080202", +"满都拉": "101080203", +"土默特右旗": "101080204", +"固阳": "101080205", +"达尔罕茂明安联合旗": "101080206", +"石拐": "101080207", +"乌海": "101080301", +"集宁": "101080401", +"卓资": "101080402", +"化德": "101080403", +"商都": "101080404", +"希拉穆仁": "101080405", +"兴和": "101080406", +"凉城": "101080407", +"察哈尔右翼前旗": "101080408", +"察哈尔右翼中旗": "101080409", +"察哈尔右翼后旗": "101080410", +"四子王旗": "101080411", +"丰镇": "101080412", +"通辽": "101080501", +"舍伯吐": "101080502", +"科尔沁左翼中旗": "101080503", +"科尔沁左翼后旗": "101080504", +"青龙山": "101080505", +"开鲁": "101080506", +"库伦旗": "101080507", +"奈曼旗": "101080508", +"扎鲁特旗": "101080509", +"高力板": "101080510", +"巴雅尔吐胡硕": "101080511", +"通辽钱家店": "101080512", +"赤峰": "101080601", +"赤峰郊区站": "101080602", +"阿鲁科尔沁旗": "101080603", +"浩尔吐": "101080604", +"巴林左旗": "101080605", +"巴林右旗": "101080606", +"林西": "101080607", +"克什克腾旗": "101080608", +"翁牛特旗": "101080609", +"岗子": "101080610", +"喀喇沁旗": "101080611", +"八里罕": "101080612", +"宁城": "101080613", +"敖汉旗": "101080614", +"宝过图": "101080615", +"鄂尔多斯": "101080701", +"达拉特旗": "101080703", +"准格尔旗": "101080704", +"鄂托克前旗": "101080705", +"河南": "101080706", +"伊克乌素": "101080707", +"鄂托克旗": "101080708", +"杭锦旗": "101080709", +"乌审旗": "101080710", +"伊金霍洛旗": "101080711", +"乌审召": "101080712", +"东胜": "101080713", +"临河": "101080801", +"五原": "101080802", +"磴口": "101080803", +"乌拉特前旗": "101080804", +"大佘太": "101080805", +"乌拉特中旗": "101080806", +"乌拉特后旗": "101080807", +"海力素": "101080808", +"那仁宝力格": "101080809", +"杭锦后旗": "101080810", +"巴盟农试站": "101080811", +"锡林浩特": "101080901", +"朝克乌拉": "101080902", +"二连浩特": "101080903", +"阿巴嘎旗": "101080904", +"伊和郭勒": "101080905", +"苏尼特左旗": "101080906", +"苏尼特右旗": "101080907", +"朱日和": "101080908", +"东乌珠穆沁旗": "101080909", +"西乌珠穆沁旗": "101080910", +"太仆寺旗": "101080911", +"镶黄旗": "101080912", +"正镶白旗": "101080913", +"正兰旗": "101080914", +"多伦": "101080915", +"博克图": "101080916", +"乌拉盖": "101080917", +"白日乌拉": "101080918", +"那日图": "101080919", +"呼伦贝尔": "101081000", +"海拉尔": "101081001", +"小二沟": "101081002", +"阿荣旗": "101081003", +"莫力达瓦旗": "101081004", +"鄂伦春旗": "101081005", +"鄂温克旗": "101081006", +"陈巴尔虎旗": "101081007", +"新巴尔虎左旗": "101081008", +"新巴尔虎右旗": "101081009", +"满洲里": "101081010", +"牙克石": "101081011", +"扎兰屯": "101081012", +"额尔古纳": "101081014", +"根河": "101081015", +"图里河": "101081016", +"乌兰浩特": "101081101", +"阿尔山": "101081102", +"科尔沁右翼中旗": "101081103", +"胡尔勒": "101081104", +"扎赉特旗": "101081105", +"索伦": "101081106", +"突泉": "101081107", +"霍林郭勒": "101081108", +"阿拉善左旗": "101081201", +"阿拉善右旗": "101081202", +"额济纳旗": "101081203", +"拐子湖": "101081204", +"吉兰太": "101081205", +"锡林高勒": "101081206", +"头道湖": "101081207", +"中泉子": "101081208", +"巴彦诺尔贡": "101081209", +"雅布赖": "101081210", +"乌斯太": "101081211", +"孪井滩": "101081212", +"石家庄": "101090101", +"井陉": "101090102", +"正定": "101090103", +"栾城": "101090104", +"行唐": "101090105", +"灵寿": "101090106", +"高邑": "101090107", +"深泽": "101090108", +"赞皇": "101090109", +"无极": "101090110", +"平山": "101090111", +"元氏": "101090112", +"赵县": "101090113", +"辛集": "101090114", +"藁城": "101090115", +"晋洲": "101090116", +"新乐": "101090117", +"保定": "101090201", +"满城": "101090202", +"阜平": "101090203", +"徐水": "101090204", +"唐县": "101090205", +"高阳": "101090206", +"容城": "101090207", +"紫荆关": "101090208", +"涞源": "101090209", +"望都": "101090210", +"安新": "101090211", +"易县": "101090212", +"涞水": "101090213", +"曲阳": "101090214", +"蠡县": "101090215", +"顺平": "101090216", +"雄县": "101090217", +"涿州": "101090218", +"定州": "101090219", +"安国": "101090220", +"高碑店": "101090221", +"张家口": "101090301", +"宣化": "101090302", +"张北": "101090303", +"康保": "101090304", +"沽源": "101090305", +"尚义": "101090306", +"蔚县": "101090307", +"阳原": "101090308", +"怀安": "101090309", +"万全": "101090310", +"怀来": "101090311", +"涿鹿": "101090312", +"赤城": "101090313", +"崇礼": "101090314", +"承德": "101090402", +"承德县": "101090403", +"兴隆": "101090404", +"平泉": "101090405", +"滦平": "101090406", +"隆化": "101090407", +"丰宁": "101090408", +"宽城": "101090409", +"围场": "101090410", +"塞罕坎": "101090411", +"唐山": "101090501", +"丰南": "101090502", +"丰润": "101090503", +"滦县": "101090504", +"滦南": "101090505", +"乐亭": "101090506", +"迁西": "101090507", +"玉田": "101090508", +"唐海": "101090509", +"遵化": "101090510", +"迁安": "101090511", +"廊坊": "101090601", +"固安": "101090602", +"永清": "101090603", +"香河": "101090604", +"大城": "101090605", +"文安": "101090606", +"大厂": "101090607", +"霸州": "101090608", +"三河": "101090609", +"沧州": "101090701", +"青县": "101090702", +"东光": "101090703", +"海兴": "101090704", +"盐山": "101090705", +"肃宁": "101090706", +"南皮": "101090707", +"吴桥": "101090708", +"献县": "101090709", +"孟村": "101090710", +"泊头": "101090711", +"任丘": "101090712", +"黄骅": "101090713", +"河间": "101090714", +"曹妃甸": "101090715", +"衡水": "101090801", +"枣强": "101090802", +"武邑": "101090803", +"武强": "101090804", +"饶阳": "101090805", +"安平": "101090806", +"故城": "101090807", +"景县": "101090808", +"阜城": "101090809", +"冀州": "101090810", +"深州": "101090811", +"邢台": "101090901", +"临城": "101090902", +"邢台县浆水": "101090903", +"内邱": "101090904", +"柏乡": "101090905", +"隆尧": "101090906", +"南和": "101090907", +"宁晋": "101090908", +"巨鹿": "101090909", +"新河": "101090910", +"广宗": "101090911", +"平乡": "101090912", +"威县": "101090913", +"清河": "101090914", +"临西": "101090915", +"南宫": "101090916", +"沙河": "101090917", +"任县": "101090918", +"邯郸": "101091001", +"峰峰": "101091002", +"临漳": "101091003", +"成安": "101091004", +"大名": "101091005", +"涉县": "101091006", +"磁县": "101091007", +"肥乡": "101091008", +"永年": "101091009", +"邱县": "101091010", +"鸡泽": "101091011", +"广平": "101091012", +"馆陶": "101091013", +"魏县": "101091014", +"曲周": "101091015", +"武安": "101091016", +"秦皇岛": "101091101", +"青龙": "101091102", +"昌黎": "101091103", +"抚宁": "101091104", +"卢龙": "101091105", +"北戴河": "101091106", +"太原": "101100101", +"清徐": "101100102", +"阳曲": "101100103", +"娄烦": "101100104", +"太原古交区": "101100105", +"太原北郊": "101100106", +"太原南郊": "101100107", +"大同": "101100201", +"阳高": "101100202", +"大同县": "101100203", +"天镇": "101100204", +"广灵": "101100205", +"灵邱": "101100206", +"浑源": "101100207", +"左云": "101100208", +"阳泉": "101100301", +"盂县": "101100302", +"平定": "101100303", +"晋中": "101100401", +"榆次": "101100402", +"榆社": "101100403", +"左权": "101100404", +"和顺": "101100405", +"昔阳": "101100406", +"寿阳": "101100407", +"太谷": "101100408", +"祁县": "101100409", +"平遥": "101100410", +"灵石": "101100411", +"介休": "101100412", +"长治": "101100501", +"黎城": "101100502", +"屯留": "101100503", +"潞城": "101100504", +"襄垣": "101100505", +"平顺": "101100506", +"武乡": "101100507", +"沁县": "101100508", +"长子": "101100509", +"沁源": "101100510", +"壶关": "101100511", +"晋城": "101100601", +"沁水": "101100602", +"阳城": "101100603", +"陵川": "101100604", +"高平": "101100605", +"临汾": "101100701", +"曲沃": "101100702", +"永和": "101100703", +"隰县": "101100704", +"大宁": "101100705", +"吉县": "101100706", +"襄汾": "101100707", +"蒲县": "101100708", +"汾西": "101100709", +"洪洞": "101100710", +"霍州": "101100711", +"乡宁": "101100712", +"翼城": "101100713", +"侯马": "101100714", +"浮山": "101100715", +"安泽": "101100716", +"古县": "101100717", +"运城": "101100801", +"临猗": "101100802", +"稷山": "101100803", +"万荣": "101100804", +"河津": "101100805", +"新绛": "101100806", +"绛县": "101100807", +"闻喜": "101100808", +"垣曲": "101100809", +"永济": "101100810", +"芮城": "101100811", +"夏县": "101100812", +"平陆": "101100813", +"朔州": "101100901", +"平鲁": "101100902", +"山阴": "101100903", +"右玉": "101100904", +"应县": "101100905", +"怀仁": "101100906", +"忻州": "101101001", +"定襄": "101101002", +"五台县豆村": "101101003", +"河曲": "101101004", +"偏关": "101101005", +"神池": "101101006", +"宁武": "101101007", +"代县": "101101008", +"繁峙": "101101009", +"五台山": "101101010", +"保德": "101101011", +"静乐": "101101012", +"岢岚": "101101013", +"五寨": "101101014", +"原平": "101101015", +"吕梁": "101101100", +"离石": "101101101", +"临县": "101101102", +"兴县": "101101103", +"岚县": "101101104", +"柳林": "101101105", +"石楼": "101101106", +"方山": "101101107", +"交口": "101101108", +"中阳": "101101109", +"孝义": "101101110", +"汾阳": "101101111", +"文水": "101101112", +"交城": "101101113", +"西安": "101110101", +"长安": "101110102", +"临潼": "101110103", +"蓝田": "101110104", +"周至": "101110105", +"户县": "101110106", +"高陵": "101110107", +"杨凌": "101110108", +"咸阳": "101110200", +"三原": "101110201", +"礼泉": "101110202", +"永寿": "101110203", +"淳化": "101110204", +"泾阳": "101110205", +"武功": "101110206", +"乾县": "101110207", +"彬县": "101110208", +"长武": "101110209", +"旬邑": "101110210", +"兴平": "101110211", +"延安": "101110300", +"延长": "101110301", +"延川": "101110302", +"子长": "101110303", +"宜川": "101110304", +"富县": "101110305", +"志丹": "101110306", +"安塞": "101110307", +"甘泉": "101110308", +"洛川": "101110309", +"黄陵": "101110310", +"黄龙": "101110311", +"吴起": "101110312", +"榆林": "101110401", +"府谷": "101110402", +"神木": "101110403", +"佳县": "101110404", +"定边": "101110405", +"靖边": "101110406", +"横山": "101110407", +"米脂": "101110408", +"子洲": "101110409", +"绥德": "101110410", +"吴堡": "101110411", +"清涧": "101110412", +"渭南": "101110501", +"华县": "101110502", +"潼关": "101110503", +"大荔": "101110504", +"白水": "101110505", +"富平": "101110506", +"蒲城": "101110507", +"澄城": "101110508", +"合阳": "101110509", +"韩城": "101110510", +"华阴": "101110511", +"华山": "101110512", +"商洛": "101110601", +"洛南": "101110602", +"柞水": "101110603", +"镇安": "101110605", +"丹凤": "101110606", +"商南": "101110607", +"山阳": "101110608", +"安康": "101110701", +"紫阳": "101110702", +"石泉": "101110703", +"汉阴": "101110704", +"旬阳": "101110705", +"岚皋": "101110706", +"平利": "101110707", +"白河": "101110708", +"镇坪": "101110709", +"宁陕": "101110710", +"汉中": "101110801", +"略阳": "101110802", +"勉县": "101110803", +"留坝": "101110804", +"洋县": "101110805", +"城固": "101110806", +"西乡": "101110807", +"佛坪": "101110808", +"宁强": "101110809", +"南郑": "101110810", +"镇巴": "101110811", +"宝鸡": "101110901", +"宝鸡县": "101110902", +"千阳": "101110903", +"麟游": "101110904", +"岐山": "101110905", +"凤翔": "101110906", +"扶风": "101110907", +"眉县": "101110908", +"太白": "101110909", +"凤县": "101110910", +"陇县": "101110911", +"铜川": "101111001", +"耀县": "101111002", +"宜君": "101111003", +"济南": "101120101", +"长清": "101120102", +"商河": "101120103", +"章丘": "101120104", +"平阴": "101120105", +"济阳": "101120106", +"青岛": "101120201", +"崂山": "101120202", +"潮连岛": "101120203", +"即墨": "101120204", +"胶州": "101120205", +"胶南": "101120206", +"莱西": "101120207", +"平度": "101120208", +"淄博": "101120301", +"淄川": "101120302", +"博山": "101120303", +"高青": "101120304", +"周村": "101120305", +"沂源": "101120306", +"桓台": "101120307", +"临淄": "101120308", +"德州": "101120401", +"武城": "101120402", +"临邑": "101120403", +"陵县": "101120404", +"齐河": "101120405", +"乐陵": "101120406", +"庆云": "101120407", +"平原": "101120408", +"宁津": "101120409", +"夏津": "101120410", +"禹城": "101120411", +"烟台": "101120501", +"莱州": "101120502", +"长岛": "101120503", +"蓬莱": "101120504", +"龙口": "101120505", +"招远": "101120506", +"栖霞": "101120507", +"福山": "101120508", +"牟平": "101120509", +"莱阳": "101120510", +"海阳": "101120511", +"千里岩": "101120512", +"潍坊": "101120601", +"青州": "101120602", +"寿光": "101120603", +"临朐": "101120604", +"昌乐": "101120605", +"昌邑": "101120606", +"安丘": "101120607", +"高密": "101120608", +"诸城": "101120609", +"济宁": "101120701", +"嘉祥": "101120702", +"微山": "101120703", +"鱼台": "101120704", +"兖州": "101120705", +"金乡": "101120706", +"汶上": "101120707", +"泗水": "101120708", +"梁山": "101120709", +"曲阜": "101120710", +"邹城": "101120711", +"泰安": "101120801", +"新泰": "101120802", +"泰山": "101120803", +"肥城": "101120804", +"东平": "101120805", +"宁阳": "101120806", +"临沂": "101120901", +"莒南": "101120902", +"沂南": "101120903", +"苍山": "101120904", +"临沭": "101120905", +"郯城": "101120906", +"蒙阴": "101120907", +"平邑": "101120908", +"费县": "101120909", +"沂水": "101120910", +"马站": "101120911", +"菏泽": "101121001", +"鄄城": "101121002", +"郓城": "101121003", +"东明": "101121004", +"定陶": "101121005", +"巨野": "101121006", +"曹县": "101121007", +"成武": "101121008", +"单县": "101121009", +"滨州": "101121101", +"博兴": "101121102", +"无棣": "101121103", +"阳信": "101121104", +"惠民": "101121105", +"沾化": "101121106", +"邹平": "101121107", +"东营": "101121201", +"河口": "101121202", +"垦利": "101121203", +"利津": "101121204", +"广饶": "101121205", +"威海": "101121301", +"文登": "101121302", +"荣成": "101121303", +"乳山": "101121304", +"成山头": "101121305", +"石岛": "101121306", +"枣庄": "101121401", +"薛城": "101121402", +"峄城": "101121403", +"台儿庄": "101121404", +"滕州": "101121405", +"日照": "101121501", +"五莲": "101121502", +"莒县": "101121503", +"莱芜": "101121601", +"聊城": "101121701", +"冠县": "101121702", +"阳谷": "101121703", +"高唐": "101121704", +"茌平": "101121705", +"东阿": "101121706", +"临清": "101121707", +"朝城": "101121708", +"莘县": "101121709", +"乌鲁木齐": "101130101", +"蔡家湖": "101130102", +"小渠子": "101130103", +"巴仑台": "101130104", +"达坂城": "101130105", +"十三间房气象站": "101130106", +"天山大西沟": "101130107", +"乌鲁木齐牧试站": "101130108", +"白杨沟": "101130110", +"克拉玛依": "101130201", +"石河子": "101130301", +"炮台": "101130302", +"莫索湾": "101130303", +"乌兰乌苏": "101130304", +"昌吉": "101130401", +"呼图壁": "101130402", +"米泉": "101130403", +"阜康": "101130404", +"吉木萨尔": "101130405", +"奇台": "101130406", +"玛纳斯": "101130407", +"木垒": "101130408", +"北塔山": "101130409", +"吐鲁番": "101130501", +"托克逊": "101130502", +"吐鲁番东坎": "101130503", +"鄯善": "101130504", +"红柳河": "101130505", +"库尔勒": "101130601", +"轮台": "101130602", +"尉犁": "101130603", +"若羌": "101130604", +"且末": "101130605", +"和静": "101130606", +"焉耆": "101130607", +"和硕": "101130608", +"库米什": "101130609", +"巴音布鲁克": "101130610", +"铁干里克": "101130611", +"博湖": "101130612", +"塔中": "101130613", +"阿拉尔": "101130701", +"阿克苏": "101130801", +"乌什": "101130802", +"温宿": "101130803", +"拜城": "101130804", +"新和": "101130805", +"沙雅": "101130806", +"库车": "101130807", +"柯坪": "101130808", +"阿瓦提": "101130809", +"喀什": "101130901", +"英吉沙": "101130902", +"塔什库尔干": "101130903", +"麦盖提": "101130904", +"莎车": "101130905", +"叶城": "101130906", +"泽普": "101130907", +"巴楚": "101130908", +"岳普湖": "101130909", +"伽师": "101130910", +"伊宁": "101131001", +"察布查尔": "101131002", +"尼勒克": "101131003", +"伊宁县": "101131004", +"巩留": "101131005", +"新源": "101131006", +"昭苏": "101131007", +"特克斯": "101131008", +"霍城": "101131009", +"霍尔果斯": "101131010", +"塔城": "101131101", +"裕民": "101131102", +"额敏": "101131103", +"和布克赛尔": "101131104", +"托里": "101131105", +"乌苏": "101131106", +"沙湾": "101131107", +"和丰": "101131108", +"哈密": "101131201", +"沁城": "101131202", +"巴里坤": "101131203", +"伊吾": "101131204", +"淖毛湖": "101131205", +"和田": "101131301", +"皮山": "101131302", +"策勒": "101131303", +"墨玉": "101131304", +"洛浦": "101131305", +"民丰": "101131306", +"于田": "101131307", +"阿勒泰": "101131401", +"哈巴河": "101131402", +"一八五团": "101131403", +"黑山头": "101131404", +"吉木乃": "101131405", +"布尔津": "101131406", +"福海": "101131407", +"富蕴": "101131408", +"青河": "101131409", +"安德河": "101131410", +"阿图什": "101131501", +"乌恰": "101131502", +"阿克陶": "101131503", +"阿合奇": "101131504", +"吐尔尕特": "101131505", +"博乐": "101131601", +"温泉": "101131602", +"精河": "101131603", +"阿拉山口": "101131606", +"拉萨": "101140101", +"当雄": "101140102", +"尼木": "101140103", +"墨竹贡卡": "101140104", +"日喀则": "101140201", +"拉孜": "101140202", +"南木林": "101140203", +"聂拉木": "101140204", +"定日": "101140205", +"江孜": "101140206", +"帕里": "101140207", +"山南": "101140301", +"贡嘎": "101140302", +"琼结": "101140303", +"加查": "101140304", +"浪卡子": "101140305", +"错那": "101140306", +"隆子": "101140307", +"泽当": "101140308", +"林芝": "101140401", +"波密": "101140402", +"米林": "101140403", +"察隅": "101140404", +"昌都": "101140501", +"丁青": "101140502", +"类乌齐": "101140503", +"洛隆": "101140504", +"左贡": "101140505", +"芒康": "101140506", +"八宿": "101140507", +"那曲": "101140601", +"嘉黎": "101140603", +"班戈": "101140604", +"安多": "101140605", +"索县": "101140606", +"比如": "101140607", +"阿里": "101140701", +"改则": "101140702", +"申扎": "101140703", +"狮泉河": "101140704", +"普兰": "101140705", +"西宁": "101150101", +"大通": "101150102", +"湟源": "101150103", +"湟中": "101150104", +"铁卜加": "101150105", +"铁卜加寺": "101150106", +"中心站": "101150107", +"海东": "101150201", +"乐都": "101150202", +"民和": "101150203", +"互助": "101150204", +"化隆": "101150205", +"循化": "101150206", +"冷湖": "101150207", +"平安": "101150208", +"黄南": "101150301", +"尖扎": "101150302", +"泽库": "101150303", +"海南": "101150401", +"江西沟": "101150402", +"贵德": "101150404", +"河卡": "101150405", +"兴海": "101150406", +"贵南": "101150407", +"同德": "101150408", +"共和": "101150409", +"果洛": "101150501", +"班玛": "101150502", +"甘德": "101150503", +"达日": "101150504", +"久治": "101150505", +"玛多": "101150506", +"玛沁": "101150508", +"玉树": "101150601", +"托托河": "101150602", +"治多": "101150603", +"杂多": "101150604", +"囊谦": "101150605", +"曲麻莱": "101150606", +"海西": "101150701", +"格尔木": "101150702", +"察尔汉": "101150703", +"野牛沟": "101150704", +"五道梁": "101150705", +"小灶火": "101150706", +"天峻": "101150708", +"乌兰": "101150709", +"都兰": "101150710", +"诺木洪": "101150711", +"茫崖": "101150712", +"大柴旦": "101150713", +"茶卡": "101150714", +"香日德": "101150715", +"德令哈": "101150716", +"海北": "101150801", +"门源": "101150802", +"祁连": "101150803", +"海晏": "101150804", +"托勒": "101150805", +"刚察": "101150806", +"兰州": "101160101", +"皋兰": "101160102", +"永登": "101160103", +"榆中": "101160104", +"定西": "101160201", +"通渭": "101160202", +"陇西": "101160203", +"渭源": "101160204", +"临洮": "101160205", +"漳县": "101160206", +"岷县": "101160207", +"安定": "101160208", +"平凉": "101160301", +"泾川": "101160302", +"灵台": "101160303", +"崇信": "101160304", +"华亭": "101160305", +"庄浪": "101160306", +"静宁": "101160307", +"崆峒": "101160308", +"庆阳": "101160401", +"西峰": "101160402", +"环县": "101160403", +"华池": "101160404", +"合水": "101160405", +"正宁": "101160406", +"宁县": "101160407", +"镇原": "101160408", +"庆城": "101160409", +"武威": "101160501", +"民勤": "101160502", +"古浪": "101160503", +"乌鞘岭": "101160504", +"天祝": "101160505", +"金昌": "101160601", +"永昌": "101160602", +"张掖": "101160701", +"肃南": "101160702", +"民乐": "101160703", +"临泽": "101160704", +"高台": "101160705", +"山丹": "101160706", +"酒泉": "101160801", +"鼎新": "101160802", +"金塔": "101160803", +"马鬃山": "101160804", +"瓜州": "101160805", +"肃北": "101160806", +"玉门镇": "101160807", +"敦煌": "101160808", +"天水": "101160901", +"北道区": "101160902", +"清水": "101160903", +"秦安": "101160904", +"甘谷": "101160905", +"武山": "101160906", +"张家川": "101160907", +"麦积": "101160908", +"武都": "101161001", +"成县": "101161002", +"文县": "101161003", +"宕昌": "101161004", +"康县": "101161005", +"西和": "101161006", +"礼县": "101161007", +"徽县": "101161008", +"两当": "101161009", +"临夏": "101161101", +"康乐": "101161102", +"永靖": "101161103", +"广河": "101161104", +"和政": "101161105", +"东乡": "101161106", +"合作": "101161201", +"临潭": "101161202", +"卓尼": "101161203", +"舟曲": "101161204", +"迭部": "101161205", +"玛曲": "101161206", +"碌曲": "101161207", +"夏河": "101161208", +"白银": "101161301", +"靖远": "101161302", +"会宁": "101161303", +"华家岭": "101161304", +"景泰": "101161305", +"银川": "101170101", +"永宁": "101170102", +"灵武": "101170103", +"贺兰": "101170104", +"石嘴山": "101170201", +"惠农": "101170202", +"平罗": "101170203", +"陶乐": "101170204", +"石炭井": "101170205", +"大武口": "101170206", +"吴忠": "101170301", +"同心": "101170302", +"盐池": "101170303", +"韦州": "101170304", +"麻黄山": "101170305", +"青铜峡": "101170306", +"固原": "101170401", +"西吉": "101170402", +"隆德": "101170403", +"泾源": "101170404", +"六盘山": "101170405", +"彭阳": "101170406", +"中卫": "101170501", +"中宁": "101170502", +"兴仁堡": "101170503", +"海原": "101170504", +"郑州": "101180101", +"巩义": "101180102", +"荥阳": "101180103", +"登封": "101180104", +"新密": "101180105", +"新郑": "101180106", +"中牟": "101180107", +"郑州农试站": "101180108", +"安阳": "101180201", +"汤阴": "101180202", +"滑县": "101180203", +"内黄": "101180204", +"林州": "101180205", +"新乡": "101180301", +"获嘉": "101180302", +"原阳": "101180303", +"辉县": "101180304", +"卫辉": "101180305", +"延津": "101180306", +"封丘": "101180307", +"长垣": "101180308", +"许昌": "101180401", +"鄢陵": "101180402", +"襄城": "101180403", +"长葛": "101180404", +"禹州": "101180405", +"平顶山": "101180501", +"郏县": "101180502", +"宝丰": "101180503", +"汝州": "101180504", +"叶县": "101180505", +"舞钢": "101180506", +"鲁山": "101180507", +"信阳": "101180601", +"息县": "101180602", +"罗山": "101180603", +"光山": "101180604", +"新县": "101180605", +"淮滨": "101180606", +"潢川": "101180607", +"固始": "101180608", +"商城": "101180609", +"鸡公山": "101180610", +"信阳地区农试站": "101180611", +"南阳": "101180701", +"南召": "101180702", +"方城": "101180703", +"社旗": "101180704", +"西峡": "101180705", +"内乡": "101180706", +"镇平": "101180707", +"淅川": "101180708", +"新野": "101180709", +"唐河": "101180710", +"邓州": "101180711", +"桐柏": "101180712", +"开封": "101180801", +"杞县": "101180802", +"尉氏": "101180803", +"通许": "101180804", +"兰考": "101180805", +"洛阳": "101180901", +"新安": "101180902", +"孟津": "101180903", +"宜阳": "101180904", +"洛宁": "101180905", +"伊川": "101180906", +"嵩县": "101180907", +"偃师": "101180908", +"栾川": "101180909", +"汝阳": "101180910", +"商丘": "101181001", +"睢阳区": "101181002", +"睢县": "101181003", +"民权": "101181004", +"虞城": "101181005", +"柘城": "101181006", +"宁陵": "101181007", +"夏邑": "101181008", +"永城": "101181009", +"焦作": "101181101", +"修武": "101181102", +"武陟": "101181103", +"沁阳": "101181104", +"博爱": "101181106", +"温县": "101181107", +"孟州": "101181108", +"鹤壁": "101181201", +"浚县": "101181202", +"淇县": "101181203", +"濮阳": "101181301", +"台前": "101181302", +"南乐": "101181303", +"清丰": "101181304", +"范县": "101181305", +"周口": "101181401", +"扶沟": "101181402", +"太康": "101181403", +"淮阳": "101181404", +"西华": "101181405", +"商水": "101181406", +"项城": "101181407", +"郸城": "101181408", +"鹿邑": "101181409", +"沈丘": "101181410", +"黄泛区": "101181411", +"漯河": "101181501", +"临颍": "101181502", +"舞阳": "101181503", +"驻马店": "101181601", +"西平": "101181602", +"遂平": "101181603", +"上蔡": "101181604", +"汝南": "101181605", +"泌阳": "101181606", +"平舆": "101181607", +"新蔡": "101181608", +"确山": "101181609", +"正阳": "101181610", +"三门峡": "101181701", +"灵宝": "101181702", +"渑池": "101181703", +"卢氏": "101181704", +"济源": "101181801", +"南京": "101190101", +"溧水": "101190102", +"高淳": "101190103", +"江宁": "101190104", +"六合": "101190105", +"江浦": "101190106", +"浦口": "101190107", +"无锡": "101190201", +"江阴": "101190202", +"宜兴": "101190203", +"镇江": "101190301", +"丹阳": "101190302", +"扬中": "101190303", +"句容": "101190304", +"丹徒": "101190305", +"苏州": "101190401", +"常熟": "101190402", +"张家港": "101190403", +"昆山": "101190404", +"吴县东山": "101190405", +"吴县": "101190406", +"吴江": "101190407", +"太仓": "101190408", +"南通": "101190501", +"海安": "101190502", +"如皋": "101190503", +"如东": "101190504", +"吕泗": "101190505", +"吕泗渔场": "101190506", +"启东": "101190507", +"海门": "101190508", +"扬州": "101190601", +"宝应": "101190602", +"仪征": "101190603", +"高邮": "101190604", +"江都": "101190605", +"邗江": "101190606", +"盐城": "101190701", +"响水": "101190702", +"滨海": "101190703", +"阜宁": "101190704", +"射阳": "101190705", +"建湖": "101190706", +"东台": "101190707", +"大丰": "101190708", +"盐都": "101190709", +"徐州": "101190801", +"徐州农试站": "101190802", +"丰县": "101190803", +"沛县": "101190804", +"邳州": "101190805", +"睢宁": "101190806", +"新沂": "101190807", +"淮安": "101190901", +"金湖": "101190902", +"盱眙": "101190903", +"洪泽": "101190904", +"涟水": "101190905", +"淮阴县": "101190906", +"淮阴": "101190907", +"楚州": "101190908", +"连云港": "101191001", +"东海": "101191002", +"赣榆": "101191003", +"灌云": "101191004", +"灌南": "101191005", +"西连岛": "101191006", +"燕尾港": "101191007", +"常州": "101191101", +"溧阳": "101191102", +"金坛": "101191103", +"泰州": "101191201", +"兴化": "101191202", +"泰兴": "101191203", +"姜堰": "101191204", +"靖江": "101191205", +"宿迁": "101191301", +"沭阳": "101191302", +"泗阳": "101191303", +"泗洪": "101191304", +"武汉": "101200101", +"蔡甸": "101200102", +"黄陂": "101200103", +"新洲": "101200104", +"江夏": "101200105", +"襄樊": "101200201", +"襄阳": "101200202", +"保康": "101200203", +"南漳": "101200204", +"宜城": "101200205", +"老河口": "101200206", +"谷城": "101200207", +"枣阳": "101200208", +"鄂州": "101200301", +"孝感": "101200401", +"安陆": "101200402", +"云梦": "101200403", +"大悟": "101200404", +"应城": "101200405", +"汉川": "101200406", +"黄冈": "101200501", +"红安": "101200502", +"麻城": "101200503", +"罗田": "101200504", +"英山": "101200505", +"浠水": "101200506", +"蕲春": "101200507", +"黄梅": "101200508", +"武穴": "101200509", +"黄石": "101200601", +"大冶": "101200602", +"阳新": "101200603", +"咸宁": "101200701", +"赤壁": "101200702", +"嘉鱼": "101200703", +"崇阳": "101200704", +"通城": "101200705", +"通山": "101200706", +"荆州": "101200801", +"江陵": "101200802", +"公安": "101200803", +"石首": "101200804", +"监利": "101200805", +"洪湖": "101200806", +"松滋": "101200807", +"宜昌": "101200901", +"远安": "101200902", +"秭归": "101200903", +"兴山": "101200904", +"宜昌县": "101200905", +"五峰": "101200906", +"当阳": "101200907", +"长阳": "101200908", +"宜都": "101200909", +"枝江": "101200910", +"三峡": "101200911", +"夷陵": "101200912", +"恩施": "101201001", +"利川": "101201002", +"建始": "101201003", +"咸丰": "101201004", +"宣恩": "101201005", +"鹤峰": "101201006", +"来凤": "101201007", +"巴东": "101201008", +"绿葱坡": "101201009", +"十堰": "101201101", +"竹溪": "101201102", +"郧西": "101201103", +"郧县": "101201104", +"竹山": "101201105", +"房县": "101201106", +"丹江口": "101201107", +"神农架": "101201201", +"随州": "101201301", +"广水": "101201302", +"荆门": "101201401", +"钟祥": "101201402", +"京山": "101201403", +"天门": "101201501", +"仙桃": "101201601", +"潜江": "101201701", +"杭州": "101210101", +"萧山": "101210102", +"桐庐": "101210103", +"淳安": "101210104", +"建德": "101210105", +"余杭": "101210106", +"临安": "101210107", +"富阳": "101210108", +"湖州": "101210201", +"长兴": "101210202", +"安吉": "101210203", +"德清": "101210204", +"嘉兴": "101210301", +"嘉善": "101210302", +"海宁": "101210303", +"桐乡": "101210304", +"平湖": "101210305", +"海盐": "101210306", +"宁波": "101210401", +"慈溪": "101210403", +"余姚": "101210404", +"奉化": "101210405", +"象山": "101210406", +"石浦": "101210407", +"宁海": "101210408", +"鄞县": "101210409", +"北仑": "101210410", +"鄞州": "101210411", +"镇海": "101210412", +"绍兴": "101210501", +"诸暨": "101210502", +"上虞": "101210503", +"新昌": "101210504", +"嵊州": "101210505", +"台州": "101210601", +"括苍山": "101210602", +"玉环": "101210603", +"三门": "101210604", +"天台": "101210605", +"仙居": "101210606", +"温岭": "101210607", +"大陈": "101210608", +"洪家": "101210609", +"温州": "101210701", +"泰顺": "101210702", +"文成": "101210703", +"平阳": "101210704", +"瑞安": "101210705", +"洞头": "101210706", +"乐清": "101210707", +"永嘉": "101210708", +"苍南": "101210709", +"丽水": "101210801", +"遂昌": "101210802", +"龙泉": "101210803", +"缙云": "101210804", +"青田": "101210805", +"云和": "101210806", +"庆元": "101210807", +"金华": "101210901", +"浦江": "101210902", +"兰溪": "101210903", +"义乌": "101210904", +"东阳": "101210905", +"武义": "101210906", +"永康": "101210907", +"磐安": "101210908", +"衢州": "101211001", +"常山": "101211002", +"开化": "101211003", +"龙游": "101211004", +"江山": "101211005", +"舟山": "101211101", +"嵊泗": "101211102", +"嵊山": "101211103", +"岱山": "101211104", +"普陀": "101211105", +"定海": "101211106", +"合肥": "101220101", +"长丰": "101220102", +"肥东": "101220103", +"肥西": "101220104", +"蚌埠": "101220201", +"怀远": "101220202", +"固镇": "101220203", +"五河": "101220204", +"芜湖": "101220301", +"繁昌": "101220302", +"芜湖县": "101220303", +"南陵": "101220304", +"淮南": "101220401", +"凤台": "101220402", +"马鞍山": "101220501", +"当涂": "101220502", +"安庆": "101220601", +"枞阳": "101220602", +"太湖": "101220603", +"潜山": "101220604", +"怀宁": "101220605", +"宿松": "101220606", +"望江": "101220607", +"岳西": "101220608", +"桐城": "101220609", +"宿州": "101220701", +"砀山": "101220702", +"灵璧": "101220703", +"泗县": "101220704", +"萧县": "101220705", +"阜阳": "101220801", +"阜南": "101220802", +"颍上": "101220803", +"临泉": "101220804", +"界首": "101220805", +"太和": "101220806", +"亳州": "101220901", +"涡阳": "101220902", +"利辛": "101220903", +"蒙城": "101220904", +"黄山站": "101221001", +"黄山区": "101221002", +"屯溪": "101221003", +"祁门": "101221004", +"黟县": "101221005", +"歙县": "101221006", +"休宁": "101221007", +"黄山市": "101221008", +"滁州": "101221101", +"凤阳": "101221102", +"明光": "101221103", +"定远": "101221104", +"全椒": "101221105", +"来安": "101221106", +"天长": "101221107", +"淮北": "101221201", +"濉溪": "101221202", +"铜陵": "101221301", +"宣城": "101221401", +"泾县": "101221402", +"旌德": "101221403", +"宁国": "101221404", +"绩溪": "101221405", +"广德": "101221406", +"郎溪": "101221407", +"六安": "101221501", +"霍邱": "101221502", +"寿县": "101221503", +"南溪": "101221504", +"金寨": "101221505", +"霍山": "101221506", +"舒城": "101221507", +"巢湖": "101221601", +"庐江": "101221602", +"无为": "101221603", +"含山": "101221604", +"和县": "101221605", +"池州": "101221701", +"东至": "101221702", +"青阳": "101221703", +"九华山": "101221704", +"石台": "101221705", +"福州": "101230101", +"闽清": "101230102", +"闽侯": "101230103", +"罗源": "101230104", +"连江": "101230105", +"马祖": "101230106", +"永泰": "101230107", +"平潭": "101230108", +"福州郊区": "101230109", +"长乐": "101230110", +"福清": "101230111", +"平潭海峡大桥": "101230112", +"厦门": "101230201", +"同安": "101230202", +"宁德": "101230301", +"古田": "101230302", +"霞浦": "101230303", +"寿宁": "101230304", +"周宁": "101230305", +"福安": "101230306", +"柘荣": "101230307", +"福鼎": "101230308", +"屏南": "101230309", +"莆田": "101230401", +"仙游": "101230402", +"秀屿港": "101230403", +"泉州": "101230501", +"安溪": "101230502", +"九仙山": "101230503", +"永春": "101230504", +"德化": "101230505", +"南安": "101230506", +"崇武": "101230507", +"晋江": "101230509", +"漳州": "101230601", +"长泰": "101230602", +"南靖": "101230603", +"平和": "101230604", +"龙海": "101230605", +"漳浦": "101230606", +"诏安": "101230607", +"东山": "101230608", +"云霄": "101230609", +"华安": "101230610", +"龙岩": "101230701", +"长汀": "101230702", +"连城": "101230703", +"武平": "101230704", +"上杭": "101230705", +"永定": "101230706", +"漳平": "101230707", +"三明": "101230801", +"宁化": "101230802", +"清流": "101230803", +"泰宁": "101230804", +"将乐": "101230805", +"建宁": "101230806", +"明溪": "101230807", +"沙县": "101230808", +"尤溪": "101230809", +"永安": "101230810", +"大田": "101230811", +"南平": "101230901", +"顺昌": "101230902", +"光泽": "101230903", +"邵武": "101230904", +"武夷山": "101230905", +"浦城": "101230906", +"建阳": "101230907", +"松溪": "101230908", +"政和": "101230909", +"建瓯": "101230910", +"南昌": "101240101", +"新建": "101240102", +"南昌县": "101240103", +"安义": "101240104", +"进贤": "101240105", +"莲塘": "101240106", +"九江": "101240201", +"瑞昌": "101240202", +"庐山": "101240203", +"武宁": "101240204", +"德安": "101240205", +"永修": "101240206", +"湖口": "101240207", +"彭泽": "101240208", +"星子": "101240209", +"都昌": "101240210", +"棠荫": "101240211", +"修水": "101240212", +"上饶": "101240301", +"鄱阳": "101240302", +"婺源": "101240303", +"康山": "101240304", +"余干": "101240305", +"万年": "101240306", +"德兴": "101240307", +"上饶县": "101240308", +"弋阳": "101240309", +"横峰": "101240310", +"铅山": "101240311", +"玉山": "101240312", +"广丰": "101240313", +"波阳": "101240314", +"抚州": "101240401", +"广昌": "101240402", +"乐安": "101240403", +"崇仁": "101240404", +"金溪": "101240405", +"资溪": "101240406", +"宜黄": "101240407", +"南城": "101240408", +"南丰": "101240409", +"黎川": "101240410", +"宜春": "101240501", +"铜鼓": "101240502", +"宜丰": "101240503", +"万载": "101240504", +"上高": "101240505", +"靖安": "101240506", +"奉新": "101240507", +"高安": "101240508", +"樟树": "101240509", +"丰城": "101240510", +"吉安": "101240601", +"吉安县": "101240602", +"吉水": "101240603", +"新干": "101240604", +"峡江": "101240605", +"永丰": "101240606", +"永新": "101240607", +"井冈山": "101240608", +"万安": "101240609", +"遂川": "101240610", +"泰和": "101240611", +"安福": "101240612", +"宁冈": "101240613", +"赣州": "101240701", +"崇义": "101240702", +"上犹": "101240703", +"南康": "101240704", +"大余": "101240705", +"信丰": "101240706", +"宁都": "101240707", +"石城": "101240708", +"瑞金": "101240709", +"于都": "101240710", +"会昌": "101240711", +"安远": "101240712", +"全南": "101240713", +"龙南": "101240714", +"定南": "101240715", +"寻乌": "101240716", +"兴国": "101240717", +"景德镇": "101240801", +"乐平": "101240802", +"萍乡": "101240901", +"莲花": "101240902", +"新余": "101241001", +"分宜": "101241002", +"鹰潭": "101241101", +"余江": "101241102", +"贵溪": "101241103", +"长沙": "101250101", +"宁乡": "101250102", +"浏阳": "101250103", +"马坡岭": "101250104", +"湘潭": "101250201", +"韶山": "101250202", +"湘乡": "101250203", +"株洲": "101250301", +"攸县": "101250302", +"醴陵": "101250303", +"株洲县": "101250304", +"茶陵": "101250305", +"炎陵": "101250306", +"衡阳": "101250401", +"衡山": "101250402", +"衡东": "101250403", +"祁东": "101250404", +"衡阳县": "101250405", +"常宁": "101250406", +"衡南": "101250407", +"耒阳": "101250408", +"南岳": "101250409", +"郴州": "101250501", +"桂阳": "101250502", +"嘉禾": "101250503", +"宜章": "101250504", +"临武": "101250505", +"桥口": "101250506", +"资兴": "101250507", +"汝城": "101250508", +"安仁": "101250509", +"永兴": "101250510", +"桂东": "101250511", +"常德": "101250601", +"安乡": "101250602", +"桃源": "101250603", +"汉寿": "101250604", +"澧县": "101250605", +"临澧": "101250606", +"石门": "101250607", +"益阳": "101250700", +"赫山区": "101250701", +"南县": "101250702", +"桃江": "101250703", +"安化": "101250704", +"沅江": "101250705", +"娄底": "101250801", +"双峰": "101250802", +"冷水江": "101250803", +"冷水滩": "101250804", +"新化": "101250805", +"涟源": "101250806", +"邵阳": "101250901", +"隆回": "101250902", +"洞口": "101250903", +"新邵": "101250904", +"邵东": "101250905", +"绥宁": "101250906", +"新宁": "101250907", +"武冈": "101250908", +"城步": "101250909", +"邵阳县": "101250910", +"岳阳": "101251001", +"华容": "101251002", +"湘阴": "101251003", +"汨罗": "101251004", +"平江": "101251005", +"临湘": "101251006", +"张家界": "101251101", +"桑植": "101251102", +"慈利": "101251103", +"怀化": "101251201", +"鹤城区": "101251202", +"沅陵": "101251203", +"辰溪": "101251204", +"靖州": "101251205", +"会同": "101251206", +"通道": "101251207", +"麻阳": "101251208", +"新晃": "101251209", +"芷江": "101251210", +"溆浦": "101251211", +"黔阳": "101251301", +"洪江": "101251302", +"永州": "101251401", +"祁阳": "101251402", +"东安": "101251403", +"双牌": "101251404", +"道县": "101251405", +"宁远": "101251406", +"江永": "101251407", +"蓝山": "101251408", +"新田": "101251409", +"江华": "101251410", +"吉首": "101251501", +"保靖": "101251502", +"永顺": "101251503", +"古丈": "101251504", +"凤凰": "101251505", +"泸溪": "101251506", +"龙山": "101251507", +"花垣": "101251508", +"贵阳": "101260101", +"白云": "101260102", +"花溪": "101260103", +"乌当": "101260104", +"息烽": "101260105", +"开阳": "101260106", +"修文": "101260107", +"清镇": "101260108", +"遵义": "101260201", +"遵义县": "101260202", +"仁怀": "101260203", +"绥阳": "101260204", +"湄潭": "101260205", +"凤冈": "101260206", +"桐梓": "101260207", +"赤水": "101260208", +"习水": "101260209", +"道真": "101260210", +"正安": "101260211", +"务川": "101260212", +"余庆": "101260213", +"汇川": "101260214", +"安顺": "101260301", +"普定": "101260302", +"镇宁": "101260303", +"平坝": "101260304", +"紫云": "101260305", +"关岭": "101260306", +"都匀": "101260401", +"贵定": "101260402", +"瓮安": "101260403", +"长顺": "101260404", +"福泉": "101260405", +"惠水": "101260406", +"龙里": "101260407", +"罗甸": "101260408", +"平塘": "101260409", +"独山": "101260410", +"三都": "101260411", +"荔波": "101260412", +"凯里": "101260501", +"岑巩": "101260502", +"施秉": "101260503", +"镇远": "101260504", +"黄平": "101260505", +"黄平旧洲": "101260506", +"麻江": "101260507", +"丹寨": "101260508", +"三穗": "101260509", +"台江": "101260510", +"剑河": "101260511", +"雷山": "101260512", +"黎平": "101260513", +"天柱": "101260514", +"锦屏": "101260515", +"榕江": "101260516", +"从江": "101260517", +"炉山": "101260518", +"铜仁": "101260601", +"江口": "101260602", +"玉屏": "101260603", +"万山": "101260604", +"思南": "101260605", +"塘头": "101260606", +"印江": "101260607", +"石阡": "101260608", +"沿河": "101260609", +"德江": "101260610", +"松桃": "101260611", +"毕节": "101260701", +"赫章": "101260702", +"金沙": "101260703", +"威宁": "101260704", +"大方": "101260705", +"纳雍": "101260706", +"织金": "101260707", +"六盘水": "101260801", +"六枝": "101260802", +"水城": "101260803", +"盘县": "101260804", +"黔西": "101260901", +"晴隆": "101260902", +"兴仁": "101260903", +"贞丰": "101260904", +"望谟": "101260905", +"兴义": "101260906", +"安龙": "101260907", +"册亨": "101260908", +"普安": "101260909", +"成都": "101270101", +"龙泉驿": "101270102", +"新都": "101270103", +"温江": "101270104", +"金堂": "101270105", +"双流": "101270106", +"郫县": "101270107", +"大邑": "101270108", +"蒲江": "101270109", +"新津": "101270110", +"都江堰": "101270111", +"彭州": "101270112", +"邛崃": "101270113", +"崇州": "101270114", +"崇庆": "101270115", +"彭县": "101270116", +"攀枝花": "101270201", +"仁和": "101270202", +"米易": "101270203", +"盐边": "101270204", +"自贡": "101270301", +"富顺": "101270302", +"荣县": "101270303", +"绵阳": "101270401", +"三台": "101270402", +"盐亭": "101270403", +"安县": "101270404", +"梓潼": "101270405", +"北川": "101270406", +"平武": "101270407", +"江油": "101270408", +"南充": "101270501", +"南部": "101270502", +"营山": "101270503", +"蓬安": "101270504", +"仪陇": "101270505", +"西充": "101270506", +"阆中": "101270507", +"达州": "101270601", +"宣汉": "101270602", +"开江": "101270603", +"大竹": "101270604", +"渠县": "101270605", +"万源": "101270606", +"达川": "101270607", +"遂宁": "101270701", +"蓬溪": "101270702", +"射洪": "101270703", +"广安": "101270801", +"岳池": "101270802", +"武胜": "101270803", +"邻水": "101270804", +"华蓥山": "101270805", +"巴中": "101270901", +"通江": "101270902", +"南江": "101270903", +"平昌": "101270904", +"泸州": "101271001", +"泸县": "101271003", +"合江": "101271004", +"叙永": "101271005", +"古蔺": "101271006", +"纳溪": "101271007", +"宜宾": "101271101", +"宜宾农试站": "101271102", +"宜宾县": "101271103", +"江安": "101271105", +"长宁": "101271106", +"高县": "101271107", +"珙县": "101271108", +"筠连": "101271109", +"兴文": "101271110", +"屏山": "101271111", +"内江": "101271201", +"东兴": "101271202", +"威远": "101271203", +"资中": "101271204", +"隆昌": "101271205", +"资阳": "101271301", +"安岳": "101271302", +"乐至": "101271303", +"简阳": "101271304", +"乐山": "101271401", +"犍为": "101271402", +"井研": "101271403", +"夹江": "101271404", +"沐川": "101271405", +"峨边": "101271406", +"马边": "101271407", +"峨眉": "101271408", +"峨眉山": "101271409", +"眉山": "101271501", +"仁寿": "101271502", +"彭山": "101271503", +"洪雅": "101271504", +"丹棱": "101271505", +"青神": "101271506", +"凉山": "101271601", +"木里": "101271603", +"盐源": "101271604", +"德昌": "101271605", +"会理": "101271606", +"会东": "101271607", +"宁南": "101271608", +"普格": "101271609", +"西昌": "101271610", +"金阳": "101271611", +"昭觉": "101271612", +"喜德": "101271613", +"冕宁": "101271614", +"越西": "101271615", +"甘洛": "101271616", +"雷波": "101271617", +"美姑": "101271618", +"布拖": "101271619", +"雅安": "101271701", +"名山": "101271702", +"荣经": "101271703", +"汉源": "101271704", +"石棉": "101271705", +"天全": "101271706", +"芦山": "101271707", +"宝兴": "101271708", +"甘孜": "101271801", +"康定": "101271802", +"泸定": "101271803", +"丹巴": "101271804", +"九龙": "101271805", +"雅江": "101271806", +"道孚": "101271807", +"炉霍": "101271808", +"新龙": "101271809", +"德格": "101271810", +"白玉": "101271811", +"石渠": "101271812", +"色达": "101271813", +"理塘": "101271814", +"巴塘": "101271815", +"乡城": "101271816", +"稻城": "101271817", +"得荣": "101271818", +"阿坝": "101271901", +"汶川": "101271902", +"理县": "101271903", +"茂县": "101271904", +"松潘": "101271905", +"九寨沟": "101271906", +"金川": "101271907", +"小金": "101271908", +"黑水": "101271909", +"马尔康": "101271910", +"壤塘": "101271911", +"若尔盖": "101271912", +"红原": "101271913", +"南坪": "101271914", +"德阳": "101272001", +"中江": "101272002", +"广汉": "101272003", +"什邡": "101272004", +"绵竹": "101272005", +"罗江": "101272006", +"广元": "101272101", +"旺苍": "101272102", +"青川": "101272103", +"剑阁": "101272104", +"苍溪": "101272105", +"广州": "101280101", +"番禺": "101280102", +"从化": "101280103", +"增城": "101280104", +"花都": "101280105", +"天河": "101280106", +"韶关": "101280201", +"乳源": "101280202", +"始兴": "101280203", +"翁源": "101280204", +"乐昌": "101280205", +"仁化": "101280206", +"南雄": "101280207", +"新丰": "101280208", +"曲江": "101280209", +"惠州": "101280301", +"博罗": "101280302", +"惠阳": "101280303", +"惠东": "101280304", +"龙门": "101280305", +"梅州": "101280401", +"兴宁": "101280402", +"蕉岭": "101280403", +"大埔": "101280404", +"丰顺": "101280406", +"平远": "101280407", +"五华": "101280408", +"梅县": "101280409", +"汕头": "101280501", +"潮阳": "101280502", +"澄海": "101280503", +"南澳": "101280504", +"云澳": "101280505", +"南澎岛": "101280506", +"深圳": "101280601", +"珠海": "101280701", +"斗门": "101280702", +"黄茅洲": "101280703", +"佛山": "101280800", +"顺德": "101280801", +"三水": "101280802", +"南海": "101280803", +"肇庆": "101280901", +"广宁": "101280902", +"四会": "101280903", +"德庆": "101280905", +"怀集": "101280906", +"封开": "101280907", +"高要": "101280908", +"湛江": "101281001", +"吴川": "101281002", +"雷州": "101281003", +"徐闻": "101281004", +"廉江": "101281005", +"硇洲": "101281006", +"遂溪": "101281007", +"江门": "101281101", +"开平": "101281103", +"新会": "101281104", +"恩平": "101281105", +"台山": "101281106", +"上川岛": "101281107", +"鹤山": "101281108", +"河源": "101281201", +"紫金": "101281202", +"连平": "101281203", +"和平": "101281204", +"龙川": "101281205", +"清远": "101281301", +"连南": "101281302", +"连州": "101281303", +"连山": "101281304", +"阳山": "101281305", +"佛冈": "101281306", +"英德": "101281307", +"云浮": "101281401", +"罗定": "101281402", +"新兴": "101281403", +"郁南": "101281404", +"潮州": "101281501", +"饶平": "101281502", +"东莞": "101281601", +"中山": "101281701", +"阳江": "101281801", +"阳春": "101281802", +"揭阳": "101281901", +"揭西": "101281902", +"普宁": "101281903", +"惠来": "101281904", +"茂名": "101282001", +"高州": "101282002", +"化州": "101282003", +"电白": "101282004", +"信宜": "101282005", +"汕尾": "101282101", +"海丰": "101282102", +"陆丰": "101282103", +"遮浪": "101282104", +"东沙岛": "101282105", +"昆明": "101290101", +"昆明农试站": "101290102", +"东川": "101290103", +"寻甸": "101290104", +"晋宁": "101290105", +"宜良": "101290106", +"石林": "101290107", +"呈贡": "101290108", +"富民": "101290109", +"嵩明": "101290110", +"禄劝": "101290111", +"安宁": "101290112", +"太华山": "101290113", +"大理": "101290201", +"云龙": "101290202", +"漾鼻": "101290203", +"永平": "101290204", +"宾川": "101290205", +"弥渡": "101290206", +"祥云": "101290207", +"魏山": "101290208", +"剑川": "101290209", +"洱源": "101290210", +"鹤庆": "101290211", +"南涧": "101290212", +"红河": "101290301", +"石屏": "101290302", +"建水": "101290303", +"弥勒": "101290304", +"元阳": "101290305", +"绿春": "101290306", +"开远": "101290307", +"个旧": "101290308", +"蒙自": "101290309", +"屏边": "101290310", +"泸西": "101290311", +"金平": "101290312", +"曲靖": "101290401", +"沾益": "101290402", +"陆良": "101290403", +"富源": "101290404", +"马龙": "101290405", +"师宗": "101290406", +"罗平": "101290407", +"会泽": "101290408", +"宣威": "101290409", +"保山": "101290501", +"富宁": "101290502", +"龙陵": "101290503", +"施甸": "101290504", +"昌宁": "101290505", +"腾冲": "101290506", +"文山": "101290601", +"西畴": "101290602", +"马关": "101290603", +"麻栗坡": "101290604", +"砚山": "101290605", +"邱北": "101290606", +"广南": "101290607", +"玉溪": "101290701", +"澄江": "101290702", +"江川": "101290703", +"通海": "101290704", +"华宁": "101290705", +"新平": "101290706", +"易门": "101290707", +"峨山": "101290708", +"元江": "101290709", +"楚雄": "101290801", +"大姚": "101290802", +"元谋": "101290803", +"姚安": "101290804", +"牟定": "101290805", +"南华": "101290806", +"武定": "101290807", +"禄丰": "101290808", +"双柏": "101290809", +"永仁": "101290810", +"普洱": "101290901", +"景谷": "101290902", +"景东": "101290903", +"澜沧": "101290904", +"墨江": "101290906", +"江城": "101290907", +"孟连": "101290908", +"西盟": "101290909", +"镇源": "101290910", +"镇沅": "101290911", +"宁洱": "101290912", +"昭通": "101291001", +"鲁甸": "101291002", +"彝良": "101291003", +"镇雄": "101291004", +"威信": "101291005", +"巧家": "101291006", +"绥江": "101291007", +"永善": "101291008", +"盐津": "101291009", +"大关": "101291010", +"临沧": "101291101", +"沧源": "101291102", +"耿马": "101291103", +"双江": "101291104", +"凤庆": "101291105", +"永德": "101291106", +"云县": "101291107", +"镇康": "101291108", +"怒江": "101291201", +"福贡": "101291203", +"兰坪": "101291204", +"泸水": "101291205", +"六库": "101291206", +"贡山": "101291207", +"香格里拉": "101291301", +"德钦": "101291302", +"维西": "101291303", +"中甸": "101291304", +"丽江": "101291401", +"永胜": "101291402", +"华坪": "101291403", +"宁蒗": "101291404", +"德宏": "101291501", +"潞江坝": "101291502", +"陇川": "101291503", +"盈江": "101291504", +"畹町镇": "101291505", +"瑞丽": "101291506", +"梁河": "101291507", +"潞西": "101291508", +"景洪": "101291601", +"大勐龙": "101291602", +"勐海": "101291603", +"景洪电站": "101291604", +"勐腊": "101291605", +"南宁": "101300101", +"南宁城区": "101300102", +"邕宁": "101300103", +"横县": "101300104", +"隆安": "101300105", +"马山": "101300106", +"上林": "101300107", +"武鸣": "101300108", +"宾阳": "101300109", +"硕龙": "101300110", +"崇左": "101300201", +"天等": "101300202", +"龙州": "101300203", +"凭祥": "101300204", +"大新": "101300205", +"扶绥": "101300206", +"宁明": "101300207", +"海渊": "101300208", +"柳州": "101300301", +"柳城": "101300302", +"沙塘": "101300303", +"鹿寨": "101300304", +"柳江": "101300305", +"融安": "101300306", +"融水": "101300307", +"三江": "101300308", +"来宾": "101300401", +"忻城": "101300402", +"金秀": "101300403", +"象州": "101300404", +"武宣": "101300405", +"桂林": "101300501", +"桂林农试站": "101300502", +"龙胜": "101300503", +"永福": "101300504", +"临桂": "101300505", +"兴安": "101300506", +"灵川": "101300507", +"全州": "101300508", +"灌阳": "101300509", +"阳朔": "101300510", +"恭城": "101300511", +"平乐": "101300512", +"荔浦": "101300513", +"资源": "101300514", +"梧州": "101300601", +"藤县": "101300602", +"太平": "101300603", +"苍梧": "101300604", +"蒙山": "101300605", +"岑溪": "101300606", +"贺州": "101300701", +"昭平": "101300702", +"富川": "101300703", +"钟山": "101300704", +"信都": "101300705", +"贵港": "101300801", +"桂平": "101300802", +"平南": "101300803", +"玉林": "101300901", +"博白": "101300902", +"北流": "101300903", +"容县": "101300904", +"陆川": "101300905", +"百色": "101301001", +"那坡": "101301002", +"田阳": "101301003", +"德保": "101301004", +"靖西": "101301005", +"田东": "101301006", +"平果": "101301007", +"隆林": "101301008", +"西林": "101301009", +"乐业": "101301010", +"凌云": "101301011", +"田林": "101301012", +"钦州": "101301101", +"浦北": "101301102", +"灵山": "101301103", +"河池": "101301201", +"天峨": "101301202", +"东兰": "101301203", +"巴马": "101301204", +"环江": "101301205", +"罗城": "101301206", +"宜州": "101301207", +"凤山": "101301208", +"南丹": "101301209", +"都安": "101301210", +"北海": "101301301", +"合浦": "101301302", +"涠洲岛": "101301303", +"防城港": "101301401", +"上思": "101301402", +"板栏": "101301404", +"防城": "101301405", +"海口": "101310101", +"琼山": "101310102", +"三亚": "101310201", +"东方": "101310202", +"临高": "101310203", +"澄迈": "101310204", +"儋州": "101310205", +"昌江": "101310206", +"白沙": "101310207", +"琼中": "101310208", +"定安": "101310209", +"屯昌": "101310210", +"琼海": "101310211", +"文昌": "101310212", +"清兰": "101310213", +"保亭": "101310214", +"万宁": "101310215", +"陵水": "101310216", +"西沙": "101310217", +"珊瑚岛": "101310218", +"永署礁": "101310219", +"南沙岛": "101310220", +"乐东": "101310221", +"五指山": "101310222", +"通什": "101310223", +"香港": "101320101", +"新界": "101320103", +"中环": "101320104", +"铜锣湾": "101320105", +"澳门": "101330101", +"台北县": "101340101", +"台北市": "101340102", +"高雄": "101340201", +"大武": "101340203", +"恒春": "101340204", +"兰屿": "101340205", +"台南": "101340301", +"台中": "101340401", +"桃园": "101340501", +"新竹县": "101340601", +"新竹市": "101340602", +"公馆": "101340603", +"宜兰": "101340701", +"马公": "101340801", +"东吉屿": "101340802", +"嘉义": "101340901", +"阿里山": "101340902", +"新港": "101340904", +} \ No newline at end of file diff --git a/plugins/weather.py b/plugins/weather.py index ec1ceea..950ed73 100644 --- a/plugins/weather.py +++ b/plugins/weather.py @@ -9,7 +9,7 @@ from core.managers.command_manager import matcher from core.managers.image_manager import image_manager from core.utils.logger import logger from models import MessageEvent, MessageSegment - +from .resource.city_code import CITY_CODES # 插件元数据 __plugin_meta__ = { "name": "weather", @@ -17,46 +17,6 @@ __plugin_meta__ = { "usage": "/天气 [城市代码] - 查询指定城市的天气信息\n例如:/天气 101190207 (南京)", } -# 城市代码映射(可以扩展) -CITY_CODES = { - "北京": "101010100", - "上海": "101020100", - "广州": "101280101", - "深圳": "101280601", - "南京": "101190101", - "苏州": "101190401", - "杭州": "101210101", - "武汉": "101200101", - "成都": "101270101", - "重庆": "101040100", - "西安": "101110101", - "天津": "101030100", - "沈阳": "101070101", - "大连": "101070201", - "青岛": "101120201", - "济南": "101120101", - "郑州": "101180101", - "长沙": "101250101", - "南昌": "101240101", - "合肥": "101220101", - "福州": "101230101", - "厦门": "101230201", - "南宁": "101300101", - "海口": "101310101", - "昆明": "101290101", - "贵阳": "101260101", - "拉萨": "101140101", - "兰州": "101160101", - "西宁": "101150101", - "银川": "101170101", - "乌鲁木齐": "101130101", - "哈尔滨": "101050101", - "长春": "101060101", - "呼和浩特": "101080101", - "太原": "101100101", - "石家庄": "101090101", -} - HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" } From 014c6c9092e3a3d5bf48ed4a1688d40f9ba55160 Mon Sep 17 00:00:00 2001 From: K2cr2O1 <2221577113@qq.com> Date: Sat, 28 Feb 2026 20:57:48 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat(reverse=5Fws):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=8F=8D=E5=90=91WebSocket=E6=94=AF=E6=8C=81=E5=8F=8A=E8=B4=9F?= =?UTF-8?q?=E8=BD=BD=E5=9D=87=E8=A1=A1=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增反向WebSocket管理器模块,支持多客户端连接 - 实现负载均衡机制,自动选择健康且负载最低的客户端 - 添加防重复事件处理机制,防止消息重复处理 - 更新配置模型和加载器以支持反向WebSocket配置 - 添加示例文件和文档说明使用方法 - 修改主程序启动逻辑以支持反向WebSocket服务 --- REVERSE_WS_LOAD_BALANCE.md | 211 ++++++++++++++ config.toml | 7 + core/config_loader.py | 10 +- core/config_models.py | 21 ++ core/managers/__init__.py | 5 + core/managers/image_manager.py | 8 +- core/managers/reverse_ws_manager.py | 438 ++++++++++++++++++++++++++++ examples/reverse_ws_example.py | 58 ++++ main.py | 15 +- plugins/furry.py | 2 +- 10 files changed, 769 insertions(+), 6 deletions(-) create mode 100644 REVERSE_WS_LOAD_BALANCE.md create mode 100644 core/managers/reverse_ws_manager.py create mode 100644 examples/reverse_ws_example.py diff --git a/REVERSE_WS_LOAD_BALANCE.md b/REVERSE_WS_LOAD_BALANCE.md new file mode 100644 index 0000000..25ed48e --- /dev/null +++ b/REVERSE_WS_LOAD_BALANCE.md @@ -0,0 +1,211 @@ +# 反向 WebSocket 负载均衡配置 + +## 功能特性 + +### 1. 负载均衡 + +当有多个前端(NapCat等)连接到反向WebSocket服务端时,系统会自动进行负载均衡: + +- **自动选择负载最低的客户端**:API调用时会自动选择负载最低的健康客户端 +- **健康检查**:系统会记录每个客户端的最后活动时间,只选择最近30秒内有活动的客户端 +- **负载计数**:每个客户端的消息处理次数会被记录,用于负载均衡计算 + +### 2. 防重复发送 + +系统实现了多层防重复机制: + +- **事件ID检查**:通过事件ID(`id`、`post_id`或`time`)识别重复事件 +- **消息锁机制**:使用异步锁防止同一消息被并发处理 +- **双重检查**:在锁内再次检查是否重复,防止并发竞争条件 +- **自动清理**:定期清理过期的事件ID和消息锁(默认60秒和300秒) + +### 3. 工作原理 + +``` +┌─────────────┐ +│ Frontend │ +│ (NapCat) │ +└──────┬──────┘ + │ + │ WebSocket + │ +┌──────▼──────┐ +│ │ +│ ReverseWS │ ←── 负载均衡 + 防重复 +│ Manager │ +│ │ +└──────┬──────┘ + │ + │ 处理事件 + │ +┌──────▼──────┐ +│ Command │ +│ Manager │ +│ │ +└─────────────┘ +``` + +## 配置说明 + +在 `config.toml` 中配置: + +```toml +[reverse_ws] +enabled = true # 启用反向WebSocket +host = "0.0.0.0" # 监听地址 +port = 3002 # 监听端口 +token = "" # 访问令牌(可选) +``` + +## 使用方法 + +### 启动配置 + +1. 在 `config.toml` 中设置 `enabled = true` +2. 确保防火墙允许指定端口的连接 +3. 启动机器人服务 + +### 前端配置 + +在 NapCat 等前端配置中,将 WebSocket 连接地址改为: + +``` +ws://your-server-ip:3002 +``` + +多个前端可以连接到同一个地址,系统会自动进行负载均衡。 + +## API 调用 + +### 使用负载均衡(推荐) + +```python +from core.managers import reverse_ws_manager + +# 自动选择负载最低的健康客户端 +response = await reverse_ws_manager.call_api( + action="send_group_msg", + params={ + "group_id": 123456, + "message": "Hello" + }, + use_load_balance=True # 默认为 True +) +``` + +### 指定客户端 + +```python +# 向特定客户端发送 +response = await reverse_ws_manager.call_api( + action="send_group_msg", + params={ + "group_id": 123456, + "message": "Hello" + }, + client_id="specific-client-id", + use_load_balance=False +) +``` + +### 获取客户端信息 + +```python +# 获取所有连接的客户端 +clients = reverse_ws_manager.get_connected_clients() + +# 获取健康的客户端(最近30秒有活动) +healthy = reverse_ws_manager.get_healthy_clients() + +# 获取负载最低的客户端 +least_load = reverse_ws_manager.get_client_with_least_load() +``` + +## 负载均衡策略 + +系统采用以下策略选择客户端: + +1. **健康检查**:只选择最近30秒内有活动的客户端 +2. **负载计数**:在健康客户端中选择负载最低的 +3. **自动切换**:如果负载最低的客户端不健康,自动选择下一个 + +## 防重复机制 + +### 事件ID检查 + +系统通过以下方式识别事件: + +- 优先使用 `id` 字段 +- 其次使用 `post_id` 字段 +- 最后使用 `time` 字段 + +### 消息锁 + +消息处理使用异步锁,防止并发重复处理: + +```python +async with self._get_message_lock(message_key): + # 处理消息 + await matcher.handle_event(None, event) +``` + +### 自动清理 + +系统每10秒清理一次过期数据: + +- 事件ID保留时间:60秒 +- 消息锁保留时间:300秒 + +## 监控和调试 + +### 查看客户端状态 + +```python +# 查看所有客户端 +print("所有客户端:", reverse_ws_manager.get_connected_clients()) + +# 查看健康客户端 +print("健康客户端:", reverse_ws_manager.get_healthy_clients()) + +# 查看负载情况 +print("客户端负载:", reverse_ws_manager._client_load) + +# 查看健康时间 +print("客户端健康时间:", reverse_ws_manager._client_health) +``` + +### 日志输出 + +系统会输出以下日志: + +- 客户端连接/断开 +- 检测到重复事件 +- 负载均衡选择 +- API调用结果 + +## 最佳实践 + +1. **多前端部署**:建议部署2-3个前端实例进行负载均衡 +2. **健康检查**:定期检查前端连接状态 +3. **监控日志**:关注重复事件日志,排查网络问题 +4. **合理设置TTL**:根据消息频率调整事件ID保留时间 + +## 故障排查 + +### 问题:消息重复处理 + +**原因**:网络延迟导致前端重复发送 + +**解决**:检查事件ID是否正确设置,系统已自动处理 + +### 问题:API调用超时 + +**原因**:选择的客户端不健康或网络问题 + +**解决**:系统会自动切换到其他健康客户端 + +### 问题:所有客户端都不健康 + +**原因**:前端断开连接或网络问题 + +**解决**:检查前端连接状态和网络连接 diff --git a/config.toml b/config.toml index d5943dc..cb60f44 100644 --- a/config.toml +++ b/config.toml @@ -9,6 +9,13 @@ token = "KoIAF.mcEHzxrPYF" # 重连间隔(秒) reconnect_interval = 5 + +[reverse_ws] +enabled = true # 是否启用 +host = "0.0.0.0" # 监听地址 +port = 3002 # 监听端口 +token = "" + # Bot 基础配置 [bot] # 命令前缀列表 diff --git a/core/config_loader.py b/core/config_loader.py index 8910756..9f42118 100644 --- a/core/config_loader.py +++ b/core/config_loader.py @@ -7,7 +7,7 @@ from pathlib import Path import tomllib from pydantic import ValidationError -from .config_models import ConfigModel, NapCatWSModel, BotModel, RedisModel, DockerModel, ImageManagerModel, MySQLModel +from .config_models import ConfigModel, NapCatWSModel, BotModel, RedisModel, DockerModel, ImageManagerModel, MySQLModel, ReverseWSModel from .utils.logger import ModuleLogger from .utils.exceptions import ConfigError, ConfigNotFoundError, ConfigValidationError @@ -129,6 +129,14 @@ class Config: """ return self._model.image_manager + @property + def reverse_ws(self) -> ReverseWSModel: + """ + 获取反向 WebSocket 配置 + """ + return self._model.reverse_ws + + # 实例化全局配置对象 global_config = Config() diff --git a/core/config_models.py b/core/config_models.py index 13c0d7b..829ede3 100644 --- a/core/config_models.py +++ b/core/config_models.py @@ -25,6 +25,15 @@ class BotModel(BaseModel): ignore_self_message: bool = True permission_denied_message: str = "权限不足,需要 {permission_name} 权限" +class ReverseWSModel(BaseModel): + """ + 对应 `config.toml` 中的 `[reverse_ws]` 配置块。 + """ + enabled: bool = False + host: str = "0.0.0.0" + port: int = 3002 + token: Optional[str] = None + class RedisModel(BaseModel): """ @@ -46,6 +55,7 @@ class MySQLModel(BaseModel): password: str db: str charset: str = "utf8mb4" + class DockerModel(BaseModel): @@ -69,6 +79,16 @@ class ImageManagerModel(BaseModel): image_width: int = 1080 +class ReverseWSModel(BaseModel): + """ + 对应 `config.toml` 中的 `[reverse_ws]` 配置块。 + """ + enabled: bool = False + host: str = "0.0.0.0" + port: int = 3002 + token: Optional[str] = None + + class ConfigModel(BaseModel): """ 顶层配置模型,整合了所有子配置块。 @@ -79,5 +99,6 @@ class ConfigModel(BaseModel): mysql: MySQLModel docker: DockerModel image_manager: ImageManagerModel + reverse_ws: ReverseWSModel diff --git a/core/managers/__init__.py b/core/managers/__init__.py index a221ee3..4870997 100644 --- a/core/managers/__init__.py +++ b/core/managers/__init__.py @@ -11,6 +11,7 @@ from .redis_manager import RedisManager from .mysql_manager import MySQLManager from .browser_manager import BrowserManager from .image_manager import ImageManager +from .reverse_ws_manager import ReverseWSManager # --- 实例化所有单例管理器 --- @@ -36,6 +37,9 @@ browser_manager = BrowserManager() # 图片管理器 image_manager = ImageManager() +# 反向 WebSocket 管理器 +reverse_ws_manager = ReverseWSManager() + __all__ = [ "permission_manager", "command_manager", @@ -45,4 +49,5 @@ __all__ = [ "mysql_manager", "browser_manager", "image_manager", + "reverse_ws_manager", ] diff --git a/core/managers/image_manager.py b/core/managers/image_manager.py index 101131a..3d92944 100644 --- a/core/managers/image_manager.py +++ b/core/managers/image_manager.py @@ -35,7 +35,7 @@ class ImageManager(Singleton): # 模板缓存 self._template_cache: Dict[str, Template] = {} - async def render_template(self, template_name: str, data: Dict[str, Any], output_name: str = "output.png", quality: int = 80, image_type: str = "png") -> Optional[str]: + async def render_template(self, template_name: str, data: Dict[str, Any], output_name: str = "output.png", quality: int = 80, image_type: str = "png", width: int = 1920, height: int = 1080) -> Optional[str]: """ 使用 Playwright 渲染 Jinja2 模板并保存为图片文件 @@ -45,6 +45,8 @@ class ImageManager(Singleton): output_name (str, optional): 输出文件名. Defaults to "output.png". quality (int, optional): JPEG 质量 (0-100). 仅在 image_type 为 jpeg 时有效. Defaults to 80. image_type (str, optional): 图片类型 ('png' or 'jpeg'). Defaults to "png". + width (int, optional): 图片宽度. Defaults to 1920. + height (int, optional): 图片高度. Defaults to 1080. Returns: Optional[str]: 生成图片的绝对路径,如果失败则返回 None @@ -74,8 +76,8 @@ class ImageManager(Singleton): return None try: - width = global_config.image_manager.image_width - height = global_config.image_manager.image_height + width = data.get("width", width) + height = data.get("height", height) await page.set_viewport_size({"width": width, "height": height}) # 加载内容 diff --git a/core/managers/reverse_ws_manager.py b/core/managers/reverse_ws_manager.py new file mode 100644 index 0000000..d506573 --- /dev/null +++ b/core/managers/reverse_ws_manager.py @@ -0,0 +1,438 @@ +""" +反向 WebSocket 管理器模块 + +该模块提供了反向 WebSocket 服务端功能,允许 OneBot 实现(如 NapCat) +主动连接到机器人服务器,而不是由机器人主动连接到 OneBot 实现。 +""" +import asyncio +import orjson +import websockets +from websockets.server import WebSocketServerProtocol +from typing import Dict, Any, Optional, Set +from datetime import datetime +import uuid +import random + +from ..config_loader import global_config +from ..utils.logger import ModuleLogger +from ..utils.exceptions import WebSocketError, WebSocketConnectionError +from ..utils.error_codes import ErrorCode, create_error_response +from .command_manager import matcher +from models.events.factory import EventFactory +from .redis_manager import redis_manager + + +class ReverseWSManager: + """ + 反向 WebSocket 管理器,作为服务端接收 OneBot 实现的连接。 + 支持多前端负载均衡和防重复发送机制。 + """ + + def __init__(self): + """ + 初始化反向 WebSocket 管理器。 + """ + self.server = None + self.clients: Dict[str, WebSocketServerProtocol] = {} + self.client_self_ids: Dict[str, int] = {} + self._pending_requests: Dict[str, asyncio.Future] = {} + self._running = False + self.logger = ModuleLogger("ReverseWSManager") + + # 负载均衡相关 + self._active_client_id: Optional[str] = None # 当前活跃的客户端(用于消息发送) + self._client_load: Dict[str, int] = {} # 客户端负载计数 + self._client_health: Dict[str, datetime] = {} # 客户端健康检查时间 + + # 防重复发送相关 + self._processed_events: Dict[str, datetime] = {} # 已处理的事件ID和时间 + self._event_ttl = 60 # 事件ID保留时间(秒) + self._message_locks: Dict[str, asyncio.Lock] = {} # 消息处理锁 + self._lock_ttl = 300 # 锁保留时间(秒) + + # 启动清理任务 + self._cleanup_task = None + + async def start(self, host: str = "0.0.0.0", port: int = 3002) -> None: + """ + 启动反向 WebSocket 服务端。 + + Args: + host: 监听地址,默认为 0.0.0.0 + port: 监听端口,默认为 3002 + """ + self._running = True + self.server = await websockets.serve( + self._handle_client, + host, + port, + ping_interval=20, + ping_timeout=20 + ) + self.logger.success(f"反向 WebSocket 服务端已启动: ws://{host}:{port}") + + # 启动清理任务 + self._cleanup_task = asyncio.create_task(self._cleanup_expired_data()) + + async def stop(self) -> None: + """ + 停止反向 WebSocket 服务端。 + """ + self._running = False + + # 停止清理任务 + if self._cleanup_task: + self._cleanup_task.cancel() + try: + await self._cleanup_task + except asyncio.CancelledError: + pass + + if self.server: + self.server.close() + await self.server.wait_closed() + + for client_id in list(self.clients.keys()): + await self._disconnect_client(client_id) + + self.logger.success("反向 WebSocket 服务端已停止") + + async def _handle_client( + self, + websocket: WebSocketServerProtocol, + path: str = None + ) -> None: + """ + 处理客户端连接。 + + Args: + websocket: WebSocket 连接对象 + path: 连接路径 + """ + client_id = str(uuid.uuid4()) + self.clients[client_id] = websocket + self.logger.info(f"新客户端连接: {client_id}") + + try: + async for message in websocket: + try: + data = orjson.loads(message) + + # 处理 API 响应 + echo_id = data.get("echo") + if echo_id and echo_id in self._pending_requests: + future = self._pending_requests.pop(echo_id) + if not future.done(): + future.set_result(data) + continue + + # 处理上报事件 + if "post_type" in data: + asyncio.create_task(self._on_event(client_id, data)) + + except orjson.JSONDecodeError as e: + self.logger.error(f"JSON 解析失败: {str(e)}") + except Exception as e: + self.logger.exception(f"处理消息异常: {str(e)}") + + except websockets.exceptions.ConnectionClosed as e: + self.logger.info(f"客户端断开连接: {client_id} - {str(e)}") + except Exception as e: + self.logger.exception(f"客户端异常: {str(e)}") + finally: + await self._disconnect_client(client_id) + + async def _cleanup_expired_data(self) -> None: + """ + 清理过期的事件ID和消息锁 + """ + while self._running: + try: + await asyncio.sleep(10) # 每10秒清理一次 + + current_time = datetime.now() + + # 清理过期的事件ID + expired_events = [ + event_id for event_id, timestamp in self._processed_events.items() + if (current_time - timestamp).total_seconds() > self._event_ttl + ] + for event_id in expired_events: + del self._processed_events[event_id] + + # 清理过期的消息锁 + expired_locks = [ + lock_key for lock_key, timestamp in self._message_locks.items() + if (current_time - timestamp).total_seconds() > self._lock_ttl + ] + for lock_key in expired_locks: + del self._message_locks[lock_key] + + except asyncio.CancelledError: + break + except Exception as e: + self.logger.error(f"清理过期数据失败: {str(e)}") + + async def _disconnect_client(self, client_id: str) -> None: + """ + 断开客户端连接。 + + Args: + client_id: 客户端 ID + """ + if client_id in self.clients: + del self.clients[client_id] + if client_id in self.client_self_ids: + del self.client_self_ids[client_id] + + async def _on_event(self, client_id: str, event_data: Dict[str, Any]) -> None: + """ + 处理事件,包含防重复发送和负载均衡逻辑。 + + Args: + client_id: 客户端 ID + event_data: 事件数据 + """ + try: + event = EventFactory.create_event(event_data) + + if hasattr(event, 'self_id'): + self.client_self_ids[client_id] = event.self_id + + event.bot = None + + # 记录客户端健康状态 + self._client_health[client_id] = datetime.now() + + # 检查是否为重复事件 + if self._is_duplicate_event(event_data): + self.logger.debug(f"检测到重复事件,已忽略: {event_data.get('id')}") + return + + # 标记事件已处理 + self._mark_event_processed(event_data) + + # 处理消息事件 + if event.post_type == "message": + sender_name = event.sender.nickname if hasattr(event, "sender") and event.sender else "Unknown" + message_type = getattr(event, "message_type", "Unknown") + user_id = getattr(event, "user_id", "Unknown") + raw_message = getattr(event, "raw_message", "") + self.logger.info(f"[消息] {message_type} | {user_id}({sender_name}): {raw_message}") + + # 使用锁防止同一消息被多次处理 + message_key = self._get_message_key(event_data) + async with self._get_message_lock(message_key): + # 再次检查是否重复(防止并发问题) + if self._is_duplicate_event(event_data): + self.logger.debug(f"并发检测到重复消息,已忽略: {message_key}") + return + self._mark_event_processed(event_data) + + # 更新客户端负载 + self._update_client_load(client_id) + + await matcher.handle_event(None, event) + + elif event.post_type == "notice": + notice_type = getattr(event, "notice_type", "Unknown") + self.logger.info(f"[通知] {notice_type}") + await matcher.handle_event(None, event) + + elif event.post_type == "request": + request_type = getattr(event, "request_type", "Unknown") + self.logger.info(f"[请求] {request_type}") + await matcher.handle_event(None, event) + + elif event.post_type == "meta_event": + meta_event_type = getattr(event, "meta_event_type", "Unknown") + self.logger.debug(f"[元事件] {meta_event_type}") + await matcher.handle_event(None, event) + + except Exception as e: + self.logger.exception(f"事件处理异常: {str(e)}") + + async def call_api( + self, + action: str, + params: Optional[Dict[Any, Any]] = None, + client_id: Optional[str] = None, + use_load_balance: bool = True + ) -> Dict[Any, Any]: + """ + 向客户端发送 API 请求。 + + Args: + action: API 动作名称 + params: API 参数 + client_id: 客户端 ID,如果为 None 则根据负载均衡策略选择 + use_load_balance: 是否使用负载均衡,默认为 True + + Returns: + API 响应数据 + """ + if not self.clients: + self.logger.error("调用 API 失败: 没有可用的客户端连接") + return create_error_response( + code=ErrorCode.WS_DISCONNECTED, + message="没有可用的客户端连接", + data={"action": action, "params": params} + ) + + # 如果没有指定客户端,使用负载均衡 + if client_id is None and use_load_balance: + # 优先选择健康的客户端 + healthy_clients = self.get_healthy_clients() + if healthy_clients: + # 选择负载最低的客户端 + client_id = self.get_client_with_least_load() + if client_id is None and healthy_clients: + client_id = list(healthy_clients.keys())[0] + else: + # 如果没有健康客户端,使用所有客户端中的一个 + client_id = list(self.clients.keys())[0] + + echo_id = str(uuid.uuid4()) + payload = {"action": action, "params": params or {}, "echo": echo_id} + + loop = asyncio.get_running_loop() + future = loop.create_future() + self._pending_requests[echo_id] = future + + try: + targets = [client_id] if client_id else list(self.clients.keys()) + + for cid in targets: + if cid in self.clients: + await self.clients[cid].send(orjson.dumps(payload)) + + return await asyncio.wait_for(future, timeout=30.0) + except asyncio.TimeoutError: + self._pending_requests.pop(echo_id, None) + self.logger.warning(f"API 调用超时: action={action}, params={params}") + return create_error_response( + code=ErrorCode.TIMEOUT_ERROR, + message="API调用超时", + data={"action": action, "params": params} + ) + except Exception as e: + self._pending_requests.pop(echo_id, None) + self.logger.exception(f"API 调用异常: action={action}, error={str(e)}") + return create_error_response( + code=ErrorCode.WS_MESSAGE_ERROR, + message=f"API调用异常: {str(e)}", + data={"action": action, "params": params} + ) + + def get_connected_clients(self) -> Dict[str, int]: + """ + 获取已连接的客户端列表。 + + Returns: + 客户端 ID 和 self_id 的映射字典 + """ + return self.client_self_ids.copy() + + def _is_duplicate_event(self, event_data: Dict[str, Any]) -> bool: + """ + 检查是否为重复事件。 + + Args: + event_data: 事件数据 + + Returns: + 是否为重复事件 + """ + event_id = event_data.get('id') or event_data.get('post_id') or event_data.get('time') + if not event_id: + return False + + event_key = f"{event_data.get('post_type')}:{event_id}" + return event_key in self._processed_events + + def _mark_event_processed(self, event_data: Dict[str, Any]) -> None: + """ + 标记事件已处理。 + + Args: + event_data: 事件数据 + """ + event_id = event_data.get('id') or event_data.get('post_id') or event_data.get('time') + if not event_id: + return + + event_key = f"{event_data.get('post_type')}:{event_id}" + self._processed_events[event_key] = datetime.now() + + def _get_message_key(self, event_data: Dict[str, Any]) -> str: + """ + 获取消息唯一标识。 + + Args: + event_data: 事件数据 + + Returns: + 消息唯一标识 + """ + if event_data.get('post_type') == 'message': + message_id = event_data.get('message_id') or event_data.get('id') + user_id = event_data.get('user_id') + return f"msg:{message_id}:{user_id}" + return str(uuid.uuid4()) + + def _get_message_lock(self, key: str) -> asyncio.Lock: + """ + 获取消息处理锁。 + + Args: + key: 消息唯一标识 + + Returns: + asyncio.Lock 实例 + """ + if key not in self._message_locks: + self._message_locks[key] = asyncio.Lock() + return self._message_locks[key] + + def _update_client_load(self, client_id: str) -> None: + """ + 更新客户端负载。 + + Args: + client_id: 客户端 ID + """ + if client_id not in self._client_load: + self._client_load[client_id] = 0 + self._client_load[client_id] += 1 + + def get_client_with_least_load(self) -> Optional[str]: + """ + 获取负载最低的客户端。 + + Returns: + 客户端 ID,如果没有客户端则返回 None + """ + if not self._client_load: + return None + + return min(self._client_load.keys(), key=lambda k: self._client_load[k]) + + def get_healthy_clients(self) -> Dict[str, int]: + """ + 获取健康的客户端列表(最近30秒内有活动)。 + + Returns: + 健康的客户端 ID 和 self_id 的映射字典 + """ + current_time = datetime.now() + healthy = {} + + for client_id, last_health in self._client_health.items(): + if (current_time - last_health).total_seconds() < 30: + if client_id in self.client_self_ids: + healthy[client_id] = self.client_self_ids[client_id] + + return healthy + + +reverse_ws_manager = ReverseWSManager() diff --git a/examples/reverse_ws_example.py b/examples/reverse_ws_example.py new file mode 100644 index 0000000..f3c9b23 --- /dev/null +++ b/examples/reverse_ws_example.py @@ -0,0 +1,58 @@ +""" +反向 WebSocket 使用示例 + +该文件展示了如何使用反向 WebSocket 功能。 +""" + +from core.managers import reverse_ws_manager + + +async def example_usage(): + """ + 使用示例 + """ + # 1. 启动反向 WebSocket 服务端 + await reverse_ws_manager.start(host="0.0.0.0", port=3002) + + # 2. 等待客户端连接 + # 此时 OneBot 实现(如 NapCat)应该连接到 ws://your-server-ip:3002 + + # 3. 查看已连接的客户端 + connected_clients = reverse_ws_manager.get_connected_clients() + print(f"已连接的客户端: {connected_clients}") + + # 4. 查看健康的客户端 + healthy_clients = reverse_ws_manager.get_healthy_clients() + print(f"健康的客户端: {healthy_clients}") + + # 5. 调用 API(使用负载均衡) + response = await reverse_ws_manager.call_api( + action="get_login_info", + params={}, + use_load_balance=True # 启用负载均衡 + ) + print(f"API 响应: {response}") + + # 6. 调用 API(向特定客户端发送) + if connected_clients: + client_id = list(connected_clients.keys())[0] + response = await reverse_ws_manager.call_api( + action="get_login_info", + params={}, + client_id=client_id, + use_load_balance=False # 不使用负载均衡 + ) + print(f"特定客户端 API 响应: {response}") + + # 7. 获取负载最低的客户端 + least_load_client = reverse_ws_manager.get_client_with_least_load() + if least_load_client: + print(f"负载最低的客户端: {least_load_client}") + + # 8. 停止服务端 + await reverse_ws_manager.stop() + + +if __name__ == "__main__": + import asyncio + asyncio.run(example_usage()) diff --git a/main.py b/main.py index f553846..0c6458e 100644 --- a/main.py +++ b/main.py @@ -15,7 +15,7 @@ from core.utils.logger import logger # 核心模块导入 from core.ws import WS -from core.managers import plugin_manager, matcher, permission_manager +from core.managers import plugin_manager, matcher, permission_manager, reverse_ws_manager from core.managers.redis_manager import redis_manager from core.managers.browser_manager import browser_manager from core.utils.executor import run_in_thread_pool, initialize_executor @@ -142,6 +142,15 @@ async def main(): # 初始化浏览器管理器 (使用页面池) await browser_manager.init_pool(size=3) + # 启动反向 WebSocket 服务端(如果启用) + if config.reverse_ws.enabled: + logger.info("正在启动反向 WebSocket 服务端...") + asyncio.create_task(reverse_ws_manager.start( + host=config.reverse_ws.host, + port=config.reverse_ws.port + )) + logger.success(f"反向 WebSocket 服务端已启动: ws://{config.reverse_ws.host}:{config.reverse_ws.port}") + # 启动文件监控 # 监控 plugins 目录 plugin_path = os.path.join(os.path.dirname(__file__), "plugins") @@ -186,6 +195,10 @@ async def main(): if websocket_client: await websocket_client.close() + # 关闭反向 WebSocket 服务端 + if config.reverse_ws.enabled and reverse_ws_manager.server: + await reverse_ws_manager.stop() + # 关闭浏览器管理器 await browser_manager.shutdown() diff --git a/plugins/furry.py b/plugins/furry.py index 747ede1..7fbbe42 100644 --- a/plugins/furry.py +++ b/plugins/furry.py @@ -12,7 +12,7 @@ from models.message import MessageSegment __plugin_meta__ = { "name": "furry", "description": "处理 /furry 指令,发送furry出毛图片", - "usage": "/furry - 发送一条furry图", + "usage": "/furry - 发送一条furry图,1-10", } @matcher.command("furry")