Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
# 传统人格更新(其他类型)
UPDATE_TYPE_TRADITIONAL = "traditional"

# 内部黑话重学标记:保护已人工确认/完成的释义不被自动重学覆盖
PRESERVE_COMPLETED_JARGON_KEY = "_preserve_completed"

# 兼容性:旧的update_type值映射
# 用于数据库中已存在的旧记录
LEGACY_UPDATE_TYPE_MAPPING = {
Expand Down
91 changes: 71 additions & 20 deletions pages/dashboard/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,52 @@
else modal.removeAttribute("open");
}

function showConfirm(title, message, confirmText = t("actions.confirm", "确定")) {
return new Promise((resolve) => {
const modal = $("detail-modal");
if (!modal) {
showToast(message, "error");
resolve(false);
return;
}

if (modal.open && typeof modal.close === "function") modal.close();
const closeButton = $("modal-close");
let settled = false;
const done = (result) => {
if (settled) return;
settled = true;
modal.removeEventListener("close", onClose);
closeButton?.removeEventListener("click", onCloseClick);
if (typeof modal.close === "function" && modal.open) modal.close();
else modal.removeAttribute("open");
resolve(result);
};
const onClose = () => done(false);
const onCloseClick = () => done(false);

setText("modal-title", title);
setHtml("modal-body", `
<p class="confirm-message">${escapeHtml(message)}</p>
<div class="confirm-actions">
<button class="ghost-button" type="button" data-confirm-cancel>${escapeHtml(t("actions.cancel", "取消"))}</button>
<button class="solid-button" type="button" data-confirm-ok>${escapeHtml(confirmText)}</button>
</div>
`);
$("modal-body")?.querySelector("[data-confirm-cancel]")?.addEventListener("click", () => done(false), { once: true });
$("modal-body")?.querySelector("[data-confirm-ok]")?.addEventListener("click", () => done(true), { once: true });
modal.addEventListener("close", onClose, { once: true });
closeButton?.addEventListener("click", onCloseClick, { once: true });
if (typeof modal.showModal === "function") {
try {
modal.showModal();
return;
} catch (_) {}
}
modal.setAttribute("open", "");
});
}

function resolvePageFromHash() {
const raw = window.location.hash.replace(/^#\/?/, "");
return PAGE_META[raw] ? raw : "home";
Expand Down Expand Up @@ -672,25 +718,30 @@
]));
const items = ((data.list || {}).jargon_list || []);
pruneSelection(state.selectedJargon, items.map((item) => item.id));
const html = items.map((item) => `
<div class="table-row rich-row selectable-row">
${jargonCheckbox(item.id)}
<div>
<strong>${escapeHtml(item.term || item.content || `#${item.id}`)}</strong>
<small>${escapeHtml(item.meaning || item.definition || t("empty.definition", "暂无释义"))}</small>
</div>
<span>${escapeHtml(item.group_id || "global")}</span>
${pill(item.is_confirmed ? t("jargon.confirmed", "已确认") : t("jargon.pending", "待确认"), item.is_confirmed ? "ok" : "warn")}
${pill(item.is_global ? t("jargon.global", "全局") : t("jargon.local", "本地"))}
<div class="row-actions">
${button(t("actions.edit", "编辑"), `data-jargon-action="edit" data-id="${escapeAttr(item.id)}"`)}
const html = items.map((item) => {
const reviewActions = item.is_confirmed ? "" : `
${button(t("actions.confirm", "确认"), `data-jargon-action="approve" data-id="${escapeAttr(item.id)}"`)}
${button(t("actions.reject", "驳回"), `data-jargon-action="reject" data-id="${escapeAttr(item.id)}"`)}
${button(item.is_global ? t("actions.unsetGlobal", "取消全局") : t("actions.setGlobal", "设为全局"), `data-jargon-action="toggle_global" data-id="${escapeAttr(item.id)}"`)}
${button(t("actions.delete", "删除"), `data-jargon-action="delete" data-id="${escapeAttr(item.id)}"`, "danger-button")}
`;
return `
<div class="table-row rich-row selectable-row">
${jargonCheckbox(item.id)}
<div>
<strong>${escapeHtml(item.term || item.content || `#${item.id}`)}</strong>
<small>${escapeHtml(item.meaning || item.definition || t("empty.definition", "暂无释义"))}</small>
</div>
<span>${escapeHtml(item.group_id || "global")}</span>
${pill(item.is_confirmed ? t("jargon.confirmed", "已确认") : t("jargon.pending", "待确认"), item.is_confirmed ? "ok" : "warn")}
${pill(item.is_global ? t("jargon.global", "全局") : t("jargon.local", "本地"))}
<div class="row-actions">
${button(t("actions.edit", "编辑"), `data-jargon-action="edit" data-id="${escapeAttr(item.id)}"`)}
${reviewActions}
${button(item.is_global ? t("actions.unsetGlobal", "取消全局") : t("actions.setGlobal", "设为全局"), `data-jargon-action="toggle_global" data-id="${escapeAttr(item.id)}"`)}
${button(t("actions.delete", "删除"), `data-jargon-action="delete" data-id="${escapeAttr(item.id)}"`, "danger-button")}
</div>
</div>
</div>
`).join("");
`;
}).join("");
setHtml("jargon-list", html || empty(t("empty.jargon", "暂无黑话数据")));
state.pageData.lastJargonItems = items;
state.pageData.currentJargonData = data;
Expand Down Expand Up @@ -1095,11 +1146,11 @@
const typeText = { persona: t("reviews.personaUpdates", "人格更新"), style: t("reviews.expressionReviews", "表达审查"), jargon: t("reviews.jargonCandidates", "黑话候选") }[kind] || t("reviews.items", "审查项");
const actionText = action === "approve" ? t("actions.pass", "通过") : action === "reject" ? t("actions.reject", "拒绝") : t("actions.delete", "删除");
const scopeText = selectedReviewIds(kind).length ? t("selection.selected", "选中") : t("selection.currentPage", "当前页");
if (!window.confirm(t("reviews.confirmBatch", "确定批量{action}{scope} {count} 条{type}?")
if (!await showConfirm(t("reviews.batchConfirmTitle", "批量操作确认"), t("reviews.confirmBatch", "确定批量{action}{scope} {count} 条{type}?")
.replace("{action}", actionText)
.replace("{scope}", scopeText)
.replace("{count}", fmt(ids.length, 0))
.replace("{type}", typeText))) return;
.replace("{type}", typeText), actionText)) return;

const payload = {
action: action === "delete"
Expand Down Expand Up @@ -1305,7 +1356,7 @@
return;
}
const actionText = action === "approve" ? t("actions.confirm", "确认") : action === "reject" ? t("actions.rejectBack", "驳回") : t("actions.delete", "删除");
if (!window.confirm(t("jargon.confirmBatch", "确定批量{action}选中的 {count} 条黑话?").replace("{action}", actionText).replace("{count}", fmt(ids.length, 0)))) return;
if (!await showConfirm(t("jargon.batchConfirmTitle", "批量操作确认"), t("jargon.confirmBatch", "确定批量{action}选中的 {count} 条黑话?").replace("{action}", actionText).replace("{count}", fmt(ids.length, 0)), actionText)) return;

const result = await apiPost("jargon/action", {
action: action === "delete" ? "batch_delete" : "batch_review",
Expand Down Expand Up @@ -1367,7 +1418,7 @@
return;
}
const actionText = action === "approve" ? t("actions.approve", "批准") : action === "reject" ? t("actions.reject", "拒绝") : t("actions.delete", "删除");
if (!window.confirm(t("style.confirmBatch", "确定批量{action}选中的 {count} 条表达审查?").replace("{action}", actionText).replace("{count}", fmt(ids.length, 0)))) return;
if (!await showConfirm(t("style.batchConfirmTitle", "批量操作确认"), t("style.confirmBatch", "确定批量{action}选中的 {count} 条表达审查?").replace("{action}", actionText).replace("{count}", fmt(ids.length, 0)), actionText)) return;

const result = await apiPost("style/action", {
action: action === "delete" ? "batch_delete" : "batch_review",
Expand Down
12 changes: 12 additions & 0 deletions pages/dashboard/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1186,6 +1186,18 @@ textarea {
box-shadow: var(--shadow);
}

.confirm-message {
margin: 0 0 16px;
line-height: 1.6;
}

.confirm-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
flex-wrap: wrap;
}

@media (max-width: 1120px) {
.app-shell {
grid-template-columns: 1fr;
Expand Down
19 changes: 18 additions & 1 deletion services/core_learning/v2_learning_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

from astrbot.api import logger

from ...constants import PRESERVE_COMPLETED_JARGON_KEY
from ...config import PluginConfig
from ...core.interfaces import MessageData
from ...utils.cache_manager import get_cache_manager
Expand Down Expand Up @@ -733,6 +734,16 @@ async def _jargon_batch(group_id: str) -> None:
return
for candidate in candidates[:10]:
try:
existing = None
if db and hasattr(db, "get_jargon"):
existing = await db.get_jargon(group_id, candidate["term"])
if existing and existing.get("is_complete"):
logger.debug(
f"[V2Integration] Skip completed jargon "
f"'{candidate['term']}'"
)
continue

meaning = await llm.generate_response(
f"Explain the slang/jargon term "
f"'{candidate['term']}' in the context of an "
Expand All @@ -744,15 +755,21 @@ async def _jargon_batch(group_id: str) -> None:
and db
and hasattr(db, "save_or_update_jargon")
):
observed_count = max(
int(candidate.get("frequency") or 1),
int((existing or {}).get("count") or 0),
1,
)
await db.save_or_update_jargon(
group_id,
candidate["term"],
{
"meaning": meaning,
"raw_content": "[]",
"is_jargon": True,
"count": 1,
"count": observed_count,
"is_complete": True,
PRESERVE_COMPLETED_JARGON_KEY: True,
},
)
except Exception as exc:
Expand Down
92 changes: 80 additions & 12 deletions services/database/facades/jargon_facade.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@

from ._base import BaseFacade
try:
from ....constants import PRESERVE_COMPLETED_JARGON_KEY
from ....models.orm.jargon import Jargon
from ....utils.text_utils import truncate_for_db
except ImportError:
from constants import PRESERVE_COMPLETED_JARGON_KEY
from models.orm.jargon import Jargon
from utils.text_utils import truncate_for_db

Expand Down Expand Up @@ -728,20 +730,24 @@ async def _save_or_update_jargon_select_then_write(
now_ts = self._coerce_jargon_timestamp()

if record:
preserve_completed = (
self._preserve_completed_jargon(jargon_data)
and bool(record.is_complete)
)
# 更新已有记录
if 'meaning' in jargon_data:
if 'meaning' in jargon_data and not preserve_completed:
record.meaning = jargon_data['meaning']
if 'raw_content' in jargon_data:
if 'raw_content' in jargon_data and not preserve_completed:
record.raw_content = truncate_for_db(jargon_data['raw_content'])
if 'is_jargon' in jargon_data:
if 'is_jargon' in jargon_data and not preserve_completed:
record.is_jargon = jargon_data['is_jargon']
if 'count' in jargon_data:
if 'count' in jargon_data and not preserve_completed:
record.count = jargon_data['count']
if 'last_inference_count' in jargon_data:
record.last_inference_count = jargon_data['last_inference_count']
if 'is_complete' in jargon_data:
if 'is_complete' in jargon_data and not preserve_completed:
record.is_complete = jargon_data['is_complete']
if 'is_global' in jargon_data:
if 'is_global' in jargon_data and not preserve_completed:
record.is_global = jargon_data['is_global']
record.updated_at = now_ts

Expand Down Expand Up @@ -825,6 +831,27 @@ async def _save_or_update_jargon_sqlite(
def _coerce_jargon_timestamp() -> int:
return int(time.time())

@staticmethod
def _preserve_completed_jargon(jargon_data: Dict[str, Any]) -> bool:
return bool(jargon_data.get(PRESERVE_COMPLETED_JARGON_KEY))

@staticmethod
def _completed_preserving_value(new_value: Any, old_column: Any):
return case(
(Jargon.is_complete.is_(True), old_column),
else_=new_value,
)

@staticmethod
def _completed_preserving_count(new_value: Any):
return case(
(
Jargon.is_complete.is_(True),
Jargon.count,
),
else_=new_value,
)

@staticmethod
def _jargon_insert_values(
chat_id: str,
Expand Down Expand Up @@ -852,20 +879,61 @@ def _jargon_update_values(
now_ts: int,
) -> Dict[str, Any]:
update_values = {"updated_at": now_ts}
preserve_completed = JargonFacade._preserve_completed_jargon(jargon_data)
if "meaning" in jargon_data:
update_values["meaning"] = jargon_data["meaning"]
update_values["meaning"] = (
JargonFacade._completed_preserving_value(
jargon_data["meaning"],
Jargon.meaning,
)
if preserve_completed
else jargon_data["meaning"]
)
if "raw_content" in jargon_data:
update_values["raw_content"] = truncate_for_db(jargon_data["raw_content"])
raw_content = truncate_for_db(jargon_data["raw_content"])
update_values["raw_content"] = (
JargonFacade._completed_preserving_value(
raw_content,
Jargon.raw_content,
)
if preserve_completed
else raw_content
)
if "is_jargon" in jargon_data:
update_values["is_jargon"] = jargon_data["is_jargon"]
update_values["is_jargon"] = (
JargonFacade._completed_preserving_value(
jargon_data["is_jargon"],
Jargon.is_jargon,
)
if preserve_completed
else jargon_data["is_jargon"]
)
if "count" in jargon_data:
update_values["count"] = jargon_data["count"]
update_values["count"] = (
JargonFacade._completed_preserving_count(jargon_data["count"])
if preserve_completed
else jargon_data["count"]
)
if "last_inference_count" in jargon_data:
update_values["last_inference_count"] = jargon_data["last_inference_count"]
if "is_complete" in jargon_data:
update_values["is_complete"] = jargon_data["is_complete"]
update_values["is_complete"] = (
JargonFacade._completed_preserving_value(
jargon_data["is_complete"],
Jargon.is_complete,
)
if preserve_completed
else jargon_data["is_complete"]
)
if "is_global" in jargon_data:
update_values["is_global"] = jargon_data["is_global"]
update_values["is_global"] = (
JargonFacade._completed_preserving_value(
jargon_data["is_global"],
Jargon.is_global,
)
if preserve_completed
else jargon_data["is_global"]
)
return update_values

@staticmethod
Expand Down
Loading
Loading