deva 1.3.0__tar.gz → 1.3.2__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.
- {deva-1.3.0 → deva-1.3.2}/PKG-INFO +1 -1
- {deva-1.3.0 → deva-1.3.2}/deva/__init__.py +2 -6
- {deva-1.3.0 → deva-1.3.2}/deva/admin.py +374 -122
- {deva-1.3.0 → deva-1.3.2}/deva/bus.py +2 -2
- {deva-1.3.0 → deva-1.3.2}/deva/core.py +5 -7
- deva-1.3.2/deva/gpt.py +290 -0
- {deva-1.3.0 → deva-1.3.2}/deva/page.py +0 -2
- {deva-1.3.0 → deva-1.3.2}/deva/pipe.py +34 -28
- {deva-1.3.0 → deva-1.3.2}/deva/sources.py +472 -195
- {deva-1.3.0 → deva-1.3.2}/deva/topic.py +1 -1
- {deva-1.3.0 → deva-1.3.2}/deva.egg-info/PKG-INFO +1 -1
- {deva-1.3.0 → deva-1.3.2}/deva.egg-info/SOURCES.txt +0 -1
- {deva-1.3.0 → deva-1.3.2}/setup.py +1 -1
- deva-1.3.0/deva/dask.py +0 -202
- deva-1.3.0/deva/gpt.py +0 -174
- {deva-1.3.0 → deva-1.3.2}/README.rst +0 -0
- {deva-1.3.0 → deva-1.3.2}/deva/browser.py +0 -0
- {deva-1.3.0 → deva-1.3.2}/deva/compute.py +0 -0
- {deva-1.3.0 → deva-1.3.2}/deva/endpoints.py +0 -0
- {deva-1.3.0 → deva-1.3.2}/deva/future.py +0 -0
- {deva-1.3.0 → deva-1.3.2}/deva/graph.py +0 -0
- {deva-1.3.0 → deva-1.3.2}/deva/lambdas.py +0 -0
- {deva-1.3.0 → deva-1.3.2}/deva/monitor.py +0 -0
- {deva-1.3.0 → deva-1.3.2}/deva/namespace.py +0 -0
- {deva-1.3.0 → deva-1.3.2}/deva/search.py +0 -0
- {deva-1.3.0 → deva-1.3.2}/deva/store.py +0 -0
- {deva-1.3.0 → deva-1.3.2}/deva/utils/__init__.py +0 -0
- {deva-1.3.0 → deva-1.3.2}/deva/utils/simhash.py +0 -0
- {deva-1.3.0 → deva-1.3.2}/deva/utils/sqlitedict.py +0 -0
- {deva-1.3.0 → deva-1.3.2}/deva/utils/time.py +0 -0
- {deva-1.3.0 → deva-1.3.2}/deva/utils/whooshalchemy.py +0 -0
- {deva-1.3.0 → deva-1.3.2}/deva/when.py +0 -0
- {deva-1.3.0 → deva-1.3.2}/deva.egg-info/dependency_links.txt +0 -0
- {deva-1.3.0 → deva-1.3.2}/deva.egg-info/not-zip-safe +0 -0
- {deva-1.3.0 → deva-1.3.2}/deva.egg-info/requires.txt +0 -0
- {deva-1.3.0 → deva-1.3.2}/deva.egg-info/top_level.txt +0 -0
- {deva-1.3.0 → deva-1.3.2}/setup.cfg +0 -0
|
@@ -18,11 +18,7 @@ from .lambdas import _
|
|
|
18
18
|
from .browser import browser,tab,tabs
|
|
19
19
|
from .gpt import async_gpt,sync_gpt
|
|
20
20
|
|
|
21
|
-
try:
|
|
22
|
-
# import panel as pn
|
|
23
|
-
from .dask import DaskStream, scatter
|
|
24
21
|
|
|
25
|
-
except ImportError:
|
|
26
|
-
pass
|
|
27
22
|
|
|
28
|
-
|
|
23
|
+
|
|
24
|
+
__version__ = '1.3.2'
|
|
@@ -27,43 +27,36 @@ Deva 管理面板 - 基于 PyWebIO 和 Tornado 的 Web 应用程序
|
|
|
27
27
|
# coding: utf-8
|
|
28
28
|
|
|
29
29
|
# In[2]:
|
|
30
|
+
import asyncio
|
|
30
31
|
import os
|
|
31
32
|
import traceback
|
|
33
|
+
import json
|
|
34
|
+
import time
|
|
32
35
|
from urllib.parse import urljoin
|
|
33
|
-
from deva import (
|
|
34
|
-
NW, NB, log, ls, Stream, first, sample, Deva, print, timer,NS,concat, Dtalk
|
|
35
|
-
)
|
|
36
|
-
from deva.browser import browser,tab,tabs
|
|
37
|
-
from deva.page import page #这里为了给流注入 webview 方法和sse 方法
|
|
38
|
-
from pywebio.output import (
|
|
39
|
-
put_text, put_markdown, set_scope, put_table,use_scope, clear, toast, put_button, put_collapse, put_datatable,
|
|
40
|
-
put_buttons, put_row, put_html, put_link, popup,close_popup
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
from tornado.web import create_signed_value, decode_signed_value
|
|
44
36
|
from typing import Callable, Union
|
|
45
|
-
from pywebio.platform.tornado import webio_handler
|
|
46
|
-
from pywebio_battery import put_logbox, logbox_append, set_localstorage, get_localstorage
|
|
47
|
-
from pywebio.pin import pin, put_input
|
|
48
|
-
from pywebio.session import set_env, run_async, run_js, run_asyncio_coroutine, get_session_implement
|
|
49
37
|
|
|
50
|
-
from pywebio.input import input, input_group, PASSWORD, textarea, actions, TEXT
|
|
51
38
|
import pandas as pd
|
|
52
|
-
import json
|
|
53
|
-
import time
|
|
54
|
-
|
|
55
39
|
from openai import AsyncOpenAI
|
|
56
40
|
from tornado.web import create_signed_value, decode_signed_value
|
|
57
|
-
|
|
41
|
+
|
|
42
|
+
from deva import (
|
|
43
|
+
NW, NB, log, ls, Stream, first, sample, Deva, print, timer, NS, concat, Dtalk
|
|
44
|
+
)
|
|
45
|
+
from deva.browser import browser, tab, tabs
|
|
46
|
+
from deva.bus import warn
|
|
47
|
+
from deva.page import page # 这里为了给流注入 webview 方法和sse 方法
|
|
48
|
+
from deva.gpt import async_gpt, sync_gpt
|
|
49
|
+
|
|
50
|
+
from pywebio.output import (
|
|
51
|
+
put_text, put_markdown, set_scope, put_table, use_scope, clear, toast,
|
|
52
|
+
put_button, put_collapse, put_datatable, put_buttons, put_row, put_html,
|
|
53
|
+
put_link, popup, close_popup, put_tabs
|
|
54
|
+
)
|
|
58
55
|
from pywebio.platform.tornado import webio_handler
|
|
59
56
|
from pywebio_battery import put_logbox, logbox_append, set_localstorage, get_localstorage
|
|
60
57
|
from pywebio.pin import pin, put_input
|
|
61
58
|
from pywebio.session import set_env, run_async, run_js, run_asyncio_coroutine, get_session_implement
|
|
62
59
|
from pywebio.input import input, input_group, PASSWORD, textarea, actions, TEXT
|
|
63
|
-
import pandas as pd
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
from deva.gpt import async_gpt,sync_gpt
|
|
67
60
|
|
|
68
61
|
|
|
69
62
|
@timer(5,start=False)
|
|
@@ -89,9 +82,30 @@ async def get_gpt_response(prompt, session=None, scope=None, model_type='deepsee
|
|
|
89
82
|
Returns:
|
|
90
83
|
str: 完整的GPT响应内容
|
|
91
84
|
"""
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
85
|
+
config = NB(model_type)
|
|
86
|
+
required_configs = ['api_key', 'base_url', 'model']
|
|
87
|
+
missing_configs = [config for config in required_configs if config not in config]
|
|
88
|
+
if missing_configs:
|
|
89
|
+
message = "警告: 在NB配置中缺少以下必要配置项: " + ', '.join(missing_configs) + ". 请确保在其他地方正确设置这些配置项的值。"
|
|
90
|
+
message >> warn
|
|
91
|
+
example_code = "from deva.namespace import NB\n\n"
|
|
92
|
+
example_code += "# 配置示例:\n"
|
|
93
|
+
example_code += "NB('deepseek').update({\n"
|
|
94
|
+
for config in missing_configs:
|
|
95
|
+
if config == 'api_key':
|
|
96
|
+
example_code += " 'api_key': 'your-api-key-here',\n"
|
|
97
|
+
elif config == 'base_url':
|
|
98
|
+
example_code += " 'base_url': 'https://api.example.com/v1',\n"
|
|
99
|
+
elif config == 'model':
|
|
100
|
+
example_code += " 'model': 'model-name',\n"
|
|
101
|
+
example_code += "})"
|
|
102
|
+
example_code >> warn
|
|
103
|
+
return ""
|
|
104
|
+
|
|
105
|
+
api_key = config.get('api_key')
|
|
106
|
+
base_url = config.get('base_url')
|
|
107
|
+
model = config.get('model')
|
|
108
|
+
|
|
95
109
|
gpt_client = AsyncOpenAI(api_key=api_key, base_url=base_url)
|
|
96
110
|
start_time = time.time()
|
|
97
111
|
|
|
@@ -323,7 +337,7 @@ log.start_cache(200, cache_max_age_seconds=60 * 60 * 24 * 30)
|
|
|
323
337
|
log.map(lambda x: log.recent(10) >> concat('<br>'))\
|
|
324
338
|
>> NS('访问日志', cache_max_len=1, cache_max_age_seconds=60 * 60 * 24 * 30)
|
|
325
339
|
|
|
326
|
-
|
|
340
|
+
|
|
327
341
|
|
|
328
342
|
streams = [NS('访问日志'), NS('实时新闻'), NS(
|
|
329
343
|
'涨跌停'), NS('领涨领跌板块'), NS('1分钟板块异动'), NS('30秒板块异动')]
|
|
@@ -1023,7 +1037,7 @@ def create_sidebar():
|
|
|
1023
1037
|
''')
|
|
1024
1038
|
# 在右边栏添加默认iframe
|
|
1025
1039
|
with use_scope('sidebar'):
|
|
1026
|
-
put_html(f
|
|
1040
|
+
put_html(f"""<iframe src="{hash(NS('访问日志'))}" style="width:100%;height:120vh;border:none;"></iframe>""")
|
|
1027
1041
|
|
|
1028
1042
|
async def init_admin_ui(title):
|
|
1029
1043
|
"""初始化管理界面UI
|
|
@@ -1064,38 +1078,6 @@ scheduler.start()
|
|
|
1064
1078
|
# 存储任务信息
|
|
1065
1079
|
tasks = {}
|
|
1066
1080
|
|
|
1067
|
-
async def async_code_gpt( prompts):
|
|
1068
|
-
"""
|
|
1069
|
-
异步查询大模型
|
|
1070
|
-
|
|
1071
|
-
参数:
|
|
1072
|
-
prompts: 提示词列表或字符串
|
|
1073
|
-
|
|
1074
|
-
返回:
|
|
1075
|
-
大模型返回的结果
|
|
1076
|
-
"""
|
|
1077
|
-
from openai import AsyncOpenAI
|
|
1078
|
-
config = NB('deepseek')
|
|
1079
|
-
api_key = config['api_key']
|
|
1080
|
-
base_url = config['base_url']
|
|
1081
|
-
model = config['model'] # 从配置中获取模型名称
|
|
1082
|
-
|
|
1083
|
-
# 初始化同步和异步客户端
|
|
1084
|
-
async_client = AsyncOpenAI(api_key=api_key, base_url=base_url)
|
|
1085
|
-
|
|
1086
|
-
if isinstance(prompts, str):
|
|
1087
|
-
prompts = [prompts]
|
|
1088
|
-
|
|
1089
|
-
messages = [{"role": "user", "content": prompt} for prompt in prompts]
|
|
1090
|
-
completion = await async_client.chat.completions.create(
|
|
1091
|
-
model=model,
|
|
1092
|
-
messages=messages,
|
|
1093
|
-
stream=False,
|
|
1094
|
-
max_tokens=8000,
|
|
1095
|
-
|
|
1096
|
-
)
|
|
1097
|
-
|
|
1098
|
-
return completion.choices[0].message.content
|
|
1099
1081
|
|
|
1100
1082
|
async def watch_topic(topic):
|
|
1101
1083
|
"""分析主题并显示结果"""
|
|
@@ -1109,8 +1091,10 @@ async def watch_topic(topic):
|
|
|
1109
1091
|
)
|
|
1110
1092
|
return result
|
|
1111
1093
|
|
|
1094
|
+
|
|
1112
1095
|
async def create_task():
|
|
1113
1096
|
"""创建定时任务"""
|
|
1097
|
+
|
|
1114
1098
|
task_info = await input_group("创建定时任务", [
|
|
1115
1099
|
input("任务名称", name="name", type=TEXT),
|
|
1116
1100
|
textarea("任务描述", name="description", placeholder="请输入任务描述"),
|
|
@@ -1131,6 +1115,7 @@ async def create_task():
|
|
|
1131
1115
|
return
|
|
1132
1116
|
|
|
1133
1117
|
task_info >>log
|
|
1118
|
+
toast("开始创建定时任务,需要一点时间...") # 通知用户开始创建
|
|
1134
1119
|
# 生成任务代码
|
|
1135
1120
|
samplecode = """我有下面这些功能可以调用,使用例子如下,请选择使用里面的功能来合理完成需求:
|
|
1136
1121
|
from deva import write_to_file,httpx,Dtalk
|
|
@@ -1139,12 +1124,12 @@ async def create_task():
|
|
|
1139
1124
|
写入文件: 'some text' >>write_to_file('filename')
|
|
1140
1125
|
抓取网页: response = await httpx(url)
|
|
1141
1126
|
查找网页标签:response.html.search('<title>{}</title>')
|
|
1142
|
-
|
|
1143
|
-
|
|
1127
|
+
发送到钉钉通知我:'@md@焦点分析|'+'some text'>>Dtalk()
|
|
1128
|
+
定期关注总结查看话题: content = await watch_topic('话题')
|
|
1144
1129
|
"""
|
|
1145
1130
|
# 调用GPT生成Python代码
|
|
1146
|
-
prompt = f"仅限于以下的功能和使用方法:{samplecode},根据以下描述: {description},生成一个Python异步函数,只生成函数主体就可以,不需要执行代码,所有 import
|
|
1147
|
-
result =
|
|
1131
|
+
prompt = f"仅限于以下的功能和使用方法:{samplecode},根据以下描述: {description},生成一个Python异步函数,只生成函数主体就可以,不需要执行代码,所有 import 都放在函数内部,详细的代码注释"
|
|
1132
|
+
result = await asyncio.create_task(async_gpt(prompts=prompt))
|
|
1148
1133
|
def get_python_code_from_deepseek(content):
|
|
1149
1134
|
# 假设返回的内容中 Python 代码被标记为 ```python ... ```
|
|
1150
1135
|
import re
|
|
@@ -1157,24 +1142,20 @@ async def create_task():
|
|
|
1157
1142
|
job_code>>log
|
|
1158
1143
|
import ast
|
|
1159
1144
|
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
# 使用 AST 解析代码
|
|
1145
|
+
# 使用 AST 解析代码
|
|
1163
1146
|
tree = ast.parse(job_code)
|
|
1164
1147
|
|
|
1165
|
-
# 获取所有函数定义的名称
|
|
1148
|
+
# 获取所有函数定义的名称
|
|
1166
1149
|
namespace = {}
|
|
1167
1150
|
exec(job_code,globals(), namespace) # 动态执行生成的代码
|
|
1168
1151
|
function_names = [node.name for node in ast.walk(tree) if isinstance(node, ast.AsyncFunctionDef)]
|
|
1169
1152
|
job_name = function_names[0]
|
|
1170
|
-
|
|
1153
|
+
|
|
1171
1154
|
job = namespace[job_name]
|
|
1172
1155
|
|
|
1173
1156
|
if task_type == "interval":
|
|
1174
1157
|
try:
|
|
1175
1158
|
interval = int(time_value)
|
|
1176
|
-
# j = timer(interval=interval,start=True)(job)
|
|
1177
|
-
# print(j)
|
|
1178
1159
|
scheduler.add_job(job, "interval", seconds=interval, id=name)
|
|
1179
1160
|
except ValueError:
|
|
1180
1161
|
toast("间隔时间必须为整数!", color="error")
|
|
@@ -1187,6 +1168,8 @@ async def create_task():
|
|
|
1187
1168
|
toast("时间格式应为 HH:MM!", color="error")
|
|
1188
1169
|
return
|
|
1189
1170
|
|
|
1171
|
+
|
|
1172
|
+
|
|
1190
1173
|
tasks[name] = {
|
|
1191
1174
|
"type": task_type,
|
|
1192
1175
|
"time": time_value,
|
|
@@ -1194,70 +1177,169 @@ async def create_task():
|
|
|
1194
1177
|
"description": description,
|
|
1195
1178
|
"job_code": job_code
|
|
1196
1179
|
}
|
|
1197
|
-
|
|
1180
|
+
# 将任务信息保存到数据库
|
|
1181
|
+
db = NB('tasks')
|
|
1182
|
+
db[name]=tasks[name]
|
|
1183
|
+
toast(f"任务 '{name}' 创建成功!", color="success") # 通知用户创建成功
|
|
1184
|
+
manage_tasks() # 刷新页面对应内容
|
|
1198
1185
|
# run_js("location.reload()") # 刷新页面,刷新后 session 会失效
|
|
1199
|
-
|
|
1200
1186
|
def manage_tasks():
|
|
1201
1187
|
"""管理定时任务"""
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1188
|
+
async def edit_code(name):
|
|
1189
|
+
code = await textarea("输入代码", code={
|
|
1190
|
+
"mode": "python", # 代码模式,可选 'javascript'、'html' 等
|
|
1191
|
+
"theme": "darcula", # 主题
|
|
1192
|
+
},value=tasks[name]["job_code"])
|
|
1193
|
+
code>>log
|
|
1194
|
+
tasks[name]['job_code']=code
|
|
1195
|
+
NB('tasks')[name] = tasks[name]
|
|
1196
|
+
# 删除老的任务
|
|
1197
|
+
try:
|
|
1198
|
+
scheduler.remove_job(name)
|
|
1199
|
+
except Exception as e:
|
|
1200
|
+
e>>log
|
|
1201
|
+
# 重新加载定时任务
|
|
1202
|
+
job_code = tasks[name]["job_code"]
|
|
1203
|
+
import ast
|
|
1204
|
+
tree = ast.parse(job_code)
|
|
1205
|
+
namespace = {}
|
|
1206
|
+
exec(job_code, globals(), namespace)
|
|
1207
|
+
function_names = [node.name for node in ast.walk(tree) if isinstance(node, ast.AsyncFunctionDef)]
|
|
1208
|
+
job_name = function_names[0]
|
|
1209
|
+
job_func = namespace[job_name]
|
|
1210
|
+
if tasks[name]["type"] == "interval":
|
|
1211
|
+
scheduler.add_job(job_func, "interval", seconds=int(tasks[name]["time"]), id=name)
|
|
1212
|
+
elif tasks[name]["type"] == "cron":
|
|
1213
|
+
hour, minute = map(int, tasks[name]["time"].split(":"))
|
|
1214
|
+
scheduler.add_job(job_func, "cron", hour=hour, minute=minute, id=name)
|
|
1215
|
+
|
|
1216
|
+
print("当前所有计划任务:")
|
|
1217
|
+
for job in scheduler.get_jobs():
|
|
1218
|
+
print(f"任务名称: {job.id}, 任务类型: {job.trigger.__class__.__name__}, 下次执行时间: {job.next_run_time}")
|
|
1219
|
+
# popup('显示代码',content=put_code(content=tasks[name]["job_code"],language='python'))
|
|
1220
|
+
with use_scope('task_management',clear=True):
|
|
1221
|
+
if not tasks:
|
|
1222
|
+
put_text("当前没有定时任务。")
|
|
1223
|
+
return
|
|
1224
|
+
tasks>>log
|
|
1225
|
+
active_table_data = []
|
|
1226
|
+
deleted_table_data = []
|
|
1227
|
+
for name, info in tasks.items():
|
|
1228
|
+
row = [
|
|
1229
|
+
name,
|
|
1230
|
+
info["description"],
|
|
1231
|
+
info["type"],
|
|
1232
|
+
info["time"],
|
|
1233
|
+
info["status"]
|
|
1234
|
+
]
|
|
1235
|
+
if info["status"] in ["运行中", "已停止"]:
|
|
1236
|
+
row.append(put_row([
|
|
1237
|
+
put_button("源码", onclick=lambda n=name: edit_code(n), color="primary"),
|
|
1238
|
+
put_button("停止", onclick=lambda n=name: stop_task(n), color="danger" if info["status"] == "运行中" else "secondary", disabled=info["status"] != "运行中"),
|
|
1239
|
+
put_button("启动", onclick=lambda n=name: start_task(n), color="success" if info["status"] == "已停止" else "secondary", disabled=info["status"] != "已停止"),
|
|
1240
|
+
put_button("删除", onclick=lambda n=name: delete_task(n), color="warning"),
|
|
1241
|
+
]))
|
|
1242
|
+
active_table_data.append(row)
|
|
1243
|
+
elif info["status"] == "已删除":
|
|
1244
|
+
row.append(put_row([
|
|
1245
|
+
put_button("源码", onclick=lambda n=name: edit_code(n), color="primary"),
|
|
1246
|
+
put_button("恢复", onclick=lambda n=name: recover_task(n), color="success"),
|
|
1247
|
+
put_button("彻底删除", onclick=lambda n=name: remove_task_forever(n), color="danger"),
|
|
1248
|
+
]))
|
|
1249
|
+
deleted_table_data.append(row)
|
|
1250
|
+
if active_table_data:
|
|
1251
|
+
put_table(
|
|
1252
|
+
active_table_data,
|
|
1253
|
+
header=["任务名称", "任务描述", "任务类型", "时间/间隔", "状态", "操作"]
|
|
1254
|
+
)
|
|
1221
1255
|
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1256
|
+
if deleted_table_data:
|
|
1257
|
+
with put_collapse("已删除任务", open=False):
|
|
1258
|
+
put_table(
|
|
1259
|
+
deleted_table_data,
|
|
1260
|
+
header=["任务名称", "任务描述", "任务类型", "时间/间隔", "状态", "操作"]
|
|
1261
|
+
)
|
|
1227
1262
|
|
|
1228
1263
|
def stop_task(name):
|
|
1229
1264
|
"""停止任务"""
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1265
|
+
scheduler.pause_job(name)
|
|
1266
|
+
tasks[name]["status"] = "已停止"
|
|
1267
|
+
toast(f"任务 '{name}' 已停止!", color="success")
|
|
1268
|
+
run_js("location.reload()") # 刷新页面
|
|
1269
|
+
# 更新数据库中的任务状态
|
|
1270
|
+
db = NB('tasks')
|
|
1271
|
+
db[name]=tasks[name]
|
|
1272
|
+
manage_tasks()
|
|
1235
1273
|
|
|
1236
1274
|
def start_task(name):
|
|
1237
1275
|
"""启动任务"""
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1276
|
+
scheduler.resume_job(name)
|
|
1277
|
+
tasks[name]["status"] = "运行中"
|
|
1278
|
+
toast(f"任务 '{name}' 已启动!", color="success")
|
|
1279
|
+
run_js("location.reload()") # 刷新页面
|
|
1280
|
+
# 更新数据库中的任务状态
|
|
1281
|
+
db = NB('tasks')
|
|
1282
|
+
db[name]=tasks[name]
|
|
1283
|
+
manage_tasks()
|
|
1243
1284
|
|
|
1244
1285
|
def delete_task(name):
|
|
1245
1286
|
"""删除任务"""
|
|
1246
|
-
|
|
1247
|
-
scheduler.remove_job(name)
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1287
|
+
try:
|
|
1288
|
+
scheduler.remove_job(name)>>log
|
|
1289
|
+
except Exception as e:
|
|
1290
|
+
e>>log
|
|
1291
|
+
tasks[name].update({"status": "已删除"})
|
|
1292
|
+
# 更新数据库中的任务状态为已删除
|
|
1293
|
+
db = NB('tasks')
|
|
1294
|
+
db[name]=tasks[name]
|
|
1295
|
+
toast(f"任务 '{name}' 已删除!", color="success")
|
|
1296
|
+
manage_tasks()
|
|
1297
|
+
|
|
1298
|
+
def recover_task(name):
|
|
1299
|
+
db = NB('tasks')
|
|
1300
|
+
info = db[name]
|
|
1301
|
+
info['status'] = '运行中'
|
|
1302
|
+
tasks[name] = info
|
|
1303
|
+
db[name] = tasks[name]
|
|
1304
|
+
|
|
1305
|
+
job_code = info["job_code"]
|
|
1306
|
+
# 对job_code进行转换,生成函数
|
|
1307
|
+
import ast
|
|
1308
|
+
tree = ast.parse(job_code)
|
|
1309
|
+
namespace = {}
|
|
1310
|
+
exec(job_code, globals(), namespace)
|
|
1311
|
+
function_names = [node.name for node in ast.walk(tree) if isinstance(node, ast.AsyncFunctionDef)]
|
|
1312
|
+
job_name = function_names[0]
|
|
1251
1313
|
|
|
1314
|
+
job_func = namespace[job_name]
|
|
1315
|
+
if info["type"] == "interval":
|
|
1316
|
+
scheduler.add_job(job_func, "interval", seconds=int(info["time"]), id=name)
|
|
1317
|
+
elif info["type"] == "cron":
|
|
1318
|
+
hour, minute = map(int, info["time"].split(":"))
|
|
1319
|
+
scheduler.add_job(job_func, "cron", hour=hour, minute=minute, id=name)
|
|
1320
|
+
|
|
1321
|
+
|
|
1322
|
+
manage_tasks()
|
|
1323
|
+
|
|
1324
|
+
def remove_task_forever(name):
|
|
1325
|
+
"""彻底删除任务,永久无法恢复"""
|
|
1326
|
+
try:
|
|
1327
|
+
scheduler.remove_job(name)>>log
|
|
1328
|
+
except Exception as e:
|
|
1329
|
+
e>>log
|
|
1330
|
+
del tasks[name]
|
|
1331
|
+
# 更新数据库中的任务状态为已删除
|
|
1332
|
+
db = NB('tasks')
|
|
1333
|
+
del db[name]
|
|
1334
|
+
toast(f"任务 '{name}' 已删除!", color="success")
|
|
1335
|
+
manage_tasks()
|
|
1336
|
+
|
|
1252
1337
|
async def taskadmin():
|
|
1253
1338
|
await init_admin_ui('Deva任务管理')
|
|
1254
1339
|
|
|
1255
|
-
|
|
1256
1340
|
put_button("创建定时任务", onclick=create_task)
|
|
1257
1341
|
manage_tasks() # 直接展示任务列表
|
|
1258
1342
|
set_scope('task_log')
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
1343
|
|
|
1262
1344
|
|
|
1263
1345
|
async def dbadmin():
|
|
@@ -1270,7 +1352,151 @@ async def streamadmin():
|
|
|
1270
1352
|
put_markdown('### 数据流')
|
|
1271
1353
|
put_buttons([s.name for s in streams],onclick=stream_click)
|
|
1272
1354
|
|
|
1273
|
-
|
|
1355
|
+
async def inspect_object(obj):
|
|
1356
|
+
"""在Web界面上展示Python对象的所有相关信息
|
|
1357
|
+
|
|
1358
|
+
参数:
|
|
1359
|
+
obj: 要展示的Python对象
|
|
1360
|
+
"""
|
|
1361
|
+
# 创建popup内容
|
|
1362
|
+
content = []
|
|
1363
|
+
|
|
1364
|
+
# 显示对象基本信息
|
|
1365
|
+
content.append(put_markdown(f"### 对象信息"))
|
|
1366
|
+
content.append(put_table([
|
|
1367
|
+
['类型', type(obj).__name__],
|
|
1368
|
+
['ID', id(obj)],
|
|
1369
|
+
['哈希值', hash(obj) if not isinstance(obj, dict) else 'N/A (字典不可哈希)'],
|
|
1370
|
+
['可调用', callable(obj)]
|
|
1371
|
+
]))
|
|
1372
|
+
|
|
1373
|
+
# 显示对象文档
|
|
1374
|
+
if obj.__doc__:
|
|
1375
|
+
content.append(put_markdown("#### 文档说明"))
|
|
1376
|
+
if obj.__doc__:
|
|
1377
|
+
# 将文档内容按行分割
|
|
1378
|
+
doc_lines = obj.__doc__.split('\n')
|
|
1379
|
+
formatted_doc = []
|
|
1380
|
+
in_code_block = False
|
|
1381
|
+
|
|
1382
|
+
for line in doc_lines:
|
|
1383
|
+
# 检测代码块开始
|
|
1384
|
+
if line.strip().startswith('>>>') or line.strip().startswith('...'):
|
|
1385
|
+
if not in_code_block:
|
|
1386
|
+
formatted_doc.append('```python')
|
|
1387
|
+
in_code_block = True
|
|
1388
|
+
formatted_doc.append(line)
|
|
1389
|
+
else:
|
|
1390
|
+
if in_code_block:
|
|
1391
|
+
formatted_doc.append('```')
|
|
1392
|
+
in_code_block = False
|
|
1393
|
+
formatted_doc.append(line)
|
|
1394
|
+
|
|
1395
|
+
# 处理最后一个代码块
|
|
1396
|
+
if in_code_block:
|
|
1397
|
+
formatted_doc.append('```')
|
|
1398
|
+
|
|
1399
|
+
# 将格式化后的文档内容添加到显示
|
|
1400
|
+
content.append(put_markdown('\n'.join(formatted_doc)))
|
|
1401
|
+
|
|
1402
|
+
# 显示对象属性
|
|
1403
|
+
content.append(put_markdown("#### 属性"))
|
|
1404
|
+
attrs = []
|
|
1405
|
+
for attr in dir(obj):
|
|
1406
|
+
if not attr.startswith('__'):
|
|
1407
|
+
try:
|
|
1408
|
+
value = getattr(obj, attr)
|
|
1409
|
+
# 过滤掉方法,只显示非可调用属性
|
|
1410
|
+
if not callable(value):
|
|
1411
|
+
attrs.append([attr, type(value).__name__, str(value)[:100]])
|
|
1412
|
+
except Exception as e:
|
|
1413
|
+
attrs.append([attr, '无法访问', str(e)])
|
|
1414
|
+
content.append(put_table([['属性名', '类型', '值']] + attrs))
|
|
1415
|
+
|
|
1416
|
+
# 显示对象方法
|
|
1417
|
+
content.append(put_markdown("#### 方法"))
|
|
1418
|
+
methods = []
|
|
1419
|
+
for attr in dir(obj):
|
|
1420
|
+
if not attr.startswith('__'):
|
|
1421
|
+
try:
|
|
1422
|
+
value = getattr(obj, attr)
|
|
1423
|
+
if callable(value):
|
|
1424
|
+
doc = value.__doc__ or '无文档说明'
|
|
1425
|
+
methods.append([attr, doc[:200]])
|
|
1426
|
+
except Exception as e:
|
|
1427
|
+
methods.append([attr, f'无法访问: {str(e)}'])
|
|
1428
|
+
content.append(put_table([['方法名', '文档说明']] + methods))
|
|
1429
|
+
|
|
1430
|
+
# 在popup中显示内容
|
|
1431
|
+
popup(title="对象详情", content=content, size='large')
|
|
1432
|
+
async def document():
|
|
1433
|
+
await init_admin_ui("Deva管理面板")
|
|
1434
|
+
# 获取deva下所有子模块
|
|
1435
|
+
import deva
|
|
1436
|
+
import pkgutil
|
|
1437
|
+
import inspect
|
|
1438
|
+
|
|
1439
|
+
# 遍历deva下的所有子模块
|
|
1440
|
+
# 创建tab容器
|
|
1441
|
+
tabs = []
|
|
1442
|
+
|
|
1443
|
+
for module_info in pkgutil.iter_modules(deva.__path__):
|
|
1444
|
+
module_name = module_info.name
|
|
1445
|
+
try:
|
|
1446
|
+
# 动态导入模块
|
|
1447
|
+
module = __import__(f'deva.{module_name}', fromlist=['*'])
|
|
1448
|
+
|
|
1449
|
+
# 创建模块内容
|
|
1450
|
+
with use_scope(f'module_{module_name}', clear=True):
|
|
1451
|
+
# 创建类和函数表格
|
|
1452
|
+
module_table = [['名称', '类型', '文档说明']]
|
|
1453
|
+
|
|
1454
|
+
# 获取模块中定义的成员,包括全局变量
|
|
1455
|
+
members = inspect.getmembers(module)
|
|
1456
|
+
|
|
1457
|
+
# 获取模块的全局变量
|
|
1458
|
+
global_vars = {k: v for k, v in module.__dict__.items()
|
|
1459
|
+
if not k.startswith('__') and not inspect.ismodule(v)}
|
|
1460
|
+
|
|
1461
|
+
# 合并成员和全局变量
|
|
1462
|
+
all_objects = {**dict(members), **global_vars}
|
|
1463
|
+
|
|
1464
|
+
# 过滤出类和函数和全局对象
|
|
1465
|
+
for name, obj in all_objects.items():
|
|
1466
|
+
# 检查是否为类、函数、被装饰的函数或全局对象
|
|
1467
|
+
if (inspect.isclass(obj) or inspect.isfunction(obj) or
|
|
1468
|
+
hasattr(obj, '__wrapped__') or
|
|
1469
|
+
(not inspect.ismodule(obj) and not name.startswith('_'))) :
|
|
1470
|
+
doc = obj.__doc__ or '无文档说明'
|
|
1471
|
+
if isinstance(doc, str):
|
|
1472
|
+
# 判断对象类型
|
|
1473
|
+
if inspect.isclass(obj):
|
|
1474
|
+
obj_type = '类'
|
|
1475
|
+
elif inspect.isfunction(obj) or hasattr(obj, '__wrapped__'):
|
|
1476
|
+
obj_type = '函数'
|
|
1477
|
+
else:
|
|
1478
|
+
obj_type = '全局对象'
|
|
1479
|
+
|
|
1480
|
+
# 对于被装饰器修饰的函数,获取原始函数的文档
|
|
1481
|
+
if hasattr(obj, '__wrapped__'):
|
|
1482
|
+
doc = obj.__wrapped__.__doc__ or doc
|
|
1483
|
+
|
|
1484
|
+
# 添加点击事件
|
|
1485
|
+
action_button = put_button(name, onclick=lambda o=obj: run_async(inspect_object(o)))
|
|
1486
|
+
module_table.append([action_button, obj_type, doc[:200]])
|
|
1487
|
+
# 添加模块内容到tab
|
|
1488
|
+
tabs.append({
|
|
1489
|
+
'title': module_name,
|
|
1490
|
+
'content': put_table(module_table)
|
|
1491
|
+
})
|
|
1492
|
+
except ImportError as e:
|
|
1493
|
+
print(f"无法导入模块 {module_name}: {e}")
|
|
1494
|
+
tabs.append({
|
|
1495
|
+
'title': module_name,
|
|
1496
|
+
'content': put_text(f"无法加载模块: {str(e)}")
|
|
1497
|
+
})
|
|
1498
|
+
# 显示所有模块的tab
|
|
1499
|
+
put_tabs(tabs)
|
|
1274
1500
|
async def main():
|
|
1275
1501
|
# await my_timer()
|
|
1276
1502
|
# 这个将会把会话协程卡在这里不动,采用 run_async则不会堵塞
|
|
@@ -1284,6 +1510,10 @@ async def main():
|
|
|
1284
1510
|
|
|
1285
1511
|
set_table_style() # 调用函数应用样式
|
|
1286
1512
|
|
|
1513
|
+
# 获取所有函数定义的名称
|
|
1514
|
+
|
|
1515
|
+
|
|
1516
|
+
|
|
1287
1517
|
|
|
1288
1518
|
# 获取所有主题数据
|
|
1289
1519
|
topics = NB('topics').items()
|
|
@@ -1892,7 +2122,7 @@ def create_nav_menu():
|
|
|
1892
2122
|
{name: '数据库', path: '/dbadmin', action: () => window.location.href = '/dbadmin'},
|
|
1893
2123
|
{name: '实时流', path: '/streamadmin', action: () => window.location.href = '/streamadmin'},
|
|
1894
2124
|
{name: '任务', path: '/taskadmin', action: () => window.location.href = '/taskadmin'},
|
|
1895
|
-
{name: '
|
|
2125
|
+
{name: '文档', path: '/document', action: () => window.location.href = '/document'}
|
|
1896
2126
|
];
|
|
1897
2127
|
|
|
1898
2128
|
menuItems.forEach(item => {
|
|
@@ -1935,15 +2165,37 @@ def create_nav_menu():
|
|
|
1935
2165
|
if __name__ == '__main__':
|
|
1936
2166
|
from deva.page import page
|
|
1937
2167
|
|
|
2168
|
+
# 从数据库读取任务信息
|
|
2169
|
+
db = NB('tasks')
|
|
2170
|
+
for name, info in db.items():
|
|
2171
|
+
tasks[name] = info
|
|
2172
|
+
if info["status"] == "运行中":
|
|
2173
|
+
job_code = info["job_code"]
|
|
2174
|
+
# 对job_code进行转换,生成函数
|
|
2175
|
+
import ast
|
|
2176
|
+
tree = ast.parse(job_code)
|
|
2177
|
+
namespace = {}
|
|
2178
|
+
exec(job_code, globals(), namespace)
|
|
2179
|
+
function_names = [node.name for node in ast.walk(tree) if isinstance(node, ast.AsyncFunctionDef)]
|
|
2180
|
+
job_name = function_names[0]
|
|
2181
|
+
|
|
2182
|
+
job_func = namespace[job_name]
|
|
2183
|
+
if info["type"] == "interval":
|
|
2184
|
+
scheduler.add_job(job_func, "interval", seconds=int(info["time"]), id=name)
|
|
2185
|
+
elif info["type"] == "cron":
|
|
2186
|
+
hour, minute = map(int, info["time"].split(":"))
|
|
2187
|
+
scheduler.add_job(job_func, "cron", hour=hour, minute=minute, id=name)
|
|
1938
2188
|
|
|
1939
2189
|
# 创建一个名为'stream_webview'的Web服务器实例,监听所有网络接口(0.0.0.0)
|
|
1940
2190
|
# 然后为该服务器添加路由处理器,将'/admin'路径映射到dbadmin处理函数
|
|
1941
2191
|
# 使用PyWebIO的webio_handler进行封装,并指定CDN地址
|
|
2192
|
+
cdn='https://fastly.jsdelivr.net/gh/wang0618/PyWebIO-assets@v1.8.3/'
|
|
1942
2193
|
handlers = [
|
|
1943
|
-
(r'/dbadmin', webio_handler(dbadmin, cdn=
|
|
1944
|
-
(r'/streamadmin', webio_handler(streamadmin, cdn=
|
|
1945
|
-
(r'/', webio_handler(main, cdn=
|
|
1946
|
-
(r'/taskadmin', webio_handler(taskadmin, cdn=
|
|
2194
|
+
(r'/dbadmin', webio_handler(dbadmin, cdn=cdn)),
|
|
2195
|
+
(r'/streamadmin', webio_handler(streamadmin, cdn=cdn)),
|
|
2196
|
+
(r'/', webio_handler(main, cdn=cdn)),
|
|
2197
|
+
(r'/taskadmin', webio_handler(taskadmin, cdn=cdn)),
|
|
2198
|
+
(r'/document', webio_handler(document, cdn=cdn))
|
|
1947
2199
|
]
|
|
1948
2200
|
NW('stream_webview',host='0.0.0.0').application.add_handlers('.*$', handlers)
|
|
1949
2201
|
|