Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa9c237f98 |
@@ -108,7 +108,7 @@ ComfyUI-Manager 收录审核检查的就是这几类口子(2026-10 审 Anomalo
|
||||
1. **不执行下载来的或用户文件里的代码。** 数据不用 `pickle`、`eval`、`exec`、动态导入读;配置文件当数据解析。模型文件只读 safetensors / GGUF 的文件头,`.ckpt` / `.pt` / `.pth` / `.bin` 不打开,界面提醒它们可能带代码。
|
||||
2. **下载要能核对。** 只从允许的 https 站点下载,先写 `.part`,核对 SHA-256 后才改名放入;没有可核对的指纹时在界面上说明。下载的东西只当数据用。
|
||||
3. **接口不接受任意本机路径。** 只收“类型 + 根目录序号 + 相对路径”,在后端用 `resolve_within` 校验;需要指向本机某个文件时,由服务端自己找到它,只给页面一个令牌。
|
||||
4. **会写入、移动、删除文件或替用户联网的接口只接受本机请求**(请求来自本机,没有经过代理,Host 也是本机)。用 `--listen` 开放到局域网时,别的设备只能查看。新接口必须做到;已有接口还没全部做到,改到哪个补哪个。
|
||||
4. **会写入、移动、删除文件或替用户联网的接口只接受本机请求**(请求来自本机,没有经过代理,Host 也是本机)。用 `--listen` 开放到局域网时,别的设备只能查看。由 `api/local_guard.py` 的中间件统一把关:`/anomalous/` 下 GET 以外的请求默认只接受本机,确实只读的 POST 列进 `OPEN_POSTS`,不只是读的 GET(备份文件、联网、本机文件夹)列进 `LOCAL_GETS`;新接口要想清楚属于哪一类,不要为了方便放进 `OPEN_POSTS`。
|
||||
5. **用户的文件删除一律进回收站,没有回收站就拒绝;从不覆盖**(同名另存为“名字 (2)”或拒绝)。插件自己产生的临时文件(`.part`、暂存区)可以直接删。
|
||||
6. **只读的接口也按第 3 条来。** “只是读”不是例外;能按名字、编号取的,就不收路径,由服务端在自己登记的范围里找到文件。
|
||||
7. **读用户文件要有上限。** 限制读取的大小和条数;读不了或超大的文件跳过并在结果里说明,不让一个坏文件拖垮整个请求。读到的内容进页面只当纯文本(本节第二条)。
|
||||
|
||||
+4
-1
@@ -111,6 +111,8 @@ covered by the `styles.css` manifest.
|
||||
lifecycle; `api/materials.py` owns HTTP mapping and compatibility entry points.
|
||||
`api/node_material.py` saves one canvas node's current values as a
|
||||
`node_parameter_selection` material (the current-node panel's "save").
|
||||
- `api/local_guard.py` decides which requests only this computer may make (every change under
|
||||
`/anomalous/`; other devices may look), as one middleware; see backend.md.
|
||||
- `api/media_routes.py`, `api/gallery_routes.py`, `api/translation_routes.py`, and
|
||||
`api/folder_types.py` own the formerly mixed utility route families.
|
||||
- `model_policies.py` owns shared backend rename and protected-category policy.
|
||||
@@ -294,7 +296,8 @@ covered by the `styles.css` manifest.
|
||||
seeking, search, paging, download and deletion, showing the speech/voice/seed
|
||||
recorded in each file, plus a section for unsaved temp previews that can be
|
||||
copied into `output/audio/`.
|
||||
- `ui_sidebar.js` assembles the browser window (rail, list column, header, page panels)
|
||||
- `ui_sidebar.js` assembles the browser window (rail, list column, header, page panels;
|
||||
`remote_view.js` adds the header's "view only" chip when opened from another device)
|
||||
and renders the model folder list. `ui_shell_rail.js` owns the left icon rail: one
|
||||
entry per page, the tool slots and the settings slot. `ui_shell_nav.js` owns page
|
||||
navigation (`goTo`): the domain each page needs, which pages have a list and whether
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# 📈 Anomalous Model Browser Changelog
|
||||
|
||||
## v1.58.1 Beta (Security: only this computer can change things) — 2026-10-12
|
||||
|
||||
### 🔒 Other devices can look, not change (别的设备只能看,不能改)
|
||||
- When ComfyUI is started with `--listen`, other devices on your network can open it. Until now they could also use most of Anomalous Model Browser's actions: delete or rename models, save and delete workflows, combos and prompts, start downloads and scans, export a backup of all your data, translate with your translation key, change settings, switch the plugin's version. Now every change answers only the computer ComfyUI runs on; other devices can still browse models, pictures and workflows.
|
||||
- A window opened from another device shows **View only** next to the page title; pressing a button that changes something there says why it did nothing.
|
||||
- A web page that reaches ComfyUI through another name for this computer, or a proxy, counts as another device too.
|
||||
|
||||
## v1.58.0 Beta (New Layout, Keep & Reuse, Prompt Studio, Model Check, Voices) — 2026-10-09
|
||||
|
||||
### 🧭 New layout (新布局)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
[](https://github.com/ltdrdata/ComfyUI-Manager)
|
||||
[](LICENSE)
|
||||
[](CHANGELOG.md)
|
||||
[](CHANGELOG.md)
|
||||
[](https://www.bilibili.com/video/BV1a1bv68EuA/)
|
||||
[](https://youtu.be/hAvsj7uiaCw)
|
||||
|
||||
|
||||
+5
-1
@@ -1,7 +1,7 @@
|
||||
from . import (
|
||||
activity_log, audio_catalog, folder_types, gallery_routes, materials, media_routes, model_catalog,
|
||||
model_media, model_metadata, model_resolution, model_type_listing, kept_images, node_material, recipe_packages, recipes, scan_report, scan_summary,
|
||||
translation_routes, version_manager,
|
||||
local_guard, translation_routes, version_manager,
|
||||
)
|
||||
from .scanner import *
|
||||
from .config import *
|
||||
@@ -29,6 +29,10 @@ async def no_cache_extension_middleware(request, handler):
|
||||
def setup_routes(app):
|
||||
if hasattr(app, 'middlewares') and no_cache_extension_middleware not in app.middlewares:
|
||||
app.middlewares.append(no_cache_extension_middleware)
|
||||
# Other devices on the network may look; only this computer may change things (local_guard.py).
|
||||
if hasattr(app, 'middlewares') and local_guard.local_only_middleware not in app.middlewares:
|
||||
app.middlewares.append(local_guard.local_only_middleware)
|
||||
app.router.add_get('/anomalous/access', local_guard.api_access)
|
||||
app.router.add_get('/anomalous/folders', model_catalog.api_get_folders)
|
||||
app.router.add_get('/anomalous/all_folder_types', folder_types.api_get_all_folder_types)
|
||||
app.router.add_get('/anomalous/models', model_catalog.api_get_models)
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
"""What only this computer may do. ComfyUI started with --listen lets other devices on the
|
||||
network open the page: they may look (lists, searches, pictures, covers), but every change
|
||||
under /anomalous/ (writing, moving and deleting files, downloads, scans, translations, backups,
|
||||
version switches, saved settings) answers only requests from the computer ComfyUI runs on
|
||||
(AGENTS.md 安全底线, rule 4). One middleware decides it for all routes: a request other than
|
||||
GET is local-only unless it is a read sent as POST (OPEN_POSTS); a GET is open unless it is not
|
||||
a plain read (LOCAL_GETS). A request is local when it comes from a loopback address, through no
|
||||
proxy, naming a loopback host; checking the Host also keeps out a page that reached this server
|
||||
under another name (DNS rebinding)."""
|
||||
|
||||
import ipaddress
|
||||
import urllib.parse
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
PREFIX = "/anomalous/"
|
||||
PROXY_HEADERS = ("Forwarded", "X-Forwarded-For", "X-Real-IP", "X-Forwarded-Host")
|
||||
LOCAL_NAMES = ("localhost",)
|
||||
# Reads sent as POST (their lists are long): open to other devices too. They write nothing.
|
||||
OPEN_POSTS = frozenset({
|
||||
"/anomalous/resolve_paths_to_previews",
|
||||
"/anomalous/resolve_hash_batch",
|
||||
"/anomalous/inspect_image_material",
|
||||
})
|
||||
# GETs that are more than a read: a backup of all your data, a call to GitHub, your folders.
|
||||
LOCAL_GETS = frozenset({
|
||||
"/anomalous/backup/file",
|
||||
"/anomalous/version/releases",
|
||||
"/anomalous/import/folders",
|
||||
})
|
||||
# Routes that answer other devices themselves (MCP replies in JSON-RPC).
|
||||
OWN_CHECK = frozenset({"/anomalous/mcp"})
|
||||
READS = ("GET", "HEAD", "OPTIONS")
|
||||
MESSAGE = ("只能在运行 ComfyUI 的电脑上操作,别的设备只能浏览。"
|
||||
" Only the computer ComfyUI runs on can do this; other devices can only look.")
|
||||
|
||||
|
||||
def loopback_host(name):
|
||||
if not name:
|
||||
return False
|
||||
name = name.strip("[]").lower()
|
||||
if name in LOCAL_NAMES:
|
||||
return True
|
||||
try:
|
||||
return ipaddress.ip_address(name).is_loopback
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def is_local(request):
|
||||
"""The request comes from this computer, directly, and names a loopback host."""
|
||||
if not loopback_host(request.remote or ""):
|
||||
return False
|
||||
if any(header in request.headers for header in PROXY_HEADERS):
|
||||
return False
|
||||
return loopback_host(urllib.parse.urlsplit("//" + request.headers.get("Host", "")).hostname)
|
||||
|
||||
|
||||
def needs_local(method, path):
|
||||
"""Whether a request to `path` with `method` must come from this computer."""
|
||||
if not path.startswith(PREFIX) or path in OWN_CHECK:
|
||||
return False
|
||||
if method.upper() in READS:
|
||||
return path in LOCAL_GETS
|
||||
return path not in OPEN_POSTS
|
||||
|
||||
|
||||
def refusal():
|
||||
return web.json_response({"status": "error", "success": False, "error": "local_only", "code": "local_only",
|
||||
"message": MESSAGE}, status=403)
|
||||
|
||||
|
||||
@web.middleware
|
||||
async def local_only_middleware(request, handler):
|
||||
if needs_local(request.method, request.path) and not is_local(request):
|
||||
return refusal()
|
||||
return await handler(request)
|
||||
|
||||
|
||||
async def api_access(request):
|
||||
"""GET /anomalous/access - whether this page may change things: {"local": bool}."""
|
||||
return web.json_response({"local": is_local(request)})
|
||||
@@ -40,6 +40,20 @@ inventory recognizes GGUF and PTH case-insensitively in addition to the existing
|
||||
list formats. This route-level inventory does not expand global model-format,
|
||||
scanner, metadata-parser, recovery, or cloud-identification support.
|
||||
|
||||
## Requests from other devices
|
||||
|
||||
ComfyUI started with `--listen` lets other devices on the network open the page. They may
|
||||
look; every change under `/anomalous/` answers only the computer ComfyUI runs on. One
|
||||
middleware (`api/local_guard.py`, registered first in `setup_routes`) decides it for all routes:
|
||||
a request other than GET must be local unless its path is in `OPEN_POSTS` (reads sent as POST:
|
||||
`resolve_paths_to_previews`, `resolve_hash_batch`, `inspect_image_material`); a GET is open
|
||||
unless its path is in `LOCAL_GETS` (`backup/file`, `version/releases`, `import/folders`).
|
||||
`/anomalous/mcp` answers other devices itself. Local means: a loopback peer, no proxy headers
|
||||
(`Forwarded`, `X-Forwarded-For`, `X-Real-IP`, `X-Forwarded-Host`) and a loopback `Host`, so a
|
||||
page that reached the server under another name (DNS rebinding) is refused too. A refusal is
|
||||
HTTP 403 with `code: local_only` and a bilingual message. `GET /anomalous/access` tells the page
|
||||
whether it is local; `remote_view.js` then shows a "view only" chip in the header.
|
||||
|
||||
## Storage ownership
|
||||
|
||||
Runtime UI settings and newly stored API keys live in `api/config.json`.
|
||||
|
||||
@@ -571,6 +571,8 @@ export const i18n = {
|
||||
detailBackAssistant: '⬅ 返回助手面板',
|
||||
detailJumpToBottom: '跳到底部',
|
||||
detailEdit: '编辑',
|
||||
remoteViewChip: '仅浏览',
|
||||
remoteViewHint: '这个窗口是从别的设备打开的:可以浏览;删除、移动、下载、扫描、保存这类改动,只能在运行 ComfyUI 的电脑上做。',
|
||||
detailNotesTitle: '📓 专属备注 (Notes)',
|
||||
detailEditNotes: '编辑备注',
|
||||
detailGeneratedGallery: '查看历史生成作品',
|
||||
@@ -2467,6 +2469,8 @@ export const i18n = {
|
||||
detailBackAssistant: '⬅ Back to Assistant',
|
||||
detailJumpToBottom: 'Jump to bottom',
|
||||
detailEdit: 'Edit',
|
||||
remoteViewChip: 'View only',
|
||||
remoteViewHint: 'This window was opened from another device: you can look around; deleting, moving, downloading, scanning, saving and other changes can only be made on the computer ComfyUI runs on.',
|
||||
detailNotesTitle: '📓 Custom Notes',
|
||||
detailEditNotes: 'Edit Notes',
|
||||
detailGeneratedGallery: 'View Generated Gallery',
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Opened from another device (ComfyUI started with --listen): a chip in the header says this
|
||||
* window can look but not change anything (the server refuses changes from other devices,
|
||||
* api/local_guard.py). Nothing shows on the computer ComfyUI runs on.
|
||||
*/
|
||||
|
||||
import { translate as t } from './locales.js';
|
||||
|
||||
/** Asks the server once whether this page is on its computer; when not, marks the window and adds the chip to `group`. */
|
||||
export async function markRemoteView(container, group) {
|
||||
try {
|
||||
const response = await fetch('/anomalous/access', { cache: 'no-store' });
|
||||
const data = response.ok ? await response.json() : null;
|
||||
if (!data || data.local !== false) return;
|
||||
} catch {
|
||||
return; // an older server without the route: nothing to say
|
||||
}
|
||||
container.classList.add('anomalous-remote-view');
|
||||
const chip = document.createElement('span');
|
||||
chip.className = 'anomalous-remote-chip';
|
||||
chip.textContent = t('remoteViewChip');
|
||||
chip.title = t('remoteViewHint');
|
||||
group.append(chip);
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
* The browser window (rail, list column, header, page panels) and the model folder list.
|
||||
*/
|
||||
|
||||
import { markRemoteView } from './remote_view.js';
|
||||
import { translate } from './locales.js';
|
||||
import { escapeHtml } from './safe_dom.js';
|
||||
import { showUpdateGuide } from './ui_update_guide.js';
|
||||
@@ -157,6 +158,7 @@ export function createDOM() {
|
||||
|
||||
rightGroup.append(updateNoticeBtn, dockBtn, closeBtn);
|
||||
header.append(leftGroup, centerGroup, rightGroup);
|
||||
markRemoteView(container, leftGroup); // opened from another device: "view only"
|
||||
|
||||
this.grid = panel('anomalous-grid', '');
|
||||
this.detailPanel = panel('anomalous-detail');
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
@import url("./styles/12-tts-setup.css?v=20261002-theme-1");
|
||||
@import url("./styles/13-script-director.css?v=20261002-voiceover-2");
|
||||
@import url("./styles/14-version-manager.css?v=20261002-theme-1");
|
||||
@import url("./styles/15-shell-rail.css?v=20261003-feedback-2");
|
||||
@import url("./styles/15-shell-rail.css?v=20261012-remote");
|
||||
@import url("./styles/16-activity.css?v=20261002-theme-1");
|
||||
@import url("./styles/17-model-types.css?v=20261002-theme-1");
|
||||
@import url("./styles/18-scan-page.css?v=20261003-folders-1");
|
||||
|
||||
@@ -517,3 +517,16 @@
|
||||
color: var(--amb-btn-primary-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Opened from another device (remote_view.js): a quiet "view only" chip beside the page title. */
|
||||
.anomalous-remote-chip {
|
||||
flex-shrink: 0;
|
||||
margin-left: 8px;
|
||||
padding: 2px 9px;
|
||||
border: 1px dashed var(--amb-border-strong);
|
||||
border-radius: 999px;
|
||||
color: var(--amb-text-muted);
|
||||
font-size: 11.5px;
|
||||
white-space: nowrap;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user