88from pydantic import BaseModel , ConfigDict
99
1010from api .support import require_admin , require_identity , resolve_image_base_url
11+ from services .auth_service import auth_service
1112from services .backup_service import BackupError , backup_service
1213from services .config import config
13- from services .image_service import delete_images , download_images_zip , get_image_download_response , get_thumbnail_response , list_images
14+ from services .image_owners_service import owner_counts
15+ from services .image_service import count_total_images , delete_images , download_images_zip , get_image_download_response , get_thumbnail_response , list_images
1416from services .image_tags_service import delete_tag , get_all_tags , set_tags
1517from services .log_service import log_service
1618from services .proxy_service import test_proxy
1719
1820
21+ def _admin_owner_ids () -> set [str ]:
22+ """收集所有可能落在 image_owners.json 里的 admin id:
23+ - "admin":旧 auth_key(CHATGPT2API_AUTH_KEY / config.json.auth-key)的固定 id
24+ - 其余:通过 auth_service 创建的 admin 角色密钥
25+ 用来把"管理员生成"和"真孤儿"两个桶区分开,别再混在一起。
26+ """
27+ ids : set [str ] = {"admin" }
28+ for item in auth_service .list_keys (role = "admin" ):
29+ uid = str (item .get ("id" ) or "" ).strip ()
30+ if uid :
31+ ids .add (uid )
32+ return ids
33+
34+
35+ def _admin_owner_ids () -> set [str ]:
36+ """收集所有可能落在 image_owners.json 里的 admin id:
37+ - "admin":旧 auth_key(CHATGPT2API_AUTH_KEY / config.json.auth-key)的固定 id
38+ - 其余:通过 auth_service 创建的 admin 角色密钥
39+ 用来把"管理员生成"和"真孤儿"两个桶区分开,别再混在一起。
40+ """
41+ ids : set [str ] = {"admin" }
42+ for item in auth_service .list_keys (role = "admin" ):
43+ uid = str (item .get ("id" ) or "" ).strip ()
44+ if uid :
45+ ids .add (uid )
46+ return ids
47+
48+
1949class SettingsUpdateRequest (BaseModel ):
2050 model_config = ConfigDict (extra = "allow" )
2151
@@ -28,6 +58,7 @@ class ImageDeleteRequest(BaseModel):
2858 paths : list [str ] = []
2959 start_date : str = ""
3060 end_date : str = ""
61+ owner : str = ""
3162 all_matching : bool = False
3263
3364class ImageDownloadRequest (BaseModel ):
@@ -72,9 +103,68 @@ async def save_settings(body: SettingsUpdateRequest, authorization: str | None =
72103 return {"config" : config .update (body .model_dump (mode = "python" ))}
73104
74105 @router .get ("/api/images" )
75- async def get_images (request : Request , start_date : str = "" , end_date : str = "" , authorization : str | None = Header (default = None )):
106+ async def get_images (
107+ request : Request ,
108+ start_date : str = "" ,
109+ end_date : str = "" ,
110+ owner : str = "" ,
111+ authorization : str | None = Header (default = None ),
112+ ):
113+ require_admin (authorization )
114+ return list_images (
115+ resolve_image_base_url (request ),
116+ start_date = start_date .strip (),
117+ end_date = end_date .strip (),
118+ owner = owner .strip (),
119+ admin_ids = _admin_owner_ids (),
120+ )
121+
122+ @router .get ("/api/images/owners" )
123+ async def get_image_owners (authorization : str | None = Header (default = None )):
124+ """图片管理页用户筛选下拉的数据源。
125+ 三类语义,互不混淆:
126+ 1. 普通用户:列出所有用户密钥(即便 count=0),admin 期望"我建过的密钥都能筛"
127+ 2. 管理员(__admin__):所有 admin 角色(含旧 auth_key 的 "admin" id)生成的图聚合
128+ 3. 未归属(__unowned__):image_owners.json 里没记录的真孤儿,多半是老数据
129+ 孤儿 user id(用户密钥已被删但归属表还留着)单列出来,标记 deleted=true。
130+ """
76131 require_admin (authorization )
77- return list_images (resolve_image_base_url (request ), start_date = start_date .strip (), end_date = end_date .strip ())
132+ counts = owner_counts ()
133+ admin_ids = _admin_owner_ids ()
134+ users = auth_service .list_keys (role = "user" )
135+ items : list [dict [str , object ]] = []
136+ seen_ids : set [str ] = set ()
137+ for user in users :
138+ uid = str (user .get ("id" ) or "" ).strip ()
139+ if not uid :
140+ continue
141+ seen_ids .add (uid )
142+ items .append ({
143+ "id" : uid ,
144+ "name" : user .get ("name" ) or uid ,
145+ "deleted" : False ,
146+ "count" : int (counts .get (uid , 0 )),
147+ })
148+ # admin 集合本身已经独立成一桶,所以 seen_ids 里要带上 admin_ids 防止重复
149+ seen_ids |= admin_ids
150+ admin_count = sum (int (c ) for k , c in counts .items () if k in admin_ids )
151+ for owner_id , count in counts .items ():
152+ if not owner_id or owner_id in seen_ids :
153+ continue
154+ items .append ({
155+ "id" : owner_id ,
156+ "name" : owner_id ,
157+ "deleted" : True ,
158+ "count" : int (count ),
159+ })
160+ items .sort (key = lambda x : (- int (x .get ("count" ) or 0 ), str (x .get ("name" ) or "" )))
161+ # 真孤儿 = 总图片数 − 已挂归属的所有图(含 admin / 用户 / 已删用户)
162+ owned_total = sum (int (v ) for v in counts .values ())
163+ unowned_count = max (0 , count_total_images () - owned_total )
164+ # 两个固定桶;前端会把它们置顶到列表最上方。
165+ items .append ({"id" : "__admin__" , "name" : "管理员" , "deleted" : False , "count" : admin_count })
166+ items .append ({"id" : "__unowned__" , "name" : "未归属" , "deleted" : False , "count" : unowned_count })
167+ return {"items" : items }
78168
79169 @router .get ("/image-thumbnails/{image_path:path}" , include_in_schema = False )
80170 async def get_image_thumbnail (image_path : str ):
@@ -83,7 +173,14 @@ async def get_image_thumbnail(image_path: str):
83173 @router .post ("/api/images/delete" )
84174 async def delete_images_endpoint (body : ImageDeleteRequest , authorization : str | None = Header (default = None )):
85175 require_admin (authorization )
86- return delete_images (body .paths , start_date = body .start_date .strip (), end_date = body .end_date .strip (), all_matching = body .all_matching )
176+ return delete_images (
177+ body .paths ,
178+ start_date = body .start_date .strip (),
179+ end_date = body .end_date .strip (),
180+ owner = body .owner .strip (),
181+ all_matching = body .all_matching ,
182+ admin_ids = _admin_owner_ids (),
183+ )
87184
88185 @router .post ("/api/images/download" )
89186 async def download_images_endpoint (body : ImageDownloadRequest , authorization : str | None = Header (default = None )):
0 commit comments