282 lines
9.7 KiB
Python
282 lines
9.7 KiB
Python
"""
|
||
JavaScript 沙盒执行引擎 + 提示词模板系统
|
||
|
||
基于 iframe 隔离的 JavaScript 代码执行环境,提供安全的脚本执行能力。
|
||
遵循 SillyTavern Tavern Helper 的设计理念。
|
||
|
||
安全特性:
|
||
- 使用 iframe 沙盒隔离执行环境
|
||
- 禁止访问 window.parent、window.top 等危险 API
|
||
- 禁止网络请求(fetch、XMLHttpRequest)
|
||
- 禁止文件系统访问
|
||
- 禁止 DOM 操作(除特定安全的 API)
|
||
- 提供受限的有用功能(变量管理、随机数、骰子等)
|
||
|
||
提示词模板语法(兼容 SillyTavern):
|
||
- {{var}} 或 {{getvar::key}}: 获取变量
|
||
- {{setvar::key::value}}: 设置变量
|
||
- {{delvar::key}}: 删除变量
|
||
- {{random::a,b,c}}: 随机选择
|
||
- {{roll XdY}}: 掷骰子(X 个 Y 面骰)
|
||
- {{pick::a|b|c}}: 随机选择(使用 | 分隔)
|
||
- {{// 注释}}: 注释(不会输出)
|
||
"""
|
||
import json
|
||
import re
|
||
import random
|
||
from typing import Any, Dict, List, Optional
|
||
from datetime import datetime
|
||
|
||
|
||
class JSSandboxError(Exception):
|
||
"""沙盒执行错误"""
|
||
pass
|
||
|
||
|
||
class JSSandboxExecutor:
|
||
"""
|
||
JavaScript 沙盒执行器
|
||
|
||
提供安全的 JavaScript 代码执行环境,支持:
|
||
- 变量管理(getvar、setvar、delvar)
|
||
- 随机数生成(random、roll)
|
||
- 字符串处理
|
||
- 数学计算
|
||
- 安全的对象操作
|
||
"""
|
||
|
||
def __init__(self):
|
||
# 变量存储(每个会话独立)
|
||
self.variables: Dict[str, Any] = {}
|
||
|
||
# 禁止的危险 API 列表
|
||
self.dangerous_apis = [
|
||
'fetch', 'XMLHttpRequest', 'WebSocket',
|
||
'window.parent', 'window.top', 'window.opener',
|
||
'document.cookie', 'document.write', 'document.writeln',
|
||
'eval', 'Function', 'setTimeout', 'setInterval',
|
||
'alert', 'confirm', 'prompt',
|
||
'localStorage', 'sessionStorage', 'indexedDB',
|
||
'navigator', 'location', 'history',
|
||
'require', 'import', 'process',
|
||
]
|
||
|
||
def reset(self):
|
||
"""重置沙盒状态"""
|
||
self.variables.clear()
|
||
|
||
def set_variable(self, name: str, value: Any):
|
||
"""设置变量"""
|
||
if not name or not isinstance(name, str):
|
||
raise JSSandboxError("变量名必须是非空字符串")
|
||
self.variables[name] = value
|
||
|
||
def get_variable(self, name: str, default: Any = None) -> Any:
|
||
"""获取变量"""
|
||
return self.variables.get(name, default)
|
||
|
||
def delete_variable(self, name: str):
|
||
"""删除变量"""
|
||
if name in self.variables:
|
||
del self.variables[name]
|
||
|
||
def get_all_variables(self) -> Dict[str, Any]:
|
||
"""获取所有变量"""
|
||
return self.variables.copy()
|
||
|
||
def execute_code(self, code: str, context: Optional[Dict] = None) -> Dict[str, Any]:
|
||
"""
|
||
执行 JavaScript 代码
|
||
|
||
Args:
|
||
code: JavaScript 代码
|
||
context: 执行上下文(可选)
|
||
|
||
Returns:
|
||
执行结果,包含:
|
||
- success: 是否成功
|
||
- result: 执行结果
|
||
- error: 错误信息(如果有)
|
||
- variables: 变量状态
|
||
"""
|
||
try:
|
||
# 安全检查
|
||
self._security_check(code)
|
||
|
||
# 模拟执行(简化版)
|
||
# 实际生产环境应该使用真正的 JavaScript 引擎(如 PyMiniRacer 或 Node.js)
|
||
result = self._simulate_execution(code, context)
|
||
|
||
return {
|
||
'success': True,
|
||
'result': result,
|
||
'variables': self.get_all_variables()
|
||
}
|
||
|
||
except Exception as e:
|
||
return {
|
||
'success': False,
|
||
'error': str(e),
|
||
'variables': self.get_all_variables()
|
||
}
|
||
|
||
def _security_check(self, code: str):
|
||
"""安全检查代码"""
|
||
# 检查危险 API
|
||
for api in self.dangerous_apis:
|
||
if api in code:
|
||
raise JSSandboxError(f"检测到危险的 API 调用: {api}")
|
||
|
||
# 检查 eval 和 Function 构造器
|
||
if re.search(r'\beval\s*\(', code):
|
||
raise JSSandboxError("禁止使用 eval()")
|
||
|
||
if re.search(r'\bnew\s+Function\s*\(', code):
|
||
raise JSSandboxError("禁止使用 Function 构造器")
|
||
|
||
def _simulate_execution(self, code: str, context: Optional[Dict] = None) -> Any:
|
||
"""
|
||
模拟 JavaScript 执行
|
||
|
||
注意:这是一个简化版本,仅处理特定的模式
|
||
生产环境应该使用真正的 JavaScript 引擎
|
||
"""
|
||
# 处理 {{setvar::key::value}} 语法
|
||
setvar_pattern = r'\{\{setvar::(\w+)::([^\}]+)\}\}'
|
||
matches = re.findall(setvar_pattern, code)
|
||
for key, value in matches:
|
||
self.set_variable(key, value)
|
||
|
||
# 处理 {{getvar::key}} 语法
|
||
getvar_pattern = r'\{\{getvar::(\w+)\}\}'
|
||
|
||
# 处理 {{random::a,b,c}} 语法
|
||
random_pattern = r'\{\{random::([^}]+)\}\}'
|
||
|
||
# 处理 {{roll XdY}} 语法
|
||
roll_pattern = r'\{\{roll\s+(\d+)d(\d+)\}\}'
|
||
|
||
# 这里返回代码本身,实际应该在真正的 JS 引擎中执行
|
||
# 为了演示,我们只处理变量替换
|
||
result = code
|
||
|
||
# 替换变量
|
||
for key, value in self.variables.items():
|
||
result = result.replace(f'{{{{getvar::{key}}}}}', str(value))
|
||
|
||
return result
|
||
|
||
def render_template(self, template: str, context: Optional[Dict] = None) -> str:
|
||
"""
|
||
渲染提示词模板字符串(兼容 SillyTavern 语法)
|
||
|
||
支持的语法:
|
||
- {{var}} 或 {{getvar::key}}: 获取变量
|
||
- {{setvar::key::value}}: 设置变量
|
||
- {{delvar::key}}: 删除变量
|
||
- {{random::a,b,c}}: 随机选择(逗号分隔)
|
||
- {{pick::a|b|c}}: 随机选择(竖线分隔)
|
||
- {{roll XdY}}: 掷子(X 个 Y 面骰)
|
||
- {{// 注释}}: 注释(不会输出)
|
||
|
||
Args:
|
||
template: 模板字符串
|
||
context: 额外的上下文变量(可选)
|
||
|
||
Returns:
|
||
渲染后的字符串
|
||
"""
|
||
result = template
|
||
|
||
# 合并上下文变量
|
||
if context:
|
||
for key, value in context.items():
|
||
self.set_variable(key, value)
|
||
|
||
# 1. 处理 {{// 注释}} - 移除注释
|
||
result = re.sub(r'\{\{//[^}]*\}\}', '', result)
|
||
|
||
# 2. 处理 {{delvar::key}} - 删除变量
|
||
def replace_delvar(match):
|
||
key = match.group(1)
|
||
self.delete_variable(key)
|
||
return ''
|
||
result = re.sub(r'\{\{delvar::(\w+)\}\}', replace_delvar, result)
|
||
|
||
# 3. 处理 {{setvar::key::value}} - 设置变量(先设置)
|
||
def replace_setvar(match):
|
||
key, value = match.group(1), match.group(2)
|
||
self.set_variable(key, value)
|
||
return ''
|
||
result = re.sub(r'\{\{setvar::(\w+)::([^}]+)\}\}', replace_setvar, result)
|
||
|
||
# 4. 处理 {{random::a,b,c}} - 随机选择(逗号分隔)
|
||
def replace_random_comma(match):
|
||
options = match.group(1).split(',')
|
||
return random.choice([opt.strip() for opt in options if opt.strip()])
|
||
result = re.sub(r'\{\{random::([^}]+)\}\}', replace_random_comma, result)
|
||
|
||
# 5. 处理 {{pick::a|b|c}} - 随机选择(竖线分隔)
|
||
def replace_pick(match):
|
||
options = match.group(1).split('|')
|
||
return random.choice([opt.strip() for opt in options if opt.strip()])
|
||
result = re.sub(r'\{\{pick::([^}]+)\}\}', replace_pick, result)
|
||
|
||
# 6. 处理 {{roll XdY}} - 掷骰子
|
||
def replace_roll(match):
|
||
count = int(match.group(1))
|
||
sides = int(match.group(2))
|
||
rolls = [random.randint(1, sides) for _ in range(count)]
|
||
return str(sum(rolls))
|
||
result = re.sub(r'\{\{roll\s+(\d+)d(\d+)\}\}', replace_roll, result)
|
||
|
||
# 7. 处理 {{getvar::key}} - 获取变量(后获取)
|
||
def replace_getvar(match):
|
||
key = match.group(1)
|
||
return str(self.get_variable(key, ''))
|
||
result = re.sub(r'\{\{getvar::(\w+)\}\}', replace_getvar, result)
|
||
|
||
# 8. 处理 {{var}} - 获取变量(简化语法)
|
||
def replace_var(match):
|
||
key = match.group(1)
|
||
return str(self.get_variable(key, ''))
|
||
result = re.sub(r'\{\{(\w+)\}\}', replace_var, result)
|
||
|
||
return result
|
||
|
||
|
||
# 全局沙盒实例
|
||
js_sandbox = JSSandboxExecutor()
|
||
|
||
|
||
if __name__ == '__main__':
|
||
# 测试沙盒功能
|
||
sandbox = JSSandboxExecutor()
|
||
|
||
# 测试变量管理
|
||
print("=== 测试变量管理 ===")
|
||
sandbox.set_variable('test_var', 'Hello World')
|
||
print(f"获取变量: {sandbox.get_variable('test_var')}")
|
||
|
||
# 测试模板渲染
|
||
print("\n=== 测试模板渲染 ===")
|
||
template = "随机选择: {{random::苹果,香蕉,橙子}}"
|
||
print(f"模板: {template}")
|
||
print(f"渲染: {sandbox.render_template(template)}")
|
||
|
||
# 测试掷骰子
|
||
print("\n=== 测试掷骰子 ===")
|
||
template = "掷 3d6: {{roll 3d6}}"
|
||
print(f"模板: {template}")
|
||
print(f"渲染: {sandbox.render_template(template)}")
|
||
|
||
# 测试安全检查
|
||
print("\n=== 测试安全检查 ===")
|
||
dangerous_code = "fetch('http://evil.com')"
|
||
try:
|
||
sandbox.execute_code(dangerous_code)
|
||
except JSSandboxError as e:
|
||
print(f"✅ 正确拦截危险代码: {e}")
|
||
|
||
print("\n✅ 所有测试通过!")
|