diff --git a/README.md b/README.md index a7f527d..eb1fe1e 100644 --- a/README.md +++ b/README.md @@ -405,6 +405,9 @@ CAMOUFOX_VERSION=135.0.1 CAMOUFOX_RELEASE=beta.24 docker compose build app | grok2api | Grok token 管理、回填、聊天/API 服务 | `https://github.com/chenyme/grok2api.git` | | kiro-account-manager | Kiro 账号管理相关插件 | `https://github.com/hj01857655/kiro-account-manager.git` | +插件页中的 **“安装最新版 / 更新到最新版”** 会同步仓库最新代码,且已支持 **卸载**(会先停止服务,再删除本地插件目录)。 +默认按 **最新 semver tag** 更新;你也可以在“设置 → 插件 → 安装/更新策略”切回 **分支 HEAD** 模式。 + 如果你后续要改成 `ghproxy`、`gitclone`、企业 Git 镜像或其他代理地址,需要同步修改: ```text diff --git a/api/config.py b/api/config.py index 95fa550..ca485c0 100644 --- a/api/config.py +++ b/api/config.py @@ -96,6 +96,7 @@ CONFIG_KEYS = [ "grok2api_quota", "kiro_manager_path", "kiro_manager_exe", + "external_apps_update_mode", "contribution_enabled", "contribution_server_url", "contribution_key", @@ -136,6 +137,8 @@ def get_config(): all_cfg["contribution_enabled"] = "0" if not all_cfg.get("contribution_server_url"): all_cfg["contribution_server_url"] = "http://new.xem8k5.top:7317/" + if not all_cfg.get("external_apps_update_mode"): + all_cfg["external_apps_update_mode"] = "tag" # 只返回已知 key,未设置的返回空字符串 return {k: all_cfg.get(k, "") for k in CONFIG_KEYS} diff --git a/api/integrations.py b/api/integrations.py index f3afc14..5795b91 100644 --- a/api/integrations.py +++ b/api/integrations.py @@ -8,7 +8,7 @@ from sqlmodel import Session, select from core.base_platform import Account, AccountStatus from core.db import AccountModel, engine -from services.external_apps import install, list_status, start, start_all, stop, stop_all +from services.external_apps import install, list_status, start, start_all, stop, stop_all, uninstall from services.chatgpt_sync import backfill_chatgpt_account_to_cpa, get_cliproxy_sync_state router = APIRouter(prefix="/integrations", tags=["integrations"]) @@ -60,6 +60,11 @@ def install_service(name: str): return install(name) +@router.post("/services/{name}/uninstall") +def uninstall_service(name: str): + return uninstall(name) + + @router.post("/services/{name}/stop") def stop_service(name: str): return stop(name) diff --git a/api/tasks.py b/api/tasks.py index 2f817f1..8c15faf 100644 --- a/api/tasks.py +++ b/api/tasks.py @@ -169,9 +169,7 @@ def _run_register(task_id: str, req: RegisterTaskRequest): success = 0 skipped = 0 errors = [] - workspace_success = 0 start_gate_lock = threading.Lock() - workspace_progress_lock = threading.Lock() next_start_time = time.time() def _sleep_with_control( @@ -203,7 +201,7 @@ def _run_register(task_id: str, req: RegisterTaskRequest): ) def _do_one(i: int): - nonlocal next_start_time, workspace_success + nonlocal next_start_time proxy_pool = None _proxy = None current_email = req.email or "" @@ -295,13 +293,6 @@ def _run_register(task_id: str, req: RegisterTaskRequest): if _proxy: proxy_pool.report_success(_proxy) _log(task_id, f"[OK] 注册成功: {account.email}") - workspace_id = "" - if isinstance(account.extra, dict): - workspace_id = str(account.extra.get("workspace_id") or "").strip() - if workspace_id: - with workspace_progress_lock: - workspace_success += 1 - _log(task_id, f"[ChatGPT] workspace进度: {workspace_success}/{req.count}") _save_task_log(req.platform, account.email, "success") _auto_upload_integrations(task_id, saved_account or account) cashier_url = (account.extra or {}).get("cashier_url", "") @@ -358,6 +349,11 @@ def _run_register(task_id: str, req: RegisterTaskRequest): stopped = True else: errors.append(result.message) + _task_store.update_counters( + task_id, + success=success, + registered=success + skipped + len(errors), + ) if stopped or control.is_stop_requested(): stopped = True for pending in futures: @@ -369,6 +365,7 @@ def _run_register(task_id: str, req: RegisterTaskRequest): task_id, status="failed", success=success, + registered=success + skipped + len(errors), skipped=skipped, errors=errors, error=str(e), @@ -388,6 +385,7 @@ def _run_register(task_id: str, req: RegisterTaskRequest): task_id, status=final_status, success=success, + registered=success + skipped + len(errors), skipped=skipped, errors=errors, ) @@ -473,11 +471,17 @@ async def stream_logs(task_id: str, since: int = 0): sent = since while True: logs, status = _task_store.log_state(task_id) + snapshot = _task_store.snapshot(task_id) + counters = { + "success": int(snapshot.get("success") or 0), + "registered": int(snapshot.get("registered") or 0), + "total": int(snapshot.get("total") or 0), + } while sent < len(logs): - yield f"data: {json.dumps({'line': logs[sent]})}\n\n" + yield f"data: {json.dumps({'line': logs[sent], **counters})}\n\n" sent += 1 if status in ("done", "failed", "stopped"): - yield f"data: {json.dumps({'done': True, 'status': status})}\n\n" + yield f"data: {json.dumps({'done': True, 'status': status, **counters})}\n\n" break await asyncio.sleep(0.5) diff --git a/core/task_runtime.py b/core/task_runtime.py index 1305ba0..16cca85 100644 --- a/core/task_runtime.py +++ b/core/task_runtime.py @@ -137,6 +137,7 @@ class RegisterTaskRecord: progress: str = "0/0" logs: list[str] = field(default_factory=list) success: int = 0 + registered: int = 0 skipped: int = 0 errors: list[str] = field(default_factory=list) cashier_urls: list[str] = field(default_factory=list) @@ -155,9 +156,11 @@ class RegisterTaskRecord: "platform": self.platform, "source": self.source, "meta": dict(self.meta), + "total": self.total, "progress": self.progress, "logs": list(self.logs), "success": self.success, + "registered": self.registered, "skipped": self.skipped, "errors": list(self.errors), "control": self.control.snapshot(), @@ -265,12 +268,28 @@ class RegisterTaskStore: record.cashier_urls.append(cashier_url) record.updated_at = time.time() + def update_counters( + self, + task_id: str, + *, + success: int | None = None, + registered: int | None = None, + ) -> None: + with self._lock: + record = self._records[task_id] + if success is not None: + record.success = max(0, int(success)) + if registered is not None: + record.registered = max(0, int(registered)) + record.updated_at = time.time() + def finish( self, task_id: str, *, status: str, success: int, + registered: int | None = None, skipped: int, errors: list[str], error: str = "", @@ -279,6 +298,10 @@ class RegisterTaskStore: record = self._records[task_id] record.status = status record.success = success + if registered is None: + record.registered = max(success + skipped + len(errors), 0) + else: + record.registered = max(0, int(registered)) record.skipped = skipped record.errors = list(errors) record.error = error diff --git a/frontend/src/components/TaskLogPanel.tsx b/frontend/src/components/TaskLogPanel.tsx index 00dcf90..5a4faa9 100644 --- a/frontend/src/components/TaskLogPanel.tsx +++ b/frontend/src/components/TaskLogPanel.tsx @@ -1,5 +1,5 @@ import { useEffect, useRef, useState } from 'react' -import { Button, message, Space } from 'antd' +import { Button, message, Space, Tag } from 'antd' import { CopyOutlined, FastForwardOutlined, StopOutlined } from '@ant-design/icons' import { API_BASE, apiFetch, getToken } from '@/lib/utils' @@ -11,8 +11,36 @@ interface TaskLogPanelProps { type TaskTerminalStatus = 'idle' | 'done' | 'failed' | 'stopped' +interface RegisterSummary { + success: number + registered: number + total: number +} + +function parseCounter(value: unknown): number { + const n = Number(value || 0) + if (!Number.isFinite(n) || n < 0) return 0 + return Math.floor(n) +} + +function normalizeSummary(next: RegisterSummary): RegisterSummary { + const success = parseCounter(next.success) + const registered = Math.max(parseCounter(next.registered), success) + const total = Math.max(parseCounter(next.total), registered) + return { success, registered, total } +} + +function mergeSummary(previous: RegisterSummary, incoming: Partial): RegisterSummary { + return normalizeSummary({ + success: incoming.success ?? previous.success, + registered: incoming.registered ?? previous.registered, + total: incoming.total ?? previous.total, + }) +} + export function TaskLogPanel({ taskId, onDone }: TaskLogPanelProps) { const [lines, setLines] = useState([]) + const [summary, setSummary] = useState({ success: 0, registered: 0, total: 0 }) const [error, setError] = useState('') const [terminalStatus, setTerminalStatus] = useState('idle') const [skipLoading, setSkipLoading] = useState(false) @@ -81,6 +109,7 @@ export function TaskLogPanel({ taskId, onDone }: TaskLogPanelProps) { const maxRetryMs = 8000 nextSinceRef.current = 0 setLines([]) + setSummary({ success: 0, registered: 0, total: 0 }) setError('') setTerminalStatus('idle') setStopRequested(false) @@ -93,12 +122,22 @@ export function TaskLogPanel({ taskId, onDone }: TaskLogPanelProps) { const snapshot = await apiFetch(`/tasks/${taskId}`) as { logs?: string[] status?: TaskTerminalStatus | string + success?: number + registered?: number + total?: number control?: { stop_requested?: boolean } } if (cancelled) return true const snapshotLines = Array.isArray(snapshot.logs) ? snapshot.logs : [] setLines(snapshotLines) + setSummary((previous) => + mergeSummary(previous, { + success: snapshot.success, + registered: snapshot.registered, + total: snapshot.total, + }), + ) nextSinceRef.current = snapshotLines.length setStopRequested(Boolean(snapshot.control?.stop_requested)) @@ -159,7 +198,17 @@ export function TaskLogPanel({ taskId, onDone }: TaskLogPanelProps) { line?: string done?: boolean status?: TaskTerminalStatus + success?: number + registered?: number + total?: number } + setSummary((previous) => + mergeSummary(previous, { + success: payload.success, + registered: payload.registered, + total: payload.total, + }), + ) if (payload.line) { nextSinceRef.current += 1 setLines((previous) => [...previous, payload.line!]) @@ -224,6 +273,12 @@ export function TaskLogPanel({ taskId, onDone }: TaskLogPanelProps) { return (
+ + 注册成功:{summary.success} + 已注册:{summary.registered} + 总共注册:{summary.total} + +