ErisPulse-MatrixAdapter 4.0.2__tar.gz → 4.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,34 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ErisPulse-MatrixAdapter
3
- Version: 4.0.2
3
+ Version: 4.2.0
4
4
  Summary: ErisPulse的Matrix协议适配模块
5
5
  Author-email: wsu2059 <wsu2059@qq.com>
6
6
  Project-URL: homepage, https://github.com/ErisPulse/ErisPulse-MatrixAdapter
7
7
  Requires-Python: >=3.9
8
8
  Description-Content-Type: text/markdown
9
9
 
10
- # ErisPulse Matrix Adapter
10
+ <div align="center">
11
11
 
12
- [English](#english) | [中文](#中文)
12
+ <img src=".github/assets/ErisPulseLogo.png" width="180" alt="ErisPulse MatrixAdapter" />
13
+
14
+ # ErisPulse MatrixAdapter
15
+
16
+ **Matrix protocol adapter — decentralized chat via Long Polling sync.**
17
+
18
+ A Matrix protocol adapter built on the ErisPulse framework. It receives events via the Long Polling Sync API and unifies private chats, groups and rooms into OneBot12 standard events, with full support for mentions, replies, HTML rich text, reactions and MXC media.
19
+
20
+ <p>
21
+ <a href="https://pypi.org/project/ErisPulse-MatrixAdapter/"><img src="https://img.shields.io/pypi/v/ErisPulse-MatrixAdapter?style=for-the-badge&logo=pypi&logoColor=white" alt="PyPI"></a>
22
+ <a href="https://pypi.org/project/ErisPulse-MatrixAdapter/"><img src="https://img.shields.io/badge/Python-3.10+-FFD43B?style=for-the-badge&logo=python&logoColor=blue" alt="Python"></a>
23
+ <a href="./LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue?style=for-the-badge" alt="License"></a>
24
+ <a href="https://github.com/ErisPulse/ErisPulse-MatrixAdapter"><img src="https://img.shields.io/github/stars/ErisPulse/ErisPulse-MatrixAdapter?style=for-the-badge&logo=github&color=brightgreen" alt="Stars"></a>
25
+ <a href="https://pepy.tech/project/ErisPulse-MatrixAdapter"><img src="https://img.shields.io/pepy/dt/ErisPulse-MatrixAdapter?style=for-the-badge&color=blue" alt="Downloads"></a>
26
+ <a href="https://github.com/ErisPulse/ErisPulse"><img src="https://img.shields.io/badge/Powered_by-ErisPulse-FF6B9D?style=for-the-badge&logo=bookstack&logoColor=white" alt="ErisPulse"></a>
27
+ </p>
28
+
29
+ [English](#english) | [简体中文](#中文)
30
+
31
+ </div>
13
32
 
14
33
  ---
15
34
 
@@ -2,9 +2,12 @@ import time
2
2
  import uuid
3
3
  from typing import Dict, Optional, List
4
4
 
5
+ from ErisPulse.Core.Bases import BaseConverter
5
6
 
6
- class MatrixConverter:
7
+
8
+ class MatrixConverter(BaseConverter):
7
9
  def __init__(self, bot_user_id: str = ""):
10
+ super().__init__(platform="matrix")
8
11
  self.bot_user_id = bot_user_id
9
12
  self._dm_rooms: Dict[str, str] = {}
10
13
 
@@ -31,19 +34,13 @@ class MatrixConverter:
31
34
  if sender == self.bot_user_id:
32
35
  return None
33
36
 
34
- base_event = {
35
- "id": event_id,
36
- "time": event_time,
37
- "type": "",
38
- "detail_type": "",
39
- "platform": "matrix",
40
- "self": {
41
- "platform": "matrix",
42
- "user_id": self.bot_user_id,
43
- },
44
- "matrix_raw": raw_event,
45
- "matrix_raw_type": event_type,
46
- }
37
+ # 基础事件结构(BaseConverter 骨架 + Matrix 语义覆盖)
38
+ base_event = self.build_base_event(raw_event, event_type)
39
+ base_event["id"] = str(event_id)
40
+ base_event["time"] = event_time
41
+ base_event["type"] = ""
42
+ base_event["detail_type"] = ""
43
+ base_event["self"]["user_id"] = self.bot_user_id
47
44
 
48
45
  if is_dm:
49
46
  base_event["detail_type"] = "private"
@@ -61,6 +58,7 @@ class MatrixConverter:
61
58
 
62
59
  def _handle_m_room_message(self, raw_event: Dict, base_event: Dict, room_id: str, is_dm: bool) -> Dict:
63
60
  base_event["type"] = "message"
61
+ base_event["message_id"] = str(raw_event.get("event_id", ""))
64
62
  base_event["user_id"] = raw_event.get("sender", "")
65
63
  base_event["user_nickname"] = raw_event.get("sender", "")
66
64
 
@@ -7,11 +7,22 @@ from typing import Dict, List, Optional
7
7
 
8
8
  from ErisPulse.Core import client
9
9
  from ErisPulse.Core.Bases.adapter import BaseAdapter
10
+ from ErisPulse.Core.Bases import BotAccountConfig
10
11
  from ErisPulse.Core.Event import register_event_mixin, unregister_platform_event_methods
11
- from ErisPulse.runtime.config_schema import BotAccountConfig
12
+ from ErisPulse.Core.i18n import i18n
12
13
 
13
14
  from .Converter import MatrixConverter
14
15
 
16
+ try:
17
+ from ErisPulse.runtime.tasks import spawn_background
18
+ except ImportError: # pragma: no cover
19
+ spawn_background = None
20
+
21
+ __version__ = "4.2.0"
22
+
23
+ # 软依赖的框架最低版本(运行时检测,仅提示不强制)
24
+ MIN_FRAMEWORK_VERSION = (2, 7, 1)
25
+
15
26
 
16
27
  # ============================================================================
17
28
  # 配置声明(多账户)
@@ -21,43 +32,57 @@ class MatrixAccountConfig(BotAccountConfig):
21
32
  homeserver: str = field(
22
33
  default="https://matrix.org",
23
34
  metadata={
24
- "description": "Matrix server address",
35
+ "description": {"i18n": "matrix.homeserver", "default": "Matrix 服务器地址"},
25
36
  "required": True,
26
- "webui": {"widget": "text", "group": "connection", "order": 1},
37
+ "ui": {
38
+ "widget": "text",
39
+ "group": "connection",
40
+ "order": 1,
41
+ "placeholder": {"i18n": "matrix.homeserver.ph", "default": "https://matrix.org"},
42
+ },
27
43
  },
28
44
  )
29
45
  access_token: str = field(
30
46
  default="",
31
47
  metadata={
32
- "description": "Access token (alternative to user_id/password)",
48
+ "description": {"i18n": "matrix.access_token", "default": "Access Token(替代 user_id/password)"},
33
49
  "secret": True,
34
- "webui": {"widget": "password", "group": "basic", "order": 2},
50
+ "ui": {"widget": "password", "group": "basic", "order": 2},
35
51
  },
36
52
  )
37
53
  user_id: str = field(
38
54
  default="",
39
55
  metadata={
40
- "description": "Username (used for password login)",
41
- "webui": {"widget": "text", "group": "basic", "order": 3},
56
+ "description": {"i18n": "matrix.user_id", "default": "用户名(用于密码登录)"},
57
+ "ui": {"widget": "text", "group": "basic", "order": 3},
42
58
  },
43
59
  )
44
60
  password: str = field(
45
61
  default="",
46
62
  metadata={
47
- "description": "Password (used for password login)",
63
+ "description": {"i18n": "matrix.password", "default": "密码(用于密码登录)"},
48
64
  "secret": True,
49
- "webui": {"widget": "password", "group": "basic", "order": 4},
65
+ "ui": {"widget": "password", "group": "basic", "order": 4},
50
66
  },
51
67
  )
52
68
  auto_accept_invites: bool = field(
53
69
  default=True,
54
70
  metadata={
55
- "description": "Automatically accept room invitations",
56
- "webui": {"widget": "switch", "group": "behavior", "order": 5},
71
+ "description": {"i18n": "matrix.auto_accept_invites", "default": "自动接受房间邀请"},
72
+ "ui": {"widget": "switch", "group": "behavior", "order": 5},
57
73
  },
58
74
  )
59
75
 
60
76
 
77
+ MatrixAccountConfig._schema_meta = {
78
+ "group_labels": {
79
+ "basic": {"i18n": "matrix.group.basic", "default": "基本设置"},
80
+ "connection": {"i18n": "matrix.group.connection", "default": "连接设置"},
81
+ "behavior": {"i18n": "matrix.group.behavior", "default": "行为设置"},
82
+ }
83
+ } # type: ignore[attr-defined]
84
+
85
+
61
86
  # ============================================================================
62
87
  # 事件 Mixin(平台扩展方法)
63
88
  # ============================================================================
@@ -316,10 +341,190 @@ class MatrixAdapter(BaseAdapter):
316
341
  self._heartbeat_meta_tasks: Dict[str, asyncio.Task] = {}
317
342
  self._converters: Dict[str, MatrixConverter] = {}
318
343
  self._running = False
344
+ self._message_targets: Dict[str, str] = {}
345
+ self._register_i18n()
346
+ self._check_framework_version()
347
+ self._get_logger().info(f"MatrixAdapter v{__version__} 已加载")
348
+
349
+ @staticmethod
350
+ def _parse_version(version_str: str) -> tuple:
351
+ """解析版本号为可比较的三元组(忽略 dev/预发布后缀,如 2.8.0-dev.3 → (2, 8, 0))"""
352
+ parts = []
353
+ for piece in str(version_str).split("."):
354
+ digits = "".join(ch for ch in piece if ch.isdigit())
355
+ parts.append(int(digits) if digits else 0)
356
+ while len(parts) < 3:
357
+ parts.append(0)
358
+ return tuple(parts[:3])
359
+
360
+ def _check_framework_version(self):
361
+ """软依赖检测:框架版本过低时打警告(不阻断加载)"""
362
+ try:
363
+ from importlib.metadata import version as _pkg_version
364
+
365
+ raw = _pkg_version("ErisPulse")
366
+ except Exception:
367
+ return
368
+ try:
369
+ if self._parse_version(raw) < MIN_FRAMEWORK_VERSION:
370
+ self._get_logger().warning(
371
+ f"当前 ErisPulse 版本 {raw} 过低:MatrixAdapter v{__version__} 需要 >= "
372
+ f"{'.'.join(map(str, MIN_FRAMEWORK_VERSION))}"
373
+ "(BaseConverter / Api DSL / spawn_background 等特性),"
374
+ "部分功能可能不可用,建议升级框架"
375
+ )
376
+ except Exception:
377
+ pass
378
+
379
+ def _register_i18n(self):
380
+ """注册配置字段与日志消息的 i18n 翻译"""
381
+ try:
382
+ from ErisPulse.runtime.config_schema import register_config_i18n
383
+ register_config_i18n(MatrixAccountConfig, "zh-CN", domain="matrix")
384
+ register_config_i18n(MatrixAccountConfig, "en", {
385
+ "matrix.homeserver": "Matrix server address",
386
+ "matrix.homeserver.ph": "https://matrix.org",
387
+ "matrix.access_token": "Access Token (alternative to user_id/password)",
388
+ "matrix.user_id": "Username (used for password login)",
389
+ "matrix.password": "Password (used for password login)",
390
+ "matrix.auto_accept_invites": "Automatically accept room invitations",
391
+ "matrix.group.basic": "Basic",
392
+ "matrix.group.connection": "Connection",
393
+ "matrix.group.behavior": "Behavior",
394
+ }, domain="matrix")
395
+ except Exception:
396
+ pass
397
+
398
+ zh_CN = {
399
+ "matrix.old_config_detected": "检测到旧格式单账户配置,建议迁移到 Matrix_Adapter.accounts.default",
400
+ "matrix.save_default_failed": "保存默认配置失败: {error}",
401
+ "matrix.missing_credentials": "账户 '{name}' 缺少 access_token/user_id,已跳过",
402
+ "matrix.account_disabled": "账户 '{name}' 已加载但未启用(enabled=false),请在配置中将 [Matrix_Adapter.accounts.{name}] 的 enabled 设为 true",
403
+ "matrix.accounts_loaded": "Matrix适配器初始化完成,共加载 {count} 个账户",
404
+ "matrix.authenticated": "账户 {name} Matrix 已认证: {bot_id}",
405
+ "matrix.login_success": "账户 {name} Matrix 登录成功: {bot_id}",
406
+ "matrix.login_failed": "登录失败: {message}",
407
+ "matrix.login_error": "账户 {name} Matrix 登录失败: {error}",
408
+ "matrix.initial_sync_failed_nologin": "账户 {name} 初始同步失败且无法重新登录,停止同步",
409
+ "matrix.initial_sync_still_failed": "账户 {name} 重新登录后初始同步仍失败,停止同步",
410
+ "matrix.auth_failed": "账户 {name} 认证失败 ({count}/{max}): {message}",
411
+ "matrix.relogin_success": "账户 {name} 重新登录成功,继续同步",
412
+ "matrix.sync_failed": "账户 {name} 同步失败 ({count}/{max}): {message}",
413
+ "matrix.max_failures_stop": "账户 {name} 连续 {max} 次失败,停止同步循环",
414
+ "matrix.sync_loop_error": "账户 {name} 同步循环异常: {error}",
415
+ "matrix.max_exceptions_stop": "账户 {name} 连续 {max} 次异常,停止同步循环",
416
+ "matrix.relogin_attempt": "账户 {name} 尝试重新登录...",
417
+ "matrix.relogin_done": "账户 {name} 重新登录成功",
418
+ "matrix.relogin_failed": "账户 {name} 重新登录失败: {error}",
419
+ "matrix.initial_sync_done": "账户 {name} 初始同步完成, next_batch: {batch}",
420
+ "matrix.initial_sync_failed": "账户 {name} 初始同步失败: {message}",
421
+ "matrix.dm_discovered": "账户 {name} 发现 {count} 个 DM 房间",
422
+ "matrix.process_event": "账户 {name} 处理 Matrix 事件: {event}",
423
+ "matrix.process_event_failed": "处理事件失败: {error}",
424
+ "matrix.room_invite": "账户 {name} 收到房间邀请: {room}",
425
+ "matrix.room_joined": "账户 {name} 已自动加入房间: {room}",
426
+ "matrix.join_room_failed": "加入房间失败: {error}",
427
+ "matrix.download_failed": "账户 {name} 下载文件失败: HTTP {status}",
428
+ "matrix.download_error": "账户 {name} 下载文件异常: {error}",
429
+ "matrix.account_not_found": "账户 {name} 不存在,无法上传媒体",
430
+ "matrix.upload_failed": "账户 {name} 上传媒体失败: {data}",
431
+ "matrix.upload_error": "账户 {name} 上传媒体异常: {error}",
432
+ "matrix.api_timeout": "账户 {name} Matrix API 请求超时: {endpoint}",
433
+ "matrix.timeout_msg": "请求超时",
434
+ "matrix.api_call_failed": "账户 {name} 调用 Matrix API 失败: {error}",
435
+ "matrix.api_error": "API调用失败: {error}",
436
+ "matrix.no_enabled_accounts": "没有已启用的账户,Matrix 适配器将以空闲状态启动",
437
+ "matrix.no_bot_id": "账户 {name} 无法获取 bot user_id,请检查配置",
438
+ "matrix.account_started": "账户 {name} (bot_id: {bot_id}) Matrix 已启动",
439
+ "matrix.adapter_started": "Matrix 适配器启动完成,共 {count} 个账户",
440
+ "matrix.adapter_shutdown": "Matrix 适配器已关闭",
441
+ }
442
+ en = {
443
+ "matrix.old_config_detected": "Old single-account config detected, please migrate to Matrix_Adapter.accounts.default",
444
+ "matrix.save_default_failed": "Failed to save default config: {error}",
445
+ "matrix.missing_credentials": "Account '{name}' missing access_token/user_id, skipped",
446
+ "matrix.account_disabled": "Account '{name}' loaded but disabled (enabled=false), please set enabled=true in [Matrix_Adapter.accounts.{name}]",
447
+ "matrix.accounts_loaded": "Matrix adapter initialized, loaded {count} account(s)",
448
+ "matrix.authenticated": "Account {name} Matrix authenticated: {bot_id}",
449
+ "matrix.login_success": "Account {name} Matrix login success: {bot_id}",
450
+ "matrix.login_failed": "Login failed: {message}",
451
+ "matrix.login_error": "Account {name} Matrix login failed: {error}",
452
+ "matrix.initial_sync_failed_nologin": "Account {name} initial sync failed and re-login not possible, stopping sync",
453
+ "matrix.initial_sync_still_failed": "Account {name} initial sync still failing after re-login, stopping sync",
454
+ "matrix.auth_failed": "Account {name} auth failed ({count}/{max}): {message}",
455
+ "matrix.relogin_success": "Account {name} re-login success, resuming sync",
456
+ "matrix.sync_failed": "Account {name} sync failed ({count}/{max}): {message}",
457
+ "matrix.max_failures_stop": "Account {name} failed {max} consecutive times, stopping sync loop",
458
+ "matrix.sync_loop_error": "Account {name} sync loop error: {error}",
459
+ "matrix.max_exceptions_stop": "Account {name} {max} consecutive exceptions, stopping sync loop",
460
+ "matrix.relogin_attempt": "Account {name} attempting re-login...",
461
+ "matrix.relogin_done": "Account {name} re-login success",
462
+ "matrix.relogin_failed": "Account {name} re-login failed: {error}",
463
+ "matrix.initial_sync_done": "Account {name} initial sync done, next_batch: {batch}",
464
+ "matrix.initial_sync_failed": "Account {name} initial sync failed: {message}",
465
+ "matrix.dm_discovered": "Account {name} discovered {count} DM room(s)",
466
+ "matrix.process_event": "Account {name} processing Matrix event: {event}",
467
+ "matrix.process_event_failed": "Failed to process event: {error}",
468
+ "matrix.room_invite": "Account {name} received room invite: {room}",
469
+ "matrix.room_joined": "Account {name} auto-joined room: {room}",
470
+ "matrix.join_room_failed": "Failed to join room: {error}",
471
+ "matrix.download_failed": "Account {name} download failed: HTTP {status}",
472
+ "matrix.download_error": "Account {name} download error: {error}",
473
+ "matrix.account_not_found": "Account {name} not found, cannot upload media",
474
+ "matrix.upload_failed": "Account {name} upload media failed: {data}",
475
+ "matrix.upload_error": "Account {name} upload media error: {error}",
476
+ "matrix.api_timeout": "Account {name} Matrix API request timeout: {endpoint}",
477
+ "matrix.timeout_msg": "Request timeout",
478
+ "matrix.api_call_failed": "Account {name} Matrix API call failed: {error}",
479
+ "matrix.api_error": "API call failed: {error}",
480
+ "matrix.no_enabled_accounts": "No enabled accounts, Matrix adapter will start idle",
481
+ "matrix.no_bot_id": "Account {name} could not obtain bot user_id, please check config",
482
+ "matrix.account_started": "Account {name} (bot_id: {bot_id}) Matrix started",
483
+ "matrix.adapter_started": "Matrix adapter started, {count} account(s) total",
484
+ "matrix.adapter_shutdown": "Matrix adapter shut down",
485
+ }
486
+ try:
487
+ i18n.register("zh-CN", zh_CN, domain="matrix")
488
+ i18n.register("en", en, domain="matrix")
489
+ except Exception:
490
+ pass
319
491
 
320
492
  def _get_config_key(self) -> str:
321
493
  return "Matrix_Adapter"
322
494
 
495
+ async def _delete_message_by_id(self, message_id: str, account_id: Optional[str] = None) -> dict:
496
+ """按消息登记表撤回消息(Matrix redact 需要 room_id + event_id)"""
497
+ room_id = self._message_targets.get(message_id)
498
+ if not room_id:
499
+ return self.make_error(retcode=34001, message=f"未找到消息 {message_id} 的目标上下文,无法撤回")
500
+ import uuid as _uuid
501
+
502
+ txn_id = _uuid.uuid4().hex
503
+ return await self.call_api(
504
+ f"/_matrix/client/v3/rooms/{room_id}/redact/{message_id}/{txn_id}",
505
+ method="PUT", _account_id=account_id,
506
+ )
507
+
508
+ def _migrate_legacy_config(self):
509
+ """将旧版单账户扁平配置(access_token 直接在 [Matrix_Adapter] 下)迁移到 accounts 结构"""
510
+ try:
511
+ from ErisPulse.Core import config as config_mgr
512
+
513
+ key = self._get_config_key()
514
+ data = config_mgr.getConfig(key)
515
+ if not isinstance(data, dict):
516
+ return
517
+ accounts = data.get("accounts")
518
+ if isinstance(accounts, dict) and accounts:
519
+ return
520
+ if not data.get("access_token"):
521
+ return
522
+ data["accounts"] = {"default": {**data, "enabled": True}}
523
+ config_mgr.setConfig(key, data)
524
+ self.logger.info("已将旧版单账户配置迁移到 accounts.default")
525
+ except Exception as e:
526
+ self.logger.debug(f"旧配置迁移检查跳过: {e}")
527
+
323
528
  def _get_runtime(self, name: str) -> dict:
324
529
  if name not in self._account_runtime:
325
530
  self._account_runtime[name] = {
@@ -330,52 +535,8 @@ class MatrixAdapter(BaseAdapter):
330
535
  }
331
536
  return self._account_runtime[name]
332
537
 
333
- def _load_accounts(self) -> dict:
334
- from ErisPulse.Core.config import config as config_mgr
335
- from ErisPulse.runtime.config_schema import dict_to_dataclass
336
-
337
- key = "Matrix_Adapter.accounts"
338
- data = config_mgr.getConfig(key)
339
- if not data:
340
- # 兼容旧配置:如果存在旧的单账户 Matrix_Adapter 配置且有 access_token,迁移为 default 账户
341
- old = config_mgr.getConfig("Matrix_Adapter")
342
- if old and old.get("access_token"):
343
- self.logger.warning(
344
- "检测到旧格式单账户配置,建议迁移到 Matrix_Adapter.accounts.default"
345
- )
346
- data = {"default": {**old, "enabled": True}}
347
- else:
348
- data = {
349
- "default": {
350
- "homeserver": "https://matrix.org",
351
- "access_token": "",
352
- "user_id": "",
353
- "password": "",
354
- "auto_accept_invites": True,
355
- "enabled": True,
356
- }
357
- }
358
- try:
359
- config_mgr.setConfig(key, data)
360
- except Exception as e:
361
- self.logger.error(f"保存默认配置失败: {e}")
362
- accounts = {}
363
- for name, account_data in data.items():
364
- if not isinstance(account_data, dict):
365
- continue
366
- if not account_data.get("access_token") and not account_data.get("user_id"):
367
- self.logger.warning(f"账户 '{name}' 缺少 access_token/user_id,已跳过")
368
- continue
369
- instance = dict_to_dataclass(MatrixAccountConfig, account_data)
370
- instance.name = name
371
- if not instance.enabled:
372
- self.logger.warning(
373
- f"账户 '{name}' 已加载但未启用(enabled=false),"
374
- f"请在配置中将 [Matrix_Adapter.accounts.{name}] 的 enabled 设为 true"
375
- )
376
- accounts[name] = instance
377
- self.logger.info(f"Matrix适配器初始化完成,共加载 {len(accounts)} 个账户")
378
- return accounts
538
+ # 账户加载使用框架默认实现(AccountConfigClass 模板 + 校验);
539
+ # 旧版单账户扁平配置迁移见 _migrate_legacy_config;缺少凭据的账户由 _login_if_needed 失败自然跳过。
379
540
 
380
541
  async def _login_if_needed(self, account_name: str, account: MatrixAccountConfig):
381
542
  runtime = self._get_runtime(account_name)
@@ -392,10 +553,9 @@ class MatrixAdapter(BaseAdapter):
392
553
  bot_id = result["data"].get("user_id", "")
393
554
  runtime["bot_id"] = bot_id
394
555
  runtime["access_token"] = account.access_token
395
- account.bot_id = bot_id
396
556
  if converter:
397
557
  converter.bot_user_id = bot_id
398
- self.logger.info(f"账户 {account_name} Matrix 已认证: {bot_id}")
558
+ self.logger.info(i18n.t("matrix.authenticated", name=account_name, bot_id=bot_id, default="账户 {name} Matrix 已认证: {bot_id}"))
399
559
  return
400
560
  except Exception:
401
561
  pass
@@ -416,20 +576,18 @@ class MatrixAdapter(BaseAdapter):
416
576
  if result.get("status") == "ok" and result.get("data"):
417
577
  token = result["data"].get("access_token", "")
418
578
  bot_id = result["data"].get("user_id", account.user_id)
419
- # 密码登录获得的 token 需回填到账户实例,供后续 call_api 使用
420
- account.access_token = token
579
+ # 密码登录获得的 token 写入运行时状态,供后续 call_api 使用
421
580
  runtime["access_token"] = token
422
581
  runtime["bot_id"] = bot_id
423
- account.bot_id = bot_id
424
582
  if converter:
425
583
  converter.bot_user_id = bot_id
426
- self.logger.info(f"账户 {account_name} Matrix 登录成功: {bot_id}")
584
+ self.logger.info(i18n.t("matrix.login_success", name=account_name, bot_id=bot_id, default="账户 {name} Matrix 登录成功: {bot_id}"))
427
585
  else:
428
586
  raise Exception(
429
- f"登录失败: {result.get('message', 'Unknown error')}"
587
+ i18n.t("matrix.login_failed", message=result.get('message', 'Unknown error'), default="登录失败: {message}")
430
588
  )
431
589
  except Exception as e:
432
- self.logger.error(f"账户 {account_name} Matrix 登录失败: {e}")
590
+ self.logger.error(i18n.t("matrix.login_error", name=account_name, error=e, default="账户 {name} Matrix 登录失败: {error}"))
433
591
  raise
434
592
 
435
593
  async def _sync_loop(self, account_name: str):
@@ -440,13 +598,13 @@ class MatrixAdapter(BaseAdapter):
440
598
  if not await self._initial_sync(account_name):
441
599
  if not await self._try_relogin(account_name, account):
442
600
  self.logger.error(
443
- f"账户 {account_name} 初始同步失败且无法重新登录,停止同步"
601
+ i18n.t("matrix.initial_sync_failed_nologin", name=account_name, default="账户 {name} 初始同步失败且无法重新登录,停止同步")
444
602
  )
445
603
  await self._on_sync_loop_exit(account_name)
446
604
  return
447
605
  if not await self._initial_sync(account_name):
448
606
  self.logger.error(
449
- f"账户 {account_name} 重新登录后初始同步仍失败,停止同步"
607
+ i18n.t("matrix.initial_sync_still_failed", name=account_name, default="账户 {name} 重新登录后初始同步仍失败,停止同步")
450
608
  )
451
609
  await self._on_sync_loop_exit(account_name)
452
610
  return
@@ -476,23 +634,23 @@ class MatrixAdapter(BaseAdapter):
476
634
  if is_auth_error:
477
635
  consecutive_failures += 1
478
636
  self.logger.warning(
479
- f"账户 {account_name} 认证失败 ({consecutive_failures}/{max_failures}): {message}"
637
+ i18n.t("matrix.auth_failed", name=account_name, count=consecutive_failures, max=max_failures, message=message, default="账户 {name} 认证失败 ({count}/{max}): {message}")
480
638
  )
481
639
  if await self._try_relogin(account_name, account):
482
640
  self.logger.info(
483
- f"账户 {account_name} 重新登录成功,继续同步"
641
+ i18n.t("matrix.relogin_success", name=account_name, default="账户 {name} 重新登录成功,继续同步")
484
642
  )
485
643
  consecutive_failures = 0
486
644
  continue
487
645
  else:
488
646
  consecutive_failures += 1
489
647
  self.logger.error(
490
- f"账户 {account_name} 同步失败 ({consecutive_failures}/{max_failures}): {message}"
648
+ i18n.t("matrix.sync_failed", name=account_name, count=consecutive_failures, max=max_failures, message=message, default="账户 {name} 同步失败 ({count}/{max}): {message}")
491
649
  )
492
650
 
493
651
  if consecutive_failures >= max_failures:
494
652
  self.logger.error(
495
- f"账户 {account_name} 连续 {max_failures} 次失败,停止同步循环"
653
+ i18n.t("matrix.max_failures_stop", name=account_name, max=max_failures, default="账户 {name} 连续 {max} 次失败,停止同步循环")
496
654
  )
497
655
  break
498
656
 
@@ -513,11 +671,11 @@ class MatrixAdapter(BaseAdapter):
513
671
  except asyncio.CancelledError:
514
672
  break
515
673
  except Exception as e:
516
- self.logger.error(f"账户 {account_name} 同步循环异常: {e}")
674
+ self.logger.error(i18n.t("matrix.sync_loop_error", name=account_name, error=e, default="账户 {name} 同步循环异常: {error}"))
517
675
  consecutive_failures += 1
518
676
  if consecutive_failures >= max_failures:
519
677
  self.logger.error(
520
- f"账户 {account_name} 连续 {max_failures} 次异常,停止同步循环"
678
+ i18n.t("matrix.max_exceptions_stop", name=account_name, max=max_failures, default="账户 {name} 连续 {max} 次异常,停止同步循环")
521
679
  )
522
680
  break
523
681
  await asyncio.sleep(min(5 * consecutive_failures, 30))
@@ -529,7 +687,7 @@ class MatrixAdapter(BaseAdapter):
529
687
  if not account or not account.user_id or not account.password:
530
688
  return False
531
689
  try:
532
- self.logger.info(f"账户 {account_name} 尝试重新登录...")
690
+ self.logger.info(i18n.t("matrix.relogin_attempt", name=account_name, default="账户 {name} 尝试重新登录..."))
533
691
  # 清空旧 token,强制走密码登录路径
534
692
  old_token = account.access_token
535
693
  account.access_token = ""
@@ -539,10 +697,10 @@ class MatrixAdapter(BaseAdapter):
539
697
  # 恢复旧 token,避免状态污染
540
698
  account.access_token = old_token
541
699
  raise
542
- self.logger.info(f"账户 {account_name} 重新登录成功")
700
+ self.logger.info(i18n.t("matrix.relogin_done", name=account_name, default="账户 {name} 重新登录成功"))
543
701
  return True
544
702
  except Exception as e:
545
- self.logger.error(f"账户 {account_name} 重新登录失败: {e}")
703
+ self.logger.error(i18n.t("matrix.relogin_failed", name=account_name, error=e, default="账户 {name} 重新登录失败: {error}"))
546
704
  return False
547
705
 
548
706
  async def _on_sync_loop_exit(self, account_name: str):
@@ -572,12 +730,12 @@ class MatrixAdapter(BaseAdapter):
572
730
  data = result["data"]
573
731
  runtime["_next_batch"] = data.get("next_batch", "")
574
732
  self.logger.info(
575
- f"账户 {account_name} 初始同步完成, next_batch: {runtime.get('_next_batch')}"
733
+ i18n.t("matrix.initial_sync_done", name=account_name, batch=runtime.get('_next_batch'), default="账户 {name} 初始同步完成, next_batch: {batch}")
576
734
  )
577
735
  return True
578
736
  else:
579
737
  self.logger.warning(
580
- f"账户 {account_name} 初始同步失败: {result.get('message', 'Unknown error')}"
738
+ i18n.t("matrix.initial_sync_failed", name=account_name, message=result.get('message', 'Unknown error'), default="账户 {name} 初始同步失败: {message}")
581
739
  )
582
740
  return False
583
741
 
@@ -599,7 +757,7 @@ class MatrixAdapter(BaseAdapter):
599
757
  runtime["_dm_rooms"] = dm_rooms
600
758
  if converter:
601
759
  converter.set_dm_rooms(dm_rooms)
602
- self.logger.info(f"账户 {account_name} 发现 {len(dm_rooms)} 个 DM 房间")
760
+ self.logger.info(i18n.t("matrix.dm_discovered", name=account_name, count=len(dm_rooms), default="账户 {name} 发现 {count} 个 DM 房间"))
603
761
 
604
762
  async def _process_sync_response(self, account_name: str, data: dict):
605
763
  runtime = self._get_runtime(account_name)
@@ -614,7 +772,7 @@ class MatrixAdapter(BaseAdapter):
614
772
  events = timeline.get("events", [])
615
773
 
616
774
  for event in events:
617
- self.logger.debug(f"账户 {account_name} 处理 Matrix 事件: {event}")
775
+ self.logger.debug(i18n.t("matrix.process_event", name=account_name, event=event, default="账户 {name} 处理 Matrix 事件: {event}"))
618
776
  try:
619
777
  if converter:
620
778
  onebot_event = converter.convert(
@@ -623,9 +781,13 @@ class MatrixAdapter(BaseAdapter):
623
781
  else:
624
782
  onebot_event = None
625
783
  if onebot_event:
784
+ if onebot_event.get("type") == "message" and onebot_event.get("message_id"):
785
+ self._message_targets[str(onebot_event["message_id"])] = str(room_id)
786
+ while len(self._message_targets) > 800:
787
+ self._message_targets.pop(next(iter(self._message_targets)), None)
626
788
  await self.sdk.adapter.emit(onebot_event)
627
789
  except Exception as e:
628
- self.logger.error(f"处理事件失败: {e}")
790
+ self.logger.error(i18n.t("matrix.process_event_failed", error=e, default="处理事件失败: {error}"))
629
791
 
630
792
  invite_rooms = data.get("rooms", {}).get("invite", {})
631
793
  for room_id, room_data in invite_rooms.items():
@@ -637,7 +799,7 @@ class MatrixAdapter(BaseAdapter):
637
799
  content.get("membership") == "invite"
638
800
  and event.get("state_key") == bot_id
639
801
  ):
640
- self.logger.info(f"账户 {account_name} 收到房间邀请: {room_id}")
802
+ self.logger.info(i18n.t("matrix.room_invite", name=account_name, room=room_id, default="账户 {name} 收到房间邀请: {room}"))
641
803
  if getattr(
642
804
  self.accounts.get(account_name), "auto_accept_invites", True
643
805
  ):
@@ -648,17 +810,17 @@ class MatrixAdapter(BaseAdapter):
648
810
  _account_id=account_name,
649
811
  )
650
812
  self.logger.info(
651
- f"账户 {account_name} 已自动加入房间: {room_id}"
813
+ i18n.t("matrix.room_joined", name=account_name, room=room_id, default="账户 {name} 已自动加入房间: {room}")
652
814
  )
653
815
  except Exception as e:
654
- self.logger.error(f"加入房间失败: {e}")
816
+ self.logger.error(i18n.t("matrix.join_room_failed", error=e, default="加入房间失败: {error}"))
655
817
 
656
818
  async def _download_file(self, account_name: str, url: str) -> Optional[tuple]:
657
819
  try:
658
820
  resp = await client.get(url, timeout=300)
659
821
  if resp.status != 200:
660
822
  self.logger.error(
661
- f"账户 {account_name} 下载文件失败: HTTP {resp.status}"
823
+ i18n.t("matrix.download_failed", name=account_name, status=resp.status, default="账户 {name} 下载文件失败: HTTP {status}")
662
824
  )
663
825
  return None
664
826
  content_type = (
@@ -667,7 +829,7 @@ class MatrixAdapter(BaseAdapter):
667
829
  data = await resp.read()
668
830
  return (data, content_type)
669
831
  except Exception as e:
670
- self.logger.error(f"账户 {account_name} 下载文件异常: {e}")
832
+ self.logger.error(i18n.t("matrix.download_error", name=account_name, error=e, default="账户 {name} 下载文件异常: {error}"))
671
833
  return None
672
834
 
673
835
  async def _upload_media(
@@ -679,7 +841,7 @@ class MatrixAdapter(BaseAdapter):
679
841
  ) -> Optional[str]:
680
842
  account = self.accounts.get(account_name)
681
843
  if not account:
682
- self.logger.error(f"账户 {account_name} 不存在,无法上传媒体")
844
+ self.logger.error(i18n.t("matrix.account_not_found", name=account_name, default="账户 {name} 不存在,无法上传媒体"))
683
845
  return None
684
846
  if not content_type:
685
847
  content_type_map = {
@@ -692,8 +854,11 @@ class MatrixAdapter(BaseAdapter):
692
854
 
693
855
  homeserver = (account.homeserver or "https://matrix.org").rstrip("/")
694
856
  url = f"{homeserver}/_matrix/media/v3/upload"
857
+ # 优先使用运行时 token(同 call_api)
858
+ runtime = self._get_runtime(account_name)
859
+ access_token = runtime.get("access_token") or account.access_token
695
860
  headers = {
696
- "Authorization": f"Bearer {account.access_token}",
861
+ "Authorization": f"Bearer {access_token}",
697
862
  "Content-Type": content_type,
698
863
  }
699
864
 
@@ -703,10 +868,10 @@ class MatrixAdapter(BaseAdapter):
703
868
  if resp.status == 200:
704
869
  return data.get("content_uri", "")
705
870
  else:
706
- self.logger.error(f"账户 {account_name} 上传媒体失败: {data}")
871
+ self.logger.error(i18n.t("matrix.upload_failed", name=account_name, data=data, default="账户 {name} 上传媒体失败: {data}"))
707
872
  return None
708
873
  except Exception as e:
709
- self.logger.error(f"账户 {account_name} 上传媒体异常: {e}")
874
+ self.logger.error(i18n.t("matrix.upload_error", name=account_name, error=e, default="账户 {name} 上传媒体异常: {error}"))
710
875
  return None
711
876
 
712
877
  async def call_api(
@@ -715,8 +880,11 @@ class MatrixAdapter(BaseAdapter):
715
880
  account_name, account = self._resolve_account(_account_id)
716
881
  homeserver = (account.homeserver or "https://matrix.org").rstrip("/")
717
882
  url = f"{homeserver}{endpoint}"
883
+ # 优先使用运行时 token(密码登录后获得),避免 self.accounts 每次重新读取配置导致 token 丢失
884
+ runtime = self._get_runtime(account_name)
885
+ access_token = runtime.get("access_token") or account.access_token
718
886
  headers = {
719
- "Authorization": f"Bearer {account.access_token}",
887
+ "Authorization": f"Bearer {access_token}",
720
888
  "Content-Type": "application/json",
721
889
  }
722
890
  echo = params.pop("echo", None)
@@ -773,15 +941,15 @@ class MatrixAdapter(BaseAdapter):
773
941
  return response
774
942
 
775
943
  except asyncio.TimeoutError:
776
- self.logger.error(f"账户 {account_name} Matrix API 请求超时: {endpoint}")
777
- err = self.make_error(retcode=32000, message="请求超时", raw=None)
944
+ self.logger.error(i18n.t("matrix.api_timeout", name=account_name, endpoint=endpoint, default="账户 {name} Matrix API 请求超时: {endpoint}"))
945
+ err = self.make_error(retcode=32000, message=i18n.t("matrix.timeout_msg", default="请求超时"), raw=None)
778
946
  if echo:
779
947
  err["echo"] = echo
780
948
  return err
781
949
  except Exception as e:
782
- self.logger.error(f"账户 {account_name} 调用 Matrix API 失败: {e}")
950
+ self.logger.error(i18n.t("matrix.api_call_failed", name=account_name, error=e, default="账户 {name} 调用 Matrix API 失败: {error}"))
783
951
  err = self.make_error(
784
- retcode=33000, message=f"API调用失败: {str(e)}", raw=None
952
+ retcode=33000, message=i18n.t("matrix.api_error", error=str(e), default="API调用失败: {error}"), raw=None
785
953
  )
786
954
  if echo:
787
955
  err["echo"] = echo
@@ -796,11 +964,121 @@ class MatrixAdapter(BaseAdapter):
796
964
  except asyncio.CancelledError:
797
965
  pass
798
966
 
967
+ # ==================== Api DSL ====================
968
+
969
+ class Api(BaseAdapter.Api):
970
+ """Matrix 标准 API 动作实现(ApiDSL,基于 Matrix Client-Server API)
971
+
972
+ {!--< tips >!--}
973
+ 1. get_self_info → GET /account/whoami
974
+ 2. get_user_info → GET /profile/{userId}
975
+ 3. get_group_info → GET /rooms/{roomId}/state/m.room.name
976
+ 4. get_group_list → GET /joined_rooms;get_group_member_list → GET /rooms/{roomId}/members
977
+ 5. delete_message → PUT /rooms/{roomId}/redact/{eventId}/{txn}(登记表补全 roomId)
978
+ 6. leave_group → POST /rooms/{roomId}/leave
979
+ {!--< /tips >!--}
980
+ """
981
+
982
+ async def get_self_info(self) -> dict:
983
+ r = await self._adapter.call_api(
984
+ "/_matrix/client/v3/account/whoami", method="GET",
985
+ _account_id=self._account_id,
986
+ )
987
+ if r.get("status") != "ok":
988
+ return r
989
+ u = r.get("data") or {}
990
+ r["data"] = {"user_id": str(u.get("user_id", "")), "user_name": str(u.get("user_id", "")).split(":")[0].lstrip("@")}
991
+ return r
992
+
993
+ async def get_user_info(self, user_id: str) -> dict:
994
+ r = await self._adapter.call_api(
995
+ f"/_matrix/client/v3/profile/{user_id}", method="GET",
996
+ _account_id=self._account_id,
997
+ )
998
+ if r.get("status") != "ok":
999
+ return r
1000
+ u = r.get("data") or {}
1001
+ r["data"] = {
1002
+ "user_id": str(user_id),
1003
+ "user_name": u.get("displayname", ""),
1004
+ "user_avatar": u.get("avatar_url", ""),
1005
+ }
1006
+ return r
1007
+
1008
+ async def get_group_info(self, room_id: str) -> dict:
1009
+ r = await self._adapter.call_api(
1010
+ f"/_matrix/client/v3/rooms/{room_id}/state/m.room.name", method="GET",
1011
+ _account_id=self._account_id,
1012
+ )
1013
+ if r.get("status") != "ok":
1014
+ return r
1015
+ r["data"] = {"group_id": str(room_id), "group_name": (r.get("data") or {}).get("name", "")}
1016
+ return r
1017
+
1018
+ async def get_group_list(self) -> dict:
1019
+ r = await self._adapter.call_api(
1020
+ "/_matrix/client/v3/joined_rooms", method="GET", _account_id=self._account_id
1021
+ )
1022
+ if r.get("status") != "ok":
1023
+ return r
1024
+ rooms = (r.get("data") or {}).get("joined_rooms", []) or []
1025
+ r["data"] = [{"group_id": str(x)} for x in rooms]
1026
+ return r
1027
+
1028
+ async def get_group_member_list(self, room_id: str) -> dict:
1029
+ r = await self._adapter.call_api(
1030
+ f"/_matrix/client/v3/rooms/{room_id}/members",
1031
+ method="GET", _account_id=self._account_id,
1032
+ )
1033
+ if r.get("status") != "ok":
1034
+ return r
1035
+ chunk = (r.get("data") or {}).get("chunk", []) or []
1036
+ members = []
1037
+ for m in chunk:
1038
+ if isinstance(m, dict) and m.get("sender"):
1039
+ members.append({"user_id": str(m["sender"])})
1040
+ r["data"] = members
1041
+ return r
1042
+
1043
+ async def leave_group(self, room_id: str) -> dict:
1044
+ return await self._adapter.call_api(
1045
+ f"/_matrix/client/v3/rooms/{room_id}/leave", method="POST",
1046
+ _account_id=self._account_id,
1047
+ )
1048
+
1049
+ async def delete_message(self, message_id: str) -> dict:
1050
+ return await self._adapter._delete_message_by_id(str(message_id), account_id=self._account_id)
1051
+
1052
+ async def get_status(self) -> dict:
1053
+ ad = self._adapter
1054
+ bots = []
1055
+ for name, runtime in ad._account_runtime.items():
1056
+ bots.append({
1057
+ "self": {"platform": ad.platform, "user_id": runtime.get("bot_id", ""), "account_id": name},
1058
+ "online": bool(runtime.get("bot_id")),
1059
+ })
1060
+ return ad.make_response(data={"good": any(b["online"] for b in bots), "bots": bots})
1061
+
1062
+ async def get_version(self) -> dict:
1063
+ from . import __version__
1064
+
1065
+ return self._adapter.make_response(
1066
+ data={"impl": "ErisPulse-MatrixAdapter", "version": __version__, "onebot_version": "12"}
1067
+ )
1068
+
1069
+ async def get_supported_actions(self) -> dict:
1070
+ actions = {
1071
+ "get_self_info", "get_user_info", "get_group_info", "get_group_list",
1072
+ "get_group_member_list", "leave_group", "delete_message",
1073
+ "get_status", "get_version", "get_supported_actions",
1074
+ }
1075
+ return self._adapter.make_response(data=sorted(actions))
1076
+
799
1077
  async def start(self):
800
1078
  self._running = True
801
1079
 
802
1080
  if not self.enabled_accounts:
803
- self.logger.warning("没有已启用的账户,Matrix 适配器将以空闲状态启动")
1081
+ self.logger.warning(i18n.t("matrix.no_enabled_accounts", default="没有已启用的账户,Matrix 适配器将以空闲状态启动"))
804
1082
  return
805
1083
 
806
1084
  for account_name, account in self.enabled_accounts.items():
@@ -819,23 +1097,26 @@ class MatrixAdapter(BaseAdapter):
819
1097
  runtime = self._get_runtime(account_name)
820
1098
  if not runtime.get("bot_id"):
821
1099
  self.logger.error(
822
- f"账户 {account_name} 无法获取 bot user_id,请检查配置"
1100
+ i18n.t("matrix.no_bot_id", name=account_name, default="账户 {name} 无法获取 bot user_id,请检查配置")
823
1101
  )
824
1102
  continue
825
1103
 
826
1104
  await self.emit_meta("connect", runtime.get("bot_id", ""))
827
- self._heartbeat_meta_tasks[account_name] = asyncio.create_task(
828
- self._heartbeat_meta_loop(account_name)
1105
+ hb_coro = self._heartbeat_meta_loop(account_name)
1106
+ sync_coro = self._sync_loop(account_name)
1107
+ # 生命周期任务使用 spawn_background(owner 归属,shutdown 自动回收)
1108
+ self._heartbeat_meta_tasks[account_name] = (
1109
+ spawn_background(hb_coro) if spawn_background is not None else asyncio.create_task(hb_coro)
829
1110
  )
830
- self._sync_tasks[account_name] = asyncio.create_task(
831
- self._sync_loop(account_name)
1111
+ self._sync_tasks[account_name] = (
1112
+ spawn_background(sync_coro) if spawn_background is not None else asyncio.create_task(sync_coro)
832
1113
  )
833
1114
  self.logger.info(
834
- f"账户 {account_name} (bot_id: {runtime.get('bot_id')}) Matrix 已启动"
1115
+ i18n.t("matrix.account_started", name=account_name, bot_id=runtime.get('bot_id'), default="账户 {name} (bot_id: {bot_id}) Matrix 已启动")
835
1116
  )
836
1117
 
837
1118
  self.logger.info(
838
- f"Matrix 适配器启动完成,共 {len(self.enabled_accounts)} 个账户"
1119
+ i18n.t("matrix.adapter_started", count=len(self.enabled_accounts), default="Matrix 适配器启动完成,共 {count} 个账户")
839
1120
  )
840
1121
 
841
1122
  async def shutdown(self):
@@ -870,4 +1151,4 @@ class MatrixAdapter(BaseAdapter):
870
1151
  unregister_platform_event_methods("matrix")
871
1152
  except Exception:
872
1153
  pass
873
- self.logger.info("Matrix 适配器已关闭")
1154
+ self.logger.info(i18n.t("matrix.adapter_shutdown", default="Matrix 适配器已关闭"))
@@ -0,0 +1,3 @@
1
+ from .Core import MatrixAdapter, __version__
2
+
3
+ __all__ = ["MatrixAdapter", "__version__"]
@@ -1,15 +1,34 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ErisPulse-MatrixAdapter
3
- Version: 4.0.2
3
+ Version: 4.2.0
4
4
  Summary: ErisPulse的Matrix协议适配模块
5
5
  Author-email: wsu2059 <wsu2059@qq.com>
6
6
  Project-URL: homepage, https://github.com/ErisPulse/ErisPulse-MatrixAdapter
7
7
  Requires-Python: >=3.9
8
8
  Description-Content-Type: text/markdown
9
9
 
10
- # ErisPulse Matrix Adapter
10
+ <div align="center">
11
11
 
12
- [English](#english) | [中文](#中文)
12
+ <img src=".github/assets/ErisPulseLogo.png" width="180" alt="ErisPulse MatrixAdapter" />
13
+
14
+ # ErisPulse MatrixAdapter
15
+
16
+ **Matrix protocol adapter — decentralized chat via Long Polling sync.**
17
+
18
+ A Matrix protocol adapter built on the ErisPulse framework. It receives events via the Long Polling Sync API and unifies private chats, groups and rooms into OneBot12 standard events, with full support for mentions, replies, HTML rich text, reactions and MXC media.
19
+
20
+ <p>
21
+ <a href="https://pypi.org/project/ErisPulse-MatrixAdapter/"><img src="https://img.shields.io/pypi/v/ErisPulse-MatrixAdapter?style=for-the-badge&logo=pypi&logoColor=white" alt="PyPI"></a>
22
+ <a href="https://pypi.org/project/ErisPulse-MatrixAdapter/"><img src="https://img.shields.io/badge/Python-3.10+-FFD43B?style=for-the-badge&logo=python&logoColor=blue" alt="Python"></a>
23
+ <a href="./LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue?style=for-the-badge" alt="License"></a>
24
+ <a href="https://github.com/ErisPulse/ErisPulse-MatrixAdapter"><img src="https://img.shields.io/github/stars/ErisPulse/ErisPulse-MatrixAdapter?style=for-the-badge&logo=github&color=brightgreen" alt="Stars"></a>
25
+ <a href="https://pepy.tech/project/ErisPulse-MatrixAdapter"><img src="https://img.shields.io/pepy/dt/ErisPulse-MatrixAdapter?style=for-the-badge&color=blue" alt="Downloads"></a>
26
+ <a href="https://github.com/ErisPulse/ErisPulse"><img src="https://img.shields.io/badge/Powered_by-ErisPulse-FF6B9D?style=for-the-badge&logo=bookstack&logoColor=white" alt="ErisPulse"></a>
27
+ </p>
28
+
29
+ [English](#english) | [简体中文](#中文)
30
+
31
+ </div>
13
32
 
14
33
  ---
15
34
 
@@ -1,6 +1,25 @@
1
- # ErisPulse Matrix Adapter
1
+ <div align="center">
2
2
 
3
- [English](#english) | [中文](#中文)
3
+ <img src=".github/assets/ErisPulseLogo.png" width="180" alt="ErisPulse MatrixAdapter" />
4
+
5
+ # ErisPulse MatrixAdapter
6
+
7
+ **Matrix protocol adapter — decentralized chat via Long Polling sync.**
8
+
9
+ A Matrix protocol adapter built on the ErisPulse framework. It receives events via the Long Polling Sync API and unifies private chats, groups and rooms into OneBot12 standard events, with full support for mentions, replies, HTML rich text, reactions and MXC media.
10
+
11
+ <p>
12
+ <a href="https://pypi.org/project/ErisPulse-MatrixAdapter/"><img src="https://img.shields.io/pypi/v/ErisPulse-MatrixAdapter?style=for-the-badge&logo=pypi&logoColor=white" alt="PyPI"></a>
13
+ <a href="https://pypi.org/project/ErisPulse-MatrixAdapter/"><img src="https://img.shields.io/badge/Python-3.10+-FFD43B?style=for-the-badge&logo=python&logoColor=blue" alt="Python"></a>
14
+ <a href="./LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue?style=for-the-badge" alt="License"></a>
15
+ <a href="https://github.com/ErisPulse/ErisPulse-MatrixAdapter"><img src="https://img.shields.io/github/stars/ErisPulse/ErisPulse-MatrixAdapter?style=for-the-badge&logo=github&color=brightgreen" alt="Stars"></a>
16
+ <a href="https://pepy.tech/project/ErisPulse-MatrixAdapter"><img src="https://img.shields.io/pepy/dt/ErisPulse-MatrixAdapter?style=for-the-badge&color=blue" alt="Downloads"></a>
17
+ <a href="https://github.com/ErisPulse/ErisPulse"><img src="https://img.shields.io/badge/Powered_by-ErisPulse-FF6B9D?style=for-the-badge&logo=bookstack&logoColor=white" alt="ErisPulse"></a>
18
+ </p>
19
+
20
+ [English](#english) | [简体中文](#中文)
21
+
22
+ </div>
4
23
 
5
24
  ---
6
25
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ErisPulse-MatrixAdapter"
3
- version = "4.0.2"
3
+ version = "4.2.0"
4
4
  description = "ErisPulse的Matrix协议适配模块"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.9"
@@ -1 +0,0 @@
1
- from .Core import MatrixAdapter