browser-workspace 0.4.5 → 0.4.9
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.
- package/bin/__pycache__/browser-workspacecpython-311.pyc +0 -0
- package/package.json +1 -1
- package/platform_runner.py +11 -2
- package/platforms/chatgpt/actions/_lifecycle_common.py +73 -20
- package/platforms/chatgpt/actions/_new_turn.py +1 -0
- package/platforms/chatgpt/actions/_readiness.py +29 -3
- package/platforms/chatgpt/actions/_submit_thread.py +20 -3
- package/platforms/chatgpt/actions/_thread_turn.py +31 -2
- package/platforms/chatgpt/tests/test_contract.py +107 -1
- package/platforms/iyf/AIRPLAY.md +25 -0
- package/platforms/iyf/actions/_find.py +18 -0
- package/platforms/iyf/actions/_open.py +10 -0
- package/platforms/iyf/manifest.yaml +11 -0
- package/platforms/iyf/safari-airplay +57 -0
- package/pyproject.toml +2 -2
- package/session_client.py +10 -0
- package/src/browser_workspace.egg-info/PKG-INFO +2 -2
- package/src/browser_workspace.egg-info/SOURCES.txt +53 -46
- package/src/browser_workspace.egg-info/requires.txt +1 -1
- package/src/browser_workspace_app/package.json +1 -1
- package/src/browser_workspace_app/pyproject.toml +2 -2
- package/src/browser_workspace_app/session_client.py +10 -0
- package/src/browser_workspace_app/platform_runner.py +0 -204
- package/src/browser_workspace_app/platforms/chatgpt/README.md +0 -60
- package/src/browser_workspace_app/platforms/chatgpt/actions/_bootstrap_start.py +0 -58
- package/src/browser_workspace_app/platforms/chatgpt/actions/_lifecycle_common.py +0 -119
- package/src/browser_workspace_app/platforms/chatgpt/actions/_mcp_app.py +0 -80
- package/src/browser_workspace_app/platforms/chatgpt/actions/_new_thread.py +0 -85
- package/src/browser_workspace_app/platforms/chatgpt/actions/_new_turn.py +0 -33
- package/src/browser_workspace_app/platforms/chatgpt/actions/_project_setup.py +0 -353
- package/src/browser_workspace_app/platforms/chatgpt/actions/_readiness.py +0 -52
- package/src/browser_workspace_app/platforms/chatgpt/actions/_submit_thread.py +0 -129
- package/src/browser_workspace_app/platforms/chatgpt/actions/_temporary_submit.py +0 -591
- package/src/browser_workspace_app/platforms/chatgpt/actions/_thread_turn.py +0 -362
- package/src/browser_workspace_app/platforms/chatgpt/manifest.yaml +0 -34
- package/src/browser_workspace_app/platforms/chatgpt/tests/test_contract.py +0 -183
- package/src/browser_workspace_app/platforms/chrome-web-store/README.md +0 -49
- package/src/browser_workspace_app/platforms/chrome-web-store/actions/_chrome_web_store.py +0 -425
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/check-status.md +0 -7
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/open-item.md +0 -10
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/submit-review.md +0 -10
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/update-listing.md +0 -18
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/upload-package.md +0 -11
- package/src/browser_workspace_app/platforms/chrome-web-store/flows/verify-published.md +0 -9
- package/src/browser_workspace_app/platforms/chrome-web-store/manifest.yaml +0 -33
- package/src/browser_workspace_app/platforms/chrome-web-store/tests/test_contract.py +0 -83
- package/src/browser_workspace_app/platforms/microsoft-teams/README.md +0 -32
- package/src/browser_workspace_app/platforms/microsoft-teams/actions/_meeting.py +0 -210
- package/src/browser_workspace_app/platforms/microsoft-teams/flows/instant-meeting.md +0 -21
- package/src/browser_workspace_app/platforms/microsoft-teams/manifest.yaml +0 -25
- package/src/browser_workspace_app/platforms/microsoft-teams/tests/test_contract.py +0 -24
- package/src/browser_workspace_app/platforms/tiktok/README.md +0 -10
- package/src/browser_workspace_app/platforms/tiktok/actions/_post.py +0 -180
- package/src/browser_workspace_app/platforms/tiktok/manifest.yaml +0 -99
- package/src/browser_workspace_app/platforms/wechat-channels/README.md +0 -10
- package/src/browser_workspace_app/platforms/wechat-channels/actions/_common.py +0 -163
- package/src/browser_workspace_app/platforms/wechat-channels/actions/_manage.py +0 -130
- package/src/browser_workspace_app/platforms/wechat-channels/actions/_post.py +0 -231
- package/src/browser_workspace_app/platforms/wechat-channels/manifest.yaml +0 -124
- package/src/browser_workspace_app/platforms/wechat-channels/tests/test_contract.py +0 -36
- package/src/browser_workspace_app/platforms/xhs/README.md +0 -10
- package/src/browser_workspace_app/platforms/xhs/actions/_manage.py +0 -355
- package/src/browser_workspace_app/platforms/xhs/actions/_post.py +0 -281
- package/src/browser_workspace_app/platforms/xhs/manifest.yaml +0 -107
- package/src/browser_workspace_app/platforms/xhs/tests/test_contract.py +0 -34
- package/src/browser_workspace_app/platforms/youtube/README.md +0 -10
- package/src/browser_workspace_app/platforms/youtube/actions/_post.py +0 -103
- package/src/browser_workspace_app/platforms/youtube/manifest.yaml +0 -94
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import time
|
|
3
|
-
from _readiness import wait_until_stable, submission_receipt, prompt_text_matches
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def composer_selector():
|
|
7
|
-
return js("""(() => {
|
|
8
|
-
const selectors=['#prompt-textarea','[contenteditable="true"][data-composer-markdown]','[contenteditable="true"][data-lexical-editor="true"]','textarea'];
|
|
9
|
-
for (const selector of selectors) {
|
|
10
|
-
const e=document.querySelector(selector);
|
|
11
|
-
if (!e || e.disabled) continue;
|
|
12
|
-
const r=e.getBoundingClientRect();
|
|
13
|
-
if (r.width>0 && r.height>0) return selector;
|
|
14
|
-
}
|
|
15
|
-
return null;
|
|
16
|
-
})()""")
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def composer_text(selector=None):
|
|
20
|
-
selector=selector or composer_selector()
|
|
21
|
-
if not selector: return ""
|
|
22
|
-
return js(f"""(() => {{
|
|
23
|
-
const e=document.querySelector({json.dumps(selector)});
|
|
24
|
-
if (!e) return '';
|
|
25
|
-
return (e.innerText || e.value || e.textContent || '').trim();
|
|
26
|
-
}})()""") or ""
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def send_enabled():
|
|
30
|
-
return bool(js("""(() => {
|
|
31
|
-
const b=document.querySelector('button[data-testid="send-button"],button[aria-label="Send prompt"],button[aria-label="Send"],form button[type="submit"]');
|
|
32
|
-
return !!b && !b.disabled && b.getAttribute('aria-disabled') !== 'true';
|
|
33
|
-
})()"""))
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def is_generating():
|
|
37
|
-
return bool(js("""(() => !![...document.querySelectorAll('button')].find(b => {
|
|
38
|
-
const a=(b.getAttribute('aria-label')||'').toLowerCase();
|
|
39
|
-
const t=(b.textContent||'').trim().toLowerCase();
|
|
40
|
-
const id=(b.getAttribute('data-testid')||'').toLowerCase();
|
|
41
|
-
return a.includes('stop answering') || a.includes('stop generating') || t === 'stop' || id.includes('stop');
|
|
42
|
-
}))()"""))
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def user_turns():
|
|
46
|
-
return js("""(() => {
|
|
47
|
-
const legacy=[...document.querySelectorAll('[data-message-author-role="user"]')]
|
|
48
|
-
.map(e=>({text:(e.innerText||'').trim(),id:e.getAttribute('data-message-id')||''})).filter(x=>x.text);
|
|
49
|
-
if (legacy.length) return legacy;
|
|
50
|
-
return [...document.querySelectorAll('h4')]
|
|
51
|
-
.filter(e=>(e.innerText||'').trim()==='You said:')
|
|
52
|
-
.map((e,index)=>({text:(e.parentElement?.innerText||'').replace(/^You said:\\s*/,'').trim(),id:`dom-user-${index}`}))
|
|
53
|
-
.filter(x=>x.text);
|
|
54
|
-
})()""") or []
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def wait_for_idle(timeout=300):
|
|
58
|
-
return wait_until_stable(
|
|
59
|
-
lambda: {"composer": composer_selector(), "generating": is_generating()},
|
|
60
|
-
lambda state: bool(state["composer"]) and not state["generating"],
|
|
61
|
-
timeout=timeout,
|
|
62
|
-
interval=.25,
|
|
63
|
-
stable_samples=3,
|
|
64
|
-
phase="ChatGPT turn completion",
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def fill_prompt(prompt):
|
|
69
|
-
selector=wait_until_stable(
|
|
70
|
-
lambda: composer_selector(),
|
|
71
|
-
lambda value: bool(value),
|
|
72
|
-
timeout=30,
|
|
73
|
-
phase="ChatGPT composer readiness",
|
|
74
|
-
)
|
|
75
|
-
editable=bool(js(f"""(() => document.querySelector({json.dumps(selector)})?.getAttribute('contenteditable')==='true')()"""))
|
|
76
|
-
if editable:
|
|
77
|
-
chunks=[prompt[i:i+256] for i in range(0, len(prompt), 256)]
|
|
78
|
-
ok=js(f"""(() => {{
|
|
79
|
-
const e=document.querySelector({json.dumps(selector)});
|
|
80
|
-
if(!e)return false;
|
|
81
|
-
e.focus();
|
|
82
|
-
const sel=window.getSelection(), range=document.createRange();
|
|
83
|
-
range.selectNodeContents(e); sel.removeAllRanges(); sel.addRange(range);
|
|
84
|
-
document.execCommand('delete',false,null); sel.removeAllRanges();
|
|
85
|
-
for (const chunk of {json.dumps(chunks)}) {{
|
|
86
|
-
const current=window.getSelection(), r=document.createRange();
|
|
87
|
-
r.selectNodeContents(e); r.collapse(false); current.removeAllRanges(); current.addRange(r);
|
|
88
|
-
if(!document.execCommand('insertText',false,chunk)) return false;
|
|
89
|
-
}}
|
|
90
|
-
e.dispatchEvent(new InputEvent('input',{{bubbles:true,inputType:'insertText',data:{json.dumps(prompt)}}}));
|
|
91
|
-
return true;
|
|
92
|
-
}})()""")
|
|
93
|
-
if not ok: raise RuntimeError("ChatGPT composer rejected lifecycle continuation")
|
|
94
|
-
else:
|
|
95
|
-
fill_input(selector,prompt,clear_first=True)
|
|
96
|
-
wait_until_stable(
|
|
97
|
-
lambda: {"text":composer_text(selector),"send":send_enabled()},
|
|
98
|
-
lambda state: prompt_text_matches(state["text"],prompt) and state["send"],
|
|
99
|
-
timeout=20,
|
|
100
|
-
phase="ChatGPT lifecycle continuation readiness",
|
|
101
|
-
)
|
|
102
|
-
return selector
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
def submit(prompt):
|
|
106
|
-
before=len(user_turns())
|
|
107
|
-
selector=fill_prompt(prompt)
|
|
108
|
-
clicked=js("""(() => {
|
|
109
|
-
const b=document.querySelector('button[data-testid="send-button"],button[aria-label="Send prompt"],button[aria-label="Send"],form button[type="submit"]');
|
|
110
|
-
if(!b||b.disabled||b.getAttribute('aria-disabled')==='true')return false;
|
|
111
|
-
b.click(); return true;
|
|
112
|
-
})()""")
|
|
113
|
-
if not clicked: raise RuntimeError("ChatGPT send button changed before lifecycle submit")
|
|
114
|
-
deadline=time.time()+30
|
|
115
|
-
while time.time()<deadline:
|
|
116
|
-
receipt=submission_receipt(user_turns(),before,prompt,composer_text(selector))
|
|
117
|
-
if receipt: return receipt
|
|
118
|
-
time.sleep(.2)
|
|
119
|
-
raise RuntimeError("ChatGPT lifecycle submission was not observed")
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import time
|
|
3
|
-
|
|
4
|
-
CFG = json.load(open("__CFG_PATH__", encoding="utf-8"))
|
|
5
|
-
APP_NAME = str(CFG.get("app_name") or "").strip()
|
|
6
|
-
OP = str(CFG.get("operation") or "status").strip()
|
|
7
|
-
|
|
8
|
-
if not APP_NAME:
|
|
9
|
-
raise ValueError("app_name is required")
|
|
10
|
-
if OP not in {"status", "open", "refresh-tools"}:
|
|
11
|
-
raise ValueError("operation must be status, open, or refresh-tools")
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def wait_until(pred, label, timeout=30):
|
|
15
|
-
deadline = time.time() + timeout
|
|
16
|
-
while time.time() < deadline:
|
|
17
|
-
value = pred()
|
|
18
|
-
if value:
|
|
19
|
-
return value
|
|
20
|
-
time.sleep(0.2)
|
|
21
|
-
raise RuntimeError(f"{label} not observed")
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def app_button_exists():
|
|
25
|
-
return js(f"""(() => {{
|
|
26
|
-
const name={json.dumps(APP_NAME)};
|
|
27
|
-
return Array.from(document.querySelectorAll('button,a')).some(el => {{
|
|
28
|
-
const text=(el.innerText||'').trim();
|
|
29
|
-
return text===name || text.startsWith(name+'\\n');
|
|
30
|
-
}});
|
|
31
|
-
}})()""")
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def open_app():
|
|
35
|
-
ok = js(f"""(() => {{
|
|
36
|
-
const name={json.dumps(APP_NAME)};
|
|
37
|
-
const el=Array.from(document.querySelectorAll('button,a')).find(el => {{
|
|
38
|
-
const text=(el.innerText||'').trim();
|
|
39
|
-
return text===name || text.startsWith(name+'\\n');
|
|
40
|
-
}});
|
|
41
|
-
if(!el)return false; el.click(); return true;
|
|
42
|
-
}})()""")
|
|
43
|
-
if not ok:
|
|
44
|
-
raise RuntimeError(f"installed ChatGPT plugin not found: {APP_NAME}")
|
|
45
|
-
wait_until(lambda: js("document.body?.innerText?.includes('Refresh tools') || document.body?.innerText?.includes('Disconnect')"), "plugin detail controls")
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def detail_state():
|
|
49
|
-
return js("""(() => {
|
|
50
|
-
const body=(document.body?.innerText||'');
|
|
51
|
-
const texts=Array.from(document.querySelectorAll('button,a')).map(el=>(el.innerText||el.getAttribute('aria-label')||'').trim()).filter(Boolean);
|
|
52
|
-
return {
|
|
53
|
-
connected:body.includes('Disconnect'),
|
|
54
|
-
permissions:body.includes('Permissions'),
|
|
55
|
-
refresh_tools:texts.includes('Refresh tools'),
|
|
56
|
-
editable:texts.includes('Edit'),
|
|
57
|
-
uninstallable:texts.includes('Uninstall'),
|
|
58
|
-
url:location.href
|
|
59
|
-
};
|
|
60
|
-
})()""")
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
goto_url("https://chatgpt.com/settings/plugins-settings")
|
|
64
|
-
wait_for_load()
|
|
65
|
-
wait_until(lambda: js("document.body?.innerText?.includes('Plugins')"), "ChatGPT Plugins settings")
|
|
66
|
-
wait_until(app_button_exists, f"installed plugin {APP_NAME}")
|
|
67
|
-
open_app()
|
|
68
|
-
|
|
69
|
-
if OP == "refresh-tools":
|
|
70
|
-
ok = js("""(() => {
|
|
71
|
-
const b=Array.from(document.querySelectorAll('button')).find(x=>(x.innerText||'').trim()==='Refresh tools');
|
|
72
|
-
if(!b||b.disabled)return false; b.click(); return true;
|
|
73
|
-
})()""")
|
|
74
|
-
if not ok:
|
|
75
|
-
raise RuntimeError("Refresh tools control is unavailable")
|
|
76
|
-
time.sleep(0.5)
|
|
77
|
-
|
|
78
|
-
wait_until(lambda: js("document.body?.innerText?.includes('Refresh tools') || document.body?.innerText?.includes('Disconnect')"), "plugin detail hydration")
|
|
79
|
-
state = detail_state()
|
|
80
|
-
print(json.dumps({"app_name": APP_NAME, "operation": OP, **state}, ensure_ascii=False), flush=True)
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import time
|
|
3
|
-
from urllib.parse import quote, urlsplit
|
|
4
|
-
import _lifecycle_common as lifecycle_common
|
|
5
|
-
|
|
6
|
-
lifecycle_common.js = js
|
|
7
|
-
lifecycle_common.fill_input = fill_input
|
|
8
|
-
wait_for_idle = lifecycle_common.wait_for_idle
|
|
9
|
-
submit = lifecycle_common.submit
|
|
10
|
-
|
|
11
|
-
CFG=json.load(open("__CFG_PATH__",encoding="utf-8"))
|
|
12
|
-
project_id=str(CFG.get("project_id") or "").strip() or None
|
|
13
|
-
message=str(CFG.get("message") or "").strip()
|
|
14
|
-
source_thread_id=str(CFG.get("source_thread_id") or "").strip() or None
|
|
15
|
-
temporary=bool(CFG.get("temporary", False))
|
|
16
|
-
|
|
17
|
-
if not message:
|
|
18
|
-
raise RuntimeError("message is required")
|
|
19
|
-
if temporary and project_id:
|
|
20
|
-
raise RuntimeError("temporary=true cannot be combined with project_id")
|
|
21
|
-
|
|
22
|
-
if temporary:
|
|
23
|
-
start_url="https://chatgpt.com/?temporary-chat=true"
|
|
24
|
-
elif project_id:
|
|
25
|
-
start_url=f"https://chatgpt.com/g/{quote(project_id,safe='')}/project"
|
|
26
|
-
else:
|
|
27
|
-
start_url="https://chatgpt.com/"
|
|
28
|
-
|
|
29
|
-
goto_url(start_url)
|
|
30
|
-
wait_for_load()
|
|
31
|
-
wait_for_idle(timeout=60)
|
|
32
|
-
|
|
33
|
-
if temporary:
|
|
34
|
-
is_temporary=bool(js("""(() => {
|
|
35
|
-
if (location.search.includes('temporary-chat=true')) return true;
|
|
36
|
-
return [...document.querySelectorAll('button')].some(b =>
|
|
37
|
-
(b.getAttribute('aria-label')||'').toLowerCase().includes('turn off temporary chat') ||
|
|
38
|
-
(b.textContent||'').trim().toLowerCase()==='turn off temporary chat'
|
|
39
|
-
) || [...document.querySelectorAll('h1')].some(h => (h.textContent||'').trim().toLowerCase()==='temporary chat');
|
|
40
|
-
})()"""))
|
|
41
|
-
if not is_temporary:
|
|
42
|
-
raise RuntimeError("ChatGPT Temporary Chat mode was not activated")
|
|
43
|
-
|
|
44
|
-
receipt=submit(message)
|
|
45
|
-
|
|
46
|
-
if temporary:
|
|
47
|
-
print(json.dumps({
|
|
48
|
-
"status":"submitted",
|
|
49
|
-
"mode":"new-thread",
|
|
50
|
-
"temporary":True,
|
|
51
|
-
"project_id":None,
|
|
52
|
-
"source_thread_id":source_thread_id,
|
|
53
|
-
"thread_id":None,
|
|
54
|
-
"thread_url":None,
|
|
55
|
-
"verified_by":receipt["verified_by"],
|
|
56
|
-
"user_message_id":receipt["turn"].get("id") or None,
|
|
57
|
-
},ensure_ascii=False),flush=True)
|
|
58
|
-
else:
|
|
59
|
-
deadline=time.time()+60
|
|
60
|
-
thread_id=None
|
|
61
|
-
thread_url=None
|
|
62
|
-
while time.time()<deadline:
|
|
63
|
-
current=page_info().get("url","").split("?",1)[0]
|
|
64
|
-
path=urlsplit(current).path.rstrip("/")
|
|
65
|
-
if "/c/" in path and "local-chatgpt" not in current:
|
|
66
|
-
candidate=path.rsplit("/",1)[-1]
|
|
67
|
-
if candidate:
|
|
68
|
-
thread_id=candidate
|
|
69
|
-
thread_url=current
|
|
70
|
-
break
|
|
71
|
-
time.sleep(.2)
|
|
72
|
-
if not thread_id:
|
|
73
|
-
raise RuntimeError("ChatGPT did not assign a durable thread id")
|
|
74
|
-
|
|
75
|
-
print(json.dumps({
|
|
76
|
-
"status":"submitted",
|
|
77
|
-
"mode":"new-thread",
|
|
78
|
-
"temporary":False,
|
|
79
|
-
"project_id":project_id,
|
|
80
|
-
"source_thread_id":source_thread_id,
|
|
81
|
-
"thread_id":thread_id,
|
|
82
|
-
"thread_url":thread_url,
|
|
83
|
-
"verified_by":receipt["verified_by"],
|
|
84
|
-
"user_message_id":receipt["turn"].get("id") or None,
|
|
85
|
-
},ensure_ascii=False),flush=True)
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
from urllib.parse import quote, urlsplit
|
|
3
|
-
import _lifecycle_common as lifecycle_common
|
|
4
|
-
|
|
5
|
-
lifecycle_common.js = js
|
|
6
|
-
lifecycle_common.fill_input = fill_input
|
|
7
|
-
wait_for_idle = lifecycle_common.wait_for_idle
|
|
8
|
-
submit = lifecycle_common.submit
|
|
9
|
-
|
|
10
|
-
CFG=json.load(open("__CFG_PATH__",encoding="utf-8"))
|
|
11
|
-
project_id=str(CFG.get("project_id") or "").strip() or None
|
|
12
|
-
thread_id=str(CFG.get("thread_id") or "").strip()
|
|
13
|
-
message=str(CFG.get("message") or "").strip()
|
|
14
|
-
if not thread_id: raise RuntimeError("thread_id is required")
|
|
15
|
-
if not message: raise RuntimeError("message is required")
|
|
16
|
-
if project_id:
|
|
17
|
-
thread_url=f"https://chatgpt.com/g/{quote(project_id,safe='')}/c/{quote(thread_id,safe='')}"
|
|
18
|
-
else:
|
|
19
|
-
thread_url=f"https://chatgpt.com/c/{quote(thread_id,safe='')}"
|
|
20
|
-
goto_url(thread_url)
|
|
21
|
-
wait_for_load()
|
|
22
|
-
if urlsplit(page_info().get("url","")).path.rstrip("/") != urlsplit(thread_url).path.rstrip("/"):
|
|
23
|
-
raise RuntimeError("ChatGPT thread redirect mismatch")
|
|
24
|
-
wait_for_idle(timeout=int(CFG.get("wait_timeout",300)))
|
|
25
|
-
receipt=submit(message)
|
|
26
|
-
print(json.dumps({
|
|
27
|
-
"status":"submitted",
|
|
28
|
-
"mode":"new-turn",
|
|
29
|
-
"project_id":project_id,
|
|
30
|
-
"thread_id":thread_id,
|
|
31
|
-
"verified_by":receipt["verified_by"],
|
|
32
|
-
"user_message_id":receipt["turn"].get("id") or None,
|
|
33
|
-
},ensure_ascii=False),flush=True)
|