Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
082f693d38 | ||
|
|
de3058f5af | ||
|
|
4f7adcd4c2 |
@@ -8,6 +8,7 @@
|
||||
# 用户收藏库(个人提示词与参考图,不进发布包)
|
||||
data/saved/
|
||||
data/rule_presets/
|
||||
data/greeting/
|
||||
|
||||
# 测试
|
||||
tests/
|
||||
|
||||
@@ -48,3 +48,4 @@ data/saved/
|
||||
|
||||
# AI 规则预设(个人提示词,可能包含私有写法)
|
||||
data/rule_presets/
|
||||
data/greeting/
|
||||
|
||||
@@ -1,5 +1,28 @@
|
||||
# 更新记录
|
||||
|
||||
## v2.2.4(2026-11-13)
|
||||
|
||||
### 右上角会说话了:问候与提醒
|
||||
|
||||
- 右上角多了一张独立的问候卡,和通知弹窗是两回事:它专门负责 AI 陪你聊天,有自己的入场、加载、展开、退场动画,不会打断通知。
|
||||
- 进入页面时按时间段(早上 / 中午 / 下午 / 晚上 / 深夜)说一句问候;AI 不可用时退回内置文案,离线也有话讲。
|
||||
- 四种提醒,各有开关:
|
||||
- **运行时间**:每隔 N 分钟提醒一次,标题栏显示当前在线多少分钟。
|
||||
- **图片生成**:每攒够 N 张夸一次,标题栏显示已生成多少张。
|
||||
- **生成失败时安慰一下**:五分之一概率触发;只告诉 AI「失败了」,不带报错内容和堆栈,隐私不外泄。
|
||||
- **静下来时招呼一声**:键盘鼠标安静 1 分钟就说句话;另有「随机提醒」每分钟掷一次骰子、1/N 概率说一句,N 可以自己填。
|
||||
- 问候可以单独配一套 AI(地址 / 密钥 / 模型 / 温度),也可以直接沿用助手设置里那套。它只在你自己开了对应开关时才会发请求。
|
||||
- 所有提示词集中在 `data/greeting_defaults.json`,想让 AI 说什么样的话可以直接改这份文件。
|
||||
- 问候相关设置收进设置面板的一个折叠块里,每项带问号说明。提示词统一要求「不要 emoji、可以用颜文字」。
|
||||
|
||||
### 收藏颜色在亮色主题下看得清了
|
||||
|
||||
- 收藏文件夹的淡色底按明暗主题分别取透明度:亮色背景原本就浅,之前 12% 的淡色几乎看不见。
|
||||
|
||||
### 设置面板加了「说明」页
|
||||
|
||||
- 「设置 → 双语提示词检查器 → 说明」是新增的第一项:设置页正在重构、近期功能位置可能还会调整,遇到问题欢迎去 GitHub 提 issue。
|
||||
|
||||
## v2.2.3(2026-11-11)
|
||||
|
||||
### 收藏改成了文件夹式的库
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# ComfyUI 双语提示词管理器 v2.2.3
|
||||
# ComfyUI 双语提示词管理器 v2.2.4
|
||||
|
||||
> ComfyUI 双语提示词节点:把英文提示词拆成可点击的标签,边写边看中文解释、调顺序、试效果。
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
- [收藏](#收藏)
|
||||
- [侧边栏管理面板](#侧边栏管理面板)
|
||||
- [操作反馈](#操作反馈)
|
||||
- [右上角问候与提醒](#右上角问候与提醒)
|
||||
- [界面导览](#界面导览)
|
||||
- [安装](#安装)
|
||||
- [快速上手](#快速上手)
|
||||
@@ -136,7 +137,7 @@
|
||||
|
||||
「标签管理」默认跟着你点开的节点走:在画布上点一个「双语提示词检查器」节点,侧边栏就切到它。不想要这个行为,关掉「设置 → 通用」里的「点击节点时跟随切换」,之后只能在面板顶部的节点下拉里手动选。
|
||||
|
||||
助手设置不在这个面板里:它在 ComfyUI 的「设置」面板中,「设置 → 双语提示词检查器」下有两项 ——「通用」和「助手设置」,表单都直接铺在设置面板右侧。
|
||||
助手设置不在这个面板里:它在 ComfyUI 的「设置」面板中,「设置 → 双语提示词检查器」下有三项 ——「说明」「通用」和「助手设置」,表单都直接铺在设置面板右侧。「说明」是固定的一页提示(设置页在重构、位置可能调整),问候相关的开关在「助手设置」的折叠块里。
|
||||
|
||||
### 操作反馈
|
||||
|
||||
@@ -156,6 +157,16 @@
|
||||
- **右上角弹窗**:错误、翻译被拒、批量翻译的进度与汇总、重复标签的定位走这里,红色系、停留更久。同类操作会就地更新同一张卡(比如连点重复胶囊是在同一张卡里翻「第 2 / 3 处」),不会一条接一条地冒;批量翻译期间只留一张总进度卡。
|
||||
- 两个都嫌打扰,可以在「设置 → 通用」里分别关掉「右上角通知弹窗」和「右下角提示条」,关掉后提示退回节点状态栏。
|
||||
|
||||
### 右上角问候与提醒
|
||||
|
||||
右上角还有一张独立的问候卡,负责 AI 陪你聊天(和通知弹窗互不干扰):
|
||||
|
||||
- **进页问候**:按时间段(早上 / 中午 / 下午 / 晚上 / 深夜)说一句;AI 不可用时退回内置文案,离线也有话讲。
|
||||
- **四种提醒**:「运行时间」每 N 分钟提醒一次(标题显示在线多少分钟);「图片生成」每攒够 N 张夸一次(标题显示已生成多少张);「生成失败时安慰一下」五分之一概率触发——只告诉 AI 失败了,不带报错内容和堆栈;「静下来时招呼一声」在键盘鼠标安静 1 分钟后说句话。另有「随机提醒」每分钟 1/N 概率说一句,N 可填。
|
||||
- **独立的 AI 配置**:地址、密钥、模型、温度可以单独配一套,也可以直接沿用助手设置那套。只在你开了对应开关时才发请求。
|
||||
- **提示词可改**:全部集中在 `data/greeting_defaults.json`,想让它说什么样的话直接改这份文件;统一「不要 emoji,可以用颜文字」。
|
||||
- 各项开关收在「设置 → 助手设置」的问候折叠块里,每项带问号说明。
|
||||
|
||||
---
|
||||
|
||||
## 界面导览
|
||||
@@ -318,6 +329,7 @@ git clone https://gitee.com/MengShengZhiJie/ComfyUI-Bilingual-Prompt-Inspector.g
|
||||
|
||||
- **默认不联网**:翻译服务的默认选项是纯词库,完全离线。
|
||||
- **只在你主动使用某个服务时才发请求**:调用百度或 AI 时,发送的是提示词文本与你的指令;密钥只存在后端本地,不回传浏览器、不写入工作流。
|
||||
- **问候提醒是唯一会自动发请求的功能,且默认全关**:开了对应开关后按定时 / 计数触发,发出去的只有那份提示词要求的上下文(时间段、分钟数、张数这类),「生成失败安慰」只告诉 AI 失败了、不带报错内容和堆栈;不开不发。
|
||||
- **大型词库按需加载**:不安装就不加载,装了也是本地文件查询。
|
||||
- **收藏只存在本机**:写在插件目录的 `data/saved/` 下,插件不做任何上传。
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"prompt": "你是 ComfyUI 里「双语提示词检查器」插件的小助手,住在界面右上角。\n用户随时会点你一下,你就说一句贴心又有点用的话:可以夸他刚才的提示词写得讲究,也可以提醒他歇一会儿、顺手看看显存。\n格式:第一行是标题,不超过 12 个字;后面最多两行正文。像朋友说话,别喊口号。不要 emoji,可以用颜文字,一个就够。",
|
||||
"time_prompt": "你是 ComfyUI 插件「双语提示词检查器」里的小提醒员,住在界面右上角。\n用户已经在电脑前坐了一阵子,请温柔又俏皮地劝他起身动一动、喝口水、看看远处。\n可以拿「又跑了几张图」「盯着进度条发呆」这类场景打趣,但别说教、别念健康经。\n格式:只写 2 到 3 句正文,不要标题。不要 emoji,结尾可以带一个颜文字。",
|
||||
"image_prompt": "你是 ComfyUI 插件「双语提示词检查器」里的小捧场王,住在界面右上角。\n用户刚又生成了一批图片,请用人情味的方式夸他:夸他调参数的手感、夸他蹲到的光影,或者调侃他终于把那张难产的图跑出来了。\n别空喊「太棒了」「继续加油」,最好带一点具体画面感。\n格式:只写 2 到 3 句正文,不要标题。不要 emoji,可以用颜文字,一个就够。",
|
||||
"welcome_prompt": "你是 ComfyUI 插件「双语提示词检查器」的迎宾小助手,住在界面右上角。\n用户刚打开页面,请按当前时间打一声招呼:早上说早安,中午关心他吃饭没有、天气热不热,下午提提神,傍晚让他早点收工,深夜劝他去睡。\n语气甜一点、像熟人说话,不要 emoji,可以用颜文字,一个就够。\n格式:第一行是标题(不超过 12 字),后面最多两行正文,不要写「欢迎使用」这类客套话。",
|
||||
"welcome": {
|
||||
"morning": {
|
||||
"title": "早上好呀",
|
||||
"body": "新的一天,先给自己倒杯热的东西,再开工作流也不迟。\n今天的画面慢慢来就好,反正灵感不赶时间。(。・ω・。)"
|
||||
},
|
||||
"noon": {
|
||||
"title": "吃了吗",
|
||||
"body": "别盯着进度条忘了吃饭,胃比队列更需要照顾。\n要是天气热,就把空调开低一点,脑子凉快了才转得动。(๑•̀ㅂ•́)و✧"
|
||||
},
|
||||
"afternoon": {
|
||||
"title": "下午好",
|
||||
"body": "这个点最容易犯困,起来走两步、伸个懒腰。\n回来再看那些参数,思路会顺很多。ヽ(・∀・)ノ"
|
||||
},
|
||||
"evening": {
|
||||
"title": "早点休息",
|
||||
"body": "今天跑了不少图吧,剩下的明天再调也来得及。\n眼睛和灵感都需要充电,别硬撑。(´。• ᵕ •。`)"
|
||||
},
|
||||
"night": {
|
||||
"title": "还醒着呀",
|
||||
"body": "夜深了,能交给明天的就交给明天。\n睡前记得关掉显示器,让它也歇一晚。(-_-) zzZ"
|
||||
}
|
||||
},
|
||||
"error_prompt": "你是 ComfyUI 插件「双语提示词检查器」的安慰小助手,住在界面右上角。\n用户刚才那一次生成失败了,他现在可能有点烦躁或者沮丧。请安慰他:可以说失败很正常、参数再调调就好、先去喝口水,或者拿画图这件事本身打趣。\n不要分析原因,不要提任何技术细节,不要说「检查你的代码」「看看报错」这类话。\n格式:第一行是标题(不超过 12 字),后面最多两行正文,语气温和,不要 emoji,可以用颜文字。",
|
||||
"idle_prompt": "你是 ComfyUI 插件「双语提示词检查器」的小陪伴,住在界面右上角。\n用户已经两分钟没碰键盘和鼠标了,可能在想事情、在等出图,或者只是走神了。\n请轻轻招呼一声:可以问他在忙什么、提醒他别发太久呆,或者干脆说句轻松的话。\n别问「你在吗」这种客服腔,也不要说教。\n格式:第一行是标题(不超过 12 字),后面最多两行正文,语气自然,不要 emoji,可以用颜文字。"
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
"""问候/提醒的配置:提示词 + 可选的独立 AI(不填就用助手设置里的那套)。
|
||||
|
||||
单独一个文件而不是塞进 assistant_store:问候是独立功能,以后还要加运行时长、
|
||||
生成张数这些项目,各自演化更省心。密钥只落本地文件,接口只回「是否已配」。
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
|
||||
MAX_PROMPT_CHARS = 8000
|
||||
MAX_FIELD_CHARS = 500
|
||||
DEFAULT_TEMPERATURE = 0.8
|
||||
|
||||
# 默认文案放在 data/greeting_defaults.json:不进代码、可读可改,也随发布包一起走。
|
||||
# Python 这边只负责读,读不到就当空字符串(AI 会退化成没有系统提示词的样子)。
|
||||
DEFAULTS_PATH = Path(__file__).resolve().parent / "data" / "greeting_defaults.json"
|
||||
|
||||
|
||||
def _load_defaults():
|
||||
try:
|
||||
payload = json.loads(DEFAULTS_PATH.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
payload = {}
|
||||
if not isinstance(payload, dict):
|
||||
payload = {}
|
||||
welcome = payload.get("welcome")
|
||||
return {
|
||||
"prompt": str(payload.get("prompt") or ""),
|
||||
"time_prompt": str(payload.get("time_prompt") or ""),
|
||||
"image_prompt": str(payload.get("image_prompt") or ""),
|
||||
"welcome_prompt": str(payload.get("welcome_prompt") or ""),
|
||||
"error_prompt": str(payload.get("error_prompt") or ""),
|
||||
"idle_prompt": str(payload.get("idle_prompt") or ""),
|
||||
# 进入时的问候:固定文案,按时段分,不走 AI、也不给 UI 编辑
|
||||
"welcome": welcome if isinstance(welcome, dict) else {},
|
||||
}
|
||||
|
||||
|
||||
_DEFAULTS = _load_defaults()
|
||||
DEFAULT_PROMPT = _DEFAULTS["prompt"]
|
||||
DEFAULT_TIME_PROMPT = _DEFAULTS["time_prompt"]
|
||||
DEFAULT_IMAGE_PROMPT = _DEFAULTS["image_prompt"]
|
||||
DEFAULT_WELCOME_PROMPT = _DEFAULTS["welcome_prompt"]
|
||||
DEFAULT_ERROR_PROMPT = _DEFAULTS["error_prompt"]
|
||||
DEFAULT_IDLE_PROMPT = _DEFAULTS["idle_prompt"]
|
||||
|
||||
|
||||
def _clamp_temperature(value):
|
||||
"""温度只收 0–2,写歪了就退回默认值。"""
|
||||
try:
|
||||
number = float(value)
|
||||
except (TypeError, ValueError):
|
||||
return DEFAULT_TEMPERATURE
|
||||
return max(0.0, min(2.0, round(number, 2)))
|
||||
|
||||
|
||||
def _clean_flag(value):
|
||||
"""只有一个开关的规则(失败安慰 / 静默招呼)。"""
|
||||
raw = value if isinstance(value, dict) else {}
|
||||
return {"enabled": raw.get("enabled") is True}
|
||||
|
||||
|
||||
def _clean_rule(value, number_key, default_number, low, high):
|
||||
"""一条场景规则:开关 + 一个数值 + 自己的提示词。"""
|
||||
raw = value if isinstance(value, dict) else {}
|
||||
try:
|
||||
number = int(float(raw.get(number_key, default_number)))
|
||||
except (TypeError, ValueError):
|
||||
number = default_number
|
||||
prompt = raw.get("prompt")
|
||||
return {
|
||||
"enabled": raw.get("enabled") is True,
|
||||
number_key: max(low, min(high, number)),
|
||||
"prompt": prompt.strip()[:MAX_PROMPT_CHARS] if isinstance(prompt, str) else "",
|
||||
}
|
||||
|
||||
|
||||
def _default_config_path():
|
||||
return Path(__file__).resolve().parent / "data" / "greeting" / "config.json"
|
||||
|
||||
|
||||
class GreetingStore:
|
||||
def __init__(self, config_path=None):
|
||||
self.path = Path(config_path or _default_config_path())
|
||||
self._cache = None
|
||||
|
||||
def _load(self):
|
||||
if self._cache is not None:
|
||||
return self._cache
|
||||
try:
|
||||
payload = json.loads(self.path.read_text(encoding="utf-8"))
|
||||
except (OSError, json.JSONDecodeError):
|
||||
payload = {}
|
||||
if not isinstance(payload, dict):
|
||||
payload = {}
|
||||
prompt = payload.get("prompt")
|
||||
self._cache = {
|
||||
"prompt": prompt if isinstance(prompt, str) and prompt.strip() else DEFAULT_PROMPT,
|
||||
"use_assistant_ai": payload.get("use_assistant_ai", True) is not False,
|
||||
"api_base_url": str(payload.get("api_base_url") or "")[:MAX_FIELD_CHARS],
|
||||
"model": str(payload.get("model") or "")[:MAX_FIELD_CHARS],
|
||||
"api_key": str(payload.get("api_key") or "")[:MAX_FIELD_CHARS],
|
||||
"temperature": _clamp_temperature(payload.get("temperature", DEFAULT_TEMPERATURE)),
|
||||
"error_rule": _clean_flag(payload.get("error_rule")),
|
||||
"random_rule": _clean_rule(payload.get("random_rule"), "chance", 10, 2, 1440),
|
||||
"idle_rule": _clean_flag(payload.get("idle_rule")),
|
||||
"time_rule": _clean_rule(payload.get("time_rule"), "minutes", 30, 1, 480),
|
||||
"image_rule": _clean_rule(payload.get("image_rule"), "count", 10, 1, 500),
|
||||
}
|
||||
return self._cache
|
||||
|
||||
def config(self):
|
||||
"""给前端看的版本:不回密钥,只回有没有配过。"""
|
||||
data = self._load()
|
||||
return {
|
||||
"prompt": data["prompt"],
|
||||
"default_prompt": DEFAULT_PROMPT,
|
||||
"default_time_prompt": DEFAULT_TIME_PROMPT,
|
||||
"default_image_prompt": DEFAULT_IMAGE_PROMPT,
|
||||
"welcome": _DEFAULTS["welcome"],
|
||||
"use_assistant_ai": data["use_assistant_ai"],
|
||||
"api_base_url": data["api_base_url"],
|
||||
"model": data["model"],
|
||||
"api_key_configured": bool(data["api_key"]),
|
||||
"temperature": data["temperature"],
|
||||
"error_rule": data["error_rule"],
|
||||
"random_rule": data["random_rule"],
|
||||
"idle_rule": data["idle_rule"],
|
||||
"time_rule": data["time_rule"],
|
||||
"image_rule": data["image_rule"],
|
||||
}
|
||||
|
||||
def prompt(self):
|
||||
return self._load()["prompt"]
|
||||
|
||||
def prompt_for(self, scene=""):
|
||||
data = self._load()
|
||||
if scene == "time":
|
||||
return data["time_rule"]["prompt"] or DEFAULT_TIME_PROMPT
|
||||
if scene == "images":
|
||||
return data["image_rule"]["prompt"] or DEFAULT_IMAGE_PROMPT
|
||||
if scene == "welcome":
|
||||
return DEFAULT_WELCOME_PROMPT or DEFAULT_PROMPT
|
||||
if scene == "error":
|
||||
return DEFAULT_ERROR_PROMPT or DEFAULT_PROMPT
|
||||
if scene == "idle":
|
||||
return DEFAULT_IDLE_PROMPT or DEFAULT_PROMPT
|
||||
return data["prompt"]
|
||||
|
||||
def temperature(self):
|
||||
return self._load()["temperature"]
|
||||
|
||||
def save(self, payload):
|
||||
if not isinstance(payload, dict):
|
||||
raise ValueError("请求内容格式不正确")
|
||||
data = dict(self._load())
|
||||
|
||||
if "prompt" in payload:
|
||||
text = str(payload.get("prompt") or "").strip()[:MAX_PROMPT_CHARS]
|
||||
if not text:
|
||||
raise ValueError("提示词不能为空")
|
||||
data["prompt"] = text
|
||||
if "use_assistant_ai" in payload:
|
||||
data["use_assistant_ai"] = payload.get("use_assistant_ai") is not False
|
||||
for key in ("api_base_url", "model"):
|
||||
if key in payload:
|
||||
data[key] = str(payload.get(key) or "").strip()[:MAX_FIELD_CHARS]
|
||||
# 密钥留空 = 保持原来那份,避免每次改别的字段都要重敲
|
||||
if payload.get("clear_api_key"):
|
||||
data["api_key"] = ""
|
||||
elif payload.get("api_key"):
|
||||
data["api_key"] = str(payload["api_key"]).strip()[:MAX_FIELD_CHARS]
|
||||
if "temperature" in payload:
|
||||
data["temperature"] = _clamp_temperature(payload.get("temperature"))
|
||||
if isinstance(payload.get("error_rule"), dict):
|
||||
data["error_rule"] = _clean_flag({**data["error_rule"], **payload["error_rule"]})
|
||||
if isinstance(payload.get("random_rule"), dict):
|
||||
data["random_rule"] = _clean_rule(
|
||||
{**data["random_rule"], **payload["random_rule"]}, "chance", 10, 2, 1440
|
||||
)
|
||||
if isinstance(payload.get("idle_rule"), dict):
|
||||
data["idle_rule"] = _clean_flag({**data["idle_rule"], **payload["idle_rule"]})
|
||||
if isinstance(payload.get("time_rule"), dict):
|
||||
merged = {**data["time_rule"], **payload["time_rule"]}
|
||||
data["time_rule"] = _clean_rule(merged, "minutes", 30, 1, 480)
|
||||
if isinstance(payload.get("image_rule"), dict):
|
||||
merged = {**data["image_rule"], **payload["image_rule"]}
|
||||
data["image_rule"] = _clean_rule(merged, "count", 10, 1, 500)
|
||||
|
||||
self._write(data)
|
||||
return self.config()
|
||||
|
||||
def _write(self, data):
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
fd, temp_name = tempfile.mkstemp(prefix="greeting_", suffix=".json", dir=self.path.parent)
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
||||
json.dump(data, handle, ensure_ascii=False, indent=2)
|
||||
handle.write("\n")
|
||||
os.replace(temp_name, self.path)
|
||||
finally:
|
||||
if os.path.exists(temp_name):
|
||||
os.unlink(temp_name)
|
||||
self._cache = data
|
||||
|
||||
# 走问候自己的 AI 时返回覆盖值;用助手那套则返回 None。
|
||||
# 配不全直接报错,由调用方转成用户可读的提示。
|
||||
def ai_override(self):
|
||||
data = self._load()
|
||||
if data["use_assistant_ai"]:
|
||||
return None
|
||||
base = data["api_base_url"].strip()
|
||||
model = data["model"].strip()
|
||||
key = data["api_key"].strip()
|
||||
missing = [name for name, value in (("API 地址", base), ("模型名称", model), ("密钥", key)) if not value]
|
||||
if missing:
|
||||
raise ValueError(
|
||||
f"问候的 AI 还没配全(缺 {('、'.join(missing))}):请在「问候与提醒 → 问候的 AI」里补齐,"
|
||||
"或打开「使用助手设置里的 AI」"
|
||||
)
|
||||
return {"openai_base_url": base, "openai_model": model, "openai_api_key": key}
|
||||
@@ -68,7 +68,7 @@ import {
|
||||
} from "./bpi_shared.js";
|
||||
|
||||
const NODE_NAME = "BilingualPromptInspector";
|
||||
const EXTENSION_VERSION = "v2.2.3";
|
||||
const EXTENSION_VERSION = "v2.2.4";
|
||||
const PROJECT_URL = "https://github.com/mengshengzhijie/ComfyUI-Bilingual-Prompt-Inspector";
|
||||
// 「适应高度」打开后面板能长到的上限。ComfyUI 按 min/max 分配空间,max 必须明显高于 min,
|
||||
// 否则手动拉大节点多出来的高度全变成空白(distributeSpace 只会在 min..max 之间给)。
|
||||
|
||||
+1410
File diff suppressed because it is too large
Load Diff
+19
-4
@@ -74,6 +74,20 @@ async function deleteItem(id) {
|
||||
return readJson(await bpiFetch(`${API_ROOT}/${encodeURIComponent(id)}`, { method: "DELETE" }));
|
||||
}
|
||||
|
||||
// 淡色底在深/浅主题下的观感差很多:亮色背景原本就浅,12% 几乎看不见
|
||||
function isDarkTheme() {
|
||||
const root = document.documentElement;
|
||||
const body = document.body;
|
||||
if (root.getAttribute("data-bpi-theme") === "dark") return true;
|
||||
if (root.getAttribute("data-bpi-theme") === "light") return false;
|
||||
if (body.classList.contains("dark-theme")) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function tintAlpha() {
|
||||
return isDarkTheme() ? { folder: "1f", child: "0d" } : { folder: "33", child: "1a" };
|
||||
}
|
||||
|
||||
function imageUrl(item) {
|
||||
return `${API_ROOT}/${encodeURIComponent(item.id)}/image?v=${item.updated_at ?? item.created_at ?? 0}`;
|
||||
}
|
||||
@@ -340,13 +354,14 @@ export function createLibraryView({ mount, onLoad, onToast, confirmDelete }) {
|
||||
const row = element("div", `bpi-lib-row ${isFolder ? "bpi-lib-folder" : "bpi-lib-item"}`);
|
||||
row.dataset.id = item.id;
|
||||
row.draggable = true;
|
||||
// 文件夹的淡色:存的是 #rrggbb,这里补成 12% 透明度铺一层,跟节点里提示词区那种底色一样。
|
||||
// 文件夹里的条目继承同一个颜色,但更淡一档(5%),一眼能看出它属于哪个组。
|
||||
// 文件夹的淡色:存的是 #rrggbb,这里按当前主题补透明度铺一层。
|
||||
// 文件夹里的条目继承同一个颜色,但更淡一档,一眼能看出它属于哪个组。
|
||||
const tint = tintAlpha();
|
||||
if (isFolder && item.color) {
|
||||
row.style.backgroundColor = `${item.color}1f`;
|
||||
row.style.backgroundColor = `${item.color}${tint.folder}`;
|
||||
} else if (!isFolder && item.parent) {
|
||||
const parentColor = byId(item.parent)?.color;
|
||||
if (parentColor) row.style.backgroundColor = `${parentColor}0d`;
|
||||
if (parentColor) row.style.backgroundColor = `${parentColor}${tint.child}`;
|
||||
}
|
||||
|
||||
const main = element("div", "bpi-lib-main");
|
||||
|
||||
+8
-2
@@ -170,11 +170,11 @@ function field(form, labelText, name, value = "", placeholder = "") {
|
||||
return input;
|
||||
}
|
||||
|
||||
async function runInspectorAssistant(action, text, instruction = "") {
|
||||
async function runInspectorAssistant(action, text, instruction = "", scene = "") {
|
||||
const response = await bpiFetch(`${API_ROOT}/assistant/run`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ action, text, instruction }),
|
||||
body: JSON.stringify({ action, text, instruction, scene }),
|
||||
});
|
||||
let result;
|
||||
try {
|
||||
@@ -1316,9 +1316,15 @@ function injectBpiStyles() {
|
||||
.bpi-model-line input{flex:1 1 auto;min-width:0}
|
||||
.bpi-model-line .bpi-button{flex:none;white-space:nowrap;background-color:var(--bpm-accent);border-color:var(--bpm-accent-border);color:#fff}
|
||||
.bpi-model-line .bpi-button:hover{background-color:var(--bpm-accent-hover);border-color:var(--bpm-accent-border);color:#fff}
|
||||
.bpi-switch-line{display:flex;align-items:center;gap:10px;flex-wrap:wrap}
|
||||
.bpi-greet-settings{display:flex;flex-direction:column;gap:6px;padding:4px 0 2px}
|
||||
.bpi-greet-settings .bpi-greet-prompt{width:100%;box-sizing:border-box;min-height:96px;padding:7px 9px;border:1px solid var(--bpi-field-border);border-radius:5px;background-color:var(--bpi-input-bg);color:var(--bpi-input-text);font:12px/1.6 Arial,sans-serif;resize:vertical}
|
||||
.bpi-greet-settings .bpi-modal-actions{display:flex;align-items:center;gap:8px}
|
||||
.bpi-rule-label{position:relative;display:flex;align-self:start;min-width:0;padding-bottom:16px}
|
||||
.bpi-rule-orphan{position:absolute;left:0;bottom:0;display:none;width:15px;height:15px;border:1px solid var(--bpi-status-busy);border-radius:50%;background-color:var(--bpi-surface-2);color:var(--bpi-status-busy);font-size:10px;line-height:13px;text-align:center;cursor:help;user-select:none}
|
||||
.bpi-rule-orphan.bpi-rule-orphan-on{display:block}
|
||||
/* 折叠开关的激活态:展开时按钮变成强调色,一眼看出「再点一下能收起」 */
|
||||
.bpi-mini.bpi-toggle-on{background-color:var(--bpm-accent);border-color:var(--bpm-accent-border,var(--bpm-accent));color:#fff}
|
||||
.bpi-rule-presets{display:flex;gap:6px;align-items:center;margin-left:auto;flex-wrap:nowrap}
|
||||
.bpi-rule-presets .bpi-select{max-width:150px;padding:2px 6px;font-size:12px}
|
||||
.bpi-rule-presets .bpi-mini{flex:none;white-space:nowrap;padding:2px 9px;font-size:11px;line-height:1.5}
|
||||
|
||||
+170
-34
@@ -49,6 +49,20 @@ import {
|
||||
testAssistantConnection,
|
||||
} from "./bpi_shared.js";
|
||||
import { createLibraryView, openLibraryItemDialog } from "./bpi_library.js";
|
||||
import {
|
||||
createGreetingAiPanel,
|
||||
createGreetingAutoPanel,
|
||||
createGreetingSettingsPanel,
|
||||
generateGreeting,
|
||||
greetingEnabled,
|
||||
loadGreetingConfig,
|
||||
restartGreetingAutoTimer,
|
||||
setGreetingRule,
|
||||
setWelcomeEnabled,
|
||||
welcomeEnabled,
|
||||
setGreetingEnabled,
|
||||
startGreeting,
|
||||
} from "./bpi_greeting.js";
|
||||
|
||||
const SECTIONS = [
|
||||
["tag-manager", "Tag Manager"],
|
||||
@@ -643,6 +657,27 @@ function appearanceControl() {
|
||||
return appearanceSettingsNode;
|
||||
}
|
||||
|
||||
// 「说明」单独一页,挂在插件分类下的第一项,不属于通用或助手设置
|
||||
let noticeSettingsNode = null;
|
||||
|
||||
function noticeControl() {
|
||||
if (noticeSettingsNode) {
|
||||
alignWhenMounted(noticeSettingsNode);
|
||||
return noticeSettingsNode;
|
||||
}
|
||||
noticeSettingsNode = element("div", "bpi-settings-form");
|
||||
const text = element("div", "", "说明:设置页正在重构,近期加入了不少功能,部分位置可能还会调整。");
|
||||
text.style.lineHeight = "1.9";
|
||||
text.style.fontSize = "13px";
|
||||
const second = element("div", "", "使用中遇到问题或有建议,欢迎在 GitHub 提 issue;给你带来的不便,还请谅解。");
|
||||
second.style.marginTop = "6px";
|
||||
second.style.lineHeight = "1.9";
|
||||
second.style.fontSize = "13px";
|
||||
noticeSettingsNode.append(text, second);
|
||||
alignWhenMounted(noticeSettingsNode);
|
||||
return noticeSettingsNode;
|
||||
}
|
||||
|
||||
async function buildAppearanceForm(host, loading) {
|
||||
let config;
|
||||
try {
|
||||
@@ -699,7 +734,100 @@ async function buildAppearanceForm(host, loading) {
|
||||
followToggle.addEventListener("change", () => setFollowSelectionEnabled(followToggle.checked));
|
||||
const followNote = element("div", "bpi-config-note", "开:在画布上点一个「双语提示词检查器」节点,侧边栏的「标签管理」就切到它。关:只能在「标签管理」的节点下拉里手动选。");
|
||||
followNote.style.gridColumn = "1 / -1";
|
||||
form.append(languageLabel, languageSelect, languageNote, followRow, followNote, status);
|
||||
// 问候/提醒:独立的右上角通知,自己的开关;右边的「测试」用来立刻看效果(以后会删)
|
||||
const greetLine = element("div", "bpi-switch-line");
|
||||
greetLine.style.gridColumn = "1 / -1";
|
||||
const greetRow = element("label", "bpi-switch-row bpi-switch-accent");
|
||||
const greetToggle = element("input", "bpi-switch");
|
||||
greetToggle.type = "checkbox";
|
||||
greetToggle.checked = greetingEnabled();
|
||||
const greetTrack = element("span", "bpi-switch-track");
|
||||
greetTrack.appendChild(element("span", "bpi-switch-thumb"));
|
||||
greetRow.append(greetToggle, greetTrack, element("span", "bpi-switch-text", "显示问候与提醒"));
|
||||
greetToggle.addEventListener("change", () => setGreetingEnabled(greetToggle.checked));
|
||||
// 规则直接内联展开:弹窗在这里会被 ComfyUI 的设置面板压住,输入框点不动
|
||||
// 每次进入时问候:固定文案,按时段挑一段,不走 AI
|
||||
const welcomeRow = element("label", "bpi-switch-row bpi-switch-accent");
|
||||
const welcomeToggle = element("input", "bpi-switch");
|
||||
welcomeToggle.type = "checkbox";
|
||||
welcomeToggle.checked = welcomeEnabled();
|
||||
const welcomeTrack = element("span", "bpi-switch-track");
|
||||
welcomeTrack.appendChild(element("span", "bpi-switch-thumb"));
|
||||
const welcomeHelp = element("span", "bpi-help", "?");
|
||||
welcomeHelp.appendChild(element("span", "bpi-help-pop",
|
||||
"打开页面时请 AI 按当前时间说一句问候(早上/中午/下午/晚上/深夜各不相同)。AI 不可用时退回内置文案;两者的提示词都在 data/greeting_defaults.json,界面上不提供编辑。"));
|
||||
welcomeRow.append(welcomeToggle, welcomeTrack, element("span", "bpi-switch-text", "每次进入时问候"), welcomeHelp);
|
||||
welcomeToggle.addEventListener("change", () => setWelcomeEnabled(welcomeToggle.checked));
|
||||
|
||||
// 失败安慰 / 静默招呼:开关一点就存,不用进去点保存
|
||||
const makeSideSwitch = (text, help, rule, extraNodes = []) => {
|
||||
const row = element("label", "bpi-switch-row bpi-switch-accent");
|
||||
const toggle = element("input", "bpi-switch");
|
||||
toggle.type = "checkbox";
|
||||
const track = element("span", "bpi-switch-track");
|
||||
track.appendChild(element("span", "bpi-switch-thumb"));
|
||||
const mark = element("span", "bpi-help", "?");
|
||||
mark.appendChild(element("span", "bpi-help-pop", help));
|
||||
row.append(toggle, track, element("span", "bpi-switch-text", text), mark, ...extraNodes);
|
||||
toggle.addEventListener("change", async () => {
|
||||
try {
|
||||
await setGreetingRule(rule, toggle.checked);
|
||||
restartGreetingAutoTimer();
|
||||
} catch (failure) {
|
||||
toggle.checked = !toggle.checked;
|
||||
notify(failure.message, "error");
|
||||
}
|
||||
});
|
||||
return { row, toggle };
|
||||
};
|
||||
const errorSwitch = makeSideSwitch(
|
||||
"生成失败时安慰一下",
|
||||
"生成失败时有 1/5 的概率让 AI 安慰你一句。只告诉它「刚才失败了」,不会把报错内容和堆栈发过去。提示词文件:data/greeting_defaults.json(error_prompt 段)。",
|
||||
"error_rule",
|
||||
);
|
||||
const idleSwitch = makeSideSwitch(
|
||||
"静下来时招呼一声",
|
||||
"键盘鼠标安静超过 1 分钟,就让 AI 说句话。中间只要敲键盘、点鼠标或滚动滚轮就重新计时。提示词文件:data/greeting_defaults.json(idle_prompt 段)。",
|
||||
"idle_rule",
|
||||
);
|
||||
|
||||
const greetPanel = createGreetingSettingsPanel();
|
||||
const greetSettings = button("设置", () => {
|
||||
greetPanel.hidden = !greetPanel.hidden;
|
||||
// 展开时按钮变蓝,提示「再点一下收起」
|
||||
greetSettings.classList.toggle("bpi-toggle-on", !greetPanel.hidden);
|
||||
}, "bpi-mini");
|
||||
// 请求:按「设置」里写的要求现问一次 AI,结果直接弹成右上角那张卡
|
||||
const greetRequest = button("请求", async () => {
|
||||
const card = startGreeting({ title: "正在生成" });
|
||||
try {
|
||||
const { title, body } = await generateGreeting();
|
||||
card.setTitle(title);
|
||||
card.finish(body);
|
||||
} catch (failure) {
|
||||
// 失败不占着卡片:直接收掉,用右上角通知说明原因
|
||||
card.dismiss();
|
||||
notify(failure.message || "请求失败", "error");
|
||||
}
|
||||
}, "bpi-mini");
|
||||
greetLine.append(greetRow, greetSettings, greetRequest);
|
||||
// 包成折叠块:以后生成张数、运行时长、日志这些都往这里塞
|
||||
const greetFold = element("details", "bpi-fold");
|
||||
greetFold.style.gridColumn = "1 / -1";
|
||||
const greetFoldBody = element("div", "bpi-fold-body");
|
||||
const greetAiPanel = createGreetingAiPanel();
|
||||
const greetAutoPanel = createGreetingAutoPanel();
|
||||
// 编辑区紧跟开关行:展开「问候的 AI」「自动提示」也不会把输入框挤到看不见
|
||||
greetFoldBody.append(greetLine, welcomeRow, errorSwitch.row, idleSwitch.row, greetPanel, greetAiPanel, greetAutoPanel);
|
||||
loadGreetingConfig()
|
||||
.then((config) => {
|
||||
errorSwitch.toggle.checked = config.error_rule?.enabled === true;
|
||||
idleSwitch.toggle.checked = config.idle_rule?.enabled === true;
|
||||
})
|
||||
.catch(() => {});
|
||||
greetFold.append(element("summary", "", "问候与提醒"), greetFoldBody);
|
||||
form.append(languageLabel, languageSelect, languageNote, followRow, followNote, greetFold, status);
|
||||
restartGreetingAutoTimer();
|
||||
// 开:操作提示改成右上角弹窗;关:还是写回明细表上方那条状态文字
|
||||
const fold = element("details", "bpi-fold");
|
||||
const foldBody = element("div", "bpi-fold-body");
|
||||
@@ -1058,6 +1186,12 @@ async function buildAssistantForm(host, loading) {
|
||||
form.append(openaiHead);
|
||||
openaiOnly.push(openaiHead);
|
||||
const openaiBaseUrl = addField(openaiOnly, "API URL", "assistant-openai-base-url", config.openai_base_url, "https://host/v1");
|
||||
const openaiApiKey = addField(openaiOnly, "API Key", "assistant-openai-api-key", "", config.openai_api_key_configured ? "Saved; leave empty to keep" : "Optional for local LM Studio");
|
||||
openaiApiKey.type = "password";
|
||||
const clearApiKey = clearSwitch("Clear saved API Key");
|
||||
const apiKeySpacer = element("span");
|
||||
form.append(apiKeySpacer, clearApiKey.row);
|
||||
openaiOnly.push(apiKeySpacer, clearApiKey.row);
|
||||
const openaiModel = addModelRow(openaiOnly, "assistant-openai-model", config.openai_model, "provider model ID", {
|
||||
backend: "openai_compatible",
|
||||
baseUrl: () => openaiBaseUrl,
|
||||
@@ -1074,12 +1208,6 @@ async function buildAssistantForm(host, loading) {
|
||||
presetLine.append(lmStudioPreset, element("span", "bpi-config-note", "Connects to localhost:1234 by default; no external API required."));
|
||||
form.append(presetLine);
|
||||
openaiOnly.push(presetLine);
|
||||
const openaiApiKey = addField(openaiOnly, "API Key", "assistant-openai-api-key", "", config.openai_api_key_configured ? "Saved; leave empty to keep" : "Optional for local LM Studio");
|
||||
openaiApiKey.type = "password";
|
||||
const clearApiKey = clearSwitch("Clear saved API Key");
|
||||
const apiKeySpacer = element("span");
|
||||
form.append(apiKeySpacer, clearApiKey.row);
|
||||
openaiOnly.push(apiKeySpacer, clearApiKey.row);
|
||||
const openaiTemperature = addField(openaiOnly, "Temperature", "assistant-openai-temperature", config.openai_temperature, "0–2");
|
||||
openaiTemperature.type = "number";
|
||||
openaiTemperature.min = "0";
|
||||
@@ -1132,7 +1260,7 @@ async function buildAssistantForm(host, loading) {
|
||||
ollamaOnly.push(ollamaUrlNote);
|
||||
|
||||
// 规则:只有 AI 会读,两个后端各一套(server.py 按当前后端取对应的那套)
|
||||
const addRule = (list, labelText, value) => {
|
||||
const addRule = (list, labelText, value, container = form) => {
|
||||
const labelElement = element("label", "", labelText);
|
||||
const textarea = element("textarea");
|
||||
textarea.value = value;
|
||||
@@ -1141,42 +1269,43 @@ async function buildAssistantForm(host, loading) {
|
||||
const orphan = element("span", "bpi-rule-orphan", "*");
|
||||
setTitle(orphan, "这条规则不属于任何一个预设");
|
||||
labelCell.append(labelElement, orphan);
|
||||
form.append(labelCell, textarea);
|
||||
container.append(labelCell, textarea);
|
||||
list.push(labelCell, textarea);
|
||||
textarea.orphanBadge = orphan;
|
||||
return textarea;
|
||||
};
|
||||
// 四条规则归到一个分组里:以后切规则预设时整体一起换,也方便在标题行右侧放管理入口
|
||||
const addRuleHead = (list, title, hint) => {
|
||||
const head = element("div", "bpm-toolbar");
|
||||
head.style.gridColumn = "1 / -1";
|
||||
head.append(element("strong", "", title));
|
||||
if (hint) {
|
||||
const note = element("span", "bpi-config-note", hint);
|
||||
note.style.marginLeft = "8px";
|
||||
head.append(note);
|
||||
}
|
||||
form.append(head);
|
||||
list.push(head);
|
||||
return head;
|
||||
// 四条规则收进一个折叠块:默认收起,省得设置页太长
|
||||
const addRuleFold = (list, title, hint) => {
|
||||
const fold = element("details", "bpi-fold");
|
||||
fold.style.gridColumn = "1 / -1";
|
||||
const body = element("div", "bpi-fold-body");
|
||||
const toolbar = element("div", "bpm-toolbar");
|
||||
const grid = element("div", "bpi-form");
|
||||
body.append(toolbar);
|
||||
if (hint) body.append(element("div", "bpi-config-note", hint));
|
||||
body.append(grid);
|
||||
fold.append(element("summary", "", title), body);
|
||||
form.append(fold);
|
||||
list.push(fold);
|
||||
return { fold, body, toolbar, grid };
|
||||
};
|
||||
// 规则编辑和保存是两件事:改完不点保存,刷新或执行翻译时用的还是旧规则
|
||||
const RULE_SAVE_HINT = "修改需保存后生效;未保存的规则不参与翻译";
|
||||
const openaiRulesHead = addRuleHead(openaiOnly, "AI 规则", RULE_SAVE_HINT);
|
||||
const ollamaRulesHead = addRuleHead(ollamaOnly, "AI 规则", RULE_SAVE_HINT);
|
||||
const openaiRulesFold = addRuleFold(openaiOnly, "AI 规则", RULE_SAVE_HINT);
|
||||
const ollamaRulesFold = addRuleFold(ollamaOnly, "AI 规则", RULE_SAVE_HINT);
|
||||
|
||||
const rules = {
|
||||
openai_compatible: {
|
||||
translation: addRule(openaiOnly, "Translation Rule", config.openai_translation_rule),
|
||||
translateOptimize: addRule(openaiOnly, "Translate & Optimize", config.openai_translate_optimize_rule),
|
||||
optimization: addRule(openaiOnly, "Optimize Rule", config.openai_optimization_rule),
|
||||
natural: addRule(openaiOnly, "Natural Language", config.openai_natural_rule),
|
||||
translation: addRule(openaiOnly, "Translation Rule", config.openai_translation_rule, openaiRulesFold.grid),
|
||||
translateOptimize: addRule(openaiOnly, "Translate & Optimize", config.openai_translate_optimize_rule, openaiRulesFold.grid),
|
||||
optimization: addRule(openaiOnly, "Optimize Rule", config.openai_optimization_rule, openaiRulesFold.grid),
|
||||
natural: addRule(openaiOnly, "Natural Language", config.openai_natural_rule, openaiRulesFold.grid),
|
||||
},
|
||||
ollama: {
|
||||
translation: addRule(ollamaOnly, "Translation Rule", config.ollama_translation_rule),
|
||||
translateOptimize: addRule(ollamaOnly, "Translate & Optimize", config.ollama_translate_optimize_rule),
|
||||
optimization: addRule(ollamaOnly, "Optimize Rule", config.ollama_optimization_rule),
|
||||
natural: addRule(ollamaOnly, "Natural Language", config.ollama_natural_rule),
|
||||
translation: addRule(ollamaOnly, "Translation Rule", config.ollama_translation_rule, ollamaRulesFold.grid),
|
||||
translateOptimize: addRule(ollamaOnly, "Translate & Optimize", config.ollama_translate_optimize_rule, ollamaRulesFold.grid),
|
||||
optimization: addRule(ollamaOnly, "Optimize Rule", config.ollama_optimization_rule, ollamaRulesFold.grid),
|
||||
natural: addRule(ollamaOnly, "Natural Language", config.ollama_natural_rule, ollamaRulesFold.grid),
|
||||
},
|
||||
};
|
||||
// —— AI 规则预设:标题行右侧一排(下拉 + 另存为 + 删除),一套规则一个文件 ——
|
||||
@@ -1331,8 +1460,8 @@ async function buildAssistantForm(host, loading) {
|
||||
refresh();
|
||||
return { refresh, markOrphans };
|
||||
};
|
||||
const openaiRulesBar = buildRulePresetBar(openaiRulesHead, "openai", rules.openai_compatible);
|
||||
const ollamaRulesBar = buildRulePresetBar(ollamaRulesHead, "ollama", rules.ollama);
|
||||
const openaiRulesBar = buildRulePresetBar(openaiRulesFold.toolbar, "openai", rules.openai_compatible);
|
||||
const ollamaRulesBar = buildRulePresetBar(ollamaRulesFold.toolbar, "ollama", rules.ollama);
|
||||
|
||||
form.appendChild(error);
|
||||
|
||||
@@ -1878,6 +2007,13 @@ app.registerExtension({
|
||||
// 助手设置整体挂在 ComfyUI 的「设置」面板里:项名和分类由前端原生渲染,
|
||||
// 不走插件的 t(),直接写中文;控件本体是 assistantSettingsControl() 返回的那块 DOM。
|
||||
settings: [
|
||||
{
|
||||
id: "BilingualPromptInspector.Notice",
|
||||
name: "说明",
|
||||
type: noticeControl,
|
||||
defaultValue: "",
|
||||
category: ["双语提示词检查器", "说明"],
|
||||
},
|
||||
{
|
||||
id: "BilingualPromptInspector.Appearance",
|
||||
name: "通用",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "comfyui-bilingual-prompt-inspector",
|
||||
"version": "2.2.3",
|
||||
"version": "2.2.4",
|
||||
"description": "ComfyUI bilingual prompt inspection, translation, Anima sorting, and on-demand Danbooru dictionary extension.",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "prompt-translator-manager"
|
||||
version = "2.2.3"
|
||||
version = "2.2.4"
|
||||
# 与 Comfy Registry 页面上显示的描述保持一致: publish 会用这里的值覆盖网页上的手改
|
||||
description = "ComfyUI prompt node with translation and management: bilingual tag view, drag-to-reorder, temporary hiding, multiple translation services and a sidebar dictionary manager.\nComfyUI 提示节点,支持翻译和管理:双语标签视图、拖拽排序、临时隐藏、多翻译服务和侧边栏词典管理器。"
|
||||
license = { file = "LICENSE" }
|
||||
|
||||
@@ -42,6 +42,7 @@ from .saved_prompt_store import (
|
||||
)
|
||||
from .library_store import LibraryStore
|
||||
from .rule_preset_store import RulePresetStore
|
||||
from .greeting_store import GreetingStore
|
||||
from .token_color_store import TokenColorStore
|
||||
|
||||
|
||||
@@ -205,13 +206,16 @@ async def _baidu_translate(text, to_lang):
|
||||
return result
|
||||
|
||||
|
||||
async def _call_ai_assistant(action, text, instruction, config, connection_test=False):
|
||||
async def _call_ai_assistant(action, text, instruction, config, connection_test=False, scene=""):
|
||||
"""翻译/优化都走这里。优化类恒走 AI;翻译类在翻译服务就是某个 AI 后端时也走 AI。"""
|
||||
# 两个后端各一套配置和规则。选 OpenAI / Ollama 当翻译服务时它就是后端;
|
||||
# 选词库 / 百度时翻译不走 AI,优化仍走,这时用 ai_provider 决定读哪一套
|
||||
prefix = AI_PROVIDER_PREFIXES.get(config.get("translate_service")) or AI_PROVIDER_PREFIXES.get(
|
||||
config.get("ai_provider", "openai_compatible"), "openai"
|
||||
)
|
||||
# 问候有自己的温度,和翻译那套分开:不论用哪个后端,这里都换成问候的设置
|
||||
if action == "greeting":
|
||||
config = {**config, f"{prefix}_temperature": greeting_store.temperature()}
|
||||
if not config.get(f"{prefix}_model"):
|
||||
raise ValueError("尚未填写 AI 模型名称(“翻译并优化”“优化”以及 AI 翻译都需要)")
|
||||
rule = {
|
||||
@@ -221,6 +225,8 @@ async def _call_ai_assistant(action, text, instruction, config, connection_test=
|
||||
"optimize": config[f"{prefix}_optimization_rule"],
|
||||
# 自然语言模式下的「优化」:输入可以是中文,输出是英文自然语句,不走 Anima 标点规则
|
||||
"optimize_natural": config[f"{prefix}_natural_rule"],
|
||||
# 问候按场景取:手动请求 / 久坐提醒 / 生成张数,各有各的那句要求
|
||||
"greeting": greeting_store.prompt_for(scene),
|
||||
}[action]
|
||||
if connection_test:
|
||||
rule = "你是连接测试助手。"
|
||||
@@ -236,6 +242,16 @@ async def _call_ai_assistant(action, text, instruction, config, connection_test=
|
||||
if re.search(r"[\u3400-\u9fff]", text):
|
||||
raise ValueError("“优化”在标签模式下只接受英文;中文或混合文本请使用“翻译并优化”,或把模式切到“自然语言”")
|
||||
rule = f"{rule}\n\n本次任务:整理现有英文内容,不承担翻译。"
|
||||
elif not connection_test and action == "greeting":
|
||||
if scene in {"time", "images"}:
|
||||
# 这两种自动提示的标题由前端固定(比如「当前在线 32 分钟」),AI 只负责正文
|
||||
rule = f"{rule}\n\n本次任务:只写正文,2 到 3 句,不要标题、不要列表、不要前缀标签。"
|
||||
elif scene == "welcome":
|
||||
rule = f"{rule}\n\n本次任务:按上面的要求打一声招呼,第一行写标题。"
|
||||
elif scene in {"error", "idle"}:
|
||||
rule = f"{rule}\n\n本次任务:按上面的要求说一句,第一行写标题,不要提报错细节。"
|
||||
else:
|
||||
rule = f"{rule}\n\n本次任务:按上面的要求写一条新的提醒。"
|
||||
elif not connection_test and action == "optimize_natural":
|
||||
# 自然语言模式允许中文输入:先译后整理,输出一段英文自然语句
|
||||
direction = "先把中文忠实译为英文,再整理表达" if translation_direction(text) == "to_english" else "整理现有英文内容,不承担翻译"
|
||||
@@ -293,15 +309,25 @@ async def _call_ai_assistant(action, text, instruction, config, connection_test=
|
||||
return result
|
||||
|
||||
|
||||
async def _call_configured_assistant(action, text, instruction="", connection_test=False):
|
||||
async def _call_configured_assistant(action, text, instruction="", connection_test=False, scene=""):
|
||||
config = assistant_store.config()
|
||||
if action == "greeting" and not connection_test:
|
||||
override = greeting_store.ai_override()
|
||||
if override:
|
||||
# 问候单独配的 AI:强制走 OpenAI 兼容那套分支
|
||||
config = {**config, **override, "translate_service": "openai_compatible"}
|
||||
elif config["translate_service"] not in AI_SERVICES:
|
||||
raise ValueError(
|
||||
"问候没有可用的 AI:请打开「问候与提醒 → 问候的 AI → 使用助手设置里的 AI」,"
|
||||
"并把助手设置切到 AI 翻译,或在问候里单独填一套 API"
|
||||
)
|
||||
translate_service = config["translate_service"]
|
||||
# “翻译并优化”“优化”恒走 AI;翻译/解释在翻译服务就是某个 AI 后端时也走 AI
|
||||
needs_ai = action in {"translate_optimize", "optimize", "optimize_natural"} or (
|
||||
needs_ai = action in {"translate_optimize", "optimize", "optimize_natural", "greeting"} or (
|
||||
action in {"translate", "explain"} and translate_service in AI_SERVICES
|
||||
)
|
||||
if needs_ai:
|
||||
return await _call_ai_assistant(action, text, instruction, config, connection_test)
|
||||
return await _call_ai_assistant(action, text, instruction, config, connection_test, scene)
|
||||
# 以下只处理 translate/explain 且翻译服务不是 AI
|
||||
if translate_service == "dictionary":
|
||||
if connection_test:
|
||||
@@ -418,7 +444,8 @@ async def run_assistant(request):
|
||||
action = str(payload.get("action", "translate")) if isinstance(payload, dict) else "translate"
|
||||
text = str(payload.get("text", "")) if isinstance(payload, dict) else ""
|
||||
instruction = str(payload.get("instruction", "")) if isinstance(payload, dict) else ""
|
||||
if action not in {"translate", "explain", "translate_optimize", "optimize", "optimize_natural"}:
|
||||
scene = str(payload.get("scene", "")) if isinstance(payload, dict) else ""
|
||||
if action not in {"translate", "explain", "translate_optimize", "optimize", "optimize_natural", "greeting"}:
|
||||
raise ValueError("未知的助手操作")
|
||||
if not text.strip():
|
||||
raise ValueError("输入内容为空")
|
||||
@@ -437,7 +464,7 @@ async def run_assistant(request):
|
||||
except asyncio.TimeoutError as error:
|
||||
raise AssistantLimitError("已有多个助手任务正在运行,请稍后再试") from error
|
||||
try:
|
||||
result = await _call_configured_assistant(action, text, instruction)
|
||||
result = await _call_configured_assistant(action, text, instruction, scene=scene)
|
||||
finally:
|
||||
_assistant_slots.release()
|
||||
return web.json_response({"success": True, "data": {"text": result, "translate_service": assistant_store.config()["translate_service"]}})
|
||||
@@ -449,6 +476,20 @@ async def run_assistant(request):
|
||||
return error_response(error, 502)
|
||||
|
||||
|
||||
@protected_route("get", "/bpi/greeting/config")
|
||||
async def get_greeting_config(_request):
|
||||
return web.json_response({"success": True, "data": greeting_store.config()})
|
||||
|
||||
|
||||
@protected_route("post", "/bpi/greeting/config")
|
||||
async def save_greeting_config(request):
|
||||
try:
|
||||
payload = await _read_json(request, _MAX_ASSISTANT_JSON_BYTES)
|
||||
return web.json_response({"success": True, "data": greeting_store.save(payload)})
|
||||
except (ValueError, web.HTTPBadRequest) as error:
|
||||
return error_response(error)
|
||||
|
||||
|
||||
@protected_route("get", "/bpi/dictionary")
|
||||
async def get_dictionary(_request):
|
||||
try:
|
||||
@@ -783,6 +824,7 @@ async def cancel_upstream(request):
|
||||
saved_prompt_store = SavedPromptStore()
|
||||
library_store = LibraryStore()
|
||||
rule_preset_store = RulePresetStore()
|
||||
greeting_store = GreetingStore()
|
||||
# multipart 正文 = 文本字段 + 一张可选图片(图片本身最大 5 MB)
|
||||
_MAX_SAVED_PROMPT_BYTES = MAX_IMAGE_BYTES + 512 * 1024
|
||||
|
||||
|
||||
Reference in New Issue
Block a user