feat: 脚本下载/编辑接口、前端Groovy编辑器、Groovy错误行号提取
This commit is contained in:
@@ -229,6 +229,52 @@ public class AdminController {
|
|||||||
return ApiResponse.success(configService.toDTO(config));
|
return ApiResponse.success(configService.toDTO(config));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查看指定配置的 Groovy 脚本
|
||||||
|
*/
|
||||||
|
@GetMapping("/configs/{configId}/script")
|
||||||
|
public ApiResponse<Map<String, Object>> getConfigScript(
|
||||||
|
@PathVariable Long configId, HttpServletRequest request) {
|
||||||
|
checkAdmin(request);
|
||||||
|
SiteConfig config = siteConfigMapper.selectById(configId);
|
||||||
|
if (config == null) return ApiResponse.error(404, "Config not found");
|
||||||
|
String script = null;
|
||||||
|
if (config.getScriptStoragePath() != null) {
|
||||||
|
try { script = storageService.read(config.getScriptStoragePath()); } catch (Exception ignored) {}
|
||||||
|
}
|
||||||
|
return ApiResponse.success(Map.of("id", config.getId(), "script", script != null ? script : ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑待审核配置的 Groovy 脚本
|
||||||
|
*/
|
||||||
|
@PutMapping("/configs/{configId}/script")
|
||||||
|
public ApiResponse<SiteConfigDTO> updateConfigScript(
|
||||||
|
@PathVariable Long configId,
|
||||||
|
@RequestBody Map<String, String> payload,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
checkAdmin(request);
|
||||||
|
SiteConfig config = siteConfigMapper.selectById(configId);
|
||||||
|
if (config == null) return ApiResponse.error(404, "Config not found");
|
||||||
|
if (config.getStatus() != ConfigStatus.PENDING) {
|
||||||
|
return ApiResponse.error(400, "只能编辑待审核的配置");
|
||||||
|
}
|
||||||
|
|
||||||
|
String script = payload.get("script");
|
||||||
|
if (script == null) return ApiResponse.error(400, "script is required");
|
||||||
|
|
||||||
|
String scriptPath = config.getScriptStoragePath();
|
||||||
|
if (scriptPath == null || scriptPath.isBlank()) {
|
||||||
|
scriptPath = String.format("configs/%s/pending/%s.groovy", config.getSiteId(), config.getConfigHash().substring(0, 8));
|
||||||
|
config.setScriptStoragePath(scriptPath);
|
||||||
|
}
|
||||||
|
storageService.store(scriptPath, script);
|
||||||
|
siteConfigMapper.updateById(config);
|
||||||
|
|
||||||
|
log.info("Config script updated: id={}, siteId={}", configId, config.getSiteId());
|
||||||
|
return ApiResponse.success(configService.toDTO(config));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置用户信任等级(禁止修改管理员、禁止提升为管理员)
|
* 设置用户信任等级(禁止修改管理员、禁止提升为管理员)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -117,6 +117,29 @@ public class ConfigController {
|
|||||||
.body(content);
|
.body(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载指定版本 Groovy 脚本(供 mediabot 使用)
|
||||||
|
*/
|
||||||
|
@GetMapping("/{siteId}/{version}/script")
|
||||||
|
public ResponseEntity<String> downloadScript(
|
||||||
|
@PathVariable("siteId") String siteId,
|
||||||
|
@PathVariable("version") String version) {
|
||||||
|
|
||||||
|
SiteConfig config = configService.getConfig(siteId, version);
|
||||||
|
if (config == null || config.getScriptStoragePath() == null) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
String script;
|
||||||
|
try { script = storageService.read(config.getScriptStoragePath()); }
|
||||||
|
catch (Exception e) { return ResponseEntity.status(500).body("Failed to read script"); }
|
||||||
|
if (script == null) return ResponseEntity.notFound().build();
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.header(HttpHeaders.CONTENT_TYPE, "text/plain;charset=UTF-8")
|
||||||
|
.body(script);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 提交新配置(需 HMAC 鉴权)
|
* 提交新配置(需 HMAC 鉴权)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public class GroovyScriptEngine {
|
|||||||
return buildResult(result, scriptResult);
|
return buildResult(result, scriptResult);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
result.put("matched", false);
|
result.put("matched", false);
|
||||||
result.put("message", "Groovy 脚本执行异常: " + e.getMessage());
|
result.put("message", extractGroovyError(e));
|
||||||
log.error("Groovy error for url={}", url, e);
|
log.error("Groovy error for url={}", url, e);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
@@ -91,6 +91,25 @@ public class GroovyScriptEngine {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 从 Groovy 异常中提取含行号的错误信息 */
|
||||||
|
private String extractGroovyError(Exception e) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append(e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName());
|
||||||
|
for (StackTraceElement ste : e.getStackTrace()) {
|
||||||
|
String cn = ste.getClassName();
|
||||||
|
// Groovy 编译后的类名通常是 Script1, Script2 或用户脚本类
|
||||||
|
if (cn.startsWith("Script") || cn.contains("$_run_closure") || cn.contains("$__execute")) {
|
||||||
|
sb.append("\n at line ").append(ste.getLineNumber());
|
||||||
|
if (ste.getFileName() != null) sb.append(" (").append(ste.getFileName()).append(")");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (e.getCause() != null && e.getCause() != e) {
|
||||||
|
sb.append("\nCaused by: ").append(e.getCause().getMessage());
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
public static class JsoupHelper {
|
public static class JsoupHelper {
|
||||||
public Object parse(String html) { return Jsoup.parse(html); }
|
public Object parse(String html) { return Jsoup.parse(html); }
|
||||||
public Object parse(String html, String baseUri) { return Jsoup.parse(html, baseUri); }
|
public Object parse(String html, String baseUri) { return Jsoup.parse(html, baseUri); }
|
||||||
|
|||||||
@@ -79,6 +79,8 @@ export default {
|
|||||||
pendingBySite: (siteId) => api.get(`/admin/configs/pending/${siteId}`),
|
pendingBySite: (siteId) => api.get(`/admin/configs/pending/${siteId}`),
|
||||||
getContent: (configId) => api.get(`/admin/configs/${configId}/content`),
|
getContent: (configId) => api.get(`/admin/configs/${configId}/content`),
|
||||||
updateContent: (configId, content) => api.put(`/admin/configs/${configId}/content`, { content }),
|
updateContent: (configId, content) => api.put(`/admin/configs/${configId}/content`, { content }),
|
||||||
|
getScript: (configId) => api.get(`/admin/configs/${configId}/script`),
|
||||||
|
updateScript: (configId, script) => api.put(`/admin/configs/${configId}/script`, { script }),
|
||||||
approve: (configId) => api.post(`/admin/reviews/${configId}/approve`),
|
approve: (configId) => api.post(`/admin/reviews/${configId}/approve`),
|
||||||
reject: (configId, comment) => api.post(`/admin/reviews/${configId}/reject`, null, { params: { comment: comment || '' } }),
|
reject: (configId, comment) => api.post(`/admin/reviews/${configId}/reject`, null, { params: { comment: comment || '' } }),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -48,20 +48,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 配置编辑弹窗 -->
|
<!-- 配置/脚本弹窗 -->
|
||||||
<el-dialog v-model="configVisible" title="适配配置" width="700px" :close-on-click-modal="false">
|
<el-dialog v-model="configVisible" title="适配配置" width="800px" :close-on-click-modal="false">
|
||||||
<template v-if="editing">
|
<el-tabs v-model="configTab" type="card" size="small">
|
||||||
<el-input v-model="editContent" type="textarea" :rows="20" style="font-family:monospace;font-size:12px" />
|
<el-tab-pane label="配置JSON" name="config">
|
||||||
<div style="margin-top:10px;display:flex;gap:8px">
|
<template v-if="editing">
|
||||||
<el-button type="primary" size="small" @click="saveEdit" :loading="saving">保存</el-button>
|
<el-input v-model="editContent" type="textarea" :rows="20" style="font-family:monospace;font-size:12px" />
|
||||||
<el-button size="small" @click="cancelEdit">取消</el-button>
|
</template>
|
||||||
</div>
|
<pre v-else class="config-json" v-html="highlightedConfig"></pre>
|
||||||
</template>
|
</el-tab-pane>
|
||||||
<pre v-else class="config-json" v-html="highlightedConfig"></pre>
|
<el-tab-pane label="Groovy 脚本" name="script">
|
||||||
|
<div v-if="scriptLoading" style="text-align:center;padding:20px;color:#909399">加载中...</div>
|
||||||
|
<template v-else>
|
||||||
|
<el-input v-model="scriptContent" type="textarea" :rows="20" style="font-family:monospace;font-size:12px"
|
||||||
|
placeholder="// Groovy 解析脚本 // 内置变量: content(页面内容), url(访问URL), jsoup, json, utils def doc = jsoup.parse(content) ..." />
|
||||||
|
</template>
|
||||||
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="configVisible=false">关闭</el-button>
|
<el-button @click="configVisible=false">关闭</el-button>
|
||||||
<template v-if="!editing && selectedConfig?.status==='pending'">
|
<template v-if="selectedConfig?.status==='pending'">
|
||||||
<el-button type="primary" size="small" @click="startEdit">编辑</el-button>
|
<el-button v-if="configTab==='config'" type="primary" size="small" @click="editing? saveEdit() : startEdit()">
|
||||||
|
{{ editing ? '保存' : '编辑' }}
|
||||||
|
</el-button>
|
||||||
|
<el-button v-if="configTab==='config' && editing" size="small" @click="cancelEdit">取消</el-button>
|
||||||
|
<el-button v-if="configTab==='script'" type="primary" size="small" @click="saveScript" :loading="saving">
|
||||||
|
保存脚本
|
||||||
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
@@ -158,8 +171,12 @@ const selectedConfigId = ref(null)
|
|||||||
const selectedConfig = ref(null)
|
const selectedConfig = ref(null)
|
||||||
const selectedConfigJson = ref('')
|
const selectedConfigJson = ref('')
|
||||||
const configVisible = ref(false)
|
const configVisible = ref(false)
|
||||||
|
watch(configVisible, (v) => { if (v) { configTab.value='config'; loadScript() } })
|
||||||
|
const configTab = ref('config')
|
||||||
const editing = ref(false)
|
const editing = ref(false)
|
||||||
const editContent = ref('')
|
const editContent = ref('')
|
||||||
|
const scriptContent = ref('')
|
||||||
|
const scriptLoading = ref(false)
|
||||||
const saving = ref(false)
|
const saving = ref(false)
|
||||||
const approving = ref(false)
|
const approving = ref(false)
|
||||||
const rejecting = ref(false)
|
const rejecting = ref(false)
|
||||||
@@ -259,6 +276,21 @@ const refresh = async () => {
|
|||||||
// 编辑
|
// 编辑
|
||||||
const startEdit = () => { editContent.value=selectedConfigJson.value; editing.value=true }
|
const startEdit = () => { editContent.value=selectedConfigJson.value; editing.value=true }
|
||||||
const cancelEdit = () => { editing.value=false }
|
const cancelEdit = () => { editing.value=false }
|
||||||
|
const loadScript = async () => {
|
||||||
|
if (!selectedConfig.value) return
|
||||||
|
scriptLoading.value = true
|
||||||
|
try { const r = await api.adminConfigs.getScript(selectedConfig.value.id); scriptContent.value = r.data?.script || '' }
|
||||||
|
catch { scriptContent.value = '' }
|
||||||
|
finally { scriptLoading.value = false }
|
||||||
|
}
|
||||||
|
const saveScript = async () => {
|
||||||
|
saving.value = true
|
||||||
|
try {
|
||||||
|
await api.adminConfigs.updateScript(selectedConfig.value.id, scriptContent.value)
|
||||||
|
ElMessage.success('脚本已保存'); doBatchParse()
|
||||||
|
} catch(e) { ElMessage.error(e.response?.data?.message||'保存失败') }
|
||||||
|
finally { saving.value = false }
|
||||||
|
}
|
||||||
const saveEdit = async () => {
|
const saveEdit = async () => {
|
||||||
saving.value = true
|
saving.value = true
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user