Files
tougao/application/common/AiTemplateSentenceRuleDetectService.php
2026-07-15 10:55:08 +08:00

169 lines
5.0 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
namespace app\common;
use think\Db;
/**
* 模板句规则检测t_ai_template_sentence
*/
class AiTemplateSentenceRuleDetectService
{
/** @var array<int,array>|null */
private static $ruleCache;
/**
* @param array<string,string> $sectionTexts 键abstract/introduction/...
* @return array{hits:array,rule_score:int}
*/
public function detect(array $sectionTexts): array
{
$rules = $this->loadActiveRules();
$hits = [];
$ruleScore = 0;
$normalized = [];
foreach ($sectionTexts as $key => $text) {
$section = strtolower(trim((string) $key));
if ($section === '') {
continue;
}
$normalized[$section] = ManuscriptTextCleanService::clean((string) $text);
}
$allText = ManuscriptTextCleanService::clean(implode("\n\n", array_filter($normalized)));
foreach ($rules as $rule) {
$section = strtolower(trim((string) ($rule['section'] ?? '')));
$pattern = trim((string) ($rule['sentence_pattern'] ?? ''));
if ($pattern === '') {
continue;
}
$targetText = '';
if ($section === 'all') {
$targetText = $allText;
} elseif (isset($normalized[$section])) {
$targetText = $normalized[$section];
} else {
continue;
}
if ($targetText === '') {
continue;
}
$matches = $this->matchPattern($targetText, $pattern);
foreach ($matches as $matchedText) {
$sentence = $this->extractSentenceContaining($targetText, $matchedText);
$hitScore = max(1, intval($rule['weight'] ?? 1)) * max(1, intval($rule['risk_level'] ?? 1));
$hits[] = [
'section' => $section,
'type' => '模板化表达',
'sentence' => $sentence !== '' ? $sentence : $matchedText,
'pattern' => $pattern,
'score' => $hitScore,
'remark' => (string) ($rule['remark'] ?? ''),
];
$ruleScore += $hitScore;
}
}
return [
'hits' => $hits,
'rule_score' => min(100, $ruleScore),
];
}
/**
* @return array<int,array>
*/
private function loadActiveRules(): array
{
if (self::$ruleCache !== null) {
return self::$ruleCache;
}
try {
self::$ruleCache = Db::name('ai_template_sentence')
->where('status', 1)
->field('id,sentence_pattern,section,language,risk_level,weight,remark')
->order('section asc,id asc')
->select();
} catch (\Throwable $e) {
self::$ruleCache = [];
}
return self::$ruleCache;
}
/**
* @return array<int,string>
*/
private function matchPattern(string $text, string $pattern): array
{
// 支持 regex: 前缀;否则优先按正则,失败则按字面量匹配
$raw = $pattern;
$forceRegex = false;
if (stripos($pattern, 'regex:') === 0) {
$raw = trim(substr($pattern, 6));
$forceRegex = true;
}
$out = $this->runPregMatchAll($text, '#' . $raw . '#iu');
if (!empty($out) || $forceRegex) {
return $out;
}
// 字面量回退(库内普通句子)
return $this->runPregMatchAll($text, '#' . preg_quote($raw, '#') . '#iu');
}
/**
* @return array<int,string>
*/
private function runPregMatchAll(string $text, string $regex): array
{
$error = null;
set_error_handler(function ($errno, $errstr) use (&$error) {
$error = $errstr;
return true;
});
$ok = preg_match_all($regex, $text, $matches);
restore_error_handler();
if ($ok === false || $error !== null || empty($matches[0])) {
return [];
}
$out = [];
foreach ($matches[0] as $match) {
$match = trim((string) $match);
if ($match !== '') {
$out[] = $match;
}
}
return $out;
}
private function extractSentenceContaining(string $text, string $needle): string
{
$pos = mb_stripos($text, $needle);
if ($pos === false) {
return '';
}
$before = mb_substr($text, 0, $pos);
$after = mb_substr($text, $pos);
$start = $before;
if (preg_match('/.*[.!?。!?]\s*/us', $before, $m)) {
$start = substr($before, strlen($m[0]));
}
$sentence = $start . $after;
if (preg_match('/^(.+?[.!?。!?])/us', $sentence, $m)) {
return trim($m[1]);
}
if (preg_match('/^.{1,500}/us', $sentence, $m)) {
return trim($m[0]);
}
return trim(mb_substr($sentence, 0, 300));
}
}