25/09/24 10:00~11:30 停机维护
This commit is contained in:
@@ -2,6 +2,7 @@ import signal
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import re
|
||||
import uuid
|
||||
current_directory = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.append(current_directory)
|
||||
@@ -70,6 +71,12 @@ class ChatManager:
|
||||
print("ERROR: disconnect",e)
|
||||
print("disconnect success stop code-server success")
|
||||
|
||||
def load_code_in_markdown(self):
|
||||
nowchapter_content = self.chapter_chain[self.chapter_chain_now].chapter
|
||||
code_texts = extract_code_blocks(nowchapter_content)
|
||||
for code_text in code_texts:
|
||||
self.vscode_ws.emit('code-file', {'type':'create', 'data':code_text, 'chapter_title': self.chapter_chain[self.chapter_chain_now].title}, room=self.user_uuid ,namespace='/vscode')
|
||||
|
||||
|
||||
def next_chapter(self):
|
||||
assert self.chapter_chain_now + 1 < len(self.chapter_chain), "chapter_chain_now out of range"
|
||||
@@ -85,6 +92,7 @@ class ChatManager:
|
||||
def load_next_chapter(self):
|
||||
print(f"now load next chapter markdown {self.chapter_chain_now}")
|
||||
self.ase_client.loadmarkdown(self.chapter_chain[self.chapter_chain_now].chapter, self.chapter_chain[self.chapter_chain_now].chapter_prompt, self.chapter_chain[self.chapter_chain_now].score_prompt)
|
||||
self.load_code_in_markdown()
|
||||
|
||||
|
||||
def upload_bb(self):
|
||||
@@ -278,4 +286,21 @@ class ChatManager:
|
||||
for k in keys:
|
||||
self.stop(k)
|
||||
|
||||
|
||||
|
||||
def extract_code_blocks(text):
|
||||
"""
|
||||
从文本中提取所有用```标记的代码段(包括标记本身)
|
||||
|
||||
参数:
|
||||
text: 包含代码段的原始文本
|
||||
|
||||
返回:
|
||||
提取到的代码段列表,每个元素是一个完整的代码块(包括```标记)
|
||||
"""
|
||||
# 正则表达式匹配```开始和结束的代码块
|
||||
# 考虑可能的语言指定(如```python)
|
||||
pattern = r'```.*?```'
|
||||
# 使用DOTALL模式让.匹配包括换行符在内的所有字符
|
||||
code_blocks = re.findall(pattern, text, re.DOTALL)
|
||||
|
||||
return code_blocks
|
||||
@@ -24,8 +24,7 @@ def realtime_response(config: dict, realtime_action: dict) -> None:
|
||||
assert isinstance(bb, Backboard)
|
||||
|
||||
# 统一记历史
|
||||
bb.add_history(realtime_action)
|
||||
|
||||
print(f"backboard action {realtime_action}")
|
||||
rtype = realtime_action.get("type")
|
||||
|
||||
if rtype == "workspaceFolders":
|
||||
@@ -35,7 +34,7 @@ def realtime_response(config: dict, realtime_action: dict) -> None:
|
||||
file_path = realtime_action.get("filePath")
|
||||
bb.active_file_path = file_path
|
||||
assert isinstance(file_path, str)
|
||||
with open(os.path.join(bb.root_path, bb.active_file_content), "r", encoding="utf-8") as f:
|
||||
with open(os.path.join(bb.root_path, bb.active_file_path), "r", encoding="utf-8") as f:
|
||||
bb.active_file_content = f.read()
|
||||
|
||||
elif rtype == "paste":
|
||||
@@ -43,16 +42,16 @@ def realtime_response(config: dict, realtime_action: dict) -> None:
|
||||
assert isinstance(file_path, str)
|
||||
bb.pasted_file_path = file_path
|
||||
bb.pasted_content = realtime_action.get("content")
|
||||
bb.active_file_path = file_path
|
||||
with open(os.path.join(bb.root_path, bb.active_file_content), "r", encoding="utf-8") as f:
|
||||
with open(os.path.join(bb.root_path, bb.active_file_path), "r", encoding="utf-8") as f:
|
||||
bb.active_file_content = f.read()
|
||||
|
||||
elif rtype == "fileEdit":
|
||||
file_path = realtime_action.get("filePath")
|
||||
assert isinstance(file_path, str)
|
||||
bb.active_file_path = file_path
|
||||
with open(os.path.join(bb.root_path, bb.active_file_content), "r", encoding="utf-8") as f:
|
||||
with open(os.path.join(bb.root_path, bb.active_file_path), "r", encoding="utf-8") as f:
|
||||
bb.active_file_content = f.read()
|
||||
|
||||
|
||||
action_without_config = realtime_action.copy()
|
||||
action_without_config.pop('config', None) # 使用pop并设置默认值None,避免键不存在时出错
|
||||
bb.add_history(action_without_config)
|
||||
chatmanager.upload_bb()
|
||||
@@ -15,10 +15,7 @@ from ..services.user_service import get_or_load_current_user
|
||||
class VSCodeNamespace(Namespace):
|
||||
def on_login(self,data):
|
||||
ex = current_app.extensions
|
||||
uuid2username = ex["uuid2username"]
|
||||
backboard_manager = ex["backboard_manager"]
|
||||
print("VSCode client connected")
|
||||
print(data)
|
||||
dataconfig = data['config']
|
||||
user_uuid = dataconfig.get('user_uuid')
|
||||
path = dataconfig.get('path')
|
||||
@@ -26,11 +23,18 @@ class VSCodeNamespace(Namespace):
|
||||
lesson_id = dataconfig.get('chapter_id')
|
||||
print(f"User {user_uuid} connected with path: {path}")
|
||||
join_room(user_uuid, namespace='/vscode')
|
||||
if backboard_manager.get_backboard(user_uuid) == None:
|
||||
backboard_manager.add_backboard(user_uuid,uuid2username[user_uuid], course_id, lesson_id, path)
|
||||
|
||||
chatmanager = ex['user_uuid2chatmanager'][user_uuid]
|
||||
chatmanager.vscode_ws=self
|
||||
|
||||
|
||||
|
||||
def on_load_chapter(self, data):
|
||||
dataconfig = data['config']
|
||||
user_uuid = dataconfig.get('user_uuid')
|
||||
ex = current_app.extensions
|
||||
chatmanager = ex['user_uuid2chatmanager'][user_uuid]
|
||||
chatmanager.load_code_in_markdown()
|
||||
|
||||
def on_message(self, data):
|
||||
# print(f"Received from VSCode client: {data}")
|
||||
dataconfig = data['config']
|
||||
@@ -139,6 +143,9 @@ class AgentNamespace(Namespace):
|
||||
user_uuid2ase_client[uuid].send_text(d['route'], data['data'])
|
||||
|
||||
def on_connect_to_ase(client_entry: HSAEngineClient, namespace_entry, init_data):
|
||||
'''
|
||||
此时刚刚建立好aseclient连接,并向前端发送打开code-server-like指令。
|
||||
'''
|
||||
print(f"on_connect_to_ase {client_entry}, {namespace_entry}, {init_data}")
|
||||
namespace_entry, ase_client = namespace_entry
|
||||
namespace_entry.emit('open-iframe',"", room=init_data['room'], namespace='/agent')
|
||||
|
||||
@@ -30,7 +30,8 @@ body {
|
||||
border-radius: 15px;
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
min-width: 400px;
|
||||
max-width: 600px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
69
Html/apps/static/css/register_field_tip.css
Normal file
69
Html/apps/static/css/register_field_tip.css
Normal file
@@ -0,0 +1,69 @@
|
||||
.username-group { position: relative; }
|
||||
|
||||
.field-tip {
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
left: calc(60% + 10px); /* 紧挨输入框右侧 */
|
||||
transform: translateY(-50%);
|
||||
width: 260px;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(0,0,0,0.08);
|
||||
padding: 10px 12px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #111827;
|
||||
|
||||
/* 初始隐藏 */
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
pointer-events: none;
|
||||
transition: opacity .15s ease, visibility .15s ease;
|
||||
z-index: 10;
|
||||
}
|
||||
.field-tip::after { /* 小三角箭头 */
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: -6px;
|
||||
transform: translateY(-50%);
|
||||
border-width: 6px;
|
||||
border-style: solid;
|
||||
border-color: transparent #e5e7eb transparent transparent;
|
||||
}
|
||||
.field-tip::before { /* 箭头内层白色 */
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: -5px;
|
||||
transform: translateY(-50%);
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
border-color: transparent #fff transparent transparent;
|
||||
z-index: 1;
|
||||
}
|
||||
.field-tip strong { display: block; margin-bottom: 6px; }
|
||||
.field-tip ul { margin: 0; padding-left: 16px; }
|
||||
.field-tip li { margin: 2px 0; color: #374151; }
|
||||
.field-tip span { font-weight: 600; color: #111827; }
|
||||
|
||||
/* 显示状态 */
|
||||
.field-tip.is-visible {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
/* 小屏时把提示框放到输入框下方,避免遮挡 */
|
||||
@media (max-width: 640px) {
|
||||
.field-tip {
|
||||
top: calc(60% + 8px);
|
||||
left: 0;
|
||||
transform: none;
|
||||
width: min(92vw, 320px);
|
||||
}
|
||||
.field-tip::after,
|
||||
.field-tip::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const email = document.getElementById('email').value;
|
||||
const password = document.getElementById('password').value;
|
||||
const confirmPassword = document.getElementById('confirm-password').value;
|
||||
|
||||
const u = validateLinuxUsername(username);
|
||||
if (!u.ok) {
|
||||
alert(u.error);
|
||||
return;
|
||||
}
|
||||
if (password !== confirmPassword) {
|
||||
alert('密码和确认密码不一致');
|
||||
return;
|
||||
@@ -36,4 +40,36 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
alert('注册失败');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 校验 Linux 用户名
|
||||
function validateLinuxUsername(name) {
|
||||
// 基本字符规则:^[a-z_][a-z0-9_-]*[$]?$
|
||||
const basic = /^[a-z_][a-z0-9_-]*[$]?$/;
|
||||
|
||||
if (typeof name !== 'string' || name.length === 0) {
|
||||
return { ok: false, error: '用户名不能为空' };
|
||||
}
|
||||
// 长度限制(常见系统为 <= 32)
|
||||
if (name.length > 32) {
|
||||
return { ok: false, error: '用户名长度不能超过 32 个字符' };
|
||||
}
|
||||
if (!basic.test(name)) {
|
||||
return {
|
||||
ok: false,
|
||||
error:
|
||||
'用户名需以小写字母或下划线开头,仅包含小写字母、数字、下划线、连字符,允许以 $ 结尾'
|
||||
};
|
||||
}
|
||||
// 可选:避免纯数字用户名(多数系统不推荐)
|
||||
if (/^[0-9]+$/.test(name)) {
|
||||
return { ok: false, error: '用户名不能为纯数字' };
|
||||
}
|
||||
// 可选:避免使用保留名
|
||||
const reserved = new Set(['root', 'daemon', 'bin', 'sys', 'sync', 'games', 'man']);
|
||||
if (reserved.has(name)) {
|
||||
return { ok: false, error: '该用户名为系统保留名,请更换' };
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
27
Html/apps/static/js/register_field_tip.js
Normal file
27
Html/apps/static/js/register_field_tip.js
Normal file
@@ -0,0 +1,27 @@
|
||||
(function () {
|
||||
let usernameInput = document.getElementById('username');
|
||||
if (!usernameInput){
|
||||
usernameInput = document.getElementById('teacher-username');
|
||||
}
|
||||
const tip = document.getElementById('username-tip');
|
||||
|
||||
function showTip() {
|
||||
tip.classList.add('is-visible');
|
||||
tip.setAttribute('aria-hidden', 'false');
|
||||
}
|
||||
function hideTip() {
|
||||
tip.classList.remove('is-visible');
|
||||
tip.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
|
||||
usernameInput.addEventListener('focus', showTip);
|
||||
usernameInput.addEventListener('blur', hideTip);
|
||||
|
||||
// 当用户点击提示框自身时不影响(比如复制文本)
|
||||
tip.addEventListener('mousedown', e => e.preventDefault());
|
||||
|
||||
// 可选:输入时也显示(防止因某些浏览器失焦导致闪烁)
|
||||
usernameInput.addEventListener('input', () => {
|
||||
if (document.activeElement === usernameInput) showTip();
|
||||
});
|
||||
})();
|
||||
@@ -8,7 +8,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const email = document.getElementById('teacher-email').value;
|
||||
const password = document.getElementById('teacher-password').value;
|
||||
const confirmPassword = document.getElementById('teacher-confirm-password').value;
|
||||
|
||||
const u = validateLinuxUsername(username);
|
||||
if (!u.ok) {
|
||||
alert(u.error);
|
||||
return;
|
||||
}
|
||||
// 检查密码和确认密码是否一致
|
||||
if (password !== confirmPassword) {
|
||||
alert('密码和确认密码不一致');
|
||||
@@ -39,3 +43,35 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 校验 Linux 用户名
|
||||
function validateLinuxUsername(name) {
|
||||
// 基本字符规则:^[a-z_][a-z0-9_-]*[$]?$
|
||||
const basic = /^[a-z_][a-z0-9_-]*[$]?$/;
|
||||
|
||||
if (typeof name !== 'string' || name.length === 0) {
|
||||
return { ok: false, error: '用户名不能为空' };
|
||||
}
|
||||
// 长度限制(常见系统为 <= 32)
|
||||
if (name.length > 32) {
|
||||
return { ok: false, error: '用户名长度不能超过 32 个字符' };
|
||||
}
|
||||
if (!basic.test(name)) {
|
||||
return {
|
||||
ok: false,
|
||||
error:
|
||||
'用户名需以小写字母或下划线开头,仅包含小写字母、数字、下划线、连字符,允许以 $ 结尾'
|
||||
};
|
||||
}
|
||||
// 可选:避免纯数字用户名(多数系统不推荐)
|
||||
if (/^[0-9]+$/.test(name)) {
|
||||
return { ok: false, error: '用户名不能为纯数字' };
|
||||
}
|
||||
// 可选:避免使用保留名
|
||||
const reserved = new Set(['root', 'daemon', 'bin', 'sys', 'sync', 'games', 'man']);
|
||||
if (reserved.has(name)) {
|
||||
return { ok: false, error: '该用户名为系统保留名,请更换' };
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>学生注册</title>
|
||||
<link rel="stylesheet" href="/static/css/login.css">
|
||||
<link rel="stylesheet" href="/static/css/register_field_tip.css">
|
||||
<script src="/static/js/register.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -15,6 +16,17 @@
|
||||
<div class="input-group">
|
||||
<label for="username">用户名</label>
|
||||
<input type="text" id="username" name="username" placeholder="请输入用户名" required>
|
||||
<div class="field-tip" id="username-tip" role="tooltip" aria-hidden="true">
|
||||
<strong>用户名规则</strong>
|
||||
<ul>
|
||||
<li>以<span>小写字母</span>或<span>下划线</span>开头</li>
|
||||
<li>仅含<span>小写字母</span>、<span>数字</span>、<span>下划线 _</span>、<span>连字符 -</span></li>
|
||||
<li>允许以 <span>$</span> 结尾(可选)</li>
|
||||
<li>长度 ≤ 32 个字符</li>
|
||||
<li>不可使用纯数字</li>
|
||||
<li>避免保留名:root / daemon 等</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<label for="email">邮箱</label>
|
||||
@@ -32,5 +44,8 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script src="/static/js/register_field_tip.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -5,6 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>教师注册</title>
|
||||
<link rel="stylesheet" href="/static/css/login.css">
|
||||
<link rel="stylesheet" href="/static/css/register_field_tip.css">
|
||||
<script src="/static/js/register_teacher.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -30,7 +31,20 @@
|
||||
</div>
|
||||
<button type="submit" class="login-btn">注册</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="field-tip" id="username-tip" role="tooltip" aria-hidden="true">
|
||||
<strong>用户名规则</strong>
|
||||
<ul>
|
||||
<li>以<span>小写字母</span>或<span>下划线</span>开头</li>
|
||||
<li>仅含<span>小写字母</span>、<span>数字</span>、<span>下划线 _</span>、<span>连字符 -</span></li>
|
||||
<li>允许以 <span>$</span> 结尾(可选)</li>
|
||||
<li>长度 ≤ 32 个字符</li>
|
||||
<li>不可使用纯数字</li>
|
||||
<li>避免保留名:root / daemon 等</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/register_field_tip.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
let vs_socket = null;
|
||||
let debounceTimeout;
|
||||
|
||||
// 定义代码类型到文件后缀的映射
|
||||
const typeToExtension = {
|
||||
'python': 'py',
|
||||
'cpp': 'cpp',
|
||||
'javascript': 'js',
|
||||
'java': 'java',
|
||||
'html': 'html',
|
||||
'css': 'css',
|
||||
'php': 'php',
|
||||
'ruby': 'rb',
|
||||
'go': 'go',
|
||||
'c': 'c',
|
||||
'cs': 'cs'
|
||||
// 可以根据需要添加更多映射
|
||||
};
|
||||
|
||||
// 缓冲器函数(防抖)
|
||||
function debounceSendToServer(data, delay) {
|
||||
if (debounceTimeout) {
|
||||
@@ -50,6 +65,7 @@ vs_socket.on('connect', () => {
|
||||
// vscode.window.showInformationMessage('Connected to server');
|
||||
console.log('Connected to server');
|
||||
vs_socket.emit('login', { config: code_like_config });
|
||||
vs_socket.emit('load_chapter',{config: code_like_config})
|
||||
});
|
||||
vs_socket.on('disconnect', () => {
|
||||
// vscode.window.showInformationMessage('Disconnected from server');
|
||||
@@ -60,4 +76,29 @@ vs_socket.on('terminal',(data)=>{
|
||||
socket.emit("pty_input", {"input": data.data+'\r\n'}, function(response) {
|
||||
});
|
||||
})
|
||||
|
||||
vs_socket.on('code-file',(data)=>{
|
||||
if (data.type=='create'){
|
||||
let title = data.chapter_title
|
||||
let content = data.data
|
||||
const codeBlockRegex = /^```(\w+)\s([\s\S]*?)\s```$/;
|
||||
const match = content.match(codeBlockRegex);
|
||||
|
||||
if (match && match.length >= 3) {
|
||||
const codeType = match[1].toLowerCase();
|
||||
const codeContent = match[2].trim();
|
||||
// 根据代码类型获取后缀,如果没有匹配的则使用txt
|
||||
const fileExtension = typeToExtension[codeType] || 'txt';
|
||||
|
||||
console.log('代码类型:', codeType);
|
||||
console.log('文件后缀:', fileExtension);
|
||||
console.log('代码内容:', codeContent);
|
||||
|
||||
createAndWriteNewFile(title+'.'+fileExtension, codeContent)
|
||||
|
||||
// 这里可以使用解析出的结果进行后续操作
|
||||
} else {
|
||||
console.error('无效的代码格式');
|
||||
// 处理格式错误的情况
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -262,6 +262,12 @@ function loadFileContent(item) {
|
||||
}
|
||||
|
||||
|
||||
function createAndWriteNewFile(path, content){
|
||||
fetch(`/vsc-like/create-file`, { method: 'POST', body: JSON.stringify({ path: path, parent: '.' }) , headers: { 'Content-Type': 'application/json' } })
|
||||
.then(() => {
|
||||
postSaveFile('./'+path, content)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 新建文件
|
||||
@@ -303,6 +309,12 @@ function createNewFolder(event) {
|
||||
}
|
||||
}
|
||||
|
||||
function postSaveFile(path, content){
|
||||
|
||||
fetch(`/vsc-like/save-file`, { method: 'POST', body: JSON.stringify({ path: path, content: content }) , headers: { 'Content-Type': 'application/json' } })
|
||||
.then(() => loadFileTree());
|
||||
}
|
||||
|
||||
// 保存文件
|
||||
function saveFile(event) {
|
||||
event.stopPropagation();
|
||||
@@ -311,8 +323,7 @@ function saveFile(event) {
|
||||
if (selectedItem.path==undefined){
|
||||
selectedItem.path='.';
|
||||
}
|
||||
fetch(`/vsc-like/save-file`, { method: 'POST', body: JSON.stringify({ path: selectedItem.path+'/'+selectedItem.name, content: content }) , headers: { 'Content-Type': 'application/json' } })
|
||||
.then(() => loadFileTree());
|
||||
postSaveFile(selectedItem.path+'/'+selectedItem.name, content)
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user