feat: complete image console and add verified REST and MCP workflows
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""服务层。"""
|
||||
@@ -0,0 +1,143 @@
|
||||
"""画廊服务:扫描 output/ 目录结构。
|
||||
|
||||
output/ 下的每个一级子目录 = 一个画廊,目录名 = 谁生成的图片(profile_name)。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from .. import config, db
|
||||
from ..providers import ProviderError
|
||||
|
||||
|
||||
def _folder(profile_name: str) -> Path:
|
||||
if not config.PROFILE_RE.fullmatch(profile_name):
|
||||
raise ProviderError("无效的画廊名称", status=400)
|
||||
root = config.OUTPUT_DIR.resolve()
|
||||
folder = (root / profile_name).resolve()
|
||||
if not folder.is_relative_to(root):
|
||||
raise ProviderError("画廊路径超出输出目录", status=400)
|
||||
return folder
|
||||
|
||||
|
||||
def _meta_by_file() -> dict[tuple[str, str], dict[str, Any]]:
|
||||
"""从 generations 表建立 (profile, filename) → 元信息的索引。"""
|
||||
index: dict[tuple[str, str], dict[str, Any]] = {}
|
||||
with db.session() as conn:
|
||||
rows = conn.execute("SELECT * FROM generations ORDER BY created_at DESC").fetchall()
|
||||
for row in rows:
|
||||
try:
|
||||
files = json.loads(row["files"] or "[]")
|
||||
except ValueError:
|
||||
files = []
|
||||
for name in files:
|
||||
key = (row["profile_name"], name)
|
||||
index.setdefault(
|
||||
key,
|
||||
{
|
||||
"model": row["model"],
|
||||
"provider_name": row["provider_name"],
|
||||
"created_at": row["created_at"],
|
||||
},
|
||||
)
|
||||
return index
|
||||
|
||||
|
||||
def _images_in(folder: Path) -> list[Path]:
|
||||
return sorted(
|
||||
(p for p in folder.iterdir() if p.is_file() and p.suffix.lower() in config.IMAGE_SUFFIXES),
|
||||
key=lambda p: p.stat().st_mtime,
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
|
||||
def list_galleries() -> list[dict[str, Any]]:
|
||||
config.ensure_dirs()
|
||||
meta = _meta_by_file()
|
||||
galleries: list[dict[str, Any]] = []
|
||||
for folder in sorted(config.OUTPUT_DIR.iterdir()):
|
||||
if not folder.is_dir():
|
||||
continue
|
||||
images = _images_in(folder)
|
||||
if not images:
|
||||
continue
|
||||
total_bytes = 0
|
||||
models: list[str] = []
|
||||
latest = 0.0
|
||||
for img in images:
|
||||
stat = img.stat()
|
||||
total_bytes += stat.st_size
|
||||
latest = max(latest, stat.st_mtime)
|
||||
info = meta.get((folder.name, img.name))
|
||||
if info and info["model"] and info["model"] not in models:
|
||||
models.append(info["model"])
|
||||
cover = images[0]
|
||||
galleries.append(
|
||||
{
|
||||
"name": folder.name,
|
||||
"count": len(images),
|
||||
"bytes": total_bytes,
|
||||
"updated_at": datetime.fromtimestamp(latest).strftime("%Y-%m-%d %H:%M"),
|
||||
"cover": f"/output/{folder.name}/{cover.name}",
|
||||
"models": models,
|
||||
}
|
||||
)
|
||||
galleries.sort(key=lambda g: g["updated_at"], reverse=True)
|
||||
return galleries
|
||||
|
||||
|
||||
def list_gallery(profile_name: str) -> list[dict[str, Any]]:
|
||||
config.ensure_dirs()
|
||||
folder = _folder(profile_name)
|
||||
if not folder.is_dir():
|
||||
return []
|
||||
meta = _meta_by_file()
|
||||
items: list[dict[str, Any]] = []
|
||||
for img in _images_in(folder):
|
||||
stat = img.stat()
|
||||
info = meta.get((profile_name, img.name), {})
|
||||
items.append(
|
||||
{
|
||||
"profile_name": profile_name,
|
||||
"filename": img.name,
|
||||
"url": f"/output/{profile_name}/{img.name}",
|
||||
"bytes": stat.st_size,
|
||||
"model": info.get("model", "—"),
|
||||
"provider_name": info.get("provider_name", "—"),
|
||||
"created_at": info.get("created_at")
|
||||
or datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M"),
|
||||
}
|
||||
)
|
||||
return items
|
||||
|
||||
|
||||
def delete_gallery_item(profile_name: str, filename: str) -> None:
|
||||
folder = _folder(profile_name)
|
||||
if Path(filename).name != filename or "\\" in filename:
|
||||
raise ProviderError("无效的文件名", status=400)
|
||||
target = folder / filename
|
||||
if not target.resolve().is_relative_to(folder):
|
||||
raise ProviderError("图片路径超出画廊目录", status=400)
|
||||
if not target.is_file():
|
||||
raise FileNotFoundError(f"文件不存在: {profile_name}/{filename}")
|
||||
target.unlink()
|
||||
# 同步 generations 记录
|
||||
with db.session() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT id, files FROM generations WHERE profile_name=?", (profile_name,)
|
||||
).fetchall()
|
||||
for row in rows:
|
||||
try:
|
||||
files = json.loads(row["files"] or "[]")
|
||||
except ValueError:
|
||||
files = []
|
||||
if filename in files:
|
||||
files = [f for f in files if f != filename]
|
||||
conn.execute(
|
||||
"UPDATE generations SET files=? WHERE id=?",
|
||||
(json.dumps(files, ensure_ascii=False), row["id"]),
|
||||
)
|
||||
@@ -0,0 +1,207 @@
|
||||
"""出图服务:调用适配器、落盘、记录 generations。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import time
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
from .. import config, db
|
||||
from ..providers import ProviderError, get_adapter
|
||||
from ..providers.agnes import size_to_tier_and_ratio
|
||||
from ..schemas import GenerateIn
|
||||
from . import providers as provider_service
|
||||
|
||||
WEB_PROFILE = "user"
|
||||
_SAFE = re.compile(r"[^A-Za-z0-9._-]+")
|
||||
|
||||
|
||||
def _safe_model(model: str) -> str:
|
||||
return _SAFE.sub("-", model).strip("-") or "image"
|
||||
|
||||
|
||||
def record_generation(
|
||||
*,
|
||||
provider_id: str,
|
||||
provider_name: str,
|
||||
model: str,
|
||||
profile_name: str,
|
||||
prompt: str,
|
||||
size: str,
|
||||
ratio: str,
|
||||
n: int,
|
||||
status: str,
|
||||
error: str,
|
||||
files: list[str],
|
||||
duration_ms: int,
|
||||
) -> str:
|
||||
gid = "gen_" + uuid.uuid4().hex[:12]
|
||||
with db.session() as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO generations (id,provider_id,provider_name,model,profile_name,prompt,size,"
|
||||
"ratio,n,status,error,files,duration_ms,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
(
|
||||
gid,
|
||||
provider_id,
|
||||
provider_name,
|
||||
model,
|
||||
profile_name,
|
||||
prompt,
|
||||
size,
|
||||
ratio,
|
||||
n,
|
||||
status,
|
||||
error,
|
||||
json.dumps(files, ensure_ascii=False),
|
||||
duration_ms,
|
||||
datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
),
|
||||
)
|
||||
return gid
|
||||
|
||||
|
||||
def list_generations() -> list[dict[str, Any]]:
|
||||
with db.session() as conn:
|
||||
rows = conn.execute("SELECT * FROM generations ORDER BY created_at DESC, rowid DESC").fetchall()
|
||||
out: list[dict[str, Any]] = []
|
||||
for row in rows:
|
||||
try:
|
||||
files = json.loads(row["files"] or "[]")
|
||||
except ValueError:
|
||||
files = []
|
||||
out.append(
|
||||
{
|
||||
"id": row["id"],
|
||||
"provider_id": row["provider_id"],
|
||||
"provider_name": row["provider_name"],
|
||||
"model": row["model"],
|
||||
"profile_name": row["profile_name"],
|
||||
"prompt": row["prompt"],
|
||||
"size": row["size"],
|
||||
"ratio": row["ratio"],
|
||||
"n": row["n"],
|
||||
"status": row["status"],
|
||||
"error": row["error"],
|
||||
"files": files,
|
||||
"duration_ms": row["duration_ms"],
|
||||
"created_at": row["created_at"],
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def generate(
|
||||
*,
|
||||
provider_id: str,
|
||||
model: str,
|
||||
prompt: str,
|
||||
size: str = "1024x1024",
|
||||
ratio: str = "",
|
||||
n: int = 1,
|
||||
images: list[str] | None = None,
|
||||
profile_name: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
try:
|
||||
payload = GenerateIn(provider_id=provider_id, model=model, prompt=prompt, size=size,
|
||||
ratio=ratio, n=n, images=images or [], profile_name=profile_name)
|
||||
except ValidationError as exc:
|
||||
raise ProviderError(str(exc), status=400) from exc
|
||||
prompt = payload.prompt
|
||||
size = payload.size
|
||||
profile = payload.profile_name or WEB_PROFILE
|
||||
cfg = provider_service.provider_config(provider_id, require_enabled=True)
|
||||
selected = provider_service.get_model(f"{provider_id}:{model}")
|
||||
if not selected["enabled"]:
|
||||
raise ProviderError("模型已停用", status=400)
|
||||
provider_name = cfg.name
|
||||
adapter = get_adapter(cfg)
|
||||
if images and not adapter.capabilities.supports_image_input:
|
||||
raise ProviderError("该供应商不支持参考图输入", status=400)
|
||||
if ratio and cfg.adapter != "agnes":
|
||||
raise ProviderError("该供应商不支持 ratio,请使用 WxH 尺寸", status=400)
|
||||
if cfg.adapter == "agnes":
|
||||
_, ratio = size_to_tier_and_ratio(size, ratio)
|
||||
elif "x" in size:
|
||||
width, height = map(int, size.split("x"))
|
||||
divisor = math.gcd(width, height)
|
||||
ratio = f"{width // divisor}:{height // divisor}"
|
||||
|
||||
started = time.monotonic()
|
||||
files: list[str] = []
|
||||
folder = config.OUTPUT_DIR / profile
|
||||
if not folder.resolve().is_relative_to(config.OUTPUT_DIR.resolve()):
|
||||
raise ProviderError("画廊路径超出输出目录", status=400)
|
||||
images_out: list[dict[str, Any]] = []
|
||||
try:
|
||||
results = adapter.generate(model, prompt, size, ratio, n, images or [])
|
||||
if not results:
|
||||
raise ProviderError("上游未返回图片")
|
||||
folder.mkdir(parents=True, exist_ok=True)
|
||||
stamp = uuid.uuid4().hex[:8]
|
||||
for idx, item in enumerate(results):
|
||||
if item.url and not item.data:
|
||||
item = adapter.download(item.url)
|
||||
if not item.data:
|
||||
raise ProviderError("上游返回空图片")
|
||||
suffix = item.filename if item.filename in config.IMAGE_SUFFIXES else ".png"
|
||||
filename = f"{_safe_model(model)}-{stamp}{'' if len(results) == 1 else f'-{idx + 1}'}{suffix}"
|
||||
path = folder / filename
|
||||
files.append(filename)
|
||||
path.write_bytes(item.data)
|
||||
images_out.append({"filename": filename, "url": f"/output/{profile}/{filename}",
|
||||
"bytes": path.stat().st_size})
|
||||
except (ProviderError, OSError) as exc:
|
||||
for filename in files:
|
||||
(folder / filename).unlink(missing_ok=True)
|
||||
message = exc.message if isinstance(exc, ProviderError) else "保存图片失败"
|
||||
duration = int((time.monotonic() - started) * 1000)
|
||||
record_generation(
|
||||
provider_id=provider_id,
|
||||
provider_name=provider_name,
|
||||
model=model,
|
||||
profile_name=profile,
|
||||
prompt=prompt,
|
||||
size=size,
|
||||
ratio=ratio,
|
||||
n=n,
|
||||
status="failed",
|
||||
error=message,
|
||||
files=[],
|
||||
duration_ms=duration,
|
||||
)
|
||||
raise ProviderError(message, status=exc.status if isinstance(exc, ProviderError) else 500) from exc
|
||||
|
||||
duration = int((time.monotonic() - started) * 1000)
|
||||
gid = record_generation(
|
||||
provider_id=provider_id,
|
||||
provider_name=provider_name,
|
||||
model=model,
|
||||
profile_name=profile,
|
||||
prompt=prompt,
|
||||
size=size,
|
||||
ratio=ratio or "1:1",
|
||||
n=n,
|
||||
status="success",
|
||||
error="",
|
||||
files=files,
|
||||
duration_ms=duration,
|
||||
)
|
||||
|
||||
return {
|
||||
"id": gid,
|
||||
"provider": provider_id,
|
||||
"provider_name": provider_name,
|
||||
"model": model,
|
||||
"profile_name": profile,
|
||||
"size": size,
|
||||
"ratio": ratio or "1:1",
|
||||
"images": images_out,
|
||||
"output_dir": f"output/{profile}/",
|
||||
"created_at": datetime.now().strftime("%Y-%m-%d %H:%M"),
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
"""供应商与模型的服务层:CRUD、密钥加解密、模型发现与落库。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from .. import db, security
|
||||
from ..providers import ADAPTER_LABELS, ProviderConfig, ProviderError, get_adapter
|
||||
|
||||
|
||||
def now() -> str:
|
||||
return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
def _row_to_dict(row) -> dict[str, Any]:
|
||||
models = _models_of(row["id"])
|
||||
return {
|
||||
"id": row["id"],
|
||||
"name": row["name"],
|
||||
"adapter": row["adapter"],
|
||||
"adapter_label": ADAPTER_LABELS.get(row["adapter"], row["adapter"]),
|
||||
"base_url": row["base_url"],
|
||||
"api_key_masked": security.mask(security.decrypt(row["api_key"] or "")),
|
||||
"default_model": row["default_model"],
|
||||
"enabled": bool(row["enabled"]),
|
||||
"note": row["note"],
|
||||
"model_count": len(models),
|
||||
"enabled_model_count": sum(1 for m in models if m["enabled"]),
|
||||
"models_discovered": _latest_discovered(row["id"]),
|
||||
"created_at": row["created_at"],
|
||||
"updated_at": row["updated_at"],
|
||||
}
|
||||
|
||||
|
||||
def _models_of(provider_id: str) -> list[dict[str, Any]]:
|
||||
with db.session() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM models WHERE provider_id = ? ORDER BY model_id", (provider_id,)
|
||||
).fetchall()
|
||||
return [_model_to_dict(r) for r in rows]
|
||||
|
||||
|
||||
def _model_to_dict(row) -> dict[str, Any]:
|
||||
return {
|
||||
"id": row["id"],
|
||||
"provider_id": row["provider_id"],
|
||||
"model_id": row["model_id"],
|
||||
"display_name": row["display_name"],
|
||||
"enabled": bool(row["enabled"]),
|
||||
"kind": row["kind"],
|
||||
"size_hint": row["size_hint"],
|
||||
"supports_image_input": bool(row["supports_image_input"]),
|
||||
"note": row["note"],
|
||||
"discovered_at": row["discovered_at"],
|
||||
}
|
||||
|
||||
|
||||
def _latest_discovered(provider_id: str) -> str:
|
||||
with db.session() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT MAX(discovered_at) AS d FROM models WHERE provider_id = ?", (provider_id,)
|
||||
).fetchone()
|
||||
return (row["d"] if row and row["d"] else "—") or "—"
|
||||
|
||||
|
||||
# --------------------------------------------------------------- 供应商 CRUD
|
||||
def list_providers() -> list[dict[str, Any]]:
|
||||
with db.session() as conn:
|
||||
rows = conn.execute("SELECT * FROM providers ORDER BY created_at, id").fetchall()
|
||||
return [_row_to_dict(r) for r in rows]
|
||||
|
||||
|
||||
def get_provider_row(provider_id: str):
|
||||
with db.session() as conn:
|
||||
return conn.execute("SELECT * FROM providers WHERE id = ?", (provider_id,)).fetchone()
|
||||
|
||||
|
||||
def get_provider(provider_id: str) -> dict[str, Any] | None:
|
||||
row = get_provider_row(provider_id)
|
||||
return _row_to_dict(row) if row else None
|
||||
|
||||
|
||||
def save_provider(data: dict[str, Any]) -> dict[str, Any]:
|
||||
"""新增或更新供应商。api_key 留空表示不修改。"""
|
||||
pid = data["id"].strip()
|
||||
ts = now()
|
||||
existing = get_provider_row(pid)
|
||||
if existing:
|
||||
api_key_enc = existing["api_key"]
|
||||
if data.get("api_key"):
|
||||
api_key_enc = security.encrypt(data["api_key"])
|
||||
with db.session() as conn:
|
||||
conn.execute(
|
||||
"UPDATE providers SET name=?, adapter=?, base_url=?, api_key=?, default_model=?,"
|
||||
" enabled=?, note=?, updated_at=? WHERE id=?",
|
||||
(
|
||||
data["name"],
|
||||
data["adapter"],
|
||||
data["base_url"],
|
||||
api_key_enc,
|
||||
data.get("default_model", ""),
|
||||
1 if data.get("enabled", True) else 0,
|
||||
data.get("note", ""),
|
||||
ts,
|
||||
pid,
|
||||
),
|
||||
)
|
||||
else:
|
||||
with db.session() as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO providers (id,name,adapter,base_url,api_key,default_model,enabled,"
|
||||
"note,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)",
|
||||
(
|
||||
pid,
|
||||
data["name"],
|
||||
data["adapter"],
|
||||
data["base_url"],
|
||||
security.encrypt(data.get("api_key", "")),
|
||||
data.get("default_model", ""),
|
||||
1 if data.get("enabled", True) else 0,
|
||||
data.get("note", ""),
|
||||
ts,
|
||||
ts,
|
||||
),
|
||||
)
|
||||
return get_provider(pid)
|
||||
|
||||
|
||||
def delete_provider(provider_id: str) -> None:
|
||||
with db.session() as conn:
|
||||
conn.execute("DELETE FROM providers WHERE id = ?", (provider_id,))
|
||||
|
||||
|
||||
def provider_config(provider_id: str, *, require_enabled: bool = False) -> ProviderConfig:
|
||||
row = get_provider_row(provider_id)
|
||||
if row is None:
|
||||
raise ProviderError(f"供应商不存在: {provider_id}", status=404)
|
||||
if require_enabled and not row["enabled"]:
|
||||
raise ProviderError("供应商已停用", status=400)
|
||||
return ProviderConfig(
|
||||
id=row["id"],
|
||||
name=row["name"],
|
||||
adapter=row["adapter"],
|
||||
base_url=row["base_url"],
|
||||
api_key=security.decrypt(row["api_key"] or ""),
|
||||
default_model=row["default_model"],
|
||||
)
|
||||
|
||||
|
||||
# ----------------------------------------------------------------- 模型
|
||||
def list_models(provider_id: str | None = None) -> list[dict[str, Any]]:
|
||||
with db.session() as conn:
|
||||
if provider_id:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM models WHERE provider_id = ? ORDER BY model_id", (provider_id,)
|
||||
).fetchall()
|
||||
else:
|
||||
rows = conn.execute("SELECT * FROM models ORDER BY provider_id, model_id").fetchall()
|
||||
return [_model_to_dict(r) for r in rows]
|
||||
|
||||
|
||||
def set_model_enabled(model_id: str, enabled: bool) -> dict[str, Any]:
|
||||
with db.session() as conn:
|
||||
conn.execute(
|
||||
"UPDATE models SET enabled=?, updated_at=? WHERE id=?", (1 if enabled else 0, now(), model_id)
|
||||
)
|
||||
return get_model(model_id)
|
||||
|
||||
|
||||
def set_model_note(model_id: str, note: str) -> dict[str, Any]:
|
||||
with db.session() as conn:
|
||||
conn.execute(
|
||||
"UPDATE models SET note=?, updated_at=? WHERE id=?", (note, now(), model_id)
|
||||
)
|
||||
return get_model(model_id)
|
||||
|
||||
|
||||
def set_models_enabled(model_ids: list[str], enabled: bool) -> list[dict[str, Any]]:
|
||||
ts = now()
|
||||
with db.session() as conn:
|
||||
for mid in model_ids:
|
||||
conn.execute(
|
||||
"UPDATE models SET enabled=?, updated_at=? WHERE id=?", (1 if enabled else 0, ts, mid)
|
||||
)
|
||||
return list_models()
|
||||
|
||||
|
||||
def get_model(model_id: str) -> dict[str, Any]:
|
||||
with db.session() as conn:
|
||||
row = conn.execute("SELECT * FROM models WHERE id = ?", (model_id,)).fetchone()
|
||||
if row is None:
|
||||
raise ProviderError(f"模型不存在: {model_id}", status=404)
|
||||
return _model_to_dict(row)
|
||||
|
||||
|
||||
def upsert_models(
|
||||
provider_id: str,
|
||||
model_ids: list[str],
|
||||
enabled: bool = True,
|
||||
note: str = "自动发现",
|
||||
exclusive: bool = False,
|
||||
preserve_enabled: bool = False,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""批量写入模型;已存在的仅更新启用态与发现时间。
|
||||
|
||||
exclusive=True 时,把该供应商下未被选中的模型置为停用。
|
||||
"""
|
||||
cfg = provider_config(provider_id)
|
||||
capabilities = get_adapter(cfg).capabilities
|
||||
ts = now()
|
||||
today = ts[:10]
|
||||
with db.session() as conn:
|
||||
if exclusive:
|
||||
keep = {f"{provider_id}:{m}" for m in model_ids}
|
||||
for row in conn.execute(
|
||||
"SELECT id FROM models WHERE provider_id = ?", (provider_id,)
|
||||
).fetchall():
|
||||
if row["id"] not in keep:
|
||||
conn.execute(
|
||||
"UPDATE models SET enabled=0, updated_at=? WHERE id=?", (ts, row["id"])
|
||||
)
|
||||
for mid in model_ids:
|
||||
model_id = f"{provider_id}:{mid}"
|
||||
exists = conn.execute("SELECT id FROM models WHERE id = ?", (model_id,)).fetchone()
|
||||
if exists:
|
||||
conn.execute(
|
||||
"UPDATE models SET enabled=CASE WHEN ? THEN enabled ELSE ? END,"
|
||||
" discovered_at=?, updated_at=?, kind=?, size_hint=?, supports_image_input=? WHERE id=?",
|
||||
(preserve_enabled, int(enabled), today, ts, capabilities.kind,
|
||||
capabilities.size_hint, int(capabilities.supports_image_input), model_id),
|
||||
)
|
||||
else:
|
||||
conn.execute(
|
||||
"INSERT INTO models (id,provider_id,model_id,display_name,enabled,kind,size_hint,"
|
||||
"supports_image_input,note,discovered_at,created_at,updated_at)"
|
||||
" VALUES (?,?,?,?,?,?,?,?,?,?,?,?)",
|
||||
(model_id, provider_id, mid, mid, int(enabled), capabilities.kind,
|
||||
capabilities.size_hint, int(capabilities.supports_image_input), note, today, ts, ts),
|
||||
)
|
||||
return list_models(provider_id)
|
||||
|
||||
|
||||
# ------------------------------------------------------------- 模型发现
|
||||
def discover(provider_id: str, *, require_enabled: bool = False) -> dict[str, Any]:
|
||||
"""按已保存的供应商拉取上游模型,返回 {provider_id,total,matched}。"""
|
||||
cfg = provider_config(provider_id, require_enabled=require_enabled)
|
||||
adapter = get_adapter(cfg)
|
||||
all_ids = adapter.list_models()
|
||||
matched = adapter.filter_image_models(all_ids)
|
||||
upsert_models(provider_id, matched, enabled=True, preserve_enabled=True)
|
||||
if require_enabled:
|
||||
enabled = {m["model_id"] for m in list_models(provider_id) if m["enabled"]}
|
||||
matched = [mid for mid in matched if mid in enabled]
|
||||
return {"provider_id": provider_id, "total": len(all_ids), "matched": matched}
|
||||
|
||||
|
||||
def discover_with_credentials(
|
||||
adapter_name: str, base_url: str, api_key: str
|
||||
) -> dict[str, Any]:
|
||||
"""尚未保存供应商时的临时发现。"""
|
||||
cfg = ProviderConfig(
|
||||
id="__tmp__",
|
||||
name="__tmp__",
|
||||
adapter=adapter_name,
|
||||
base_url=base_url,
|
||||
api_key=api_key,
|
||||
)
|
||||
adapter = get_adapter(cfg)
|
||||
all_ids = adapter.list_models()
|
||||
matched = adapter.filter_image_models(all_ids)
|
||||
return {"total": len(all_ids), "matched": matched}
|
||||
@@ -0,0 +1,28 @@
|
||||
"""SKILL.md 读取服务。
|
||||
|
||||
文档本身不含固定模型名,模型清单一律来自 discover_models 的实时返回。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from .. import config
|
||||
|
||||
def _read() -> str:
|
||||
if config.SKILL_PATH.is_file():
|
||||
return config.SKILL_PATH.read_text(encoding="utf-8")
|
||||
return ""
|
||||
|
||||
|
||||
def get_skill() -> dict[str, str]:
|
||||
body = _read()
|
||||
stat = config.SKILL_PATH.stat() if config.SKILL_PATH.is_file() else None
|
||||
if stat:
|
||||
mtime = datetime.fromtimestamp(stat.st_mtime)
|
||||
updated_at = mtime.strftime("%Y-%m-%d %H:%M")
|
||||
version = mtime.strftime("%Y-%m-%d")
|
||||
else:
|
||||
updated_at = "—"
|
||||
version = "—"
|
||||
return {"version": version, "updated_at": updated_at, "body": body}
|
||||
Reference in New Issue
Block a user