File size: 8,021 Bytes
8e02bdb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
#!/usr/bin/env python3
"""
即梦 AI (jimeng.jianying.com) 历史 Session 强制退出工具
==========================================================

功能:
  通过 Playwright headless 浏览器模拟登录,点击"设置 → 退出"按钮,
  批量使指定的历史 sessionid 在服务器端失效。

适用场景:
  当 sessionid 泄露或安全审计时,强制注销不再使用的历史 cookie,
  防止被未授权方继续调用 API。

使用方法:
  python3 scripts/logout-sessions.py <sessionid1> [sessionid2] ...

  或在脚本末尾的 SESSION_IDS 列表中填写需要退出的 sessionid,
  然后直接运行:
  python3 scripts/logout-sessions.py

依赖安装:
  pip install playwright
  playwright install chromium

注意:
  - 每个 sessionid 独立启动浏览器实例执行退出,互不影响
  - 已失效的 sessionid 会自动跳过
  - 退出操作不可逆,请确认 sessionid 列表无误后再执行
"""

import sys
import time
import argparse

# -------------------------------------------------------
# 在此填写需要强制退出的历史 sessionid 列表
# 也可通过命令行参数传入(见使用方法)
# -------------------------------------------------------
SESSION_IDS = [
    # 示例(已失效,仅作格式参考):
    # "aabbddddddddddddddd",
]


def logout_session(session_id: str) -> str:
    """
    对单个 sessionid 执行退出操作。

    返回值:
      "success"        - 退出成功
      "already_invalid"- sessionid 已失效,无需处理
      "error_no_button"- 找不到退出按钮
      "unknown"        - 退出状态不确定
      "error"          - 发生异常
    """
    from playwright.sync_api import sync_playwright

    with sync_playwright() as p:
        browser = p.chromium.launch(
            headless=True,
            args=[
                "--no-sandbox",
                "--disable-setuid-sandbox",
                "--disable-dev-shm-usage",
            ],
        )

        context = browser.new_context(
            viewport={"width": 1920, "height": 1080},
            user_agent=(
                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
                "AppleWebKit/537.36 (KHTML, like Gecko) "
                "Chrome/135.0.0.0 Safari/537.36"
            ),
        )

        domain = ".jianying.com"
        context.add_cookies(
            [
                {"name": "_tea_web_id", "value": "7619975442964235802", "domain": domain, "path": "/"},
                {"name": "is_staff_user",   "value": "false",      "domain": domain, "path": "/"},
                {"name": "store-region",    "value": "cn-gd",      "domain": domain, "path": "/"},
                {"name": "uid_tt",          "value": session_id,   "domain": domain, "path": "/"},
                {"name": "uid_tt_ss",       "value": session_id,   "domain": domain, "path": "/"},
                {"name": "sid_tt",          "value": session_id,   "domain": domain, "path": "/"},
                {"name": "sessionid",       "value": session_id,   "domain": domain, "path": "/"},
                {"name": "sessionid_ss",    "value": session_id,   "domain": domain, "path": "/"},
            ]
        )

        page = context.new_page()

        try:
            # 导航到即梦主页,等待页面完全加载
            page.goto(
                "https://jimeng.jianying.com",
                timeout=30000,
                wait_until="networkidle",
            )
            time.sleep(3)

            # 检查是否已登录(通过全局变量 window.__isLogined)
            is_logged_in = page.evaluate("() => window.__isLogined === true")
            if not is_logged_in:
                return "already_invalid"

            # 步骤 1:点击左侧底部设置按钮(#SiderMenuSetting)
            # 该按钮点击后会弹出下拉菜单,菜单中包含"退出"选项
            setting_el = page.wait_for_selector("#SiderMenuSetting", timeout=8000)
            setting_el.click()
            time.sleep(2)

            # 步骤 2:查找弹出菜单中的"退出"按钮并点击
            # 优先使用文字 selector,备选使用 class 包含文字方式
            exit_btn = page.query_selector("text=退出")
            if not exit_btn:
                exit_btn = page.query_selector('.lv-dropdown-menu-item:has-text("退出")')

            if not exit_btn:
                return "error_no_button"

            # 监听退出请求(可用于日志记录)
            logout_requests: list[str] = []

            def on_request(req):
                if any(k in req.url for k in ["logout", "sign_out", "revoke", "signout"]):
                    logout_requests.append(req.url)

            page.on("request", on_request)

            # 执行点击
            exit_btn.click()
            time.sleep(4)

            # 验证退出结果
            after_state = page.evaluate("() => window.__isLogined")
            current_url = page.url

            if after_state is False or "login" in current_url:
                return "success"

            # 再次确认:检查页面是否有登录入口文字
            page_content = page.content()
            if "登录" in page_content[:1000]:
                return "success"

            return "unknown"

        except Exception as e:
            print(f"    [异常] {e}")
            return "error"
        finally:
            browser.close()


def main():
    parser = argparse.ArgumentParser(
        description="即梦 AI 历史 Session 强制退出工具",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=__doc__,
    )
    parser.add_argument(
        "sessions",
        nargs="*",
        help="需要退出的 sessionid(可传多个,空格分隔)",
    )
    args = parser.parse_args()

    # 命令行参数优先,否则使用脚本顶部的 SESSION_IDS 列表
    targets = args.sessions if args.sessions else SESSION_IDS

    if not targets:
        parser.print_help()
        print(
            "\n❌ 错误:未指定任何 sessionid。\n"
            "   请通过命令行传入或在脚本顶部的 SESSION_IDS 列表中填写。"
        )
        sys.exit(1)

    # 去重并过滤空值
    targets = [s.strip() for s in targets if s.strip()]
    targets = list(dict.fromkeys(targets))  # 保序去重

    print(f"\n即梦 AI 历史 Session 强制退出工具")
    print(f"{'=' * 60}")
    print(f"共 {len(targets)} 个 sessionid 待处理\n")

    STATUS_ICONS = {
        "success":        "✅  退出成功(服务器端已失效)",
        "already_invalid":"⬜  已失效,无需处理",
        "error_no_button":"❌  找不到退出按钮(页面结构可能已变更)",
        "unknown":        "⚠️   退出状态不确定,请手动验证",
        "error":          "❌  发生异常,请查看上方错误信息",
    }

    results: dict[str, str] = {}

    for idx, sid in enumerate(targets, 1):
        print(f"[{idx}/{len(targets)}] 处理: {sid}")
        result = logout_session(sid)
        results[sid] = result
        icon = STATUS_ICONS.get(result, result)
        print(f"         → {icon}\n")
        time.sleep(1)  # 各次请求间隔,避免触发频率限制

    # 汇总报告
    print(f"\n{'=' * 60}")
    print("退出结果汇总:")
    print(f"{'=' * 60}")
    for sid, result in results.items():
        icon = STATUS_ICONS.get(result, result)
        print(f"  {sid}{icon}")

    success_count = sum(1 for r in results.values() if r == "success")
    invalid_count = sum(1 for r in results.values() if r == "already_invalid")
    fail_count = len(results) - success_count - invalid_count

    print(f"\n  ✅ 成功退出:{success_count} 个")
    print(f"  ⬜ 已失效:  {invalid_count} 个(无需处理)")
    if fail_count > 0:
        print(f"  ❌ 处理失败:{fail_count} 个(请手动处理)")
    print()


if __name__ == "__main__":
    main()