refactor(channels): make built-in channels self-contained (#4908)

* refactor(channels): own setup and instance contracts

* refactor(channels): isolate management contracts

* refactor(channels): normalize activation contracts

* fix(channels): enforce management contracts

* refactor(channels): finish setup ownership migration

* fix(channels): harden management contracts

* fix(channels): enforce lazy loading and runtime ownership

* fix(feishu): make multi-instance startup idempotent

* fix(webui): render channel setup contracts cleanly

* fix(feishu): stop websocket clients cleanly

* fix(channels): enforce persistence and activation gates

* fix(channels): preserve global feature action scope

* fix(channels): apply defaults for single plugins

* fix(channels): enforce management contract boundaries

* refactor(feishu): remove identity helper indirection

* fix(channels): preserve management setup contracts

* refactor(channels): generalize instance settings UI

* refactor(channels): package channel plugins with web UI metadata

* refactor(channels): make built-ins self-contained packages

* test(channels): colocate tests with channel packages

* fix(dingtalk): use official brand icon

* feat(channels): colocate webui translations

* docs(channels): clarify plugin ownership

* test(exec): remove output wait race

* refactor(channels): unify plugin descriptors

* fix(channels): enforce descriptor-owned contracts

* refactor(channels): finish package-owned plugin setup

* refactor(channels): use repository-owned packages only

* fix(channels): self-describe dependencies and runtime state

* fix(channels): warn about legacy entry points
This commit is contained in:
chengyongru
2026-07-19 23:30:49 +08:00
committed by GitHub
parent 7aaac37bca
commit 462a0dfb0f
388 changed files with 17093 additions and 5110 deletions
+1
View File
@@ -0,0 +1 @@
"""Matrix channel package."""
+39
View File
@@ -0,0 +1,39 @@
"""Matrix management contract."""
from nanobot.channels._manifest import GROUP_POLICIES, field, one_of, required_fields
from nanobot.channels.contracts import ChannelSetupSpec
from nanobot.channels.matrix.validation import validate
from nanobot.channels.plugin import ChannelPlugin
SETUP_SPEC = ChannelSetupSpec(
fields={
"homeserver": field(default="https://matrix.org"),
"userId": field(),
"password": field("secret"),
"accessToken": field("secret"),
"deviceId": field(),
"groupPolicy": field("enum", choices=GROUP_POLICIES, default="open"),
"allowFrom": field("list", writable=False),
},
required=(
*required_fields("homeserver", "userId"),
one_of(("password",), ("accessToken", "deviceId")),
),
official_url="https://matrix.org/ecosystem/clients/",
validator=validate,
)
PLUGIN = ChannelPlugin(
name="matrix",
display_name="Matrix",
runtime=f"{__package__}.runtime:MatrixChannel",
setup=SETUP_SPEC,
dependencies=(
"matrix-nio[e2e]>=0.25.2; sys_platform != 'win32'",
"matrix-nio>=0.25.2; sys_platform == 'win32'",
"aiohttp>=3.9.0,<4.0.0",
"mistune>=3.0.0,<4.0.0",
"nh3>=0.2.17,<1.0.0",
),
webui="webui/index.ts",
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
"""Tests for the Matrix channel package."""
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,51 @@
from __future__ import annotations
import pytest
from nanobot.channels.validation import validate_channel_config
from nanobot.config.loader import save_config
from nanobot.config.schema import Config
@pytest.mark.parametrize(
("credentials", "expected_status", "expected_missing"),
[
({}, "needs_setup", "password_or_accessToken"),
({"channels.matrix.accessToken": "token"}, "needs_setup", "deviceId"),
({"channels.matrix.password": "secret"}, "configured", None),
(
{
"channels.matrix.accessToken": "token",
"channels.matrix.deviceId": "DEVICE",
},
"configured",
None,
),
],
)
def test_validate_matrix_requires_a_complete_login_method(
tmp_path,
monkeypatch: pytest.MonkeyPatch,
credentials: dict[str, str],
expected_status: str,
expected_missing: str | None,
) -> None:
config_path = tmp_path / "config.json"
save_config(Config(), config_path)
monkeypatch.setattr("nanobot.config.loader._current_config_path", config_path)
result = validate_channel_config(
"matrix",
{
"channels.matrix.homeserver": "https://matrix.example",
"channels.matrix.userId": "@nanobot:matrix.example",
**credentials,
},
)
assert result["status"] == expected_status
assert result["can_enable"] is (expected_status == "configured")
if expected_missing is None:
assert result["missing_fields"] == []
else:
assert expected_missing in result["missing_fields"]
+46
View File
@@ -0,0 +1,46 @@
"""Matrix setup validation owned by the channel package."""
from typing import Any
from nanobot.channels.contracts import ChannelValidationContext
from nanobot.channels.validation import check, required_checks, status_from_checks, string_value
def validate(values: dict[str, Any], _context: ChannelValidationContext) -> dict[str, Any]:
checks, missing = required_checks("matrix", values)
password = string_value(values.get("password"))
access_token = string_value(values.get("accessToken"))
device_id = string_value(values.get("deviceId"))
if password:
checks.append(check("login", "Login credentials", "pass", "Password login is configured."))
elif access_token and device_id:
checks.append(
check(
"login",
"Login credentials",
"pass",
"Access token login is configured with its device ID.",
)
)
else:
if not password and not access_token:
missing.append("password_or_accessToken")
message = "Add a password, or an access token with its device ID."
else:
missing.append("deviceId")
message = "A device ID is required with an access token."
checks.append(check("login", "Login credentials", "fail", message))
checks.append(
check(
"manual_review",
"Matrix account",
"skipped",
"Room access is verified when the channel starts.",
)
)
return status_from_checks("matrix", checks, list(dict.fromkeys(missing)))
__all__ = ["validate"]
+23
View File
@@ -0,0 +1,23 @@
import type { ChannelUiContribution } from "@/channel-plugins/types";
import { chatAppGuideUrl } from "@/components/settings/channels/catalog";
export default {
presentation: {
displayName: "Matrix",
initials: "MX",
color: "#0DBD8B",
logoUrl: "https://matrix.org/favicon.ico",
setup: {
mode: "credentials",
docsUrl: chatAppGuideUrl("matrix"),
fields: [
{ key: "channels.matrix.homeserver" },
{ key: "channels.matrix.userId" },
{ key: "channels.matrix.password" },
{ key: "channels.matrix.accessToken" },
{ key: "channels.matrix.deviceId" },
{ key: "channels.matrix.groupPolicy" },
],
},
},
} satisfies ChannelUiContribution;
@@ -0,0 +1,48 @@
{
"description": "Use nanobot from Matrix rooms.",
"requirements": "Homeserver, account token, room access",
"setup": {
"docsLabel": "Open Matrix setup",
"officialLabel": "Open Matrix clients",
"tryIt": "Invite the Matrix account into a room and send a test message.",
"summary": "Matrix needs a homeserver account and either password login or an access token.",
"steps": [
"Create or choose a Matrix account for nanobot.",
"Enter the homeserver, user ID, and either password or access-token credentials.",
"Save and enable Matrix, invite the account to a room, and send a test message."
],
"fields": {
"homeserver": {
"label": "Homeserver",
"placeholder": "https://matrix.org"
},
"userId": {
"label": "User ID",
"placeholder": "@nanobot:matrix.org"
},
"password": {
"label": "Password",
"placeholder": "••••••",
"help": "Use either password login or access token login."
},
"accessToken": {
"label": "Access token",
"placeholder": "Optional token login",
"help": "Preferred when your Matrix client exposes an access token."
},
"deviceId": {
"label": "Device ID",
"placeholder": "Required with an access token",
"help": "Copy the device ID associated with the access token. Password login does not need it."
},
"groupPolicy": {
"label": "Group behavior",
"choices": {
"mention": "Mention only",
"open": "All messages",
"allowlist": "Allowlist"
}
}
}
}
}
@@ -0,0 +1,48 @@
{
"description": "Usa nanobot desde salas de Matrix.",
"requirements": "Homeserver, token de cuenta y acceso a salas",
"setup": {
"docsLabel": "Abrir guía de Matrix",
"officialLabel": "Abrir lista de clientes Matrix",
"tryIt": "Invita la cuenta de Matrix a una sala y envía un mensaje de prueba.",
"summary": "Matrix necesita una cuenta de homeserver y acceso por contraseña o token.",
"steps": [
"Crea o elige una cuenta de Matrix para nanobot.",
"Introduce el homeserver, ID de usuario y contraseña o token de acceso.",
"Guarda y activa Matrix, invita la cuenta a una sala y envía un mensaje de prueba."
],
"fields": {
"homeserver": {
"label": "Homeserver",
"placeholder": "https://matrix.org"
},
"userId": {
"label": "ID de usuario",
"placeholder": "@nanobot:matrix.org"
},
"password": {
"label": "Contraseña",
"placeholder": "••••••",
"help": "Usa contraseña o token de acceso."
},
"accessToken": {
"label": "Token de acceso",
"placeholder": "Acceso opcional con token",
"help": "Preferible si tu cliente Matrix muestra un token de acceso."
},
"deviceId": {
"label": "ID del dispositivo",
"placeholder": "Obligatorio con token",
"help": "Copia el ID asociado al token. No se necesita con contraseña."
},
"groupPolicy": {
"label": "Comportamiento en grupos",
"choices": {
"mention": "Solo menciones",
"open": "Todos los mensajes",
"allowlist": "Lista permitida"
}
}
}
}
}
@@ -0,0 +1,48 @@
{
"description": "Utilisez nanobot depuis les salons Matrix.",
"requirements": "Serveur daccueil, jeton de compte et accès aux salons",
"setup": {
"docsLabel": "Ouvrir le guide Matrix",
"officialLabel": "Ouvrir la liste des clients Matrix",
"tryIt": "Invitez le compte Matrix dans un salon et envoyez un message test.",
"summary": "Matrix nécessite un compte sur un serveur daccueil et une connexion par mot de passe ou jeton daccès.",
"steps": [
"Créez ou choisissez un compte Matrix pour nanobot.",
"Saisissez le serveur, lID utilisateur et le mot de passe ou le jeton daccès.",
"Enregistrez et activez Matrix, invitez le compte dans un salon et envoyez un message test."
],
"fields": {
"homeserver": {
"label": "Serveur daccueil",
"placeholder": "https://matrix.org"
},
"userId": {
"label": "ID utilisateur",
"placeholder": "@nanobot:matrix.org"
},
"password": {
"label": "Mot de passe",
"placeholder": "••••••",
"help": "Utilisez le mot de passe ou le jeton daccès."
},
"accessToken": {
"label": "Jeton daccès",
"placeholder": "Connexion facultative par jeton",
"help": "À privilégier si votre client Matrix expose un jeton daccès."
},
"deviceId": {
"label": "ID de lappareil",
"placeholder": "Requis avec un jeton daccès",
"help": "Copiez lID dappareil associé au jeton. Il nest pas nécessaire avec un mot de passe."
},
"groupPolicy": {
"label": "Comportement en groupe",
"choices": {
"mention": "Mentions uniquement",
"open": "Tous les messages",
"allowlist": "Liste dautorisation"
}
}
}
}
}
@@ -0,0 +1,48 @@
{
"description": "Gunakan nanobot dari ruang Matrix.",
"requirements": "Homeserver, token akun, dan akses ruang",
"setup": {
"docsLabel": "Buka panduan Matrix",
"officialLabel": "Buka daftar klien Matrix",
"tryIt": "Undang akun Matrix ke ruang dan kirim pesan uji.",
"summary": "Matrix memerlukan akun homeserver dan login dengan kata sandi atau token akses.",
"steps": [
"Buat atau pilih akun Matrix untuk nanobot.",
"Masukkan homeserver, ID pengguna, dan kata sandi atau token akses.",
"Simpan dan aktifkan Matrix, undang akun ke ruang, lalu kirim pesan uji."
],
"fields": {
"homeserver": {
"label": "Homeserver",
"placeholder": "https://matrix.org"
},
"userId": {
"label": "ID pengguna",
"placeholder": "@nanobot:matrix.org"
},
"password": {
"label": "Kata sandi",
"placeholder": "••••••",
"help": "Gunakan kata sandi atau token akses."
},
"accessToken": {
"label": "Token akses",
"placeholder": "Login token opsional",
"help": "Disarankan jika klien Matrix menyediakan token akses."
},
"deviceId": {
"label": "ID perangkat",
"placeholder": "Wajib dengan token akses",
"help": "Salin ID perangkat yang terkait token. Login kata sandi tidak memerlukannya."
},
"groupPolicy": {
"label": "Perilaku grup",
"choices": {
"mention": "Hanya sebutan",
"open": "Semua pesan",
"allowlist": "Daftar izin"
}
}
}
}
}
@@ -0,0 +1,48 @@
{
"description": "Matrix ルームから nanobot を利用します。",
"requirements": "ホームサーバー、アカウントトークン、ルームへのアクセス",
"setup": {
"docsLabel": "Matrix 設定ガイドを開く",
"officialLabel": "Matrix クライアント一覧を開く",
"tryIt": "Matrix アカウントをルームに招待し、テストメッセージを送信します。",
"summary": "Matrix にはホームサーバーのアカウントと、パスワードまたはアクセストークンが必要です。",
"steps": [
"nanobot 用の Matrix アカウントを作成または選択します。",
"ホームサーバー、ユーザー ID、パスワードまたはアクセストークンを入力します。",
"保存して Matrix を有効にし、アカウントをルームに招待してテストメッセージを送信します。"
],
"fields": {
"homeserver": {
"label": "ホームサーバー",
"placeholder": "https://matrix.org"
},
"userId": {
"label": "ユーザー ID",
"placeholder": "@nanobot:matrix.org"
},
"password": {
"label": "パスワード",
"placeholder": "••••••",
"help": "パスワードまたはアクセストークンのどちらかを使います。"
},
"accessToken": {
"label": "アクセストークン",
"placeholder": "任意のトークンログイン",
"help": "Matrix クライアントでアクセストークンを取得できる場合に推奨します。"
},
"deviceId": {
"label": "デバイス ID",
"placeholder": "アクセストークン使用時に必須",
"help": "トークンに関連付けられたデバイス ID をコピーします。パスワードログインでは不要です。"
},
"groupPolicy": {
"label": "グループでの動作",
"choices": {
"mention": "メンションのみ",
"open": "すべてのメッセージ",
"allowlist": "許可リスト"
}
}
}
}
}
@@ -0,0 +1,48 @@
{
"description": "Matrix 룸에서 nanobot을 사용합니다.",
"requirements": "홈서버, 계정 토큰 및 룸 접근 권한",
"setup": {
"docsLabel": "Matrix 설정 가이드 열기",
"officialLabel": "Matrix 클라이언트 목록 열기",
"tryIt": "Matrix 계정을 룸에 초대하고 테스트 메시지를 보내세요.",
"summary": "Matrix에는 홈서버 계정과 비밀번호 또는 액세스 토큰이 필요합니다.",
"steps": [
"nanobot용 Matrix 계정을 만들거나 선택하세요.",
"홈서버, 사용자 ID, 비밀번호 또는 액세스 토큰을 입력하세요.",
"저장하고 Matrix를 활성화한 다음 계정을 룸에 초대하고 테스트 메시지를 보내세요."
],
"fields": {
"homeserver": {
"label": "홈서버",
"placeholder": "https://matrix.org"
},
"userId": {
"label": "사용자 ID",
"placeholder": "@nanobot:matrix.org"
},
"password": {
"label": "비밀번호",
"placeholder": "••••••",
"help": "비밀번호 또는 액세스 토큰 중 하나를 사용하세요."
},
"accessToken": {
"label": "액세스 토큰",
"placeholder": "선택적 토큰 로그인",
"help": "Matrix 클라이언트에서 토큰을 확인할 수 있다면 권장합니다."
},
"deviceId": {
"label": "장치 ID",
"placeholder": "액세스 토큰 사용 시 필수",
"help": "토큰과 연결된 장치 ID를 복사하세요. 비밀번호 로그인에는 필요하지 않습니다."
},
"groupPolicy": {
"label": "그룹 동작",
"choices": {
"mention": "멘션만",
"open": "모든 메시지",
"allowlist": "허용 목록"
}
}
}
}
}
@@ -0,0 +1,48 @@
{
"description": "Use o nanobot em salas do Matrix.",
"requirements": "Homeserver, token da conta e acesso às salas",
"setup": {
"docsLabel": "Abrir guia do Matrix",
"officialLabel": "Abrir lista de clientes Matrix",
"tryIt": "Convide a conta Matrix para uma sala e envie uma mensagem de teste.",
"summary": "O Matrix precisa de uma conta no homeserver e login por senha ou token de acesso.",
"steps": [
"Crie ou escolha uma conta Matrix para o nanobot.",
"Informe o homeserver, ID de usuário e senha ou token de acesso.",
"Salve e ative o Matrix, convide a conta para uma sala e envie uma mensagem de teste."
],
"fields": {
"homeserver": {
"label": "Homeserver",
"placeholder": "https://matrix.org"
},
"userId": {
"label": "ID de usuário",
"placeholder": "@nanobot:matrix.org"
},
"password": {
"label": "Senha",
"placeholder": "••••••",
"help": "Use senha ou token de acesso."
},
"accessToken": {
"label": "Token de acesso",
"placeholder": "Login opcional por token",
"help": "Preferível quando o cliente Matrix fornece um token."
},
"deviceId": {
"label": "ID do dispositivo",
"placeholder": "Obrigatório com token",
"help": "Copie o ID associado ao token. O login por senha não precisa dele."
},
"groupPolicy": {
"label": "Comportamento em grupos",
"choices": {
"mention": "Somente menções",
"open": "Todas as mensagens",
"allowlist": "Lista de permissão"
}
}
}
}
}
@@ -0,0 +1,48 @@
{
"description": "Sử dụng nanobot từ các phòng Matrix.",
"requirements": "Homeserver, token tài khoản và quyền truy cập phòng",
"setup": {
"docsLabel": "Mở hướng dẫn Matrix",
"officialLabel": "Mở danh sách ứng dụng Matrix",
"tryIt": "Mời tài khoản Matrix vào phòng và gửi tin nhắn thử.",
"summary": "Matrix cần tài khoản homeserver và đăng nhập bằng mật khẩu hoặc token truy cập.",
"steps": [
"Tạo hoặc chọn tài khoản Matrix cho nanobot.",
"Nhập homeserver, ID người dùng và mật khẩu hoặc token truy cập.",
"Lưu và bật Matrix, mời tài khoản vào phòng rồi gửi tin nhắn thử."
],
"fields": {
"homeserver": {
"label": "Homeserver",
"placeholder": "https://matrix.org"
},
"userId": {
"label": "ID người dùng",
"placeholder": "@nanobot:matrix.org"
},
"password": {
"label": "Mật khẩu",
"placeholder": "••••••",
"help": "Dùng mật khẩu hoặc token truy cập."
},
"accessToken": {
"label": "Token truy cập",
"placeholder": "Đăng nhập token tùy chọn",
"help": "Nên dùng khi ứng dụng Matrix cung cấp token truy cập."
},
"deviceId": {
"label": "ID thiết bị",
"placeholder": "Bắt buộc khi dùng token",
"help": "Sao chép ID thiết bị gắn với token. Đăng nhập mật khẩu không cần."
},
"groupPolicy": {
"label": "Hành vi trong nhóm",
"choices": {
"mention": "Chỉ khi được nhắc",
"open": "Mọi tin nhắn",
"allowlist": "Danh sách cho phép"
}
}
}
}
}
@@ -0,0 +1,48 @@
{
"description": "在 Matrix 房间中使用 nanobot。",
"requirements": "主服务器、账户令牌和房间访问权限",
"setup": {
"docsLabel": "打开 Matrix 配置指南",
"officialLabel": "打开 Matrix 客户端列表",
"tryIt": "将 Matrix 账户邀请进房间并发送一条测试消息。",
"summary": "Matrix 需要主服务器账户,并使用密码或访问令牌登录。",
"steps": [
"为 nanobot 创建或选择一个 Matrix 账户。",
"填写主服务器、用户 ID,以及密码或访问令牌凭据。",
"保存并启用 Matrix,将账户邀请进房间,然后发送一条测试消息。"
],
"fields": {
"homeserver": {
"label": "主服务器",
"placeholder": "https://matrix.org"
},
"userId": {
"label": "用户 ID",
"placeholder": "@nanobot:matrix.org"
},
"password": {
"label": "密码",
"placeholder": "••••••",
"help": "密码登录和访问令牌登录任选其一。"
},
"accessToken": {
"label": "访问令牌",
"placeholder": "可选的令牌登录",
"help": "如果 Matrix 客户端可以显示访问令牌,建议使用该方式。"
},
"deviceId": {
"label": "设备 ID",
"placeholder": "使用访问令牌时必填",
"help": "复制与访问令牌关联的设备 ID;密码登录不需要。"
},
"groupPolicy": {
"label": "群组行为",
"choices": {
"mention": "仅提及时",
"open": "所有消息",
"allowlist": "白名单"
}
}
}
}
}
@@ -0,0 +1,48 @@
{
"description": "在 Matrix 房間中使用 nanobot。",
"requirements": "主伺服器、帳戶權杖和房間存取權限",
"setup": {
"docsLabel": "開啟 Matrix 設定指南",
"officialLabel": "開啟 Matrix 用戶端列表",
"tryIt": "將 Matrix 帳戶邀請進房間並傳送一則測試訊息。",
"summary": "Matrix 需要主伺服器帳戶,並使用密碼或存取權杖登入。",
"steps": [
"為 nanobot 建立或選擇一個 Matrix 帳戶。",
"填入主伺服器、使用者 ID,以及密碼或存取權杖憑證。",
"儲存並啟用 Matrix,將帳戶邀請進房間,然後傳送一則測試訊息。"
],
"fields": {
"homeserver": {
"label": "主伺服器",
"placeholder": "https://matrix.org"
},
"userId": {
"label": "使用者 ID",
"placeholder": "@nanobot:matrix.org"
},
"password": {
"label": "密碼",
"placeholder": "••••••",
"help": "密碼登入和存取權杖登入任選其一。"
},
"accessToken": {
"label": "存取權杖",
"placeholder": "可選的權杖登入",
"help": "若 Matrix 用戶端可顯示存取權杖,建議使用此方式。"
},
"deviceId": {
"label": "裝置 ID",
"placeholder": "使用存取權杖時必填",
"help": "複製與存取權杖關聯的裝置 ID;密碼登入不需要。"
},
"groupPolicy": {
"label": "群組行為",
"choices": {
"mention": "僅提及時",
"open": "所有訊息",
"allowlist": "允許清單"
}
}
}
}
}