feat: improve conductor files and runtime handling

This commit is contained in:
Liang Jiaqing committed 2026-07-31 21:56:27 +08:00
1 parent b913a89ace
commit 6bed654dce
6 files changed
+195 -41

No files matched your search

+3
View File
@@ -110,6 +110,9 @@ class GenericAgent:
if not self.is_running: return
print('Abort current task...')
self.stop_sig = True
for sess in getattr(self.llmclient.backend, '_sessions', [self.llmclient.backend]):
try: sess.active_response.close()
except Exception: pass
if self.handler is not None: self.handler.code_stop_signal.append(1)
def put_task(self, query, source="user", images=None):
+85 -5
View File
@@ -218,6 +218,7 @@
.composer { padding: 14px; border-top: 1px solid rgba(31,42,68,.075); display: flex; gap: 12px;
align-items: end; background: rgba(255,255,255,.40); }
.composer.dragging { background: rgba(59,130,246,.13); outline: 2px dashed #3b82f6; outline-offset: -6px; }
textarea#input { flex: 1; height: 52px; max-height: 140px; resize: none; outline: none; color: var(--text);
background: rgba(255,255,255,.92); border: 1px solid rgba(31,42,68,.12); border-radius: 18px;
padding: 14px 15px; font: inherit; line-height: 1.35; }
@@ -225,6 +226,19 @@
button { height: 52px; min-width: 92px; border: 0; border-radius: 18px; color: white; font-weight: 750;
cursor: pointer; background: linear-gradient(135deg, #3b82f6, #10b981);
box-shadow: 0 10px 26px rgba(59,130,246,.22); }
.file-open, .attach { min-width: 0; padding: 0 10px; box-shadow: none; }
.file-open { height: 28px; border-radius: 9px; }
.attach { width: 44px; height: 52px; font-size: 18px; border-radius: 18px; }
.file-link { color: inherit; text-decoration: underline; cursor: pointer; }
#fileDialog { width: min(1100px, 96vw); height: min(800px, 92vh); padding: 0; border: 0; border-radius: 18px; box-shadow: 0 24px 80px #0004; }
#fileDialog::backdrop { background: #0f172a66; backdrop-filter: blur(3px); }
.file-bar { display: flex; align-items: center; gap: 8px; padding: 12px; border-bottom: 1px solid var(--border); }
.file-bar code { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.file-bar button { min-width: 0; height: 34px; padding: 0 12px; border-radius: 10px; box-shadow: none; }
#fileBody { height: calc(100% - 59px); overflow: auto; padding: 12px; }
.file-row { display: flex; gap: 10px; padding: 9px 11px; border-radius: 9px; cursor: pointer; }
.file-row:hover { background: #eef2ff; } .file-row span:first-child { flex: 1; } .file-size { color: var(--muted); }
.file-preview { line-height: 1.55; } .file-preview pre { white-space: pre-wrap; overflow-wrap: anywhere; }
/* ---------- conductor 输出(横跨底部的独立卡片,默认收起) ---------- */
.console {
@@ -314,18 +328,29 @@
<section class="pane chat">
<div class="head">
<div class="title">Conversation <select id="llm" aria-label="Conductor model" disabled></select></div>
<div class="hint" id="wsStatus">connecting...</div>
<div class="hint"><button class="file-open" id="fileOpen">📁 文件</button> · <span id="wsStatus">connecting...</span></div>
</div>
<div class="messages" id="messages"></div>
<div class="composer">
<textarea id="input" placeholder="告诉 Conductor 你想做什么……"></textarea>
<button id="send">发送</button>
<div class="composer" id="composer">
<textarea id="input" placeholder="告诉 Conductor 你想做什么……(可拖入文件)"></textarea>
<button class="attach" id="fileUpload" title="上传文件">📎</button>
<button id="send">发送</button><input id="fileInput" type="file" multiple hidden>
</div>
</section>
</section>
</div><!-- /app-grid -->
</main>
<dialog id="fileDialog">
<div class="file-bar"><button id="fileBack">↑</button><code id="filePath"></code><button id="fileClose">关闭</button></div>
<div id="fileBody"></div>
</dialog>
<dialog id="downloadDialog" style="width:min(520px,90vw);border:0;border-radius:16px;padding:22px;box-shadow:0 24px 80px #0004">
<strong>下载文件</strong>
<code id="downloadPath" style="display:block;margin:16px 0;overflow-wrap:anywhere;color:var(--muted)"></code>
<div style="display:flex;justify-content:flex-end;gap:10px"><button id="downloadCancel">取消</button><button id="downloadConfirm">下载</button></div>
</dialog>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script>
const cardsEl = document.getElementById('cards');
@@ -341,10 +366,48 @@
const consoleBar = document.getElementById('consoleBar');
const consoleBody= document.getElementById('consoleBody');
const logCount = document.getElementById('logCount');
let ws;
const fileDialog = document.getElementById('fileDialog');
const fileBody = document.getElementById('fileBody');
const filePathEl = document.getElementById('filePath');
const fileInput = document.getElementById('fileInput');
const composer = document.getElementById('composer');
const downloadDialog = document.getElementById('downloadDialog');
const downloadPath = document.getElementById('downloadPath');
let ws, fileDir='', fileParent='';
function esc(s){ return String(s||'').replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }
function renderMd(s){ try { return marked.parse(s||'', { breaks: true, gfm: true }); } catch(e){ return esc(s); } }
function fileUrl(path){ return '/file?path='+encodeURIComponent(path); }
function humanSize(n){ return n==null?'':n<1024?n+' B':n<1048576?(n/1024).toFixed(1)+' KB':(n/1048576).toFixed(1)+' MB'; }
async function browseFiles(path=''){
const data=await fetch(path?'/files?path='+encodeURIComponent(path):'/files').then(r=>{if(!r.ok) throw Error(r.statusText); return r.json()});
fileDir=data.path; fileParent=data.parent; filePathEl.textContent=fileDir;
fileBody.innerHTML=data.items.map(x=>`<div class="file-row" data-path="${esc(x.path)}" data-dir="${x.dir?'1':''}"><span>${x.dir?'📁':'📄'} ${esc(x.name)}</span><span class="file-size">${humanSize(x.size)}</span></div>`).join('')||'<div class="empty">空目录</div>';
}
async function openFile(path){
if(!/\.(md|markdown|txt|py|js|ts|json|ya?ml|css|html?|xml|csv|log|ini|cfg|sh|ps1|bat|c|cc|cpp|h|hpp|java|go|rs|sql)$/i.test(path)){
downloadPath.textContent=path; downloadDialog.showModal();
return;
}
const text=await fetch(fileUrl(path)).then(r=>{if(!r.ok) throw Error(r.statusText); return r.text()});
filePathEl.textContent=path;
fileBody.innerHTML=/\.(md|markdown)$/i.test(path)?`<div class="file-preview">${renderMd(text)}</div>`:'';
if(!/\.(md|markdown)$/i.test(path)){ const pre=document.createElement('pre'); pre.textContent=text; fileBody.replaceChildren(pre); }
}
document.getElementById('downloadCancel').onclick=()=>downloadDialog.close();
document.getElementById('downloadConfirm').onclick=()=>{
const a=document.createElement('a'); a.href=fileUrl(downloadPath.textContent); a.download=downloadPath.textContent.split(/[\\/]/).pop();
document.body.appendChild(a); a.click(); a.remove(); downloadDialog.close();
};
async function uploadFiles(files){
const paths=[];
for(const file of files){
const r=await fetch('/file?name='+encodeURIComponent(file.name),{method:'POST',body:file});
if(!r.ok) throw Error(await r.text());
paths.push('`'+(await r.json()).path+'`');
}
inputEl.value+=(inputEl.value?'\n':'')+paths.join('\n'); checkLimit(); inputEl.focus();
}
/* ---------- subagent 卡片 ---------- */
function renderCards(items){
@@ -414,11 +477,28 @@
div.className = `msg ${cls}` + (role==='user' && !item.read ? ' unread' : '');
if(item.id) div.dataset.id = item.id;
div.innerHTML = renderMd(item.msg || '');
(item.files||[]).forEach(f=>{ const code=[...div.querySelectorAll('code')].find(x=>x.textContent===(f.ref||f.path)); if(code){ const a=document.createElement('a'); a.className='file-link'; a.href=fileUrl(f.path); a.dataset.path=f.path; code.replaceWith(a); a.append(code); } });
messagesEl.appendChild(div);
messagesEl.scrollTop = messagesEl.scrollHeight;
}
function markAllRead(){ messagesEl.querySelectorAll('.msg.me.unread').forEach(el=>el.classList.remove('unread')); }
document.getElementById('fileOpen').onclick=()=>{ fileDialog.showModal(); browseFiles().catch(e=>fileBody.textContent=e); };
document.getElementById('fileClose').onclick=()=>fileDialog.close();
document.getElementById('fileBack').onclick=()=>browseFiles(filePathEl.textContent===fileDir?fileParent:fileDir);
document.getElementById('fileUpload').onclick=()=>fileInput.click();
fileBody.addEventListener('click', e=>{ const row=e.target.closest('.file-row'); if(row) (row.dataset.dir?browseFiles:openFile)(row.dataset.path); });
messagesEl.addEventListener('click', async e=>{
const a=e.target.closest('.file-link'); if(!a) return;
e.preventDefault(); fileDialog.showModal();
const path=a.dataset.path, parent=path.replace(/[\\\/][^\\\/]*$/, '');
try { await browseFiles(parent); await openFile(path); } catch(err) { fileBody.textContent=err; }
});
fileInput.onchange=async()=>{ try { await uploadFiles(fileInput.files); } catch(e) { alert('上传失败:'+e); } fileInput.value=''; };
['dragenter','dragover'].forEach(type=>composer.addEventListener(type,e=>{ e.preventDefault(); composer.classList.add('dragging'); }));
['dragleave','drop'].forEach(type=>composer.addEventListener(type,e=>{ e.preventDefault(); composer.classList.remove('dragging'); }));
composer.addEventListener('drop',e=>{ if(e.dataTransfer.files.length) uploadFiles(e.dataTransfer.files).catch(err=>alert('上传失败:'+err)); });
/* ---------- conductor 输出 ---------- */
function fmtTs(ms){ try{ return new Date(ms).toLocaleTimeString('zh-CN',{hour12:false}); }catch(e){ return ''; } }
function logItemHtml(r){
+70 -7
View File
@@ -1,9 +1,9 @@
import os, sys, re, time, json, uuid, queue, asyncio, threading
import os, sys, re, time, json, uuid, queue, asyncio, threading, argparse, base64, secrets
from dataclasses import dataclass, field
from typing import Dict, Any, Optional, List
from contextlib import asynccontextmanager, suppress
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request, HTTPException
from fastapi.responses import FileResponse, PlainTextResponse, JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
@@ -27,6 +27,15 @@ HOST = "127.0.0.1"
PORT = 8900
HTML_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "conductor.html")
parser = argparse.ArgumentParser()
parser.add_argument("--host", default=HOST)
parser.add_argument("--port", type=int, default=PORT)
parser.add_argument("--key")
parser.add_argument("--no-browser", action="store_true")
args = parser.parse_args()
if args.host not in ("127.0.0.1", "localhost", "::1") and not args.key:
parser.error("--key is required for non-loopback listening")
def _settings_doc() -> dict:
try:
@@ -112,6 +121,19 @@ async def lifespan(app: FastAPI):
app = FastAPI(title="Conductor", lifespan=lifespan)
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
class RemoteAuth:
def __init__(self, app): self.app = app
async def __call__(self, scope, receive, send):
remote = (scope.get("client") or ("",))[0]
got = dict(scope.get("headers", [])).get(b"authorization", b"")
want = b"Basic " + base64.b64encode(f"conductor:{args.key}".encode())
if args.key and remote not in ("127.0.0.1", "::1") and not secrets.compare_digest(got, want):
if scope["type"] == "websocket": return await send({"type": "websocket.close", "code": 1008})
return await PlainTextResponse("Unauthorized", 401, {"WWW-Authenticate": 'Basic realm="Conductor"'})(scope, receive, send)
await self.app(scope, receive, send)
app.add_middleware(RemoteAuth)
class ChatIn(BaseModel):
msg: str
role: str = "conductor" # conductor | system | user
@@ -199,8 +221,23 @@ async def broadcast(payload: dict):
def push_cards(): schedule_broadcast({"type": "subagents", "items": pool.snapshot()})
FILE_ROOT = os.path.dirname(ROOT)
FILE_HOME = os.path.join(ROOT, "temp")
UPLOAD_DIR = os.path.join(FILE_HOME, "uploads")
def file_path(path: str):
path = os.path.abspath(os.path.expanduser(path))
if os.path.commonpath((FILE_ROOT, path)) != FILE_ROOT: raise ValueError("path is above file browser root")
return path
def add_chat(msg: str, role: str = "conductor", files: list = None, images: list = None):
item = {"id": short_id(), "role": role, "msg": msg, "ts": now_ms(), "read": role != "user", "files": files or [], "images": images or []}
files = list(files or [])
known = {x.get("path") for x in files}
for ref in re.findall(r"`([^`\n]+)`", msg):
try: path = file_path(ref)
except ValueError: continue
if os.path.isfile(path) and path not in known: files.append({"ref": ref, "path": path, "name": os.path.basename(path)})
item = {"id": short_id(), "role": role, "msg": msg, "ts": now_ms(), "read": role != "user", "files": files, "images": images or []}
chat_messages.append(item)
if len(chat_messages) > 200: del chat_messages[:-200]
schedule_broadcast({"type": "chat", "item": item})
@@ -318,6 +355,9 @@ Conductor API\tBase: http://{HOST}:{PORT}
POST /chat\tbody: {{"msg": "..."}}\t给用户发消息
POST /subagent\tbody: {{"prompt": "..."}}\t启动新subagent,返回 {{"id": "xxx"}};指定模型加参数llm(数字/名称)
POST /approval\tbody: {{"prompt": "...", "source": "..."}}\t推一条待批任务到前端(后端不存),用户同意则直接派发为subagent
GET /files?path=...\t列举目录(默认temp,最高到GA上一级)
GET /file?path=...\t下载文件
POST /file?name=...\t上传到temp/uploads
POST /subagent/{{id}}\tbody: {{"action": "keyinfo", "msg": "..."}}\t注入key_info(agent下轮可见)
POST /subagent/{{id}}\tbody: {{"action": "input", "msg": "..."}}\t对subagent输入下一条指令;指定模型加参数llm(数字/名称)
POST /subagent/{{id}}\tbody: {{"action": "stop"}}\t中断执行但保留(可继续input)
@@ -381,7 +421,8 @@ API: {base};requests,GET /readme查用法,GET /chat读未读对话,GET /
- 改写prompt时严禁添加用户未提及的假设、工具、前提条件。只能精炼/结构化用户原意,不能脑补,只能做很小的改写
原则:
- 信任subagent足够聪明,不要写具体步骤和容易探测的信息;能自己判断的自己判断,只在真正需要用户决策时打扰。\n
- 信任subagent足够聪明,不要写具体步骤和容易探测的信息;能自己判断的自己判断,只在真正需要用户决策时打扰。
- 向用户交付文件时,路径用反引号包裹。\n
需要处理:
{summary}"""
@@ -492,6 +533,28 @@ def conductor_token_stats():
@app.get("/")
def index(): return FileResponse(HTML_PATH)
@app.get("/files")
def files(path: str = FILE_HOME):
try: path = file_path(path)
except ValueError as e: raise HTTPException(403, str(e))
if not os.path.isdir(path): raise HTTPException(404, "directory not found")
items = [{"name": x.name, "path": x.path, "dir": x.is_dir(), "size": x.stat().st_size if x.is_file() else None}
for x in sorted(os.scandir(path), key=lambda x: (not x.is_dir(), x.name.lower()))]
return {"path": path, "parent": path if path == FILE_ROOT else os.path.dirname(path), "items": items}
@app.get("/file")
def get_file(path: str):
try: path = file_path(path)
except ValueError as e: raise HTTPException(403, str(e))
return FileResponse(path, filename=os.path.basename(path))
@app.post("/file")
async def put_file(name: str, request: Request):
os.makedirs(UPLOAD_DIR, exist_ok=True)
path = os.path.join(UPLOAD_DIR, os.path.basename(name))
with open(path, "wb") as f: f.write(await request.body())
return {"name": os.path.basename(path), "path": path}
@app.get("/readme")
def readme(): return PlainTextResponse(READMES["api"])
@@ -591,7 +654,7 @@ if __name__ == "__main__":
import uvicorn
# bridge 自启 conductor 时传 --no-browser:不在用户浏览器里弹一个独立 conductor UI,
# 用户从桌面版「指挥家」页直接连过来即可。手动跑 conductor.py(没带 flag)保持原行为。
if "--no-browser" not in sys.argv:
if not args.no_browser:
import webbrowser, threading
threading.Timer(1.0, lambda: webbrowser.open(f"http://{HOST}:{PORT}")).start()
uvicorn.run("conductor:app", host=HOST, port=PORT, reload=False)
threading.Timer(1.0, lambda: webbrowser.open(f"http://127.0.0.1:{args.port}")).start()
uvicorn.run(app, host=args.host, port=args.port)
+32 -27
View File
@@ -300,8 +300,25 @@ def _poll_main_task(max_items=256):
return done
def _render_stat_badge(is_running, target=None):
"""Render task usage/time into one stable element, without clearing its container."""
if 'task_start_ts' not in st.session_state or not hasattr(llmcore, 'STATS'):
return
end_ts = time.time() if is_running else st.session_state.get('task_end_ts', time.time())
secs = max(0, int(end_ts - st.session_state.task_start_ts))
stats = dict(llmcore.STATS)
short = lambda n: f'{n / 1000:.0f}k' if n >= 1000 else str(n)
usage = ((f"{stats['session']} │ " if stats.get('session') else '') +
f"{short(stats['ctx'])} chars·{stats['msgs']}msgs │ "
f"in {short(stats.get('inp', 0))} toks·cached{short(stats.get('cached', 0))}·out{short(stats.get('out', 0))} │ "
if 'ctx' in stats else '')
renderer = target.markdown if target is not None else st.markdown
renderer(f'<div class="ga-stat-badge">{usage}{secs // 60}:{secs % 60:02d}</div>',
unsafe_allow_html=True)
@st.fragment(run_every=timedelta(seconds=1))
def render_main_stream(frozen_host, live_slot, render_state):
def render_main_stream(frozen_host, live_slot, stats_slot, render_state):
"""Append completed turns outside the fragment; redraw only the active turn."""
done = _poll_main_task()
if done is not None:
@@ -312,7 +329,7 @@ def render_main_stream(frozen_host, live_slot, render_state):
if st.session_state.get('loop_enabled'):
b = get_controller()
b['obj'] = st.session_state.get('loop_prompt_input', ''); b['ready'] = False; b['job'] = b['epoch']; b['ev'].set()
# The final response is rendered once from history on the full-app rerun.
# The final response and frozen stats are rendered on the full-app rerun.
st.rerun(scope="app")
response = st.session_state.get("partial_response", "")
@@ -324,17 +341,18 @@ def render_main_stream(frozen_host, live_slot, render_state):
with frozen_host:
render_segments([segments[render_state['frozen']]])
render_state['frozen'] += 1
# This external slot keeps a stable delta path; only the growing final segment changes.
# These external slots keep stable delta paths; only their contents change.
with live_slot.container():
render_segments([segments[-1]], suffix=" ▌")
_render_stat_badge(is_running=True, target=stats_slot)
def mount_main_stream():
def mount_main_stream(stats_slot):
"""Create persistent targets captured by the fragment until the next full rerun."""
with st.chat_message("assistant"):
frozen_host = st.container()
live_slot = st.empty()
render_main_stream(frozen_host, live_slot, {'frozen': 0})
render_main_stream(frozen_host, live_slot, stats_slot, {'frozen': 0})
if not hasattr(agent, "_ui_messages"): agent._ui_messages = st.session_state.get("messages", [])
if "messages" not in st.session_state: st.session_state.messages = agent._ui_messages
@@ -380,6 +398,10 @@ _js_ime_fix = ("" if os.name == 'nt' else
"f();new MutationObserver(f).observe(d.body,{childList:1,subtree:1})}()")
_embed_html(f'<script>{_js_ime_fix}</script>', height=0)
# Fixed target shared by full-app rendering and the one-second stream fragment.
# The badge is position:fixed, so this placeholder does not affect its visual position.
stats_slot = st.empty()
_injected = st.session_state.pop('_inject_prompt', None)
prompt = st.chat_input("any task?") or _injected
if prompt:
@@ -454,33 +476,16 @@ if prompt:
if hasattr(agent, '_pet_req') and not prompt.startswith('/'): agent._pet_req('state=walk')
with st.chat_message("user"): st.markdown(prompt)
_start_main_task(prompt)
mount_main_stream()
mount_main_stream(stats_slot)
elif st.session_state.get('display_queue') is not None:
# No new prompt but a task is mid-flight (typically a /btw rerun) — resume drain.
mount_main_stream()
mount_main_stream(stats_slot)
# Usage/time label remains after completion; while running only its fragment updates once/sec.
# Usage/time label remains after completion. During a task render_main_stream refreshes it.
_has_task_stats = 'task_start_ts' in st.session_state
_is_running = st.session_state.get('display_queue') is not None
if _has_task_stats:
def _render_stat_badge():
if not hasattr(llmcore, 'STATS'): return
end_ts = time.time() if _is_running else st.session_state.get('task_end_ts', time.time())
secs = max(0, int(end_ts - st.session_state.task_start_ts))
stats = dict(llmcore.STATS)
short = lambda n: f'{n / 1000:.0f}k' if n >= 1000 else str(n)
usage = ((f"{stats['session']} │ " if stats.get('session') else '') +
f"{short(stats['ctx'])} chars·{stats['msgs']}msgs │ "
f"in {short(stats.get('inp', 0))} toks·cached{short(stats.get('cached', 0))}·out{short(stats.get('out', 0))} │ "
if 'ctx' in stats else '')
st.markdown(f'<div class="ga-stat-badge">{usage}{secs // 60}:{secs % 60:02d}</div>',
unsafe_allow_html=True)
if _is_running:
@st.fragment(run_every=timedelta(seconds=1))
def _live_stat_badge(): _render_stat_badge()
_live_stat_badge()
else:
_render_stat_badge()
if _has_task_stats and not _is_running:
_render_stat_badge(is_running=False, target=stats_slot)
if _has_task_stats or _is_running:
st.markdown(
+1 -1
View File
@@ -312,7 +312,7 @@ class GenericAgentHandler(BaseHandler):
cwd = os.path.normpath(os.path.abspath(raw_path))
code_cwd = os.path.normpath(self.cwd)
maxlen = self._get_tool_maxlen(10000, args)
if timeout > 600: result = '[ERROR] Timeout must be <= 600 seconds, run time-consuming code in background subprocess and monitor it!'
if timeout > 600: result = '[ERROR] Timeout must be <= 600 seconds; code not executed. Run time-consuming code in the background instead of waiting for it to finish in the foreground, verify it started successfully, and monitor it until completion or failure.'
elif code_type == 'python' and _arg(args, "inline_eval", False, bool):
ns = {'handler':self, 'parent':self.parent, 'history':json.dumps(self.parent.llmclient.backend.history)}
old_cwd = os.getcwd()
+4 -1
View File
@@ -174,7 +174,9 @@ def _parse_claude_sse(resp_lines):
if current_block and current_block.get("type") == "text": current_block["text"] += text
if text: yield text
elif delta.get("type") == "thinking_delta":
if current_block and current_block.get("type") == "thinking": current_block["thinking"] += delta.get("thinking", "")
thinking = delta.get("thinking", "")
if current_block and current_block.get("type") == "thinking": current_block["thinking"] += thinking
if thinking: yield thinking
elif delta.get("type") == "signature_delta":
if current_block and current_block.get("type") == "thinking":
current_block["signature"] = current_block.get("signature", "") + delta.get("signature", "")
@@ -396,6 +398,7 @@ def _stream_with_retry(sess, url, headers, payload, parse_fn):
try:
with requests.post(url, headers=headers, json=payload, stream=sess.stream,
timeout=(sess.connect_timeout, sess.read_timeout), proxies=sess.proxies, verify=sess.verify) as r:
sess.active_response = r
if r.status_code >= 400:
#pathlib.Path(__file__).parent.joinpath('temp','bad_requests.json').write_text(json.dumps({"url":url,"headers":headers,"payload":payload,"t":time.time()},ensure_ascii=False),encoding='utf-8')
d = _delay(r, attempt) if r.status_code in _RETRYABLE and attempt < sess.max_retries else None