Init project
parent
e7eb5be916
commit
f96ab136e7
@ -0,0 +1,4 @@
|
||||
.vs/
|
||||
vendor/
|
||||
composer.lock
|
||||
test.php
|
@ -0,0 +1,29 @@
|
||||
# Isekai AI Review
|
||||
[中文文档](README-zh.md)
|
||||
|
||||
This extension require mediawiki Moderation extension.
|
||||
Use AI to auto review revs in Moderation.
|
||||
|
||||
If you want to add more AI Review API, you can submit a issue.
|
||||
|
||||
## Useage
|
||||
First, register at Aliyun: [https://www.aliyun.com/product/lvwang](https://www.aliyun.com/product/lvwang)
|
||||
|
||||
And then, install the Moderation extension: [https://github.com/edwardspec/mediawiki-moderation](https://github.com/edwardspec/mediawiki-moderation)
|
||||
|
||||
Install composer packages (If you download the release, ignore it)
|
||||
```php
|
||||
composer update
|
||||
```
|
||||
|
||||
Finally, add config in ```LocalSettings.php```:
|
||||
```php
|
||||
wfLoadExtension('IsekaiAIReview');
|
||||
|
||||
//config
|
||||
$wgAIReviewEndpoint = 'cn-shanghai';
|
||||
$wgAIReviewAccessKeyId = '阿里云的Access key id';
|
||||
$wgAIReviewAccessKeySecret = '阿里云的Access key secret';
|
||||
$wgAIReviewBizType = 'isekaiwiki';
|
||||
$wgAIReviewRobotUID = 0; //The user account show in Moderation which approve revs
|
||||
```
|
@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "hyperzlib/isekai-ai-review",
|
||||
"type": "mediawiki-extension",
|
||||
"require": {
|
||||
"alibabacloud/sdk": "^1.8",
|
||||
"paquettg/php-html-parser": "^2.2"
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "量子复合态",
|
||||
"email": "hyperzlib@outlook.com"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "IsekaiAIReview",
|
||||
"author": "hyperzlib",
|
||||
"url": "https://www.isekai.cn",
|
||||
"descriptionmsg": "isekai-aireview-desc",
|
||||
"version": "1.0.0",
|
||||
"license-name": "MIT",
|
||||
"type": "other",
|
||||
"requires": {
|
||||
"MediaWiki": ">= 1.31.0",
|
||||
"extensions": {
|
||||
"Moderation": ">= 1.5.0"
|
||||
}
|
||||
},
|
||||
"ExtensionMessagesFiles": {
|
||||
"IsekaiAIReviewAlias": "IsekaiAIReview.alias.php"
|
||||
},
|
||||
"MessagesDirs": {
|
||||
"IsekaiAIReview": [
|
||||
"i18n"
|
||||
]
|
||||
},
|
||||
"AutoloadClasses": {
|
||||
"Isekai\\AIReview\\Hooks": "includes/Hooks.php",
|
||||
"Isekai\\AIReview\\SectionSplitter": "includes/SectionSplitter.php",
|
||||
"Isekai\\AIReview\\Utils": "includes/Utils.php",
|
||||
"Isekai\\AIReview\\AliyunAIReview": "includes/AliyunAIReview.php",
|
||||
"Isekai\\AIReview\\AIReviewJob": "includes/AIReviewJob.php",
|
||||
"Isekai\\AIReview\\LogFormatter": "includes/LogFormatter.php"
|
||||
},
|
||||
"Hooks": {
|
||||
"ModerationPending": [
|
||||
"Isekai\\AIReview\\Hooks::onModerationPending"
|
||||
]
|
||||
},
|
||||
"JobClasses": {
|
||||
"IsekaiAIReview": "Isekai\\AIReview\\AIReviewJob"
|
||||
},
|
||||
"LogTypes": [
|
||||
"aireview"
|
||||
],
|
||||
"LogActionsHandlers": {
|
||||
"aireview/*": "Isekai\\AIReview\\LogFormatter"
|
||||
},
|
||||
"LogRestrictions": {
|
||||
"aireview": "moderation"
|
||||
},
|
||||
"config": {
|
||||
"AIReviewEndpoint": "cn-shanghai",
|
||||
"AIReviewAccessKeyId": "",
|
||||
"AIReviewAccessKeySecret": "",
|
||||
"AIReviewBizType": null,
|
||||
"AIReviewRobotUID": 1
|
||||
},
|
||||
"load_composer_autoloader": true,
|
||||
"manifest_version": 1
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
<?php
|
||||
namespace Isekai\AIReview;
|
||||
|
||||
use AlibabaCloud\Client\AlibabaCloud;
|
||||
use AlibabaCloud\Green\Green;
|
||||
use Exception;
|
||||
|
||||
class AliyunAIReview {
|
||||
private const MAX_LENGTH = 10000;
|
||||
|
||||
public function __construct(){
|
||||
global $wgAIReviewEndpoint, $wgAIReviewAccessKeyId, $wgAIReviewAccessKeySecret;
|
||||
AlibabaCloud::accessKeyClient($wgAIReviewAccessKeyId, $wgAIReviewAccessKeySecret)
|
||||
->regionId($wgAIReviewEndpoint)
|
||||
->asDefaultClient();
|
||||
}
|
||||
|
||||
public function reviewText($text){
|
||||
$reqData = $this->buildRequestData($text);
|
||||
$response = $this->doRequest($reqData);
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function buildRequestData($text){
|
||||
global $wgAIReviewBizType;
|
||||
|
||||
$reqData = [
|
||||
'scenes' => ['antispam'],
|
||||
'tasks' => $this->buildTasks($text),
|
||||
];
|
||||
|
||||
if($wgAIReviewBizType) $reqData['bizType'] = $wgAIReviewBizType;
|
||||
return $reqData;
|
||||
}
|
||||
|
||||
public function buildTasks($text){
|
||||
$splitter = new SectionSplitter($text, self::MAX_LENGTH);
|
||||
$chunkList = $splitter->getChunkList();
|
||||
$taskList = [];
|
||||
foreach($chunkList as $chunk){
|
||||
$task = [
|
||||
'dataId' => uniqid(),
|
||||
'content' => $chunk,
|
||||
];
|
||||
$taskList[] = $task;
|
||||
};
|
||||
unset($chunkList);
|
||||
return $taskList;
|
||||
}
|
||||
|
||||
public function doRequest($requestData){
|
||||
$textScan = Green::v20180509()->textScan();
|
||||
$response = $textScan->setMethod('POST')->setAcceptFormat('JSON')->setContent(json_encode($requestData))->request();
|
||||
|
||||
if($response->getReasonPhrase() === 'OK'){
|
||||
return $this->parseResponse($response->toArray());
|
||||
} else {
|
||||
return ['pass' => false, 'reason' => wfMessage('isekai-aireview-aliyun-server-error', $response->getStatusCode())->escaped()];
|
||||
}
|
||||
}
|
||||
|
||||
public function parseResponse($response){
|
||||
if($response['code'] !== 200)
|
||||
return ['pass' => false, 'reason' => wfMessage('isekai-aireview-aliyun-server-error', $response['code'])->escaped()];
|
||||
|
||||
$pass = true;
|
||||
$reasons = [];
|
||||
foreach($response['data'] as $task){
|
||||
if(is_array($task['results'])){
|
||||
foreach($task['results'] as $result){
|
||||
if($result['suggestion'] !== 'pass'){
|
||||
$pass = false;
|
||||
foreach($result['details'] as $detail){
|
||||
$reason = $detail['label'];
|
||||
if(!in_array($reason, $reasons)){
|
||||
$reasons[] = $reason;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ['pass' => $pass, 'reason' => $reasons];
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
namespace Isekai\AIReview;
|
||||
|
||||
use JobQueueGroup;
|
||||
use Title;
|
||||
|
||||
class Hooks {
|
||||
public static function onModerationPending($fields, $modid){
|
||||
//加入审核队列
|
||||
$job = new AIReviewJob(Title::newFromText($fields['mod_title']), ['mod_id' => $modid]);
|
||||
JobQueueGroup::singleton()->push($job);
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
namespace Isekai\AIReview;
|
||||
|
||||
use LogFormatter as GlobalLogFormatter;
|
||||
use SpecialPage;
|
||||
use Message;
|
||||
use Linker;
|
||||
use Title;
|
||||
use User;
|
||||
|
||||
class LogFormatter extends GlobalLogFormatter {
|
||||
public function getMessageParameters(){
|
||||
$params = parent::getMessageParameters();
|
||||
|
||||
$type = $this->entry->getSubtype();
|
||||
$entryParams = $this->entry->getParameters();
|
||||
$linkRenderer = $this->getLinkRenderer();
|
||||
|
||||
switch($type){
|
||||
case 'approve':
|
||||
$modId = $entryParams['modid'];
|
||||
|
||||
$user = User::newFromId($entryParams['moduser']);
|
||||
$userLink = Linker::userLink( $user->getId(), $user->getName() );
|
||||
$params[3] = Message::rawParam( $userLink );
|
||||
|
||||
$link = $linkRenderer->makeKnownLink(
|
||||
SpecialPage::getTitleFor( 'Moderation' ),
|
||||
$this->msg( 'moderation-log-change' )->params( $modId )->text(),
|
||||
[ 'title' => $this->msg( 'tooltip-moderation-rejected-change' )->plain() ],
|
||||
[ 'modaction' => 'show', 'modid' => $modId ]
|
||||
);
|
||||
$params[4] = Message::rawParam( $link );
|
||||
|
||||
break;
|
||||
case 'reject':
|
||||
$modId = $entryParams['modid'];
|
||||
|
||||
$user = User::newFromId($entryParams['moduser']);
|
||||
$userLink = Linker::userLink( $user->getId(), $user->getName() );
|
||||
$params[3] = Message::rawParam( $userLink );
|
||||
|
||||
$link = $linkRenderer->makeKnownLink(
|
||||
SpecialPage::getTitleFor( 'Moderation' ),
|
||||
$this->msg( 'moderation-log-change' )->params( $modId )->text(),
|
||||
[ 'title' => $this->msg( 'tooltip-moderation-rejected-change' )->plain() ],
|
||||
[ 'modaction' => 'show', 'modid' => $modId ]
|
||||
);
|
||||
$params[4] = Message::rawParam( $link );
|
||||
|
||||
$params[5] = Utils::getReadableReason($entryParams['reason']);
|
||||
break;
|
||||
}
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function getPreloadTitles() {
|
||||
$type = $this->entry->getSubtype();
|
||||
$params = $this->entry->getParameters();
|
||||
|
||||
$titles = [];
|
||||
|
||||
if ( $params['moduser'] ) { # Not anonymous
|
||||
$user = User::newFromId($params['moduser']);
|
||||
$titles[] = Title::makeTitle( NS_USER, $user->getName() );
|
||||
}
|
||||
|
||||
return $titles;
|
||||
}
|
||||
}
|
@ -0,0 +1,78 @@
|
||||
<?php
|
||||
namespace Isekai\AIReview;
|
||||
|
||||
class SectionSplitter {
|
||||
private $chunkList = [''];
|
||||
private $chunkListSeek = 0;
|
||||
private $bufferLength = 0;
|
||||
private $maxLength;
|
||||
|
||||
public function __construct($text, $maxLength = 10000){
|
||||
$this->maxLength = $maxLength;
|
||||
$this->splitLine($text);
|
||||
}
|
||||
|
||||
/* 将文本推入chunk列表 */
|
||||
public function push($chunk){
|
||||
$chunkLength = mb_strlen($chunk, 'UTF-8');
|
||||
if($this->bufferLength + $chunkLength > $this->maxLength){ //满一万字
|
||||
$this->chunkListSeek ++;
|
||||
$this->chunkList[$this->chunkListSeek] = $chunk;
|
||||
$this->bufferLength = $chunkLength;
|
||||
} else { //没满一万字,接着塞
|
||||
$this->chunkList[$this->chunkListSeek] .= $chunk;
|
||||
$this->bufferLength += $chunkLength;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按照行来拆分
|
||||
*/
|
||||
public function splitLine($text){
|
||||
$text = str_replace("\r\n", "\n", $text);
|
||||
$lines = explode("\n", $text);
|
||||
foreach($lines as $line){
|
||||
if(empty($line)) continue;
|
||||
|
||||
$line .= "\n";
|
||||
if(mb_strlen($line, 'UTF-8') > $this->maxLength){ //见鬼,这个人怎么能写一万字不换行
|
||||
$this->splitSentence($line);
|
||||
} else {
|
||||
$this->push($line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按照句子来拆分
|
||||
*/
|
||||
public function splitSentence($text){ //我就不信一句话能一万字
|
||||
$sentences = explode("\0", preg_replace('/(。|\\.)/', "$1\0", $text));
|
||||
foreach($sentences as $sentence){
|
||||
if(mb_strlen($sentence, 'UTF-8') > $this->maxLength){ //一句话能说一万字吗?
|
||||
$this->forceSplit($sentence);
|
||||
} else {
|
||||
$this->push($sentence);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制拆分
|
||||
*/
|
||||
public function forceSplit($text){
|
||||
$len = mb_strlen($text, 'UTF-8');
|
||||
$times = ceil($len / $this->maxLength);
|
||||
for($i = 0; $i < $times; $i ++){
|
||||
$startPos = $i * $this->maxLength;
|
||||
$sentenceLen = min($len - 1 - $i * $startPos, $this->maxLength);
|
||||
$sentence = substr($text, $startPos, $sentenceLen);
|
||||
|
||||
$this->push($sentence);
|
||||
}
|
||||
}
|
||||
|
||||
public function getChunkList(){
|
||||
return $this->chunkList;
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
namespace Isekai\AIReview;
|
||||
|
||||
use ManualLogEntry;
|
||||
use PHPHtmlParser\Dom;
|
||||
|
||||
class Utils {
|
||||
public static function getDiffAddedLines($diffHtml){
|
||||
$dom = new Dom();
|
||||
$dom->load($diffHtml);
|
||||
$lines = [];
|
||||
|
||||
if($addedLineDomList = $dom->find('.diff-addedline')){
|
||||
/** @var \PHPHtmlParser\Dom\HtmlNode $addedLineDom */
|
||||
foreach($addedLineDomList as $addedLineDom){
|
||||
$lines[] = strip_tags($addedLineDom->innerHtml);
|
||||
}
|
||||
}
|
||||
|
||||
return trim(implode("\n", $lines));
|
||||
}
|
||||
|
||||
public static function getReadableReason($reasons){
|
||||
$allowedReasons = ['spam', 'ad', 'politics', 'terrorism', 'abuse', 'porn', 'flood', 'contraband', 'meaningless', 'customized', 'normal'];
|
||||
|
||||
if(is_string($reasons)) return $reasons;
|
||||
|
||||
$readableReasons = [];
|
||||
foreach($reasons as $reason){
|
||||
if(in_array($reason, $allowedReasons)){
|
||||
$readableReasons[] = wfMessage('isekai-aireview-aliyun-reason-' . $reason)->escaped();
|
||||
} else {
|
||||
$readableReasons[] = wfMessage('isekai-aireview-aliyun-reason-unknow', $reason)->escaped();
|
||||
}
|
||||
}
|
||||
|
||||
return implode(', ', $readableReasons);
|
||||
}
|
||||
|
||||
public static function addAIReviewLog($event, $robotUser, $modUser, $title, $modid, $reason = null){
|
||||
$entry = new ManualLogEntry('aireview', $event);
|
||||
$entry->setPerformer($robotUser);
|
||||
$entry->setTarget($title);
|
||||
|
||||
$param = [
|
||||
'modid' => $modid,
|
||||
'moduser' => $modUser,
|
||||
];
|
||||
if($reason){
|
||||
$param['reason'] = $reason;
|
||||
}
|
||||
|
||||
$entry->setParameters($param);
|
||||
$entry->insert();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue