codex-mode 0.1.0a7__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
codex_mode/__init__.py ADDED
@@ -0,0 +1,2 @@
1
+ """Independent Linux utility for switching Codex authentication routes."""
2
+ __version__ = '0.1.0a7'
codex_mode/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .cli import main
2
+
3
+ raise SystemExit(main())
codex_mode/accounts.py ADDED
@@ -0,0 +1,91 @@
1
+ """Named ChatGPT credentials; history stays in the selected CODEX_HOME."""
2
+ import json
3
+ import re
4
+ from . import engine as e
5
+
6
+ def path(root, name, create=True):
7
+ if not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9_-]{0,63}', name):
8
+ raise ValueError('账号名仅允许字母、数字、横线和下划线,最长 64 字符。')
9
+ folder=root/'auth-profiles'/'accounts'
10
+ if (root/'auth-profiles').is_symlink() or folder.is_symlink():
11
+ raise RuntimeError('账号目录不能是符号链接。')
12
+ if create:folder.mkdir(parents=True,mode=0o700,exist_ok=True)
13
+ target=folder/(name+'.json')
14
+ if target.is_symlink():raise RuntimeError('账号文件不能是符号链接。')
15
+ return target
16
+
17
+ def identity(auth):
18
+ return (auth.get('tokens') or {}).get('account_id')
19
+
20
+ def save(root, name):
21
+ auth=e.read_auth(root/'auth.json')
22
+ if not e.chat_auth(auth):raise RuntimeError('当前目录没有 ChatGPT 登录。')
23
+ target=path(root,name)
24
+ if target.exists():raise RuntimeError('账号名已存在,请使用新名称,避免覆盖凭据。')
25
+ e.atomic(target,json.dumps(auth))
26
+ settings=e.read_auth(root/'codex-mode.json')
27
+ settings['active_account']=name
28
+ e.atomic(root/'codex-mode.json',json.dumps(settings))
29
+ e.say('✓ 当前登录已保存为 '+name+'(未切换线路)。','ok')
30
+
31
+ def login(root,name):
32
+ target=path(root,name)
33
+ if target.exists():raise RuntimeError('账号名已存在,请使用新名称。')
34
+ # Official login runs in an empty staging home, never the active home.
35
+ import tempfile
36
+ import shutil
37
+ staging=e.Path(tempfile.mkdtemp(prefix='account-login-',dir=target.parent))
38
+ try:
39
+ e.atomic(staging/'config.toml','model_provider="openai"\ncli_auth_credentials_store="file"\n')
40
+ result=e.subprocess.run([e.BIN,'login','--device-auth'],env=e.codex_env(staging))
41
+ if result.returncode:raise RuntimeError('登录未完成,当前登录未修改。')
42
+ auth=e.read_auth(staging/'auth.json')
43
+ if not e.chat_auth(auth):raise RuntimeError('未获得 ChatGPT 登录凭据。')
44
+ e.atomic(target,json.dumps(auth))
45
+ e.say('✓ 已保存账号 '+name+';尚未启用。','ok')
46
+ finally:shutil.rmtree(staging)
47
+
48
+ def listing(root):
49
+ settings=e.read_auth(root/'codex-mode.json')
50
+ folder=root/'auth-profiles'/'accounts'
51
+ if folder.is_symlink():raise RuntimeError('账号目录不能是符号链接。')
52
+ names=sorted(p.stem for p in folder.glob('*.json') if not p.is_symlink())
53
+ for name in names:
54
+ print(name+(' · 当前选中' if name==settings.get('active_account') else ''))
55
+ if not names:print('尚无命名账号。运行 login --account NAME。')
56
+
57
+ def switch(root,name,force=False,disconnect_idle=False):
58
+ target=path(root,name)
59
+ selected=e.read_auth(target)
60
+ if not e.chat_auth(selected):raise RuntimeError('账号未登录:'+name)
61
+ # Stop only servers associated with this home, before changing any credentials.
62
+ if force:e.force_stop(root)
63
+ else:e.ensure_idle(root,disconnect_idle=disconnect_idle,allow_interrupt=False)
64
+ originals={p:p.read_bytes() if p.exists() else None for p in
65
+ (root/'auth.json',root/'codex-mode.json',root/'config.toml',root/'auth-profiles'/'chatgpt.auth.json')}
66
+ settings=e.read_auth(root/'codex-mode.json')
67
+ current=e.read_auth(root/'auth.json')
68
+ old=settings.get('active_account')
69
+ if not old and e.chat_auth(current):
70
+ # Preserve the pre-existing login when adopting named accounts.
71
+ old='previous-'+e.datetime.datetime.now().strftime('%Y%m%d-%H%M%S-%f')
72
+ e.atomic(path(root,old),json.dumps(current))
73
+ if old and e.chat_auth(current):
74
+ previous=path(root,old)
75
+ stored=e.read_auth(previous)
76
+ if not identity(current) or identity(current)!=identity(stored):
77
+ raise RuntimeError('当前登录与记录账号不一致,已取消,避免覆盖其他账号。')
78
+ e.atomic(previous,json.dumps(current))
79
+ # Reload: switching to the same account must use refreshed credentials.
80
+ selected=e.read_auth(target)
81
+ try:
82
+ e.atomic(root/'auth.json',json.dumps(selected))
83
+ e.switch(root,'chatgpt',disconnect_idle=disconnect_idle,force=False)
84
+ settings['active_account']=name
85
+ e.atomic(root/'codex-mode.json',json.dumps(settings))
86
+ except BaseException:
87
+ for p,data in originals.items():
88
+ if data is None:p.unlink(missing_ok=True)
89
+ else:e.atomic(p,data)
90
+ raise
91
+ e.say('账号:'+name)
@@ -0,0 +1,99 @@
1
+ """Isolated hello requests; source home is read-only and never switched."""
2
+ import json
3
+ import os
4
+ from pathlib import Path
5
+ import signal
6
+ import statistics
7
+ import subprocess
8
+ import tempfile
9
+ import time
10
+ from . import engine as e
11
+
12
+ def result(stdout, stderr, code):
13
+ events=[]
14
+ for line in stdout.splitlines():
15
+ try:
16
+ value=json.loads(line)
17
+ if isinstance(value,dict):events.append(value)
18
+ except ValueError:continue
19
+ messages=[v.get('item',{}).get('text','') for v in events
20
+ if v.get('type')=='item.completed' and v.get('item',{}).get('type')=='agent_message']
21
+ unsafe=any(v.get('item',{}).get('type') in ('command_execution','file_change','mcp_tool_call','web_search')
22
+ for v in events)
23
+ passed=code==0 and any(v.get('type')=='turn.completed' for v in events) and any(messages) and not unsafe
24
+ if passed:return 'ok'
25
+ # Never print untrusted upstream errors: they may include URLs or credentials.
26
+ combined=(stdout+'\n'+stderr).lower()
27
+ if unsafe:return 'unexpected_tool'
28
+ if 'overloaded' in combined or 'overload' in combined:return 'overloaded'
29
+ if 'usage limit' in combined or 'rate limit' in combined or '429' in combined:return 'limited'
30
+ if 'unauthorized' in combined or '401' in combined or 'refresh' in combined:return 'auth_error'
31
+ if 'not supported' in combined or 'model_not_found' in combined:return 'model_error'
32
+ return 'request_failed'
33
+
34
+ def sample(source,mode,binary,model,effort,timeout,account=None):
35
+ with tempfile.TemporaryDirectory(prefix='codex-mode-speed-') as directory:
36
+ root=Path(directory);home=root/'home';work=root/'work'
37
+ home.mkdir(mode=0o700);work.mkdir(mode=0o700)
38
+ env=e.codex_env(home)
39
+ # Scrub other alternate-auth/service overrides from this child only.
40
+ for name in ('CODEX_MODE_BENCH_KEY','CODEX_HOME','CHATGPT_BASE_URL'):
41
+ env.pop(name,None)
42
+ env['CODEX_HOME']=str(home)
43
+ provider={'name':'isolated speed test','wire_api':'responses','supports_websockets':False,
44
+ 'base_url':e.CHAT_URL if mode=='chatgpt' else e.API_URL}
45
+ if mode=='chatgpt':
46
+ if account:
47
+ from .accounts import path
48
+ auth=e.read_auth(path(source,account,create=False))
49
+ else:
50
+ auth=e.read_auth(source/'auth.json')
51
+ if not e.chat_auth(auth):auth=e.read_auth(source/'auth-profiles'/'chatgpt.auth.json')
52
+ if not e.chat_auth(auth):raise RuntimeError('ChatGPT 登录缺失')
53
+ e.atomic(home/'auth.json',json.dumps(auth))
54
+ provider['requires_openai_auth']=True
55
+ else:
56
+ if not e.API_URL:raise RuntimeError('API 地址缺失')
57
+ env['CODEX_MODE_BENCH_KEY']=e.key_for(source)
58
+ provider['env_key']='CODEX_MODE_BENCH_KEY'
59
+ config='model_provider="direct"\ncli_auth_credentials_store="file"\napproval_policy="never"\nweb_search="disabled"\n'
60
+ config+='model='+json.dumps(model)+'\nmodel_reasoning_effort='+json.dumps(effort)+'\n'
61
+ config+='developer_instructions="Respond to the greeting briefly. Do not use any tools."\n'
62
+ config+='[features]\nshell_tool=false\nunified_exec=false\ngoals=false\nshell_snapshot=false\n'
63
+ config+='[model_providers.direct]\n'
64
+ for name,value in provider.items():config+=name+'='+json.dumps(value)+'\n'
65
+ e.atomic(home/'config.toml',config)
66
+ cmd=[binary,'exec','--ephemeral','--json','--skip-git-repo-check','--ignore-rules',
67
+ '--sandbox','read-only','--color','never','--cd',str(work),'hello']
68
+ started=time.perf_counter()
69
+ process=subprocess.Popen(cmd,env=env,cwd=work,stdout=subprocess.PIPE,stderr=subprocess.PIPE,
70
+ text=True,start_new_session=True)
71
+ try:
72
+ out,err=process.communicate(timeout=timeout)
73
+ except subprocess.TimeoutExpired:
74
+ os.killpg(process.pid,signal.SIGKILL);process.communicate()
75
+ return {'status':'timeout','seconds':time.perf_counter()-started}
76
+ except BaseException:
77
+ if process.poll() is None:os.killpg(process.pid,signal.SIGKILL)
78
+ process.communicate();raise
79
+ return {'status':result(out,err,process.returncode),'seconds':time.perf_counter()-started}
80
+
81
+ def run(source,binary,model='gpt-5.6-sol',effort='medium',rounds=1,timeout=120,account=None):
82
+ e.say('测速 · '+model+' / '+effort+' · hello · '+str(rounds)+' 轮')
83
+ e.say('真实请求消耗额度/费用;含 CLI 启动、排队和重试,不是纯模型延迟。')
84
+ e.say('仅使用临时目录;不切换线路或停止现有任务。ChatGPT 测试可能刷新临时副本的登录令牌。')
85
+ measurements={'chatgpt':[],'api':[]}
86
+ for index in range(rounds):
87
+ for label in (('chatgpt','api') if index%2==0 else ('api','chatgpt')):
88
+ e.say(' '+label+' · 第 '+str(index+1)+' 轮…')
89
+ try:r=sample(source,label,binary,model,effort,timeout,account)
90
+ except (RuntimeError,OSError,ValueError):r={'status':'setup_error','seconds':None}
91
+ measurements[label].append(r)
92
+ e.say(' '+('✓' if r['status']=='ok' else '✗')+' '+label+' · '+r['status']+
93
+ (' · %.2fs'%r['seconds'] if r['seconds'] is not None else ''),
94
+ 'ok' if r['status']=='ok' else 'warn')
95
+ for label,values in measurements.items():
96
+ timings=[v['seconds'] for v in values if v['status']=='ok']
97
+ e.say(label+':成功 '+str(len(timings))+'/'+str(rounds)+
98
+ (',中位数 %.2fs'%statistics.median(timings) if timings else ',无有效测速结果'))
99
+ return measurements
codex_mode/cli.py ADDED
@@ -0,0 +1,199 @@
1
+ """Configuration and packaging entry point; credentials never enter CLI arguments."""
2
+ import argparse
3
+ import fcntl
4
+ import getpass
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ import queue
9
+ import re
10
+ import shutil
11
+ import subprocess
12
+ import sys
13
+ import tempfile
14
+ from urllib.parse import urlsplit
15
+
16
+ from . import __version__
17
+ from . import engine as e
18
+
19
+ TESTED_VERSIONS = {(0,153,4),(0,154,0)}
20
+
21
+ def url(value):
22
+ parsed=urlsplit(value)
23
+ if parsed.scheme not in ('http','https') or not parsed.hostname or parsed.username or parsed.password or parsed.query or parsed.fragment:
24
+ raise ValueError('URL must be http(s), without credentials, query or fragment.')
25
+ return value.rstrip('/')
26
+
27
+ def codex_binary(value):
28
+ found=shutil.which(value) if value else shutil.which('codex')
29
+ if not found:raise RuntimeError('Codex executable not found. Install Codex or pass --codex-bin PATH.')
30
+ return str(Path(found).absolute())
31
+
32
+ def check_version(binary):
33
+ result=subprocess.run([binary,'--version'],capture_output=True,text=True,timeout=10,check=True)
34
+ match=re.search(r'\b(\d+)\.(\d+)\.(\d+)\b',result.stdout)
35
+ if not match or tuple(map(int,match.groups())) not in TESTED_VERSIONS:
36
+ raise RuntimeError('Untested Codex version: '+result.stdout.strip()+'. Supported: 0.153.4, 0.154.0.')
37
+ return result.stdout.strip()
38
+
39
+ def configure(root,settings,binary):
40
+ e.API_URL=url(settings['api_url']) if settings.get('api_url') else ''
41
+ e.HEALTH_URL=url(settings['health_url']) if settings.get('health_url') else None
42
+ e.BIN=binary or 'codex'
43
+ aliases=settings.get('provider_aliases',['direct'])
44
+ if not isinstance(aliases,list) or 'direct' not in aliases or any(not isinstance(a,str) or not re.fullmatch(r'[A-Za-z0-9_-]+',a) or a in ('openai','ollama','lmstudio') for a in aliases):
45
+ raise ValueError('Invalid provider aliases; direct is required and built-in openai cannot be overridden.')
46
+ e.ALIASES=list(dict.fromkeys(aliases))
47
+ name=settings.get('key_env')
48
+ if name and not re.fullmatch(r'[A-Za-z_][A-Za-z0-9_]*',name):raise ValueError('Invalid key environment variable name.')
49
+
50
+ def initialize(root,args):
51
+ settings=e.read_auth(root/'codex-mode.json')
52
+ if args.api_url:settings['api_url']=url(args.api_url)
53
+ if not settings.get('api_url'):raise RuntimeError('Pass --api-url URL to initialize the API route.')
54
+ if args.health_url:settings['health_url']=url(args.health_url)
55
+ if args.codex_bin:settings['codex_bin']=codex_binary(args.codex_bin)
56
+ if args.provider_alias:settings['provider_aliases']=list(dict.fromkeys(['direct']+args.provider_alias))
57
+ settings.setdefault('provider_aliases',['direct'])
58
+ key=None
59
+ if args.key_env:settings['key_env']=args.key_env
60
+ elif args.prompt_key:
61
+ if not sys.stdin.isatty():raise RuntimeError('API key prompt requires a terminal.')
62
+ key=getpass.getpass('API key(隐藏输入):').strip();settings.pop('key_env',None)
63
+ elif args.import_api_key:
64
+ key=e.read_auth(root/'auth.json').get('OPENAI_API_KEY');settings.pop('key_env',None)
65
+ if (args.prompt_key or args.import_api_key) and (not isinstance(key,str) or not key.strip()):
66
+ raise RuntimeError('No API key was supplied/imported. Existing settings unchanged.')
67
+ configure(root,settings,None)
68
+ profile=root/'auth-profiles';profile.mkdir(mode=0o700,exist_ok=True)
69
+ # Back up only existing tool-owned files; initialization does not alter Codex config/auth.
70
+ targets=[root/'codex-mode.json']+([profile/'sub2api.auth.json'] if key else [])
71
+ if any(p.exists() for p in targets):
72
+ backup=root/'mode-backups'/('init-'+e.datetime.datetime.now().strftime('%Y%m%d-%H%M%S-%f'))
73
+ backup.mkdir(parents=True,mode=0o700)
74
+ for p in targets:
75
+ if p.exists():shutil.copy2(p,backup/p.name);os.chmod(backup/p.name,0o600)
76
+ originals={p:p.read_bytes() if p.exists() else None for p in targets}
77
+ try:
78
+ if key:e.atomic(profile/'sub2api.auth.json',json.dumps({'OPENAI_API_KEY':key.strip()}))
79
+ e.atomic(root/'codex-mode.json',json.dumps(settings,ensure_ascii=False,indent=2)+'\n')
80
+ except BaseException:
81
+ for path,data in originals.items():
82
+ if data is None:path.unlink(missing_ok=True)
83
+ else:e.atomic(path,data)
84
+ raise
85
+ e.say('✓ 初始化完成。Codex 当前配置和登录保持原样。','ok')
86
+
87
+ def doctor(root,binary):
88
+ print('Codex:',check_version(binary))
89
+ for process in e.processes(root):check_version(process['exe'])
90
+ print('Goal handling: manual pause; no goal RPC required')
91
+ print('State directory:',root)
92
+ print('API endpoint:',e.API_URL or 'not configured')
93
+ print('Provider aliases:',', '.join(e.ALIASES))
94
+ e.say('✓ 基础版本与协议检查通过(不代表网络或所有历史会话已验证)。','ok')
95
+
96
+ def run(argv=None):
97
+ parser=argparse.ArgumentParser(description='Switch Codex ChatGPT/API routes with shared history (Linux alpha).')
98
+ parser.add_argument('command',nargs='?',default='setup',choices=['setup','verify','scan','restore','init','doctor','status','login','chatgpt','api','sub2api','accounts','account-save','speed','_key'])
99
+ parser.add_argument('--model',default='gpt-5.6-sol',help='测速模型')
100
+ parser.add_argument('--effort',choices=['low','medium','high','xhigh'],default='medium')
101
+ parser.add_argument('--rounds',type=int,default=1)
102
+ parser.add_argument('--timeout',type=int,default=120)
103
+ parser.add_argument('--account',help='Named ChatGPT account; login/chatgpt/account-save/speed')
104
+ parser.add_argument('--version',action='version',version='codex-mode '+__version__)
105
+ parser.add_argument('--state-dir',default=os.environ.get('CODEX_HOME',str(Path.home()/'.codex')))
106
+ parser.add_argument('--codex-bin',help='Codex executable; otherwise use settings or PATH')
107
+ parser.add_argument('-v','--verbose',action='store_true')
108
+ parser.add_argument('--dry-run',action='store_true',help='Preview route without changing files, processes or goals')
109
+ parser.add_argument('--disconnect-idle',action='store_true')
110
+ parser.add_argument('--force',action='store_true',help='Confirm forced shutdown before switching; reconnect starts the new service')
111
+ parser.add_argument('--api-url',help='Responses API base URL; used by init')
112
+ parser.add_argument('--health-url',help='Optional unauthenticated health endpoint; used by init')
113
+ parser.add_argument('--backup',help='Completed backup directory; used by restore/verify')
114
+ parser.add_argument('--request-test',action='store_true',help='Confirm minimal real requests; used by verify')
115
+ parser.add_argument('--provider-alias',action='append',help='Explicitly manage a historical provider ID; used by init')
116
+ keys=parser.add_mutually_exclusive_group()
117
+ keys.add_argument('--key-env',help='API key environment variable name; used by init')
118
+ keys.add_argument('--prompt-key',action='store_true')
119
+ keys.add_argument('--import-api-key',action='store_true',help='Explicitly import OPENAI_API_KEY from existing auth.json')
120
+ args=parser.parse_args(argv);e.VERBOSE=args.verbose
121
+ if args.account and args.command not in ('login','chatgpt','account-save','speed'):parser.error('--account requires login/chatgpt/account-save/speed.')
122
+ if not 1<=args.rounds<=10 or not 5<=args.timeout<=600:parser.error('rounds: 1–10; timeout: 5–600 seconds.')
123
+ if args.command=='account-save' and not args.account:parser.error('account-save requires --account NAME.')
124
+ if args.force and args.command not in ('chatgpt','api','sub2api'):parser.error('--force requires a mode command.')
125
+ if args.command in ('restore','verify') and not args.backup:parser.error(args.command+' requires --backup DIR.')
126
+ if args.backup and args.command not in ('restore','verify'):parser.error('--backup requires restore/verify.')
127
+ if args.request_test and args.command!='verify':parser.error('--request-test requires verify.')
128
+ if sys.platform!='linux':raise RuntimeError('This alpha supports Linux only.')
129
+ os.umask(0o077)
130
+ root=Path(args.state_dir).expanduser().resolve()
131
+ if not (root/'config.toml').is_file() and args.command!='restore':raise RuntimeError('Missing config.toml in '+str(root)+'. Initialize Codex first.')
132
+ if not root.is_dir():raise RuntimeError('CODEX_HOME directory does not exist.')
133
+ if args.command=='_key':
134
+ if sys.stdout.isatty():raise RuntimeError('Internal credential command refuses terminal output.')
135
+ print(e.key_for(root));return
136
+ init_options=any([args.api_url,args.health_url,args.provider_alias,args.key_env,args.prompt_key,args.import_api_key])
137
+ if init_options and args.command!='init':parser.error('Setup options are only valid with init.')
138
+ if args.dry_run and args.command not in ('chatgpt','api','sub2api'):parser.error('--dry-run requires a mode command.')
139
+ settings={} if args.command in ('restore','scan') else e.read_auth(root/'codex-mode.json')
140
+ binary=None if args.command in ('init','status','scan','restore','accounts','account-save') or args.dry_run else codex_binary(args.codex_bin or settings.get('codex_bin'))
141
+ configure(root,settings,binary)
142
+ if args.command=='speed':
143
+ from .benchmark import run as speed
144
+ check_version(binary)
145
+ results=speed(root,binary,args.model,args.effort,args.rounds,args.timeout,args.account)
146
+ if any(not any(r['status']=='ok' for r in values) for values in results.values()):
147
+ raise RuntimeError('至少一条线路无有效测速结果;未切换线路。')
148
+ return
149
+ if args.command=='status':e.status(root);return
150
+ if args.command=='accounts':
151
+ from .accounts import listing
152
+ listing(root);return
153
+ if args.command=='scan':
154
+ from .onboarding import scan_home
155
+ inventory=scan_home(root)
156
+ print(json.dumps({key:value for key,value in inventory.items() if key!='threads'},ensure_ascii=False,indent=2));return
157
+ if args.command=='doctor':doctor(root,binary);return
158
+ mode='sub2api' if args.command in ('api','sub2api') else args.command
159
+ if args.dry_run:
160
+ if mode=='sub2api' and not e.API_URL:raise RuntimeError('Run init --api-url URL first.')
161
+ if args.account:
162
+ from .accounts import path
163
+ if not e.chat_auth(e.read_auth(path(root,args.account,create=False))):raise RuntimeError('账号未登录。')
164
+ e.render((root/'config.toml').read_text(),root,mode)
165
+ print('预览:'+mode+' → '+(e.CHAT_URL if mode=='chatgpt' else e.API_URL))
166
+ print('仅检查配置转换;未检查任务/网络,未修改任何文件、进程或 goal。');return
167
+ if binary:
168
+ check_version(binary)
169
+ for process in e.processes(root):check_version(process['exe'])
170
+ with (root/'.codex-mode.lock').open('a') as lock:
171
+ try:fcntl.flock(lock,fcntl.LOCK_EX|fcntl.LOCK_NB)
172
+ except BlockingIOError:raise RuntimeError('Another login or switch operation is running; finish it first.')
173
+ if args.command=='verify':
174
+ from .onboarding import verify_backup
175
+ verify_backup(root,args.backup,binary,request_tests=args.request_test)
176
+ elif args.command=='restore':
177
+ from .onboarding import restore_configuration
178
+ restore_configuration(root,args.backup)
179
+ elif args.command=='setup':
180
+ from .onboarding import setup
181
+ setup(root,binary,url)
182
+ elif args.command=='init':initialize(root,args)
183
+ elif args.command=='account-save':
184
+ from .accounts import save
185
+ e.ensure_idle(root,allow_interrupt=False)
186
+ save(root,args.account)
187
+ elif args.account:
188
+ from . import accounts
189
+ if mode=='login':accounts.login(root,args.account)
190
+ else:accounts.switch(root,args.account,force=args.force,disconnect_idle=args.disconnect_idle)
191
+ elif mode=='login':e.login(root)
192
+ else:e.switch(root,mode,disconnect_idle=args.disconnect_idle,force=args.force)
193
+
194
+ def main(argv=None):
195
+ try:run(argv);return 0
196
+ except (KeyboardInterrupt,EOFError):
197
+ print(e.color('已取消;配置若未写入则保持原样。','warn',sys.stderr),file=sys.stderr);return 130
198
+ except (RuntimeError,OSError,ValueError,queue.Empty,subprocess.SubprocessError) as error:
199
+ print(e.color('未完成:'+e.error_text(error),'error',sys.stderr),file=sys.stderr);return 1