aipyapp-qpython 0.1.24.20__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.
- aipyapp/__init__.py +1 -0
- aipyapp/__main__.py +36 -0
- aipyapp/aipy/__init__.py +7 -0
- aipyapp/aipy/config.py +105 -0
- aipyapp/aipy/i18n.py +212 -0
- aipyapp/aipy/interface.py +26 -0
- aipyapp/aipy/llm.py +540 -0
- aipyapp/aipy/plugin.py +87 -0
- aipyapp/aipy/prompt.py +684 -0
- aipyapp/aipy/prompt.zh.py +512 -0
- aipyapp/aipy/prompt_en.py +688 -0
- aipyapp/aipy/qpython.py +35 -0
- aipyapp/aipy/runner.py +182 -0
- aipyapp/aipy/task.py +264 -0
- aipyapp/aipy/taskmgr.py +108 -0
- aipyapp/aipy/template.html +290 -0
- aipyapp/aipy/templates.py +54 -0
- aipyapp/aipy/utils.py +76 -0
- aipyapp/default.toml +97 -0
- aipyapp/default_en.toml +97 -0
- aipyapp/gui.py +409 -0
- aipyapp/main.py +80 -0
- aipyapp/publish.py +60 -0
- aipyapp/saas.py +167 -0
- aipyapp/wxgui.py +367 -0
- aipyapp_qpython-0.1.24.20.dist-info/METADATA +44 -0
- aipyapp_qpython-0.1.24.20.dist-info/RECORD +29 -0
- aipyapp_qpython-0.1.24.20.dist-info/WHEEL +5 -0
- aipyapp_qpython-0.1.24.20.dist-info/top_level.txt +1 -0
aipyapp/gui.py
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
from enum import Enum, auto
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import importlib.resources as resources
|
|
8
|
+
import traceback
|
|
9
|
+
from typing import Any, Optional, Union
|
|
10
|
+
import threading
|
|
11
|
+
try:
|
|
12
|
+
import tkinter as tk
|
|
13
|
+
from tkinter import ttk
|
|
14
|
+
import tkinter.font as tkFont
|
|
15
|
+
import tkinter.scrolledtext as scrolledtext
|
|
16
|
+
except ImportError:
|
|
17
|
+
import sys
|
|
18
|
+
print("Python Tkinter package is not installed. Please install python-tk.")
|
|
19
|
+
if sys.platform == "darwin":
|
|
20
|
+
print("You can use brew to install it: brew install python-tk")
|
|
21
|
+
else:
|
|
22
|
+
raise
|
|
23
|
+
sys.exit(1)
|
|
24
|
+
|
|
25
|
+
from dynaconf import Dynaconf
|
|
26
|
+
from rich.console import Console,JustifyMethod, OverflowMethod
|
|
27
|
+
from prompt_toolkit import PromptSession
|
|
28
|
+
from prompt_toolkit.history import FileHistory
|
|
29
|
+
from prompt_toolkit.styles import Style
|
|
30
|
+
from prompt_toolkit.completion import WordCompleter
|
|
31
|
+
|
|
32
|
+
from . import __version__
|
|
33
|
+
from .aipy.config import ConfigManager
|
|
34
|
+
from .aipy import TaskManager
|
|
35
|
+
from .aipy.i18n import T,set_lang
|
|
36
|
+
#
|
|
37
|
+
__PACKAGE_NAME__ = "aipyapp"
|
|
38
|
+
|
|
39
|
+
def strip_rich_text_tags(text: str) -> str:
|
|
40
|
+
"""
|
|
41
|
+
Removes common Rich text formatting tags from a string without using regex.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
text: The input string containing Rich tags.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The string with Rich tags removed.
|
|
48
|
+
"""
|
|
49
|
+
# List of common colors and styles
|
|
50
|
+
colors = ["black", "red", "green", "yellow", "blue", "magenta", "cyan", "white"]
|
|
51
|
+
styles = ["bold", "italic", "underline", "blink", "reverse", "strike", "dim", "b", "i", "u", "s"]
|
|
52
|
+
|
|
53
|
+
tags_to_remove = []
|
|
54
|
+
|
|
55
|
+
# Simple color tags
|
|
56
|
+
for color in colors:
|
|
57
|
+
tags_to_remove.append(f"[{color}]")
|
|
58
|
+
tags_to_remove.append(f"[/{color}]")
|
|
59
|
+
|
|
60
|
+
# Simple style tags
|
|
61
|
+
for style in styles:
|
|
62
|
+
tags_to_remove.append(f"[{style}]")
|
|
63
|
+
tags_to_remove.append(f"[/{style}]")
|
|
64
|
+
|
|
65
|
+
# Combined style and color tags (e.g., [bold red])
|
|
66
|
+
for style in styles:
|
|
67
|
+
for color in colors:
|
|
68
|
+
tags_to_remove.append(f"[{style} {color}]")
|
|
69
|
+
tags_to_remove.append(f"[/{style} {color}]") # Although [/style color] is not standard, include for safety
|
|
70
|
+
tags_to_remove.append(f"[{color} {style}]") # Handle swapped order if necessary
|
|
71
|
+
tags_to_remove.append(f"[/{color} {style}]")
|
|
72
|
+
|
|
73
|
+
# Generic closing tag
|
|
74
|
+
tags_to_remove.append("[/]")
|
|
75
|
+
|
|
76
|
+
# Add specific known tags if needed
|
|
77
|
+
# tags_to_remove.extend(["[link]", "[/link]"]) # Example
|
|
78
|
+
|
|
79
|
+
cleaned_text = text
|
|
80
|
+
for tag in tags_to_remove:
|
|
81
|
+
cleaned_text = cleaned_text.replace(tag, "")
|
|
82
|
+
|
|
83
|
+
# Handle potential edge cases like incomplete tags or variations if necessary
|
|
84
|
+
# This simple replacement might leave artifacts if tags are nested complexly
|
|
85
|
+
# or if there are tags not covered in the lists.
|
|
86
|
+
|
|
87
|
+
return cleaned_text
|
|
88
|
+
|
|
89
|
+
class GUIConsole(Console):
|
|
90
|
+
def __init__(self, *args, **kwargs):
|
|
91
|
+
super().__init__(*args, **kwargs)
|
|
92
|
+
self.gui = None
|
|
93
|
+
|
|
94
|
+
def set_gui(self, gui):
|
|
95
|
+
self.gui = gui
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def print(
|
|
99
|
+
self,
|
|
100
|
+
*objects: Any,
|
|
101
|
+
sep: str = " ",
|
|
102
|
+
end: str = "\n",
|
|
103
|
+
style: Optional[Union[str, Style]] = None,
|
|
104
|
+
justify: Optional[JustifyMethod] = None,
|
|
105
|
+
overflow: Optional[OverflowMethod] = None,
|
|
106
|
+
no_wrap: Optional[bool] = None,
|
|
107
|
+
emoji: Optional[bool] = None,
|
|
108
|
+
markup: Optional[bool] = None,
|
|
109
|
+
highlight: Optional[bool] = None,
|
|
110
|
+
width: Optional[int] = None,
|
|
111
|
+
height: Optional[int] = None,
|
|
112
|
+
crop: bool = True,
|
|
113
|
+
soft_wrap: Optional[bool] = None,
|
|
114
|
+
new_line_start: bool = False,
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Print to the console and send the output to a GUI handler."""
|
|
117
|
+
message = ""
|
|
118
|
+
# if the first argument is a string, use it as the message
|
|
119
|
+
if len(objects) == 1 and isinstance(objects[0], str):
|
|
120
|
+
message = objects[0]
|
|
121
|
+
else:
|
|
122
|
+
# Otherwise, join all objects into a single string
|
|
123
|
+
#message = sep.join(str(_object) for _object in objects)
|
|
124
|
+
pass
|
|
125
|
+
# If the message is empty, return
|
|
126
|
+
if not message:
|
|
127
|
+
return
|
|
128
|
+
#print("message", message)
|
|
129
|
+
|
|
130
|
+
if self.gui:
|
|
131
|
+
message = strip_rich_text_tags(message)
|
|
132
|
+
self.gui.handle_ai_output(message)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def print_exception(self, *args, **kwargs):
|
|
136
|
+
super().print_exception(*args, **kwargs)
|
|
137
|
+
# Optionally, capture the formatted exception and display in the text_widget
|
|
138
|
+
# This requires capturing the output of super().print_exception
|
|
139
|
+
# and redirecting it to the text_widget. A full implementation is
|
|
140
|
+
# beyond the scope of this example.
|
|
141
|
+
|
|
142
|
+
class CommandType(Enum):
|
|
143
|
+
CMD_DONE = auto()
|
|
144
|
+
CMD_USE = auto()
|
|
145
|
+
CMD_EXIT = auto()
|
|
146
|
+
CMD_INVALID = auto()
|
|
147
|
+
CMD_TEXT = auto()
|
|
148
|
+
|
|
149
|
+
def parse_command(input_str, llms=set()):
|
|
150
|
+
lower = input_str.strip().lower()
|
|
151
|
+
|
|
152
|
+
if lower in ("/done", "done"):
|
|
153
|
+
return CommandType.CMD_DONE, None
|
|
154
|
+
if lower in ("/exit", "exit"):
|
|
155
|
+
return CommandType.CMD_EXIT, None
|
|
156
|
+
if lower in llms:
|
|
157
|
+
return CommandType.CMD_USE, input_str
|
|
158
|
+
|
|
159
|
+
if lower.startswith("/use "):
|
|
160
|
+
arg = input_str[5:].strip()
|
|
161
|
+
if arg in llms:
|
|
162
|
+
return CommandType.CMD_USE, arg
|
|
163
|
+
else:
|
|
164
|
+
return CommandType.CMD_INVALID, arg
|
|
165
|
+
|
|
166
|
+
if lower.startswith("use "):
|
|
167
|
+
arg = input_str[4:].strip()
|
|
168
|
+
if arg in llms:
|
|
169
|
+
return CommandType.CMD_USE, arg
|
|
170
|
+
|
|
171
|
+
return CommandType.CMD_TEXT, input_str
|
|
172
|
+
|
|
173
|
+
class AIAppGUI:
|
|
174
|
+
def __init__(self, tm, settings):
|
|
175
|
+
self.tm = tm
|
|
176
|
+
self.settings = settings
|
|
177
|
+
|
|
178
|
+
# init llm
|
|
179
|
+
self.names = tm.llm.names
|
|
180
|
+
self.task = None
|
|
181
|
+
completer = WordCompleter(['/use', 'use', '/done','done'] + list(self.names['enabled']), ignore_case=True)
|
|
182
|
+
self.history = FileHistory(str(Path.cwd() / settings.history))
|
|
183
|
+
self.session = PromptSession(history=self.history, completer=completer)
|
|
184
|
+
self.style_main = Style.from_dict({"prompt": "green"})
|
|
185
|
+
self.style_ai = Style.from_dict({"prompt": "cyan"})
|
|
186
|
+
# EOF
|
|
187
|
+
|
|
188
|
+
# GUI staff
|
|
189
|
+
self.root = tk.Tk()
|
|
190
|
+
self.root.title("aipyapp GUI")
|
|
191
|
+
# Maximize the window on startup
|
|
192
|
+
try:
|
|
193
|
+
# This works on Windows and some Linux environments
|
|
194
|
+
self.root.state('zoomed')
|
|
195
|
+
except tk.TclError:
|
|
196
|
+
# Fallback for macOS and other environments
|
|
197
|
+
# Get screen width and height
|
|
198
|
+
screen_width = self.root.winfo_screenwidth()
|
|
199
|
+
screen_height = self.root.winfo_screenheight()
|
|
200
|
+
# Set geometry to fill the screen (may include taskbar/menu bar space)
|
|
201
|
+
self.root.geometry(f"{screen_width}x{screen_height}+0+0")
|
|
202
|
+
|
|
203
|
+
# AI输出结果:
|
|
204
|
+
self.output_label = ttk.Label(self.root, text="AI 输出:")
|
|
205
|
+
self.output_label.grid(row=0, column=0, sticky="w")
|
|
206
|
+
self.output_text = scrolledtext.ScrolledText(self.root, width=60, height=20)
|
|
207
|
+
self.output_text.grid(row=1, column=0, padx=5, pady=5, sticky="nsew")
|
|
208
|
+
|
|
209
|
+
# 程序执行结果:
|
|
210
|
+
self.code_label = ttk.Label(self.root, text="代码执行结果:")
|
|
211
|
+
self.code_label.grid(row=0, column=1, sticky="w")
|
|
212
|
+
self.code_text = scrolledtext.ScrolledText(self.root, width=60, height=20)
|
|
213
|
+
self.code_text.grid(row=1, column=1, padx=5, pady=5, sticky="nsew")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
self.input_label = ttk.Label(self.root, text="输入你的需求,然后点击提交:")
|
|
218
|
+
self.input_label.grid(row=2, column=0, sticky="w", padx=5, pady=2)
|
|
219
|
+
self.input_entry = tk.Text(self.root, width=60, height=5)
|
|
220
|
+
self.input_entry.grid(row=3, column=0, padx=5, pady=2, sticky="ew")
|
|
221
|
+
|
|
222
|
+
button_font = tkFont.Font(family="Arial", size=16) # 调整 size 以改变字体大小
|
|
223
|
+
style = ttk.Style()
|
|
224
|
+
style.configure("Submit.TButton", font=button_font, padding=(10, 20, 10, 20)) # 使用 TButton 样式
|
|
225
|
+
self.submit_button = ttk.Button(self.root, text="提交", command=self.submit_prompt, style="Submit.TButton")
|
|
226
|
+
self.submit_button.grid(row=3, column=1, padx=5, pady=5, ipady=10, sticky="w")
|
|
227
|
+
|
|
228
|
+
self.continue_button = ttk.Button(self.root, text="结束会话并继续", command=self.continue_session)
|
|
229
|
+
self.continue_button.grid(row=4, column=1, padx=5, pady=2, sticky="w")
|
|
230
|
+
self.end_button = ttk.Button(self.root, text="结束会话并退出", command=self.end_session)
|
|
231
|
+
self.end_button.grid(row=4, column=1, padx=60, pady=2, sticky="e")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
self.open_work_button = ttk.Button(self.root, text="打开工作目录", command=self.open_work_dir)
|
|
235
|
+
self.open_work_button.grid(row=4, column=0, padx=5, pady=2, sticky="w")
|
|
236
|
+
|
|
237
|
+
#self.open_config_button = ttk.Button(self.root, text="打开配置文件", command=self.open_config_file)
|
|
238
|
+
#self.open_config_button.grid(row=4, column=0, padx=10, pady=2, sticky="e")
|
|
239
|
+
|
|
240
|
+
self.root.grid_columnconfigure(0, weight=1)
|
|
241
|
+
self.root.grid_columnconfigure(1, weight=1)
|
|
242
|
+
self.root.grid_rowconfigure(1, weight=1)
|
|
243
|
+
|
|
244
|
+
console.print(f"[bold cyan]🚀 [AIPyApp ({__version__}) on [[green]QPython[/green]][/bold cyan] ")
|
|
245
|
+
console.print(f"[bold cyan]🌐 http://github.com/qpython-android/aipyapp[/bold cyan]")
|
|
246
|
+
self.print_output(f"{T('default')}: {self.names['default']},{T('enabled')}: {' '.join(self.names['enabled'])}\n")
|
|
247
|
+
|
|
248
|
+
def open_work_dir(self):
|
|
249
|
+
#path = self.settings.workdir
|
|
250
|
+
path = Path.cwd()
|
|
251
|
+
if os.path.exists(path):
|
|
252
|
+
if os.name == 'nt': # Windows
|
|
253
|
+
os.startfile(path)
|
|
254
|
+
elif os.name == 'posix': # macOS or Linux
|
|
255
|
+
subprocess.Popen(['open', path]) # macOS
|
|
256
|
+
# For Linux, you might want to use 'xdg-open' instead of 'open'
|
|
257
|
+
# subprocess.Popen(['xdg-open', path])
|
|
258
|
+
else:
|
|
259
|
+
print(f"Directory not found: {path}")
|
|
260
|
+
|
|
261
|
+
def open_config_file(self):
|
|
262
|
+
path = self.settings.user_config_path
|
|
263
|
+
if os.path.exists(path):
|
|
264
|
+
if os.name == 'nt': # Windows
|
|
265
|
+
os.startfile(path)
|
|
266
|
+
elif os.name == 'posix': # macOS or Linux
|
|
267
|
+
subprocess.Popen(['open', path])
|
|
268
|
+
else:
|
|
269
|
+
print(f"File not found: {path}")
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def handle_ai_output(self, output):
|
|
273
|
+
if output[0] == '{' and output[-1] == '}':
|
|
274
|
+
# output is json
|
|
275
|
+
self.print_code("\n" + output)
|
|
276
|
+
else:
|
|
277
|
+
self.print_output(output)
|
|
278
|
+
|
|
279
|
+
def print_code(self, code):
|
|
280
|
+
self.code_text.insert(tk.END, code)
|
|
281
|
+
self.code_text.see(tk.END)
|
|
282
|
+
self.code_text.update_idletasks() # 添加此行以强制刷新 GUI
|
|
283
|
+
self.code_text.update() # Force the GUI to refresh
|
|
284
|
+
|
|
285
|
+
def print_output(self, output):
|
|
286
|
+
self.output_text.insert(tk.END, output)
|
|
287
|
+
self.output_text.see(tk.END)
|
|
288
|
+
self.output_text.update_idletasks() # 添加此行以强制刷新 GUI
|
|
289
|
+
self.output_text.update() # Force the GUI to refresh
|
|
290
|
+
|
|
291
|
+
def parse_use_command(self, user_input, llms):
|
|
292
|
+
words = user_input.split()
|
|
293
|
+
if len(words) > 2:
|
|
294
|
+
return None
|
|
295
|
+
if words[0] in ('/use', 'use'):
|
|
296
|
+
return words[1] if len(words) > 1 else ''
|
|
297
|
+
return words[0] if len(words) == 1 and words[0] in llms else None
|
|
298
|
+
|
|
299
|
+
def submit_prompt(self):
|
|
300
|
+
user_input = self.input_entry.get("1.0", tk.END)
|
|
301
|
+
|
|
302
|
+
# check use command
|
|
303
|
+
if len(user_input) < 2:
|
|
304
|
+
return
|
|
305
|
+
|
|
306
|
+
cmd, arg = parse_command(user_input, self.names['enabled'])
|
|
307
|
+
if cmd == CommandType.CMD_TEXT:
|
|
308
|
+
if self.task:
|
|
309
|
+
# in task
|
|
310
|
+
self.run_task(self.task, arg)
|
|
311
|
+
else:
|
|
312
|
+
print("new task", arg)
|
|
313
|
+
task = self.tm.new_task(arg)
|
|
314
|
+
self.run_task(task)
|
|
315
|
+
self.task = task
|
|
316
|
+
elif cmd == CommandType.CMD_DONE:
|
|
317
|
+
self.continue_session()
|
|
318
|
+
self.print_output("\n task Done\n")
|
|
319
|
+
elif cmd == CommandType.CMD_USE:
|
|
320
|
+
ret = self.tm.llm.use(arg)
|
|
321
|
+
self.print_output(f'\nUsing {arg} ok\n' if ret else '\nUsing {arg} failed\n')
|
|
322
|
+
elif cmd == CommandType.CMD_INVALID:
|
|
323
|
+
self.print_output(f"\nInvalid command: {arg}")
|
|
324
|
+
elif cmd == CommandType.CMD_EXIT:
|
|
325
|
+
self.end_session()
|
|
326
|
+
|
|
327
|
+
# clear
|
|
328
|
+
self.input_entry.delete("1.0", tk.END)
|
|
329
|
+
|
|
330
|
+
def _run_task(self, task, instruction=None):
|
|
331
|
+
try:
|
|
332
|
+
task.run(instruction=instruction)
|
|
333
|
+
except (EOFError, KeyboardInterrupt):
|
|
334
|
+
pass
|
|
335
|
+
except Exception as e:
|
|
336
|
+
#self.console.print_exception()
|
|
337
|
+
pass
|
|
338
|
+
|
|
339
|
+
def run_task(self, task, instruction=None):
|
|
340
|
+
thread = threading.Thread(target=self._run_task, args=(task, instruction))
|
|
341
|
+
thread.start()
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def end_session(self):
|
|
345
|
+
#try:
|
|
346
|
+
# self.tm.publish(verbose=False)
|
|
347
|
+
#except Exception as e:
|
|
348
|
+
# pass
|
|
349
|
+
|
|
350
|
+
try:
|
|
351
|
+
if self.task:
|
|
352
|
+
self.task.done()
|
|
353
|
+
self.task = None
|
|
354
|
+
except Exception as e:
|
|
355
|
+
#self.console.print_exception()
|
|
356
|
+
pass
|
|
357
|
+
self.root.destroy()
|
|
358
|
+
|
|
359
|
+
def continue_session(self):
|
|
360
|
+
#try:
|
|
361
|
+
# self.tm.publish(verbose=False)
|
|
362
|
+
#except Exception as e:
|
|
363
|
+
# pass
|
|
364
|
+
|
|
365
|
+
try:
|
|
366
|
+
if self.task:
|
|
367
|
+
self.task.done()
|
|
368
|
+
self.task = None
|
|
369
|
+
except Exception as e:
|
|
370
|
+
traceback.print_exc()
|
|
371
|
+
#self.console.print_exception()
|
|
372
|
+
pass
|
|
373
|
+
self.input_entry.delete("1.0", tk.END)
|
|
374
|
+
self.code_text.delete("1.0", tk.END)
|
|
375
|
+
self.output_text.delete("1.0", tk.END)
|
|
376
|
+
|
|
377
|
+
def run(self):
|
|
378
|
+
self.root.mainloop()
|
|
379
|
+
|
|
380
|
+
def main(args):
|
|
381
|
+
|
|
382
|
+
path = args.config if args.config else 'aipython.toml'
|
|
383
|
+
user_config_path = Path(path).resolve()
|
|
384
|
+
default_config_path = resources.files(__PACKAGE_NAME__) / "default.toml"
|
|
385
|
+
conf = ConfigManager(default_config_path, args.config)
|
|
386
|
+
conf.check_config()
|
|
387
|
+
settings = conf.get_config()
|
|
388
|
+
|
|
389
|
+
settings.user_config_path = user_config_path
|
|
390
|
+
# auto install package.
|
|
391
|
+
settings.auto_install = True
|
|
392
|
+
settings.auto_getenv = True
|
|
393
|
+
settings.lang="zh"
|
|
394
|
+
console = GUIConsole()
|
|
395
|
+
|
|
396
|
+
lang = settings.get('lang') or or (os.getenv('LANG')[:2] if os.getenv('LANG') else "en")
|
|
397
|
+
if lang: set_lang(lang)
|
|
398
|
+
|
|
399
|
+
try:
|
|
400
|
+
tm = TaskManager(settings, console=console)
|
|
401
|
+
except Exception as e:
|
|
402
|
+
#console.print_exception(e)
|
|
403
|
+
#console.print(f"[bold red]Error: {e}")
|
|
404
|
+
print(e)
|
|
405
|
+
return
|
|
406
|
+
|
|
407
|
+
gui = AIAppGUI(tm, settings) # Replace None with actual AI instance
|
|
408
|
+
console.set_gui(gui)
|
|
409
|
+
gui.run()
|
aipyapp/main.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
import os
|
|
4
|
+
import code
|
|
5
|
+
import builtins
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import importlib.resources as resources
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from prompt_toolkit import PromptSession
|
|
11
|
+
from prompt_toolkit.formatted_text import HTML
|
|
12
|
+
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
|
13
|
+
from prompt_toolkit.completion import WordCompleter
|
|
14
|
+
from prompt_toolkit.lexers import PygmentsLexer
|
|
15
|
+
from prompt_toolkit.history import FileHistory
|
|
16
|
+
from pygments.lexers.python import PythonLexer
|
|
17
|
+
|
|
18
|
+
from . import __version__
|
|
19
|
+
from .aipy import TaskManager
|
|
20
|
+
from .aipy.i18n import T, set_lang
|
|
21
|
+
from .aipy.config import ConfigManager
|
|
22
|
+
|
|
23
|
+
__PACKAGE_NAME__ = "aipyapp"
|
|
24
|
+
|
|
25
|
+
class PythonCompleter(WordCompleter):
|
|
26
|
+
def __init__(self, ai):
|
|
27
|
+
names = ['exit()']
|
|
28
|
+
names += [name for name in dir(builtins)]
|
|
29
|
+
names += [f"ai.{attr}" for attr in dir(ai) if not attr.startswith('_')]
|
|
30
|
+
super().__init__(names, ignore_case=True)
|
|
31
|
+
|
|
32
|
+
def get_default_config():
|
|
33
|
+
lang = os.getenv('LANG')[:2] if os.getenv('LANG') else "en"
|
|
34
|
+
conf_file = lang=="zh" and "default.toml" or "default_en.toml"
|
|
35
|
+
default_config_path = resources.files(__PACKAGE_NAME__) / conf_file
|
|
36
|
+
return str(default_config_path)
|
|
37
|
+
|
|
38
|
+
def main(args):
|
|
39
|
+
console = Console(record=True)
|
|
40
|
+
console.print(f"[bold cyan]🚀 [AIPyApp ({__version__}) on [green]QPython[/green]][/bold cyan] ")
|
|
41
|
+
console.print(f"[bold cyan]🌐 github.com/qpython-android/aipyapp[/bold cyan] ")
|
|
42
|
+
conf = ConfigManager(get_default_config(), args.config_dir)
|
|
43
|
+
conf.check_config()
|
|
44
|
+
settings = conf.get_config()
|
|
45
|
+
|
|
46
|
+
lang = settings.get('lang') or or (os.getenv('LANG')[:2] if os.getenv('LANG') else "en")
|
|
47
|
+
if lang: set_lang(lang)
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
ai = TaskManager(settings, console=console)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
console.print_exception(e)
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
if not ai.llm:
|
|
56
|
+
console.print(f"[bold red]{T('no_available_llm')}")
|
|
57
|
+
return
|
|
58
|
+
|
|
59
|
+
names = ai.llm.names
|
|
60
|
+
console.print(f"{T('banner1_python')}", style="green")
|
|
61
|
+
console.print(f"[cyan]{T('default')}: [green]{names['default']},[cyan]{T('enabled')}: [yellow]{' '.join(names['enabled'])}")
|
|
62
|
+
|
|
63
|
+
interp = code.InteractiveConsole({'ai': ai})
|
|
64
|
+
|
|
65
|
+
completer = PythonCompleter(ai)
|
|
66
|
+
lexer = PygmentsLexer(PythonLexer)
|
|
67
|
+
auto_suggest = AutoSuggestFromHistory()
|
|
68
|
+
history = FileHistory(str(Path.cwd() / settings.history))
|
|
69
|
+
session = PromptSession(history=history, completer=completer, lexer=lexer, auto_suggest=auto_suggest)
|
|
70
|
+
while True:
|
|
71
|
+
try:
|
|
72
|
+
user_input = session.prompt(HTML('<ansiblue>>> </ansiblue>'))
|
|
73
|
+
if user_input.strip() in {"exit()", "quit()"}:
|
|
74
|
+
break
|
|
75
|
+
interp.push(user_input)
|
|
76
|
+
except EOFError:
|
|
77
|
+
console.print("[bold yellow]Exiting...")
|
|
78
|
+
break
|
|
79
|
+
except Exception as e:
|
|
80
|
+
console.print(f"[bold red]Error: {e}")
|
aipyapp/publish.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
from bs4 import BeautifulSoup
|
|
8
|
+
|
|
9
|
+
API_URL = "https://ai.xxyy.eu.org/api/publish"
|
|
10
|
+
CERT_PATH = Path('client.crt')
|
|
11
|
+
|
|
12
|
+
def upload_article(api_url, html_file_path):
|
|
13
|
+
if not os.path.isfile(html_file_path):
|
|
14
|
+
print(f"错误: 文件 {html_file_path} 不存在")
|
|
15
|
+
return False
|
|
16
|
+
|
|
17
|
+
with open(html_file_path, 'r', encoding='utf-8') as file:
|
|
18
|
+
html_content = file.read()
|
|
19
|
+
|
|
20
|
+
soup = BeautifulSoup(html_content, 'html.parser')
|
|
21
|
+
title = soup.find('span', class_='r4').get_text()
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
files = {
|
|
25
|
+
'content': (html_file_path, html_content, 'text/html')
|
|
26
|
+
}
|
|
27
|
+
meta = {'author': os.getlogin()}
|
|
28
|
+
data = {
|
|
29
|
+
'title': title,
|
|
30
|
+
'metadata': json.dumps(meta)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
response = requests.post(
|
|
34
|
+
api_url,
|
|
35
|
+
files=files,
|
|
36
|
+
data=data,
|
|
37
|
+
cert=str(CERT_PATH),
|
|
38
|
+
verify=True
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if response.status_code == 201:
|
|
42
|
+
print("文章上传成功!")
|
|
43
|
+
print("访问URL:", response.json())
|
|
44
|
+
return True
|
|
45
|
+
else:
|
|
46
|
+
print(f"上传失败 (状态码: {response.status_code}):", response.text)
|
|
47
|
+
return False
|
|
48
|
+
except Exception as e:
|
|
49
|
+
print("发生错误:", str(e))
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
if len(sys.argv) != 2:
|
|
54
|
+
print("用法: python publish.py 'path/to/article.html'")
|
|
55
|
+
sys.exit(1)
|
|
56
|
+
|
|
57
|
+
html_file = sys.argv[1]
|
|
58
|
+
|
|
59
|
+
print(f"正在上传文件: {html_file})")
|
|
60
|
+
upload_article(API_URL, html_file)
|