feat: 评论前置过滤(合规检测)与 AI Foundation 隔离加载 (v1.1.0)
This commit is contained in:
+25
-11
@@ -5,16 +5,29 @@ plugins {
|
||||
|
||||
group 'top.nxxy335.commentaiautopilot.ui'
|
||||
|
||||
// Fix Gradle 9.x compatibility with pnpm symlinks
|
||||
tasks.named('pnpmInstall') {
|
||||
doNotTrackState("pnpm symlinks are not compatible with Gradle state tracking")
|
||||
// Use system pnpm directly — avoids Windows exit code 268435659
|
||||
// caused by Gradle Worker Daemon / node-gradle downloading pnpm on Windows
|
||||
node {
|
||||
download = false
|
||||
}
|
||||
|
||||
tasks.register('pnpmBuild', PnpmTask) {
|
||||
// Skip built-in pnpm tasks (they fail on Windows), replace with Exec-based tasks
|
||||
tasks.named('pnpmSetup').configure { enabled = false }
|
||||
tasks.named('pnpmInstall').configure { enabled = false }
|
||||
|
||||
tasks.register('uiInstall', Exec) {
|
||||
group = 'build'
|
||||
description = 'Install UI dependencies using system pnpm'
|
||||
workingDir layout.projectDirectory
|
||||
commandLine 'cmd', '/c', 'pnpm', 'install'
|
||||
}
|
||||
|
||||
tasks.register('uiBuild', Exec) {
|
||||
group = 'build'
|
||||
description = 'Build the UI project using pnpm'
|
||||
args = ['build']
|
||||
dependsOn tasks.named('pnpmInstall')
|
||||
workingDir layout.projectDirectory
|
||||
commandLine 'cmd', '/c', 'pnpm', 'run', 'build'
|
||||
dependsOn uiInstall
|
||||
inputs.dir(layout.projectDirectory.dir('src'))
|
||||
inputs.files(fileTree(
|
||||
dir: layout.projectDirectory,
|
||||
@@ -22,17 +35,18 @@ tasks.register('pnpmBuild', PnpmTask) {
|
||||
outputs.dir(layout.buildDirectory.dir('dist'))
|
||||
}
|
||||
|
||||
tasks.register('pnpmCheck', PnpmTask) {
|
||||
tasks.register('uiCheck', Exec) {
|
||||
group = 'verification'
|
||||
description = 'Run unit tests for the UI project using pnpm'
|
||||
args = ['test:unit']
|
||||
dependsOn tasks.named('pnpmInstall')
|
||||
workingDir layout.projectDirectory
|
||||
commandLine 'cmd', '/c', 'pnpm', 'run', 'test:unit'
|
||||
dependsOn uiInstall
|
||||
}
|
||||
|
||||
tasks.named('check') {
|
||||
dependsOn tasks.named('pnpmCheck')
|
||||
dependsOn tasks.named('uiCheck')
|
||||
}
|
||||
|
||||
tasks.named('assemble') {
|
||||
dependsOn tasks.named('pnpmBuild')
|
||||
dependsOn tasks.named('uiBuild')
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
<option value="FAIL">失败</option>
|
||||
<option value="PENDING">待审核</option>
|
||||
<option value="REJECTED">已拒绝</option>
|
||||
<option value="FILTERED">已拦截</option>
|
||||
</select>
|
||||
<select v-model="filterSentiment" class="filter-select">
|
||||
<option value="">全部情感</option>
|
||||
@@ -64,6 +65,11 @@
|
||||
<span class="card-time">{{ formatDate(reply.metadata.creationTimestamp) }}</span>
|
||||
</div>
|
||||
<div class="card-text">{{ stripHtml(reply.spec.reply) || '(空)' }}</div>
|
||||
<div v-if="reply.spec.status === 'FILTERED'" class="card-filter-reason">
|
||||
<svg class="filter-icon" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg>
|
||||
<span class="filter-category" v-if="reply.spec.filterCategory">{{ reply.spec.filterCategory }}</span>
|
||||
<span class="filter-detail">{{ reply.spec.filterReason || '未提供具体原因' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
@@ -176,7 +182,7 @@ const batchApprove = async () => { if(!selectedNames.value.size) return; try { a
|
||||
const batchReject = async () => { if(!selectedNames.value.size) return; try { await axiosInstance.post("/apis/console.api.comment-ai-autopilot.nxxy335.top/v1alpha1/replies/batch-reject", { names: Array.from(selectedNames.value) }); Toast.success("成功"); selectedNames.value.clear(); selectAll.value=false; fetchReplies() } catch(e) { Toast.error("失败") } }
|
||||
const batchDelete = async () => { if(!selectedNames.value.size) return; try { await axiosInstance.post("/apis/console.api.comment-ai-autopilot.nxxy335.top/v1alpha1/replies/batch-delete", { names: Array.from(selectedNames.value) }); Toast.success("成功"); selectedNames.value.clear(); selectAll.value=false; fetchReplies() } catch(e) { Toast.error("失败") } }
|
||||
|
||||
const getStatusLabel = (s: string) => { const m:any = { PASS: '通过', FAIL: '失败', PENDING: '待审', REJECTED: '拒绝' }; return m[s] || s }
|
||||
const getStatusLabel = (s: string) => { const m:any = { PASS: '通过', FAIL: '失败', PENDING: '待审', REJECTED: '拒绝', FILTERED: '已拦截' }; return m[s] || s }
|
||||
const getSentimentLabel = (s: string) => { const m:any = { VERY_POSITIVE: '极好', POSITIVE: '正面', NEUTRAL: '中性', NEGATIVE: '负面', VERY_NEGATIVE: '极差' }; return m[s] || s }
|
||||
const formatDate = (ts: string) => ts ? new Date(ts).toLocaleString("zh-CN") : ""
|
||||
const getPostUrl = (slug: string) => `${window.location.origin}/archives/${slug}`
|
||||
@@ -236,6 +242,10 @@ onMounted(fetchReplies)
|
||||
.tags-wrap { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||
.card-time { font-size: 12px; color: #9ca3af; }
|
||||
.card-text { font-size: 14px; color: #374151; line-height: 1.6; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.card-filter-reason { display: flex; align-items: flex-start; gap: 6px; margin-top: 8px; padding: 6px 10px; background: #fef3c7; border: 1px solid #fde68a; border-radius: 6px; font-size: 12px; color: #92400e; }
|
||||
.filter-icon { width: 14px; height: 14px; flex-shrink: 0; margin-top: 1px; }
|
||||
.filter-category { flex-shrink: 0; padding: 1px 6px; background: #b45309; color: #fff; border-radius: 3px; font-weight: 600; font-size: 11px; line-height: 1.5; }
|
||||
.filter-detail { flex: 1; line-height: 1.5; }
|
||||
.card-footer { display: flex; flex-direction: column; gap: 12px; padding: 12px 16px; background: #f9fafb; border-top: 1px solid #f3f4f6; }
|
||||
@media (min-width: 640px) { .card-footer { flex-direction: row; justify-content: space-between; align-items: center; } }
|
||||
.footer-info { font-size: 12px; color: #6b7280; display: flex; flex-wrap: wrap; gap: 12px; }
|
||||
@@ -251,7 +261,7 @@ onMounted(fetchReplies)
|
||||
|
||||
/* 标签体系 */
|
||||
.custom-tag { padding: 2px 6px; border-radius: 4px; font-size: 11px; font-weight: bold; }
|
||||
.tag-PASS { background: #dcfce7; color: #15803d; } .tag-FAIL { background: #fee2e2; color: #b91c1c; } .tag-PENDING { background: #fef9c3; color: #a16207; } .tag-REJECTED { background: #ffedd5; color: #c2410c; }
|
||||
.tag-PASS { background: #dcfce7; color: #15803d; } .tag-FAIL { background: #fee2e2; color: #b91c1c; } .tag-PENDING { background: #fef9c3; color: #a16207; } .tag-REJECTED { background: #ffedd5; color: #c2410c; } .tag-FILTERED { background: #f1f5f9; color: #b45309; border: 1px solid #fde68a; }
|
||||
.tag-published { background: #dbeafe; color: #1d4ed8; } .tag-draft { background: #f3f4f6; color: #4b5563; }
|
||||
.tag-conv { background: #f3e8ff; color: #7e22ce; }
|
||||
.tag-VERY_POSITIVE { background: #dcfce7; color: #14532d; } .tag-POSITIVE { background: #ecfdf5; color: #15803d; } .tag-NEGATIVE { background: #ffe4e6; color: #e11d48; } .tag-VERY_NEGATIVE { background: #fee2e2; color: #991b1b; }
|
||||
|
||||
@@ -57,6 +57,14 @@
|
||||
<div class="form-field__header"><span class="form-label">评论者黑名单</span><button class="btn-link" @click="openCommenterDialog">添加评论者</button></div>
|
||||
<textarea v-model="settings.basic.blockedCommenters" rows="2" class="form-textarea" placeholder="例如:张三, spam@example.com"></textarea>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-row__label"><span class="form-label">启用前置过滤</span><span class="form-hint">AI回复前检测评论合规性,拦截广告/辱骂/敏感内容,节省Token</span></div>
|
||||
<label class="toggle"><input type="checkbox" v-model="settings.basic.preFilterEnabled" /><span class="toggle__track"><span class="toggle__thumb"></span></span></label>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-row__label"><span class="form-label">违规评论设为待审核</span><span class="form-hint">检测到违规评论时自动取消通过,需人工审核</span></div>
|
||||
<label class="toggle"><input type="checkbox" v-model="settings.basic.preFilterPendingOnViolation" /><span class="toggle__track"><span class="toggle__thumb"></span></span></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -244,10 +252,14 @@ const tabItems = [
|
||||
]
|
||||
|
||||
const promptVariables = [
|
||||
{ name: '{{persona_prompt}}', desc: '角色设定' },
|
||||
{ name: '{{comment}}', desc: '评论内容' },
|
||||
{ name: '{{article}}', desc: '文章内容' },
|
||||
{ name: '{{conversation_history}}', desc: '对话历史' },
|
||||
{ name: '{{persona_prompt}}', desc: 'AI角色人格提示词(含已启用的预设)' },
|
||||
{ name: '{{safety_prompt}}', desc: '安全规范提示词' },
|
||||
{ name: '{{post_title}}', desc: '文章标题' },
|
||||
{ name: '{{post_date}}', desc: '文章发布日期' },
|
||||
{ name: '{{comment_count}}', desc: '该文章的评论数' },
|
||||
{ name: '{{article}}', desc: '文章/页面内容(含标题)' },
|
||||
{ name: '{{conversation_history}}', desc: '对话历史上下文' },
|
||||
{ name: '{{comment}}', desc: '评论内容(含评论者名称)' },
|
||||
]
|
||||
|
||||
const promptPresets = [
|
||||
@@ -258,7 +270,7 @@ const promptPresets = [
|
||||
]
|
||||
|
||||
const settings = reactive({
|
||||
basic: { autoReply: true, autoPublish: true, maxRetryCount: 3, blockedCommenters: "", maxConversationRounds: 8, rateLimitPerMinute: 10 },
|
||||
basic: { autoReply: true, autoPublish: true, maxRetryCount: 3, blockedCommenters: "", maxConversationRounds: 8, rateLimitPerMinute: 10, preFilterEnabled: true, preFilterPendingOnViolation: true },
|
||||
model: { modelName: "" },
|
||||
prompt: { customPromptTemplate: "", enabledPresets: [] as string[] },
|
||||
cleanup: { cleanupEnabled: true, retentionDays: 30 },
|
||||
|
||||
Reference in New Issue
Block a user