Seal the names the user gave: workflow cards, saved prompts, their tags

A private task still sent the names of workflow cards and saved prompts,
their tags, and which card or combo a picture was made with. Such names
often say the model or what is drawn. They are tokens now, like a combo's
name; the AI uses a card or a combo by its token.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
DemonGatanjieu
2026-11-11 19:04:08 +08:00
co-authored by Claude Fable 5.2
parent f600379dac
commit 6560347827
5 changed files with 25 additions and 12 deletions
+2
View File
@@ -70,6 +70,8 @@
- **A private task shows what the AI was sent.** New in the task's ⋯: while privacy mode is on, every call is kept as it went, what the AI wrote and the text Anomalous answered, and you can look for a word (a model name, a word of a prompt) to see whether it ever went out. The newest 300 calls are kept and removed with the task.
- **说清了隐私模式管不到的地方**:它过滤的是 AMB 发给 AI 的内容。Antigravity、Cursor、Claude Code 这类能自己读硬盘、跑命令的 AI 软件,可以直接打开出图文件夹和 AMB 的任务记录,这个拦不住;用它们开隐私任务时,标题栏下面会有一行提醒,隐私开关的说明里也写明了。只会调用工具的聊天软件不受影响。
- **Privacy mode now says what it cannot cover**: it filters what Anomalous sends to the AI. An AI app that reads the disk and runs commands by itself (Antigravity, Cursor, Claude Code and the like) can open the output folder and Anomalous's task records, and this cannot stop it; a private task run by such an app says so under its header, and so do the switch's hints. A chat app that only calls tools is not affected.
- 隐私模式里,**你起的名字也都是代号了**:工作流卡片名、提示词库的条目名、它们的标签,以及图片信息里“用哪张卡片、哪个搭配做的”。这些名字里常常写着模型名或画的内容。AI 用代号照样能放搭配、用卡片;你在聊天里说出名字,它照样搜得到。
- In privacy mode **the names you gave are tokens too**: workflow card names, saved prompt names, their tags, and which card or combo a picture was made with. Such names often say the model or what is drawn. The AI still places a combo and uses a card by its token, and still finds one by a name you say in the chat.
## v1.63.0 (AI Ways and Skills, Saves, Official Example Workflows, Privacy Mode) — 2026-11-10
+12 -8
View File
@@ -8,9 +8,10 @@ text back before the tool runs, so the canvas and the runs get it while the AI n
mask() seals what a tool's answer holds, in this order: every text input of a node (ComfyUI's node
list says which inputs are STRING: prompts, file name prefixes; numbers and choices stay), the
prompt fields of saved prompts, combos, workflows, models and changes (PROMPT_KEYS, and a few per
tool), a combo's name (made of its model's name and the start of its prompt unless the user renamed
it), the models when asked to (`models`), picture file names anywhere, and last any sealed text
wherever it still shows up.
tool), what the user named (NAMED: the names and tags of combos, workflow cards and saved prompts;
a combo's name is its model's name and the start of its prompt unless they renamed it), the models
when asked to (`models`), picture file names anywhere, and last any sealed text wherever it still
shows up.
seal_answer() takes the user's prompt box (a "prompts" panel block, ui_ai_prompt_box.js): the
prompts and reference pictures, sealed, with the tokens the AI gets for them, and the gist: the
picture in words the user wrote for the AI to read, handed on as written.
@@ -28,8 +29,11 @@ WORD = re.compile(r"[^\W\d_]") # a letter of any script
PICTURE = re.compile(r"[^\s\"'<>|*?,;()\[\]{}]*\.(?:png|jpe?g|webp|gif|bmp|mp4|webm|mov)\b", re.IGNORECASE)
# Fields whose text is a prompt, or holds one (a workflow's settings, a change's before and after).
PROMPT_KEYS = {"prompt", "prompts", "positive", "negative", "positive_prompt", "negative_prompt", "notes",
"settings", "before", "after", "filename_prefix", "trigger_words", "trainedWords", "header_words"}
TOOL_KEYS = {"get_model": {"description", "user_notes"}, "list_combos": {"more_names"}}
"settings", "before", "after", "filename_prefix", "trigger_words", "trainedWords", "header_words",
"tags", "in_cards", "recipe", "combo", "said_as"} # the last five: names the user gave, where they stand alone
NAMED = ("combos", "workflows", "prompts") # lists of what the user named: each one's name and tags are theirs
TOOL_KEYS = {"get_model": {"description", "user_notes"}, "place_combo": {"name"},
**{tool: {"more_names"} for tool in ("list_combos", "list_workflows", "list_saved_prompts")}}
MIN_SEALED = 3 # shorter texts are not looked for elsewhere (they would match inside other words)
MAX_PROMPTS = 8
MAX_PROMPT = 4000
@@ -137,9 +141,9 @@ def _walk(value, seals, info, keys):
else _walk(text, seals, info, keys) for name, text in item.items()}
elif key in keys and (isinstance(item, str) or isinstance(item, list) and all(isinstance(text, str) for text in item)):
out[key] = seal_text(seals, item)
elif key == "combos" and isinstance(item, list):
out[key] = [{**combo, "name": seal_text(seals, combo["name"])} if isinstance(combo, dict) and isinstance(combo.get("name"), str)
else combo for combo in _walk(item, seals, info, keys)]
elif key in NAMED and isinstance(item, list):
out[key] = [{**named, **{part: seal_text(seals, named[part]) for part in ("name", "filename") if part in named}}
if isinstance(named, dict) else named for named in _walk(item, seals, info, keys)]
else:
out[key] = _walk(item, seals, info, keys)
return out
+2 -1
View File
@@ -263,7 +263,8 @@ the record keeps the tokens), and every tools/call result passes `ai_privacy.gua
the run's pictures, the user's messages and the outside check were added: for a private task
(ai_privacy.py) image content items are removed and the answer says why, then `ai_sealed.mask`
turns prompt text and picture names into tokens (a node's STRING inputs by ComfyUI's node list,
prompt fields, picture file names, and any text already sealed, also in the text content).
prompt fields, the names and tags the user gave their combos, workflow cards and saved prompts
(`NAMED`), picture file names, and any text already sealed, also in the text content).
While `ai_privacy.hides_models()` (user/anomalous/ai_privacy.json `models`, true unless the user set false),
`ai_sealed_models.hide` runs between the two: every form of a library model (the materials catalog's
list, read at most every 15 s) becomes that model's "[M#]" token, sealed as its model_id; a
+1 -1
View File
@@ -167,7 +167,7 @@ claude mcp add --transport http anomalous http://127.0.0.1:8188/anomalous/mcp
**隐私模式防的是什么**:它过滤的是 **AMB 发给 AI 的内容**,只会调用工具的聊天软件(比如 Cherry Studio 接 DeepSeek)因此什么都看不到。但 Antigravity、Cursor、Claude Code、Gemini CLI 这类软件能自己读硬盘、跑命令:出图文件夹里的图、AMB 的任务记录(代号对应的原文就存在里面)它都能直接打开,AMB 拦不住。用这类软件开隐私任务时,任务标题栏下面会有一行提醒。想亲眼确认发出去了什么:任务标题栏 **⋯ → 发给 AI 了什么**,里面是隐私模式开着时 AI 每一次调用发来的参数和 AMB 回给它的文字,一字不差;在上面的框里搜一个词(某个模型名、提示词里的词),就知道它有没有出去过。只留最近 300 次调用,任务移除时一起删掉。
**隐私模式**:有些图和提示词不方便给 AI 看时,点任务标题栏的 **🔒**,这个任务就成了隐私任务。AMB 一张图都不发给 AI(放大看图、图片信息里的图、跑完附带的预览、你递给它的图、AI 软件里任务卡片上的图都一样),它只知道出了几张、成没成功。提示词原文和图片文件名换成 **[P1]**、**[img1]** 这样的代号:画布上节点的文字输入(按 ComfyUI 自己的节点定义认,第三方节点也一样;模型名、数字、选项照常给)、图片信息、提示词库、搭配(连搭配的名字:没改过名的搭配,名字就是模型名加提示词开头)、工作流的设置、模型的触发词和介绍、你自己改画布的记录都一样,同一段文字永远是同一个代号。AI 用代号去写提示词、跑图、填加载图片、放挑图面板,AMB 在工具执行前换回原文,所以画布和出图都是原样,你看到的也都是原样;时间线上记的是代号。要新提示词时,AI 会放一个 **提示词框**:写几条提示词(可以再加)、反向提示词,可以从图库选或把图拖进来当参考图,点 **交给 AI(提示词只给代号)**;原文只存在 AMB 里,交出后框里显示代号和你写的原文。提示词框最上面还有一格 **画面大意**:这一格是原样给 AI 看的,用不介意让它看到的话说说想要什么(谁、在哪、什么镜头、什么气氛),它就能照着补光影、镜头和画风,再把你的私密提示词以代号嵌进去;两格可以只填一格。框上方按画布主模型的大小提醒你(来自 skill“写提示词·动作适合模型”)。AI 怎么在隐私任务里干活写在 skill“隐私模式·怎么干活”里,开工时会被指过去。隐私任务里 AI 只拿得到这一个 skill,做法面板的 skill 一栏也只显示它;普通任务正好反过来,只显示、只给普通的 skill。**关卡** 窗口里的“隐私模式”设新任务默认开不开;默认改成关时,原来按默认开着的任务仍然保持隐私。AI 的某次调用认不出属于哪个任务时,只要默认开着或者还有没做完的隐私任务,就按隐私处理。注意:任务做到一半才打开时,之前已经给过 AI 的收不回来;你自己在留言或 AI 聊天框里写的文字和贴的图,AMB 不拦(那是你主动给的)。**模型名也藏起来**:隐私任务里,模型和 LoRA 的文件名、卡片上的名字、所在文件夹、C 站链接和指纹都不发给 AI,一个模型不管出现在哪(搜索结果、画布的加载器、搭配、报错)都是同一个 **[M1]** 这样的代号;AI 把代号填到要用模型的地方,AMB 换回真模型。它挑模型靠的是留给它的这些:类型、底模、作者推荐的权重和参数、触发词(代号),以及你在模型的 **编辑** 窗口里写的 **给 AI 的用途说明**(一句不介意让它看到的话,比如“双人互动姿势”;不填就不给)。你在聊天里说出模型名,它照样搜得到,拿到的还是代号;它拿不准时会放一个模型面板让你在真卡片里挑。AI 嘴里的 [M1] 是哪个模型,点任务标题栏 **⋯ → 模型代号对照** 查。不想藏的话,在 **关卡** 窗口把“隐私任务里的模型名”改成“照常给 AI”。有一种情况藏不住:卡片名只是一个普通英文单词的模型(比如就叫 Anima),名字出现在一整句话中间时不替换,免得把正常的说明也替换坏;它的文件名和搜索结果里的名字照样是代号。
**隐私模式**:有些图和提示词不方便给 AI 看时,点任务标题栏的 **🔒**,这个任务就成了隐私任务。AMB 一张图都不发给 AI(放大看图、图片信息里的图、跑完附带的预览、你递给它的图、AI 软件里任务卡片上的图都一样),它只知道出了几张、成没成功。提示词原文和图片文件名换成 **[P1]**、**[img1]** 这样的代号:画布上节点的文字输入(按 ComfyUI 自己的节点定义认,第三方节点也一样;模型名、数字、选项照常给)、图片信息、提示词库、搭配、工作流的设置,连同你给它们起的名字和标签(搭配名、工作流卡片名、提示词库的条目名;没改过名的搭配,名字就是模型名加提示词开头)、模型的触发词和介绍、你自己改画布的记录都一样,同一段文字永远是同一个代号。AI 用代号去写提示词、跑图、填加载图片、放挑图面板,AMB 在工具执行前换回原文,所以画布和出图都是原样,你看到的也都是原样;时间线上记的是代号。要新提示词时,AI 会放一个 **提示词框**:写几条提示词(可以再加)、反向提示词,可以从图库选或把图拖进来当参考图,点 **交给 AI(提示词只给代号)**;原文只存在 AMB 里,交出后框里显示代号和你写的原文。提示词框最上面还有一格 **画面大意**:这一格是原样给 AI 看的,用不介意让它看到的话说说想要什么(谁、在哪、什么镜头、什么气氛),它就能照着补光影、镜头和画风,再把你的私密提示词以代号嵌进去;两格可以只填一格。框上方按画布主模型的大小提醒你(来自 skill“写提示词·动作适合模型”)。AI 怎么在隐私任务里干活写在 skill“隐私模式·怎么干活”里,开工时会被指过去。隐私任务里 AI 只拿得到这一个 skill,做法面板的 skill 一栏也只显示它;普通任务正好反过来,只显示、只给普通的 skill。**关卡** 窗口里的“隐私模式”设新任务默认开不开;默认改成关时,原来按默认开着的任务仍然保持隐私。AI 的某次调用认不出属于哪个任务时,只要默认开着或者还有没做完的隐私任务,就按隐私处理。注意:任务做到一半才打开时,之前已经给过 AI 的收不回来;你自己在留言或 AI 聊天框里写的文字和贴的图,AMB 不拦(那是你主动给的)。**模型名也藏起来**:隐私任务里,模型和 LoRA 的文件名、卡片上的名字、所在文件夹、C 站链接和指纹都不发给 AI,一个模型不管出现在哪(搜索结果、画布的加载器、搭配、报错)都是同一个 **[M1]** 这样的代号;AI 把代号填到要用模型的地方,AMB 换回真模型。它挑模型靠的是留给它的这些:类型、底模、作者推荐的权重和参数、触发词(代号),以及你在模型的 **编辑** 窗口里写的 **给 AI 的用途说明**(一句不介意让它看到的话,比如“双人互动姿势”;不填就不给)。你在聊天里说出模型名,它照样搜得到,拿到的还是代号;它拿不准时会放一个模型面板让你在真卡片里挑。AI 嘴里的 [M1] 是哪个模型,点任务标题栏 **⋯ → 模型代号对照** 查。不想藏的话,在 **关卡** 窗口把“隐私任务里的模型名”改成“照常给 AI”。有一种情况藏不住:卡片名只是一个普通英文单词的模型(比如就叫 Anima),名字出现在一整句话中间时不替换,免得把正常的说明也替换坏;它的文件名和搜索结果里的名字照样是代号。
**这次跑的节点结构**:每次运行那一步下面写着一行“底模 · 几个 LoRA · 尺寸 · 步数 · 几个节点”,点开有 **简单** 和 **完整** 两种看法(AMB 记得你上次选的)。简单:按每一段采样列出“用哪个模型加哪些 LoRA、提示词是什么、从空白画布还是从上一段的结果(放大多少)开始”,最后保存在哪;后一段和前一段一样的就写“和第 1 段一样”。完整:方块图,哪个底模、LoRA 和权重、正负提示词开头、尺寸、采样器设置、保存,方块之间的线标着类型。两种看法里点任意一项都能定位到画布上的节点。这张图是 AMB 按 ComfyUI 实际收到的工作流画的,不靠 AI 描述。上面的黄色小块是 **比上次改了什么**(比如“K采样器·步数:20 → 30”“新增「加载LoRA」”)。AI 讲解时可以用 `show_run_map` 一次把同一张图放上展台,不用自己拼。
+8 -2
View File
@@ -18,7 +18,7 @@ from api import ai_callers, ai_panels, ai_privacy, ai_sealed, ai_sealed_models,
from test_mcp import rpc # noqa: E402 (its module keeps tasks and gates in temporary folders)
SETTING = os.path.join(tempfile.mkdtemp(prefix="amb_sealed_"), "ai_privacy.json")
CANARIES = ("canarypos", "canaryneg", "canaryfile", "canarytrigger", "canaryecho", "canarycombo")
CANARIES = ("canarypos", "canaryneg", "canaryfile", "canarytrigger", "canaryecho", "canarycombo", "canaryflow", "canarytag", "canarysaved")
MODEL_CANARIES = ("canarylora", "Canarycard", "canaryfolder", "canarysha", "civitai.com", "canarymissing")
LORA = "loras:0:canaryfolder/canarylora v2.safetensors"
LIBRARY = [{"id": LORA, "name": "Canarycard Pose", "file": "canarylora v2.safetensors"},
@@ -35,7 +35,10 @@ def canary_result(arguments=None):
{"id": 9, "type": "KSampler", "settings": {"sampler_name": "euler", "steps": 20, "control_after_generate": "fixed"}},
{"id": 11, "class_type": "SaveImage", "inputs": {"filename_prefix": "canaryfile"}},
{"id": 12, "type": "SomePackNode", "settings": {"wildcard": "canarypos again", "model": "flux.safetensors"}}],
"prompts": [{"name": "kept", "positive": "canarypos", "negative": "canaryneg"}],
"prompts": [{"name": "canarysaved words", "positive": "canarypos", "negative": "canaryneg"}],
"workflows": [{"name": "canaryflow card", "tags": ["canarytag"], "route": "2 passes"}],
"pictures": [{"made_with": {"recipe": "canaryflow card", "combo": "Some LoRA · canarycombo, 1girl"}}],
"models": [{"in_cards": ["canaryflow card"]}],
"combos": [{"name": "Some LoRA · canarycombo, 1girl", "base_model": "Anima"}, {"name": "2", "base_model": "Anima"}],
"images": [{"image": "sub/canaryfile_00001_.png"}, {"filename": "canaryfile_00002_.png", "subfolder": ""}],
"trigger_words": ["canarytrigger"], "echo": json.dumps(arguments or {}), "filled": {"prompt": ["6"]}}
@@ -98,6 +101,9 @@ class SealedTests(unittest.TestCase):
self.assertRegex(combos[0]["name"], r"^\[P\d+\]$", "a combo's name holds its model and prompt")
self.assertEqual(ai_sealed.expand(self.task, {"name": combos[0]["name"]}), {"name": "Some LoRA · canarycombo, 1girl"}, "and is placed by its token")
self.assertEqual(combos[1], {"name": "2", "base_model": "Anima"})
flow = result["structuredContent"]["workflows"][0]
self.assertRegex(flow["name"], r"^\[P\d+\]$", "a card's name is the user's words")
self.assertEqual((flow["route"], result["structuredContent"]["models"][0]["in_cards"]), ("2 passes", [flow["name"]]), "one name, one token")
def test_a_model_is_one_token_however_it_is_written(self):
result = ai_sealed.mask(model_result(), self.task, INFO, "search_models", True)