合并外部维护版本并同步自动每日功能
This commit is contained in:
@@ -138,6 +138,31 @@ const ensureDraftShape = (draft) => {
|
||||
draft.rules ||= [];
|
||||
draft.commands ||= {};
|
||||
draft.music_monitor.targets ||= [];
|
||||
draft.llm_reply ||= {};
|
||||
const llmDefaults = {
|
||||
enabled: false,
|
||||
provider: "openai",
|
||||
base_url: "",
|
||||
api_key: "",
|
||||
model: "",
|
||||
system_prompt: "你是B站直播间里的互动助手,直播间主播在玩原神。观众会发弹幕和你聊天,你用简短口语化的中文回复(不超过50字),语气轻松友好,可以玩梗,但不要讨论政治敏感内容,不要承诺任何游戏代练或交易。",
|
||||
temperature: 0.8,
|
||||
max_tokens: 128,
|
||||
timeout_sec: 20,
|
||||
retry_attempts: 2,
|
||||
max_reply_chars: 60,
|
||||
replies_per_minute: 6,
|
||||
per_user_cooldown_sec: 30,
|
||||
context_turns: 6,
|
||||
min_text_len: 2,
|
||||
blacklist_uids: [],
|
||||
ignore_prefixes: ["//"],
|
||||
reply_via_tts: true,
|
||||
reply_via_danmu: true
|
||||
};
|
||||
for (const [key, value] of Object.entries(llmDefaults)) {
|
||||
draft.llm_reply[key] ??= value;
|
||||
}
|
||||
draft.global.admin_uids ||= [];
|
||||
draft.global.admin_uidsText = draft.global.admin_uids.join(",");
|
||||
draft.music_monitor.targetsText = draft.music_monitor.targets.join(",");
|
||||
@@ -178,6 +203,7 @@ createApp({
|
||||
["redemption", "兑换码"],
|
||||
["songs", "点歌"],
|
||||
["media", "音乐/TTS"],
|
||||
["llm", "AI回复"],
|
||||
["system", "系统定时"],
|
||||
["logs", "日志"],
|
||||
["raw", "JSON"]
|
||||
@@ -219,6 +245,8 @@ createApp({
|
||||
giftTestNum: 1,
|
||||
giftTestValue: 0,
|
||||
giftTestResult: "",
|
||||
llmTestText: "主播在玩什么游戏呀?",
|
||||
llmTestResult: "",
|
||||
lastLoadedAt: "",
|
||||
songKeyword: "",
|
||||
songResults: [],
|
||||
@@ -288,6 +316,22 @@ createApp({
|
||||
lanAdminUrls() {
|
||||
return this.state.access_urls?.lan_admin || [];
|
||||
},
|
||||
llmIgnorePrefixesText: {
|
||||
get() {
|
||||
return (this.draft?.llm_reply?.ignore_prefixes || []).join(",");
|
||||
},
|
||||
set(value) {
|
||||
this.draft.llm_reply.ignore_prefixes = String(value || "").split(",").map((s) => s.trim()).filter(Boolean);
|
||||
}
|
||||
},
|
||||
llmBlacklistText: {
|
||||
get() {
|
||||
return (this.draft?.llm_reply?.blacklist_uids || []).join(",");
|
||||
},
|
||||
set(value) {
|
||||
this.draft.llm_reply.blacklist_uids = String(value || "").split(",").map((s) => s.trim()).filter(Boolean).map(Number).filter(Number.isFinite);
|
||||
}
|
||||
},
|
||||
songQueue() {
|
||||
return this.music.song_requests?.queue || this.music.requests || [];
|
||||
},
|
||||
@@ -1025,6 +1069,41 @@ createApp({
|
||||
this.giftTestResult = err.message || String(err);
|
||||
}
|
||||
},
|
||||
async saveLlmReply() {
|
||||
if (!this.draft?.llm_reply) return;
|
||||
this.saving = true;
|
||||
try {
|
||||
await adminApi("/api/admin/action/save_llm_reply_config", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(clone(this.draft.llm_reply))
|
||||
});
|
||||
this.notify("AI 回复配置已保存");
|
||||
} catch (err) {
|
||||
this.handleAuthLoss(err);
|
||||
this.notify(err.message || String(err), "error");
|
||||
} finally {
|
||||
this.saving = false;
|
||||
}
|
||||
},
|
||||
async testLlmReply() {
|
||||
if (!this.draft?.llm_reply) return;
|
||||
this.llmTestResult = "请求中...";
|
||||
try {
|
||||
// 先保存当前配置再测试,保证测的是面板上的最新值
|
||||
await adminApi("/api/admin/action/save_llm_reply_config", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(clone(this.draft.llm_reply))
|
||||
});
|
||||
const data = await adminApi("/api/admin/action/llm_reply_test", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ text: this.llmTestText, uname: "后台测试", broadcast: false })
|
||||
});
|
||||
this.llmTestResult = `${data.reply}(${data.provider}/${data.model},${data.duration_ms}ms)`;
|
||||
} catch (err) {
|
||||
this.handleAuthLoss(err);
|
||||
this.llmTestResult = err.message || String(err);
|
||||
}
|
||||
},
|
||||
async saveSystemSchedule() {
|
||||
if (!this.draft?.system) return;
|
||||
this.saving = true;
|
||||
@@ -1665,6 +1744,47 @@ createApp({
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="activeTab==='llm' && draft" class="form-grid">
|
||||
<div class="panel">
|
||||
<h2>AI 弹幕回复</h2>
|
||||
<label class="inline"><input v-model="draft.llm_reply.enabled" type="checkbox"> 启用 LLM 弹幕回复(触发指令的弹幕自动忽略,不会发给 AI)</label>
|
||||
<label>协议适配
|
||||
<select v-model="draft.llm_reply.provider">
|
||||
<option value="openai">OpenAI 兼容(DeepSeek/通义/Kimi/GLM/硅基流动等)</option>
|
||||
<option value="ollama">Ollama 原生</option>
|
||||
<option value="anthropic">Anthropic(Claude)</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>接口地址 base_url<input v-model="draft.llm_reply.base_url" placeholder="如 https://api.deepseek.com/v1 或 http://localhost:11434"></label>
|
||||
<label>API Key<input v-model="draft.llm_reply.api_key" type="password" placeholder="Ollama 本地可留空"></label>
|
||||
<label>模型<input v-model="draft.llm_reply.model" placeholder="如 deepseek-chat / qwen3 / claude-sonnet-4-5"></label>
|
||||
<label>人设(系统提示词)<textarea v-model="draft.llm_reply.system_prompt" rows="4"></textarea></label>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h2>回复与限流</h2>
|
||||
<label>回复最大字数<input v-model.number="draft.llm_reply.max_reply_chars" type="number" min="10" max="200"></label>
|
||||
<label>每分钟最多回复条数<input v-model.number="draft.llm_reply.replies_per_minute" type="number" min="0" max="60"></label>
|
||||
<label>同一观众冷却秒数(0=不限)<input v-model.number="draft.llm_reply.per_user_cooldown_sec" type="number" min="0" max="3600"></label>
|
||||
<label>上下文轮数<input v-model.number="draft.llm_reply.context_turns" type="number" min="0" max="20"></label>
|
||||
<label>最短触发字数<input v-model.number="draft.llm_reply.min_text_len" type="number" min="0" max="20"></label>
|
||||
<label>请求超时秒数<input v-model.number="draft.llm_reply.timeout_sec" type="number" min="3" max="120"></label>
|
||||
<label>温度<input v-model.number="draft.llm_reply.temperature" type="number" min="0" max="2" step="0.1"></label>
|
||||
<label class="inline"><input v-model="draft.llm_reply.reply_via_tts" type="checkbox"> 语音播报 AI 回复</label>
|
||||
<label class="inline"><input v-model="draft.llm_reply.reply_via_danmu" type="checkbox"> 弹幕发送 AI 回复</label>
|
||||
<label>忽略前缀(逗号分隔,这些开头的弹幕不回复)<input v-model="llmIgnorePrefixesText" placeholder="//"></label>
|
||||
<label>黑名单 UID(逗号分隔)<input v-model="llmBlacklistText" placeholder="123456,654321"></label>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<h2>测试</h2>
|
||||
<label>模拟弹幕<input v-model="llmTestText" placeholder="输入一条观众弹幕"></label>
|
||||
<div class="actions-row">
|
||||
<button class="primary" :disabled="saving" @click="saveLlmReply">保存配置</button>
|
||||
<button class="secondary" :disabled="!draft.llm_reply.enabled" @click="testLlmReply">发送测试弹幕并预览回复</button>
|
||||
</div>
|
||||
<div v-if="llmTestResult" class="llm-test-result">{{ llmTestResult }}</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="activeTab==='system' && draft" class="form-grid">
|
||||
<div class="panel">
|
||||
<h2>直播时间</h2>
|
||||
|
||||
Reference in New Issue
Block a user