Files
tougao/application/api/controller/AITools.php
wyn 8785610e6d 参考文献作者堆叠
参考文献相关性检测
作者ai写作辅助检测工作
2026-07-15 10:49:05 +08:00

300 lines
10 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* AI 工具接口 — 编辑辅助AI 辅助写作风险分析
*/
namespace app\api\controller;
use app\common\ArticleParserService;
use app\common\mq\AiWritingRiskMqPublisher;
use app\common\PlagiarismService;
use app\common\service\AiWritingRiskTaskService;
use think\Validate;
class Aitools extends Base
{
public function __construct(\think\Request $request = null)
{
parent::__construct($request);
}
/**
* @title 写作风险检测可视化页面(前后端分离入口)
* @description 仅渲染 HTML检测与进度走 JSON 接口
*/
public function index()
{
return $this->renderRiskPage();
}
/**
* @title 开始 AI 辅助写作风险检测(异步入队)
* @description 立即返回 task_id后台 RabbitMQ 消费执行检测
* @param url 稿件链接地址(.docx
* @param p_article_id 可选业务稿件ID
*/
public function realtimeWritingRiskDetect()
{
$data = $this->request->param();
$rule = new Validate([
'url' => 'require',
]);
if (!$rule->check($data)) {
return $this->detectJsonError($rule->getError());
}
$url = trim((string) $data['url']);
$pArticleId = intval($data['p_article_id'] ?? 0);
try {
$taskService = new AiWritingRiskTaskService();
$created = $taskService->createTask($url, $pArticleId);
// 新任务,或复用但仍在排队(可能丢消息)时重新投递
if (empty($created['reused']) || intval($created['status']) === AiWritingRiskTaskService::STATUS_PENDING) {
(new AiWritingRiskMqPublisher())->publishTaskStart(intval($created['task_id']), 'enqueue');
}
return json([
'code' => 200,
'msg' => !empty($created['reused']) ? '已有进行中的检测任务' : '已开始检测',
'data' => [
'task_id' => $created['task_id'],
'task_no' => $created['task_no'],
'status' => $created['status'],
'status_text' => intval($created['status']) === 1 ? 'running' : 'queued',
'progress_percent' => 0,
'message' => $created['message'],
'reused' => !empty($created['reused']),
],
]);
} catch (\Exception $e) {
return $this->detectJsonError($e->getMessage());
}
}
/**
* @title 刷新 AI 辅助写作风险检测进度
* @description
* - 浏览器 GET无 format=json→ 可视化页面:未完成显示进度,完成后显示结果
* - POST 或 format=json → JSON供前端轮询
* @param task_id 任务ID与 task_no 二选一)
* @param task_no 任务号(与 task_id 二选一)
*/
public function realtimeWritingRiskProgress()
{
$data = $this->request->param();
$taskId = intval($data['task_id'] ?? 0);
$taskNo = trim((string) ($data['task_no'] ?? ''));
if ($taskId <= 0 && $taskNo === '') {
if ($this->wantsProgressJson()) {
return $this->detectJsonError('task_id 或 task_no 必填');
}
return $this->renderRiskPage();
}
try {
$progress = (new AiWritingRiskTaskService())->getProgress($taskNo, $taskId);
} catch (\Exception $e) {
if ($this->wantsProgressJson()) {
return $this->detectJsonError($e->getMessage());
}
return $this->renderRiskPage([
'page_error' => $e->getMessage(),
'task_id' => $taskId,
'task_no' => $taskNo,
]);
}
if ($this->wantsProgressJson()) {
return json([
'code' => 200,
'msg' => 'success',
'data' => $progress,
]);
}
return $this->renderRiskPage([
'task_id' => intval($progress['task_id'] ?? $taskId),
'task_no' => (string) ($progress['task_no'] ?? $taskNo),
'initial_data' => $progress,
]);
}
/**
* 是否返回 JSONPOST、format=json、XHR/fetch 显式要 JSON
*/
private function wantsProgressJson(): bool
{
if (strtolower((string) $this->request->param('format', '')) === 'json') {
return true;
}
if ($this->request->isAjax()) {
return true;
}
$accept = strtolower((string) $this->request->header('accept', ''));
if (strpos($accept, 'application/json') !== false) {
return true;
}
// 页面内 fetch 使用 POST
if ($this->request->isPost()) {
return true;
}
return false;
}
/**
* 渲染可视化页
* @param array $opts task_id/task_no/initial_data/page_error
*/
private function renderRiskPage(array $opts = [])
{
$apiStart = (string) url('api/aitools/realtimeWritingRiskDetect');
$apiProgress = (string) url('api/aitools/realtimeWritingRiskProgress');
$initial = isset($opts['initial_data']) && is_array($opts['initial_data'])
? $opts['initial_data']
: null;
$this->assign([
'api_start_js' => json_encode($apiStart, JSON_UNESCAPED_SLASHES),
'api_progress_js' => json_encode($apiProgress, JSON_UNESCAPED_SLASHES),
'init_task_id_js' => json_encode(intval($opts['task_id'] ?? 0)),
'init_task_no_js' => json_encode((string) ($opts['task_no'] ?? ''), JSON_UNESCAPED_UNICODE),
'init_data_js' => json_encode($initial, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'page_error_js' => json_encode((string) ($opts['page_error'] ?? ''), JSON_UNESCAPED_UNICODE),
]);
return $this->fetch('aiTools/index');
}
/**
* @title AI 辅助写作风险分析(编辑辅助)
* @description 解析稿件结构,并从模版句、重复短语、句长特征等维度输出风险报告
* @param url 稿件链接地址(.docx
*/
public function writingRiskAnalysis()
{
$data = $this->request->param();
$rule = new Validate([
'url' => 'require',
]);
if (!$rule->check($data)) {
return jsonError($rule->getError());
}
try {
$filePath = $this->resolveManuscriptUrl($data['url']);
} catch (\Exception $e) {
return jsonError($e->getMessage());
}
$result = ArticleParserService::analyzeWritingRisk($filePath);
if (empty($result['status']) || intval($result['status']) !== 1) {
return jsonError(empty($result['msg']) ? '稿件分析失败' : $result['msg']);
}
return jsonSuccess([
'manuscript' => $this->pickManuscriptFields($result['data']),
'risk_analysis' => $result['data']['risk_analysis'] ?? [],
'metrics' => $this->pickMetricFields($result['data']),
]);
}
/**
* @title 根据稿件链接解析稿件结构
* @description 从稿件地址解析标题、摘要、关键词及 IMRaD 正文分节(不含风险综述)
* @param url 稿件链接地址(.docx
*/
public function parseManuscript()
{
$data = $this->request->param();
$rule = new Validate([
'url' => 'require',
]);
if (!$rule->check($data)) {
return jsonError($rule->getError());
}
try {
$filePath = $this->resolveManuscriptUrl($data['url']);
} catch (\Exception $e) {
return jsonError($e->getMessage());
}
$result = ArticleParserService::parseManuscriptStructure($filePath);
if (empty($result['status']) || intval($result['status']) !== 1) {
return jsonError(empty($result['msg']) ? '稿件解析失败' : $result['msg']);
}
return jsonSuccess($result['data']);
}
private function detectJsonError($msg)
{
return json(['code' => 500, 'msg' => (string) $msg, 'data' => []]);
}
private function pickManuscriptFields(array $data): array
{
$keys = ['title', 'abstract', 'keywords', 'Introduction', 'Methods', 'Results', 'Discussion', 'Conclusion', 'References'];
$out = [];
foreach ($keys as $key) {
if (array_key_exists($key, $data)) {
$out[$key] = $data[$key];
}
}
return $out;
}
private function pickMetricFields(array $data): array
{
$keys = [
'sentence_stats',
'sentence_stats_by_section',
'repeated_phrases',
'repeated_phrases_by_section',
'template_sentence_stats',
];
$out = [];
foreach ($keys as $key) {
if (array_key_exists($key, $data)) {
$out[$key] = $data[$key];
}
}
return $out;
}
/**
* 将稿件链接解析为本地可读文件路径
*/
private function resolveManuscriptUrl($url)
{
$url = trim((string)$url);
if ($url === '') {
throw new \Exception('稿件地址不能为空');
}
if (preg_match('/^([a-zA-Z]:[\\\\\/]|\/)/', $url) && is_file($url)) {
return $url;
}
if (preg_match('#^https?://#i', $url)) {
try {
return (new PlagiarismService())->resolveFileUrlToLocal($url);
} catch (\Exception $e) {
$path = parse_url($url, PHP_URL_PATH);
if ($path) {
$local = rtrim(ROOT_PATH, '/') . $path;
if (is_file($local)) {
return $local;
}
}
throw $e;
}
}
$local = rtrim(ROOT_PATH, '/') . '/public/' . ltrim(ltrim($url, '/'), 'public');
if (is_file($local)) {
return $local;
}
throw new \Exception('无法解析稿件文件路径: ' . $url);
}
}