feat: AI虚拟用户新闻互动系统 v1.3.0 初始提交

- 虚拟用户管理(昵称/头像/性别/简介/邮箱同步到目标平台)
- AI互动调度(点赞/收藏/评论/转发)
- 日志时间改为北京时间
- 评论达上限后继续执行点赞收藏转发
- 一键登出全部功能
- 浅色主题UI
This commit is contained in:
stefanfeng
2026-03-31 10:20:57 +08:00
commit 0cfc9bf9c8
53 changed files with 8457 additions and 0 deletions
+239
View File
@@ -0,0 +1,239 @@
<template>
<div class="page-container">
<div class="page-header">
<span class="page-title">AI模型配置</span>
<el-button type="primary" size="small" @click="openCreate">
<el-icon><Plus /></el-icon> 添加模型
</el-button>
</div>
<div class="models-grid">
<div v-for="m in models" :key="m.id" class="model-card" :class="{ 'is-default': m.is_default }">
<div class="model-head">
<div class="model-name">
<span class="provider-badge" :class="m.provider">{{ providerLabels[m.provider] || m.provider }}</span>
<span class="model-title">{{ m.model_name }}</span>
</div>
<div style="display:flex;gap:6px;align-items:center">
<el-tag v-if="m.is_default" type="success" size="small">默认</el-tag>
<el-tag v-if="!m.is_enabled" type="danger" size="small">禁用</el-tag>
</div>
</div>
<div class="model-meta">
<span>版本: {{ m.model_version || '--' }}</span>
<span>温度: {{ m.temperature }}</span>
<span>Max Tokens: {{ m.max_tokens }}</span>
<span>超时: {{ m.timeout_seconds }}s</span>
<span>API Key: {{ m.has_api_key ? '✓ 已配置' : '✗ 未配置' }}</span>
</div>
<div class="model-actions">
<el-button size="small" @click="openTest(m)">测试</el-button>
<el-button size="small" @click="openEdit(m)">编辑</el-button>
<el-button v-if="!m.is_default" size="small" type="primary" @click="setDefault(m)">设为默认</el-button>
<el-button size="small" type="danger" @click="doDelete(m)">删除</el-button>
</div>
</div>
<div v-if="!models.length" class="empty-state">
<el-empty description="暂无模型配置,请添加AI模型" />
</div>
</div>
<!-- Create/Edit Dialog -->
<el-dialog v-model="dialogVisible" :title="editModel ? '编辑模型' : '添加AI模型'" width="540px">
<el-form :model="form" label-width="110px" :rules="rules" ref="formRef">
<el-form-item label="模型名称" prop="model_name">
<el-input v-model="form.model_name" placeholder="如: GPT-4 生产环境" />
</el-form-item>
<el-form-item label="提供商" prop="provider">
<el-select v-model="form.provider" style="width:100%" @change="onProviderChange">
<el-option v-for="(l,v) in providerLabels" :key="v" :label="l" :value="v" />
</el-select>
</el-form-item>
<el-form-item label="API地址">
<el-input v-model="form.api_base_url" placeholder="留空使用默认地址" />
</el-form-item>
<el-form-item label="API Key">
<el-input v-model="form.api_key" type="password" show-password :placeholder="editModel ? '不修改请留空' : '输入API Key'" />
</el-form-item>
<el-form-item label="模型版本">
<el-input v-model="form.model_version" placeholder="如: gpt-4-turbo, glm-4" />
</el-form-item>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="温度">
<el-input-number v-model="form.temperature" :min="0" :max="2" :step="0.1" style="width:100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="Max Tokens">
<el-input-number v-model="form.max_tokens" :min="100" :max="32000" :step="100" style="width:100%" />
</el-form-item>
</el-col>
</el-row>
<el-form-item label="超时(秒)">
<el-input-number v-model="form.timeout_seconds" :min="5" :max="300" style="width:160px" />
</el-form-item>
<el-form-item label="设为默认">
<el-switch v-model="form.is_default" :active-value="1" :inactive-value="0" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submit" :loading="submitting">保存</el-button>
</template>
</el-dialog>
<!-- Test Dialog -->
<el-dialog v-model="testVisible" title="模型测试" width="560px">
<el-form label-width="90px">
<el-form-item label="测试指令">
<el-input v-model="testPrompt" type="textarea" :rows="3" />
</el-form-item>
</el-form>
<div v-if="testResult" class="test-result">
<div class="result-meta">
<el-tag :type="testResult.success ? 'success' : 'danger'">
{{ testResult.success ? '成功' : '失败' }}
</el-tag>
<span v-if="testResult.success" style="font-size:12px;color:var(--color-text-muted)">
耗时 {{ testResult.elapsed_seconds }}s · 消耗 {{ testResult.tokens }} tokens
</span>
</div>
<div class="result-content" style="white-space:pre-wrap;word-break:break-all;">
{{ testResult.success ? (testResult.content || '(响应为空)') : testResult.error }}
</div>
<div v-if="testResult.success" style="margin-top:8px;font-size:12px;color:var(--color-text-muted)">
耗时 {{ testResult.elapsed_seconds }}s &nbsp;·&nbsp; Token {{ testResult.tokens }}
</div>
</div>
<template #footer>
<el-button @click="testVisible = false">关闭</el-button>
<el-button type="primary" @click="runTest" :loading="testing">发送测试</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getAIModels, createAIModel, updateAIModel, deleteAIModel, testAIModel } from '@/api'
const models = ref([])
const dialogVisible = ref(false)
const editModel = ref(null)
const submitting = ref(false)
const formRef = ref()
const testVisible = ref(false)
const testingModel = ref(null)
const testPrompt = ref('你好,请简单介绍一下你自己,并写一条关于"人工智能改变新闻业"的新闻评论。')
const testResult = ref(null)
const testing = ref(false)
const providerLabels = { openai: 'OpenAI', zhipu: '智谱GLM', wenxin: '文心一言', qianwen: '通义千问', local: '本地模型' }
const form = reactive({ model_name: '', provider: 'openai', api_base_url: '', api_key: '', model_version: '', temperature: 0.7, max_tokens: 1000, timeout_seconds: 30, is_default: 0 })
const rules = { model_name: [{ required: true, message: '请输入模型名称' }], provider: [{ required: true }] }
async function load() {
const res = await getAIModels()
models.value = res.data || []
}
const PROVIDER_DEFAULTS = {
openai: { api_base_url: 'https://api.openai.com/v1', model_version: 'gpt-4-turbo' },
zhipu: { api_base_url: 'https://open.bigmodel.cn/api/paas/v4', model_version: 'glm-4' },
wenxin: { api_base_url: 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat', model_version: 'ERNIE-Bot-4' },
qianwen: { api_base_url: 'https://dashscope.aliyuncs.com/compatible-mode/v1', model_version: 'qwen-turbo' },
local: { api_base_url: 'http://127.0.0.1:11434/v1', model_version: 'qwen2' },
}
function onProviderChange(provider) {
const def = PROVIDER_DEFAULTS[provider] || {}
form.api_base_url = def.api_base_url || ''
form.model_version = def.model_version || ''
}
function openCreate() {
editModel.value = null
Object.assign(form, { model_name: '', provider: 'openai', api_base_url: PROVIDER_DEFAULTS.openai.api_base_url, api_key: '', model_version: PROVIDER_DEFAULTS.openai.model_version, temperature: 0.7, max_tokens: 1000, timeout_seconds: 30, is_default: 0 })
dialogVisible.value = true
}
function openEdit(m) {
editModel.value = m
Object.assign(form, { model_name: m.model_name, provider: m.provider, api_base_url: m.api_base_url || '', api_key: '', model_version: m.model_version || '', temperature: m.temperature, max_tokens: m.max_tokens, timeout_seconds: m.timeout_seconds, is_default: m.is_default })
dialogVisible.value = true
}
async function submit() {
await formRef.value.validate()
submitting.value = true
try {
const data = { ...form }
if (editModel.value) {
if (!data.api_key) delete data.api_key
await updateAIModel(editModel.value.id, data)
} else {
await createAIModel(data)
}
ElMessage.success('保存成功')
dialogVisible.value = false
load()
} finally { submitting.value = false }
}
async function doDelete(m) {
await ElMessageBox.confirm(`确认删除模型 "${m.model_name}"?`, '确认', { type: 'warning' })
await deleteAIModel(m.id)
ElMessage.success('删除成功')
load()
}
async function setDefault(m) {
await updateAIModel(m.id, { is_default: 1 })
ElMessage.success(`"${m.model_name}" 已设为默认模型`)
load()
}
function openTest(m) {
testingModel.value = m
testResult.value = null
testVisible.value = true
}
async function runTest() {
testing.value = true
testResult.value = null
try {
const res = await testAIModel({ model_id: testingModel.value.id, test_prompt: testPrompt.value })
testResult.value = res.data
} finally { testing.value = false }
}
onMounted(load)
</script>
<style scoped>
.models-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(380px, 1fr)); gap: 16px; }
.model-card {
background: var(--color-bg-card); border: 1px solid var(--color-border);
border-radius: 12px; padding: 18px;
transition: border-color 0.2s, box-shadow 0.2s;
}
.model-card.is-default { border-color: var(--color-accent-green); box-shadow: 0 0 12px rgba(63,185,80,0.15); }
.model-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
.model-name { display: flex; align-items: center; gap: 8px; }
.provider-badge { font-size: 11px; padding: 2px 8px; border-radius: 12px; font-weight: 600; }
.provider-badge.openai { background: rgba(16,163,127,0.15); color: #10a37f; }
.provider-badge.zhipu { background: rgba(88,166,255,0.15); color: var(--color-accent); }
.provider-badge.wenxin { background: rgba(240,136,62,0.15); color: var(--color-accent-orange); }
.provider-badge.qianwen { background: rgba(210,153,34,0.15); color: var(--color-accent-yellow); }
.provider-badge.local { background: rgba(188,140,255,0.15); color: var(--color-accent-purple); }
.model-title { font-size: 15px; font-weight: 600; }
.model-meta { display: flex; flex-wrap: wrap; gap: 10px; font-size: 12px; color: var(--color-text-muted); margin-bottom: 14px; }
.model-actions { display: flex; gap: 6px; flex-wrap: wrap; }
.test-result { margin-top: 16px; }
.result-meta { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
.result-content { background: var(--color-bg); border: 1px solid var(--color-border); border-radius: 8px; padding: 12px; font-size: 13px; line-height: 1.6; white-space: pre-wrap; max-height: 200px; overflow-y: auto; }
.empty-state { grid-column: 1/-1; padding: 40px; }
</style>