This commit is contained in:
Your Name
2026-08-07 17:51:57 +08:00
parent 6119fdd767
commit 327a0bc42f
29 changed files with 1610 additions and 579 deletions
+41 -33
View File
@@ -44,11 +44,15 @@ from desktop_router import router as desktop_router
from link_cards_router import router as link_cards_router, UPLOAD_DIR as LINK_CARD_UPLOAD_DIR
from auth.dependencies import (
get_current_user,
require_accounts_write,
require_admin,
require_accounts_cookie,
require_accounts_create,
require_accounts_delete,
require_accounts_start,
require_accounts_stop,
require_accounts_update,
require_messages_write,
require_rules_write,
require_write,
require_system_logs_clear,
)
from auth.account_limits import ensure_can_add_account
from auth.scopes import (
@@ -61,7 +65,7 @@ from auth.scopes import (
received_logs_for_user,
system_logs_for_user,
)
from auth.roles import has_permission, is_admin
from auth.roles import has_global_scope, has_permission, is_admin
from auth.permissions import LOGS_READ, RECEIVED_MESSAGES_READ, SYSTEM_LOGS_READ
from auth.passwords import hash_password
from rpa_engine.batch_start import BatchStartQueue
@@ -1413,7 +1417,7 @@ async def get_account_options(
Account.reply_cooldown_seconds,
Account.quota_disabled,
)
if not is_admin(user.role):
if not has_global_scope(user.role):
stmt = stmt.where(Account.owner_id == user.id)
rows = (await db.execute(stmt.order_by(Account.id.asc()))).all()
@@ -1632,9 +1636,9 @@ async def update_account(
account_id: int,
body: AccountUpdate,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_accounts_write),
user: User = Depends(require_accounts_update),
):
account = await get_owned_account(db, user, account_id, write=True)
account = await get_owned_account(db, user, account_id, write=True, write_permission="accounts.update")
follow_config_changed = bool(
{"follow_welcome_enabled", "follow_welcome_content"}
& set(body.model_fields_set)
@@ -1700,7 +1704,7 @@ async def get_reply_queue_summaries(
allowed_ids = await owned_account_ids(db, user)
worker_entries = list(manager.workers.items())
else:
if is_admin(user.role):
if has_global_scope(user.role):
allowed_ids = set(requested_ids)
elif requested_ids:
owned_result = await db.execute(
@@ -1793,7 +1797,7 @@ async def send_account_queued_reply_now(
user: User = Depends(require_messages_write),
):
"""将指定任务原子移入紧急队列,并把它后面的普通任务前移一槽。"""
await get_owned_account(db, user, account_id, write=True)
await get_owned_account(db, user, account_id, write=True, write_permission="messages.write")
worker = manager.workers.get(account_id)
service = worker._im_service if worker else None
if not worker or not worker.is_running or not service or not service._running:
@@ -1888,7 +1892,7 @@ async def get_account_cookie(
account_id: int,
purpose: Optional[str] = None,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
user: User = Depends(require_accounts_cookie),
):
"""Read a Cookie for management or for legacy desktop login clients.
@@ -1921,7 +1925,7 @@ async def get_account_cookie(
async def get_desktop_login_credential(
account_id: int,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
user: User = Depends(require_accounts_cookie),
):
"""Return credentials only when the selected Douyin identity is verified.
@@ -1945,7 +1949,7 @@ async def update_account_cookie(
account_id: int,
body: AccountCookieUpdate,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_accounts_write),
user: User = Depends(require_accounts_cookie),
):
# Validate first: malformed input must not take a healthy hosted account
# offline. Filesystem and database mutations happen only after the worker
@@ -1960,7 +1964,7 @@ async def update_account_cookie(
# preparations. Keep the lock until the new Cookie and cleared identity
# are committed so no worker can start in the stop/commit gap.
async with manager.preparation_lock(account_id):
account = await get_owned_account(db, user, account_id, write=True)
account = await get_owned_account(db, user, account_id, write=True, write_permission="accounts.cookie")
await _release_db_connection(db)
await batch_start_queue.cancel_account(account_id)
await manager.stop_worker(account_id)
@@ -1994,13 +1998,13 @@ async def update_account_cookie(
async def delete_account_cookie(
account_id: int,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_accounts_write),
user: User = Depends(require_accounts_cookie),
):
# Deleting credentials uses the same preparation lock as starting a
# worker, preventing a new worker from appearing after stop_worker but
# before the cleared credentials are committed.
async with manager.preparation_lock(account_id):
account = await get_owned_account(db, user, account_id, write=True)
account = await get_owned_account(db, user, account_id, write=True, write_permission="accounts.cookie")
await _release_db_connection(db)
await batch_start_queue.cancel_account(account_id)
await manager.stop_worker(account_id)
@@ -2023,7 +2027,7 @@ async def delete_account_cookie(
async def create_account(
account_in: AccountCreate,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_accounts_write),
user: User = Depends(require_accounts_create),
):
cookie_data = (account_in.cookie_data or "").strip()
standard_json_str = None
@@ -2061,9 +2065,9 @@ async def create_account(
async def delete_account(
account_id: int,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_accounts_write),
user: User = Depends(require_accounts_delete),
):
await get_owned_account(db, user, account_id, write=True)
await get_owned_account(db, user, account_id, write=True, write_permission="accounts.delete")
# 停止运行中的任务
await _release_db_connection(db)
await batch_start_queue.cancel_account(account_id)
@@ -2079,7 +2083,7 @@ async def delete_account(
async def validate_account_credential(
account_id: int,
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
user: User = Depends(require_accounts_cookie),
):
account = await get_owned_account(db, user, account_id)
@@ -2096,9 +2100,9 @@ async def validate_account_credential(
async def reset_account_credentials(
account_id: int,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_accounts_write),
user: User = Depends(require_accounts_cookie),
):
await get_owned_account(db, user, account_id, write=True)
await get_owned_account(db, user, account_id, write=True, write_permission="accounts.cookie")
await _release_db_connection(db)
await batch_start_queue.cancel_account(account_id)
account = await _reset_account_credentials(account_id, db)
@@ -2251,9 +2255,11 @@ async def start_account_rpa(
account_id: int,
body: StartAccountRequest = StartAccountRequest(),
db: AsyncSession = Depends(get_db),
user: User = Depends(require_accounts_write),
user: User = Depends(require_accounts_start),
):
account = await get_owned_account(db, user, account_id, write=True)
account = await get_owned_account(
db, user, account_id, write=True, write_permission="accounts.start"
)
# Cancelling waits for an in-flight queued start, and the preparation lock
# waits for whichever start owns this account. Neither may keep a pooled
# connection checked out while it waits.
@@ -2267,7 +2273,7 @@ async def start_account_rpa(
async def submit_account_start_batch(
body: BatchStartRequest,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_accounts_write),
user: User = Depends(require_accounts_start),
):
requested_ids = list(
dict.fromkeys(int(value) for value in body.account_ids if int(value) > 0)
@@ -2280,7 +2286,7 @@ async def submit_account_start_batch(
# The submit path needs only ids and the disabled flag. Do not hydrate
# every account's large cookie/session/QR columns just to enqueue ids.
candidate_stmt = select(Account.id, Account.quota_disabled)
if not is_admin(user.role):
if not has_global_scope(user.role):
candidate_stmt = candidate_stmt.where(Account.owner_id == user.id)
if not body.all_accounts:
candidate_stmt = candidate_stmt.where(Account.id.in_(requested_ids))
@@ -2331,9 +2337,11 @@ async def get_account_start_batch(
async def stop_account_rpa(
account_id: int,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_accounts_write),
user: User = Depends(require_accounts_stop),
):
account = await get_owned_account(db, user, account_id, write=True)
account = await get_owned_account(
db, user, account_id, write=True, write_permission="accounts.stop"
)
# Cancelling drains an in-flight queued start, which can take as long as
# the batch per-account deadline. Do not hold a pooled connection for it.
@@ -2415,7 +2423,7 @@ async def create_rule(
rule_in.keyword = ""
if rule_in.account_id is None:
raise HTTPException(status_code=400, detail="请选择适用账号,每条规则必须绑定一个托管账号")
await get_owned_account(db, user, rule_in.account_id, write=True)
await get_owned_account(db, user, rule_in.account_id, write=True, write_permission="rules.write")
sort_stmt = select(func.max(AutoReplyRule.sort_order)).where(
AutoReplyRule.account_id == rule_in.account_id
@@ -2451,7 +2459,7 @@ async def update_rule(
rule_in.keyword = ""
if rule_in.account_id is None:
raise HTTPException(status_code=400, detail="请选择适用账号,每条规则必须绑定一个托管账号")
await get_owned_account(db, user, rule_in.account_id, write=True)
await get_owned_account(db, user, rule_in.account_id, write=True, write_permission="rules.write")
rule.account_id = rule_in.account_id
rule.keyword = rule_in.keyword
@@ -2637,7 +2645,7 @@ async def get_system_logs(
category=category,
limit=max(1, min(int(limit or 200), 1000)),
)
if not is_admin(user.role):
if not has_global_scope(user.role):
allowed = await owned_account_ids(db, user)
entries = [e for e in entries if e.get("account_id") in allowed]
return [SystemLogResponse(**e) for e in entries]
@@ -2646,7 +2654,7 @@ async def get_system_logs(
@app.delete("/api/system-logs")
async def clear_system_logs(
db: AsyncSession = Depends(get_db),
_: User = Depends(require_admin),
_: User = Depends(require_system_logs_clear),
):
"""清空系统诊断日志(内存缓冲区 + 数据库历史)。"""
system_logger.clear()
@@ -2712,7 +2720,7 @@ async def upload_message_image(
db: AsyncSession = Depends(get_db),
user: User = Depends(require_messages_write),
):
account = await get_owned_account(db, user, account_id, write=True)
account = await get_owned_account(db, user, account_id, write=True, write_permission="messages.write")
if not file.content_type or not file.content_type.startswith("image/"):
raise HTTPException(status_code=400, detail="仅支持上传图片文件")
@@ -2803,7 +2811,7 @@ async def send_account_message(
db: AsyncSession = Depends(get_db),
user: User = Depends(require_messages_write),
):
account = await get_owned_account(db, user, account_id, write=True)
account = await get_owned_account(db, user, account_id, write=True, write_permission="messages.write")
content = normalize_outgoing_content(
content=body.content or "",