tenko-plugin-example 0.1.0__py3-none-any.whl
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.
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Tenko 第三方插件示例:问候计数。
|
|
2
|
+
|
|
3
|
+
演示 tenko_api 的主要能力:声明元数据(含群等级)、读取配置、带版本的 JSON
|
|
4
|
+
存储、插件自己的 SQLite 库(``Model``),以及声明式权限装饰器。写法与内置插件一样,
|
|
5
|
+
都是 Entari 原生的 ``plugin.metadata``、``command.on``。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from sqlalchemy import String, select
|
|
9
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
10
|
+
|
|
11
|
+
from arclet.alconna import Alconna, Args, CommandMeta
|
|
12
|
+
from arclet.entari import BasicConfModel, Session, command, plugin
|
|
13
|
+
from arclet.entari.command import Query
|
|
14
|
+
|
|
15
|
+
from tenko_api.context import context_from_session
|
|
16
|
+
from tenko_api.features import declare
|
|
17
|
+
from tenko_api.perm import Permission, require
|
|
18
|
+
from tenko_api.storage import Model, Store, get_session
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ExampleConfig(BasicConfModel):
|
|
22
|
+
"""对应 config/tenko.toml 中的 ``[plugin_config.example]``。"""
|
|
23
|
+
|
|
24
|
+
greeting: str = "你好"
|
|
25
|
+
"""回复使用的问候语。"""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
plugin.metadata(
|
|
29
|
+
"示例插件",
|
|
30
|
+
author=["13"],
|
|
31
|
+
version="0.1.0",
|
|
32
|
+
description="记录每位用户使用示例命令的次数,并保存备注。",
|
|
33
|
+
config=ExampleConfig,
|
|
34
|
+
# 可选:插件目录会下载这张图,处理成 128×128 PNG 后自行托管。
|
|
35
|
+
# icon="https://raw.githubusercontent.com/you/tenko-plugin-example/main/icon.png",
|
|
36
|
+
)
|
|
37
|
+
# Tenko 专有元数据:按群开关、新群默认开启、每次调用计入限流窗口的权重,以及
|
|
38
|
+
# 插件要求的群等级(低于它的群里命令被静默忽略,ActiveGroup 即默认值)。
|
|
39
|
+
declare(
|
|
40
|
+
scope="group",
|
|
41
|
+
default_enabled=True,
|
|
42
|
+
rate_weight=1,
|
|
43
|
+
level=Permission.ActiveGroup,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
config = plugin.get_config(ExampleConfig)
|
|
47
|
+
# .tenko/plugin-data/example/visits.json;以后改数据结构时提高 version,并提供 migrate。
|
|
48
|
+
visits = Store("visits", version=1)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Note(Model):
|
|
52
|
+
"""备注历史。表建在 .tenko/plugin-data/example/data.db,表名默认为 example_note。
|
|
53
|
+
|
|
54
|
+
宿主启动时自动建表,并自动执行新增表、新增可空列、新增索引;删除或修改列需要
|
|
55
|
+
显式迁移脚本,否则会被拒绝。
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
id: Mapped[int] = mapped_column(primary_key=True)
|
|
59
|
+
user_id: Mapped[str] = mapped_column(String(32), index=True)
|
|
60
|
+
text: Mapped[str] = mapped_column(String(200))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
example_command = Alconna(
|
|
64
|
+
"示例",
|
|
65
|
+
Args["note?", str],
|
|
66
|
+
meta=CommandMeta(
|
|
67
|
+
"统计你使用示例命令的次数,可附带一条备注",
|
|
68
|
+
usage="示例 [备注]",
|
|
69
|
+
example="/示例 今天也要加油",
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# 权限用装饰器声明(放在 command.on 之下):不足时宿主统一回复,/帮助 <编号> 也会显示。
|
|
75
|
+
@command.on(example_command)
|
|
76
|
+
@require(Permission.User)
|
|
77
|
+
async def example(session: Session, note: Query[str] = Query("note", "")):
|
|
78
|
+
ctx = context_from_session(session)
|
|
79
|
+
# Store 是同步读写:读改写之间没有 await,协程不会交错,所以不需要加锁。
|
|
80
|
+
records = visits.load(default={})
|
|
81
|
+
record = records.setdefault(ctx.user_id, {"count": 0})
|
|
82
|
+
record["count"] += 1
|
|
83
|
+
visits.save(records)
|
|
84
|
+
|
|
85
|
+
# 会话按模型自动路由到插件自己的库;事务由 commit 显式提交。
|
|
86
|
+
async with get_session() as db:
|
|
87
|
+
previous = await db.scalar(
|
|
88
|
+
select(Note.text)
|
|
89
|
+
.where(Note.user_id == ctx.user_id)
|
|
90
|
+
.order_by(Note.id.desc())
|
|
91
|
+
.limit(1)
|
|
92
|
+
)
|
|
93
|
+
if note.result:
|
|
94
|
+
db.add(Note(user_id=ctx.user_id, text=note.result))
|
|
95
|
+
await db.commit()
|
|
96
|
+
|
|
97
|
+
reply = f"{config.greeting},这是你第 {record['count']} 次使用示例命令"
|
|
98
|
+
return f"{reply}(上次备注:{previous})" if previous else reply
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: tenko-plugin-example
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Tenko 第三方插件示例与模板
|
|
5
|
+
Project-URL: Homepage, https://github.com/g1331/tenko/tree/main/examples/tenko-plugin-example
|
|
6
|
+
Project-URL: Repository, https://github.com/g1331/tenko
|
|
7
|
+
License-Expression: GPL-3.0-only
|
|
8
|
+
Requires-Python: <3.14,>=3.11
|
|
9
|
+
Requires-Dist: sqlalchemy>=2.0
|
|
10
|
+
Requires-Dist: tenko-api<0.3,>=0.2
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# tenko-plugin-example
|
|
14
|
+
|
|
15
|
+
[Tenko](https://github.com/g1331/tenko) 第三方插件的示例与起步模板。它演示:
|
|
16
|
+
|
|
17
|
+
- 用 `tenko_api.features.declare` 声明按群开关、默认状态、限流权重和群等级;
|
|
18
|
+
- 用 `[plugin_config.example]` 配置插件(Entari 的 `BasicConfModel`);
|
|
19
|
+
- 用 `tenko_api.storage.Store` 保存带版本号的 JSON 数据,用 `Model` 和 `get_session()` 使用插件自己的 SQLite 库;
|
|
20
|
+
- 用 `tenko_api.perm` 的装饰器(`@require` 等)声明命令权限,用 Entari 原生的 `command.on` 注册命令。
|
|
21
|
+
|
|
22
|
+
插件命令 `/示例 [备注]` 记录你使用命令的次数(JSON)和备注历史(SQLite)。
|
|
23
|
+
|
|
24
|
+
## 从模板开始
|
|
25
|
+
|
|
26
|
+
1. 复制本目录作为新项目(在 GitHub 上可以把它放进自己的仓库)。
|
|
27
|
+
2. 改名:分发名用 `tenko-plugin-xxx`,模块名用 `tenko_plugin_xxx`。同步修改
|
|
28
|
+
`pyproject.toml` 的 `name`、`[tool.hatch.build.targets.wheel]`、`src/` 目录名,以及
|
|
29
|
+
入口 `[project.entry-points."tenko.plugins"]` 里的 `<插件 ID> = "<模块>"`。
|
|
30
|
+
插件 ID 匹配 `[a-z][a-z0-9_]*`,全局唯一,不能和内置插件同名;它是功能开关、
|
|
31
|
+
配置节和数据目录的键。
|
|
32
|
+
3. 换成你自己的信息:`plugin.metadata` 的 `author`、`description`,以及 `pyproject.toml`
|
|
33
|
+
的 `[project.urls]`(插件目录靠这里的 GitHub 地址确认仓库归你所有)。示例本身作为真实
|
|
34
|
+
插件发布在 PyPI 上,这些地方写的是 Tenko 维护者的信息。
|
|
35
|
+
4. **不要声明 `entari.plugin` 入口**:Entari 会无视 Tenko 的启用状态自动加载它。
|
|
36
|
+
5. 依赖只写 `tenko-api` 的兼容范围(预览期为 `>=0.2,<0.3`)和插件自己的依赖,
|
|
37
|
+
不要依赖 `tenko` 本体,也不要用 `==` 锁死依赖版本。
|
|
38
|
+
|
|
39
|
+
## 开发与测试
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
uv sync # 创建环境,开发依赖里包含从 Git 安装的 Tenko 宿主(发布后请固定到版本标签)
|
|
43
|
+
uv run pytest -q
|
|
44
|
+
uv run ruff check .
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`tests/conftest.py` 用真实的宿主加载插件:按 `tenko.plugins` 入口发现、启用、传入配置,
|
|
48
|
+
然后直接调用命令处理函数。不需要启动 Tenko,也不需要连接聊天平台。
|
|
49
|
+
|
|
50
|
+
写法要点:
|
|
51
|
+
|
|
52
|
+
- 宿主能力一律从 `tenko_api` 导入,不要导入 `tenko.*` 内部模块。
|
|
53
|
+
- `declare(...)` 必须在插件模块导入期间调用(与 `plugin.metadata(...)` 放在一起)。
|
|
54
|
+
- `data_dir()` 和 `Store` 只能在插件自己的模块里使用,数据位于 `.tenko/plugin-data/<插件 ID>/`。
|
|
55
|
+
数据结构变化时提高 `Store` 的版本号并提供 `migrate`,宿主会先把旧文件备份为
|
|
56
|
+
`<名称>.json.bak-v<旧版本>`;插件被降级、读到更新版本的文件时会报错而不是覆盖。
|
|
57
|
+
- `Model` 子类在插件模块导入期间定义,表建在 `data.db`;启动时自动建表和增加可空列,
|
|
58
|
+
删除或修改列会被拒绝,需要时写官方的 `register_custom_migration` 脚本。测试里要像宿主
|
|
59
|
+
那样先加载数据库插件、再加载插件、最后初始化并迁移(见 `tests/conftest.py`)。
|
|
60
|
+
版本变化后宿主会先备份库到 `backups/`。
|
|
61
|
+
- 命令权限用 `tenko_api.perm` 的装饰器声明,不要在函数体里重复检查;装饰器在
|
|
62
|
+
`@command.on(...)` 之上或之下都生效。因为装饰器由 Entari 调度,测试里直接调用
|
|
63
|
+
`callable_target` 不会经过它们,应检查登记的要求(见 `tests/test_plugin.py`),
|
|
64
|
+
或像 `tests/tenko` 里那样走真实分发。
|
|
65
|
+
- Entari 对标准 dataclass 配置模型不做类型校验,需要时在 `__post_init__` 里检查。
|
|
66
|
+
|
|
67
|
+
## 在 Tenko 里启用
|
|
68
|
+
|
|
69
|
+
1. 在运行 Tenko 的环境里安装插件:`uv pip install tenko-plugin-xxx`(本地调试用
|
|
70
|
+
`uv pip install -e /path/to/plugin`)。
|
|
71
|
+
2. 打开 WebUI 的插件页,启用该插件(带“第三方”标记)。第三方插件默认关闭,启用后
|
|
72
|
+
写入 `.tenko/plugins/state.json`。
|
|
73
|
+
3. 如有配置,在 `config/tenko.toml` 里添加 `[plugin_config.<插件 ID>]`,见仓库中
|
|
74
|
+
`config/tenko.toml.example` 末尾的说明。
|
|
75
|
+
4. 重启 Tenko(聊天里的 `/重启` 或重新运行进程),插件才会被加载。
|
|
76
|
+
|
|
77
|
+
之后可以在 WebUI 开关页或用 `/开启`、`/关闭` 按群控制,插件命令与内置命令走同一套
|
|
78
|
+
维护模式、功能开关和限流。
|
|
79
|
+
|
|
80
|
+
## 发布
|
|
81
|
+
|
|
82
|
+
**发布到 PyPI(推荐,使用 Trusted Publishing,不保存 token)**
|
|
83
|
+
|
|
84
|
+
1. 在 PyPI 上添加 pending publisher:项目名填你的分发名,所有者和仓库填你的 GitHub
|
|
85
|
+
仓库,工作流填 `publish.yml`,环境填 `pypi`。
|
|
86
|
+
2. 在 GitHub 仓库设置里创建名为 `pypi` 的 Environment。
|
|
87
|
+
3. 把 `pyproject.toml` 的版本改为 `X.Y.Z`,提交后打标签并推送:
|
|
88
|
+
|
|
89
|
+
```text
|
|
90
|
+
git tag vX.Y.Z && git push origin vX.Y.Z
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
`.github/workflows/publish.yml` 会检查标签与版本一致、构建并发布。
|
|
94
|
+
|
|
95
|
+
**只用 GitHub**:不发布到 PyPI 时,使用者可以直接
|
|
96
|
+
`uv pip install "tenko-plugin-xxx @ git+https://github.com/<你>/<仓库>@<标签或提交>"`,
|
|
97
|
+
请固定到标签或提交哈希。
|
|
98
|
+
|
|
99
|
+
## 收录到插件目录
|
|
100
|
+
|
|
101
|
+
发布到 PyPI(或打好 Git 标签)后,在 [tenko-registry](https://github.com/g1331/tenko-registry)
|
|
102
|
+
用投稿表单填写来源即可。插件 ID、名称、简介、作者、许可证和兼容范围都由目录的 CI 从
|
|
103
|
+
`pyproject.toml` 和 `plugin.metadata(...)` 读取,之后的新版本也会自动追加。
|
|
104
|
+
|
|
105
|
+
图标可选:在 `plugin.metadata(icon="https://…")` 里给出图片地址,或在投稿表单里上传。要求
|
|
106
|
+
透明背景的单色图形(PNG 或 WebP,不接受 JPEG、SVG 和动图),短边至少 64 px;目录会裁成
|
|
107
|
+
正方形、缩放到 128×128 PNG 并自行托管,Tenko 只显示目录托管的图标。
|
|
108
|
+
|
|
109
|
+
## 兼容性
|
|
110
|
+
|
|
111
|
+
`tenko-api` 遵循语义化版本(1.0 之前为预览,接口可能调整)。插件声明的范围由宿主在
|
|
112
|
+
加载前检查:宿主提供的 `tenko-api` 不在范围内时,插件不加载,日志说明原因。注意 `uv pip install`
|
|
113
|
+
遇到不兼容的范围不会报错,而是把环境里的 `tenko-api` 换成插件要的版本,所以不能靠安装结果判断兼容性。
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
tenko_plugin_example/__init__.py,sha256=kw3xc5ZzWJfvRkXHiT8HChjQtfiKGTXFx4BO9iD5SLM,3697
|
|
2
|
+
tenko_plugin_example-0.1.0.dist-info/METADATA,sha256=I8TfnzEqCucYsfWMEG50j1C7sAWkupTnwi0ZGTMXqm4,6593
|
|
3
|
+
tenko_plugin_example-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
4
|
+
tenko_plugin_example-0.1.0.dist-info/entry_points.txt,sha256=qSHykmN2U5CuIqvTzMZLNZt4h3DyOpj7GzlhaJVLzkw,47
|
|
5
|
+
tenko_plugin_example-0.1.0.dist-info/RECORD,,
|