Skip to content

Commit 3c6e91e

Browse files
feat:强化插件功能,规范化包
1 parent 0f12894 commit 3c6e91e

11 files changed

Lines changed: 1192 additions & 683 deletions

File tree

.github/workflows/CI.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ jobs:
2626
run: |
2727
uv venv
2828
uv sync
29+
uv pip install .
2930
3031
- name: Get Python path
3132
run: |
@@ -40,7 +41,7 @@ jobs:
4041
run: |
4142
source ./.venv/bin/activate
4243
echo "DATABASE_URL=aiosqlite+:///db.sqlite3" > .env
43-
uv run nb orm upgrade
44+
uv run amrita orm upgrade
4445
- name: Run Load Test
4546
run: |
4647
echo "ADMIN_GROUP=1" >> .env

.github/workflows/PR.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ jobs:
2525
run: |
2626
uv venv
2727
uv sync
28+
uv pip install .
2829
2930
- name: Get Python path
3031
run: |
@@ -40,7 +41,7 @@ jobs:
4041
run: |
4142
source ./.venv/bin/activate
4243
echo "DATABASE_URL=aiosqlite+:///db.sqlite3" > .env
43-
uv run nb orm upgrade
44+
uv run amrita orm upgrade
4445
- name: Run Load Test
4546
run: |
4647
echo "ADMIN_GROUP=1" >> .env

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,3 +178,10 @@ pyrightconfig.json
178178
# DOTENV
179179
.env.prod
180180
.env.dev
181+
182+
# Amrita
183+
config/
184+
data/
185+
logs/
186+
tmp/
187+
cache/

=0.6.0

Whitespace-only changes.

README.md

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,45 @@ Amrita的命令执行插件
1414
uv add amrita-plugin-exec
1515
```
1616

17-
或者使用pip安装
17+
或者使用amrita-cli安装
1818

1919
```bash
20-
pip install amrita-plugin-exec
20+
amrita plugin install amrita-plugin-exec
21+
```
22+
23+
## 配置
24+
25+
插件提供了以下配置选项,可以在 .env 或环境变量中进行配置:
26+
27+
```dotenv
28+
ENABLE_DOCKER=false
29+
# 是否启用Docker下的指令执行,默认为 False
30+
31+
PLUGIN_EXEC_IMAGE_NAME=alpine:latest
32+
# 要使用的Docker镜像名称,默认为 "alpine:latest"
33+
34+
PLUGIN_EXEC_DOCKER_HOST=unix://var/run/docker.sock
35+
# Docker守护进程的地址,默认为 "unix://var/run/docker.sock"
36+
37+
PLUGIN_EXEC_SHELL_NAME=sh
38+
# 在容器中执行命令时使用的shell名称,默认为 "sh"
39+
40+
AUTO_REBUILD_CONTAINER=true
41+
# 是否在运行完成后自动重建容器,默认为 true
2142
```
2243

2344
## 使用方法
2445

2546
在Amrita机器人中,授权用户可以使用以下命令:
26-
- `/exec <command>`:执行指定的服务器命令。
47+
- `/exec <command>`:执行指定的服务器命令。
48+
49+
## 权限节点
50+
51+
插件添加了以下权限节点:
52+
53+
- `amrita.exec.full`:允许用户执行宿主命令。
54+
- `amrita.exec.safe`:允许用户容器内执行命令。
55+
56+
## TODO
57+
58+
- [ ] 包装为Amrita的Tool
Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,4 @@
1111
type="application",
1212
)
1313

14-
__all__ = ["container_exec","main"]
15-
14+
__all__ = ["container_exec", "main"]

amrita_plugin_exec/config.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from nonebot import get_plugin_config
2+
from pydantic import BaseModel
3+
4+
5+
class Config(BaseModel):
6+
enable_docker: bool = False
7+
image_name: str = "alpine:latest"
8+
docker_host: str = "unix://var/run/docker.sock"
9+
shell_name: str = "sh"
10+
auto_rebuild_container: bool = True
11+
12+
13+
CONFIG = get_plugin_config(Config)
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,33 @@
1+
import asyncio
2+
13
import docker
24
from docker.errors import DockerException, ImageNotFound
35
from nonebot import logger
46

7+
from .config import CONFIG
8+
59

610
async def pull_image(image_name: str):
711
try:
8-
client = docker.from_env()
12+
client: docker.DockerClient = docker.DockerClient(base_url=CONFIG.docker_host)
913
client.images.pull(image_name)
1014
return True
1115
except DockerException as e:
12-
print(f"Failed to pull image: {e}")
16+
logger.opt(exception=e, colors=True).exception(f"Failed to pull image: {e}")
1317
return False
1418

19+
1520
async def execute_in_docker(*cmd_parts):
1621
image_name = "alpine:latest"
1722
try:
18-
client = docker.from_env()
23+
client = docker.DockerClient(base_url=CONFIG.docker_host)
1924
# 将命令部分组合成完整的命令字符串
2025
cmd_text = " ".join(cmd_parts)
2126
container = client.containers.run(
2227
image_name,
23-
f"sh -c '{cmd_text}'",
28+
f"{CONFIG.shell_name} -c '{cmd_text}'",
2429
detach=True,
25-
remove=False,
30+
remove=CONFIG.auto_rebuild_container,
2631
network_mode="none",
2732
mem_limit="128m",
2833
cpu_period=100000,
@@ -33,19 +38,26 @@ async def execute_in_docker(*cmd_parts):
3338
)
3439

3540
try:
36-
result = container.wait(timeout=10)
41+
result = await asyncio.to_thread(
42+
lambda: container.wait(timeout=10),
43+
)
3744
logs = container.logs().decode("utf-8")
3845
exit_code = result.get("StatusCode", 0)
39-
container.remove(force=True)
46+
4047
return logs, exit_code
4148
except DockerException as e:
42-
print(f"Failed to get container logs: {e}")
43-
container.remove(force=True)
49+
logger.opt(exception=e, colors=True).exception(
50+
f"Failed to get container logs: {e}"
51+
)
52+
4453
return "", 1
54+
finally:
55+
if CONFIG.auto_rebuild_container:
56+
container.remove(force=True)
4557
except ImageNotFound:
4658
logger.warning(f"Image {image_name} not found. Pulling...")
4759
await pull_image(image_name)
4860
return await execute_in_docker(*cmd_parts)
4961
except DockerException as e:
5062
logger.error(f"Failed to run container: {e}")
51-
return f"Docker执行失败: {e}", 1
63+
return f"Docker执行失败: {e}", 1
Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,46 @@
22
import shlex
33
import subprocess
44

5+
from amrita.plugins.menu.models import MatcherData
6+
from amrita.plugins.perm.API.rules import UserPermissionChecker
7+
from amrita.utils.send import send_forward_msg
58
from nonebot import on_command
69
from nonebot.adapters.onebot.v11 import Bot, Message, MessageEvent, MessageSegment
710
from nonebot.exception import FinishedException
811
from nonebot.params import CommandArg
12+
from nonebot.permission import Permission
913

10-
from amrita.plugins.menu.models import MatcherData
11-
from amrita.plugins.perm.API.rules import UserPermissionChecker
12-
from amrita.utils.send import send_forward_msg
1314
from . import container_exec
15+
from .config import CONFIG
1416

1517
docker_check = UserPermissionChecker(permission="admin.exec.safe")
1618
host_check = UserPermissionChecker(permission="admin.exec.full")
1719
permission = host_check.checker()
1820
permission_docker = docker_check.checker()
1921

20-
execute_in_docker = on_command("exec",
21-
state=MatcherData(name="执行命令(docker)", usage="/exec <command>", description="在docker执行命令").model_dump(),
22-
priority=1,
23-
block=True,
24-
rule=permission_docker or permission)
22+
execute_in_docker = on_command(
23+
"exec",
24+
state=MatcherData(
25+
name="执行命令(docker)", usage="/exec <command>", description="在docker执行命令"
26+
).model_dump(),
27+
priority=1,
28+
block=True,
29+
permission=Permission(permission_docker, permission),
30+
rule=lambda: CONFIG.enable_docker,
31+
)
32+
33+
execute = on_command(
34+
"exec.host",
35+
state=MatcherData(
36+
name="执行命令(host)",
37+
usage="/exec.host <command>",
38+
description="在宿主机执行命令",
39+
).model_dump(),
40+
priority=1,
41+
block=True,
42+
rule=permission,
43+
)
2544

26-
execute = on_command("exec.host",
27-
state=MatcherData(name="执行命令(host)", usage="/exec.host <command>", description="在宿主机执行命令").model_dump(),
28-
priority=1,
29-
block=True,
30-
rule=permission)
3145

3246
@execute.handle()
3347
async def _(event: MessageEvent, bot: Bot, args: Message = CommandArg()):
@@ -41,20 +55,27 @@ async def _(event: MessageEvent, bot: Bot, args: Message = CommandArg()):
4155
*cmd_parts, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False
4256
)
4357
try:
44-
stdout, stderr = await asyncio.wait_for(execute_result.communicate(), timeout=10)
58+
stdout, stderr = await asyncio.wait_for(
59+
execute_result.communicate(), timeout=10
60+
)
4561
except asyncio.TimeoutError:
4662
execute_result.kill()
4763
await execute.finish("执行超时")
4864
results = [
4965
MessageSegment.text(f"执行结果:{stdout.decode('utf-8')}"),
50-
MessageSegment.text(f"执行失败:{stderr.decode('utf-8')}") if stderr else None
66+
MessageSegment.text(f"执行失败:{stderr.decode('utf-8')}")
67+
if stderr
68+
else None,
5169
]
52-
await send_forward_msg(bot, event, name="执行结果", uin=bot.self_id, msgs=results)
70+
await send_forward_msg(
71+
bot, event, name="执行结果", uin=bot.self_id, msgs=results
72+
)
5373
except FinishedException:
5474
pass
5575
except Exception as e:
5676
await bot.send(event, f"执行失败:{e}")
5777

78+
5879
@execute_in_docker.handle()
5980
async def _(event: MessageEvent, bot: Bot, args: Message = CommandArg()):
6081
try:
@@ -63,14 +84,12 @@ async def _(event: MessageEvent, bot: Bot, args: Message = CommandArg()):
6384
await execute_in_docker.finish("请输入要执行的命令")
6485
cmd_parts = shlex.split(cmd_text)
6586

66-
logs, exit_code = await container_exec.execute_in_docker(
67-
*cmd_parts
68-
)
69-
results = [
70-
MessageSegment.text(f"exit_code: {exit_code}\n执行结果:{logs}")
71-
]
72-
await send_forward_msg(bot, event, name="执行结果", uin=bot.self_id, msgs=results)
87+
logs, exit_code = await container_exec.execute_in_docker(*cmd_parts)
88+
results = [MessageSegment.text(f"exit_code: {exit_code}\n执行结果:{logs}")]
89+
await send_forward_msg(
90+
bot, event, name="执行结果", uin=bot.self_id, msgs=results
91+
)
7392
except FinishedException:
7493
pass
7594
except Exception as e:
76-
await bot.send(event, f"执行失败:{e}")
95+
await bot.send(event, f"执行失败:{e}")

pyproject.toml

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
11
[project]
22
name = "amrita-plugin-exec"
3-
description = ""
4-
version = "0.1.1"
3+
description = "Command execution plugin for Amrita"
4+
version = "0.1.2"
55
dependencies = [
66
"amrita[full]>=0.4.2",
7-
"docker>=6.0.0"
7+
"docker>=6.0.0",
88
]
99
readme = "README.md"
1010
requires-python = ">=3.10, <3.14"
1111

1212
[tool.nonebot]
1313
plugins = [
1414
"nonebot_plugin_orm",
15-
"amrita.plugins.exec",
1615
]
1716
plugin_dirs = []
1817

18+
[tool.amrita]
19+
plugins=["amrita_plugin_exec"] # 修改插件名称,使用下划线格式
20+
1921
[tool.setuptools.packages.find]
20-
where = ["src"]
21-
include = ["amrita*"]
22+
include = ["amrita-plugin-*"]
2223

2324
[[tool.nonebot.adapters]]
2425
name = "OneBot V11"
@@ -60,4 +61,4 @@ ignore = [
6061
]
6162

6263
[tool.pyright]
63-
typeCheckingMode = "standard"
64+
typeCheckingMode = "standard"

0 commit comments

Comments
 (0)