把 PromptQL(Hasura DDN 架构的 AI agent playground)逆向封装成 OpenAI / Anthropic 兼容的本地网关,并附带全自动账号注册机(多账号池)。
对外暴露:
| 接口 | 兼容 |
|---|---|
GET /v1/models |
OpenAI |
POST /v1/chat/completions |
OpenAI(流式 + 非流式 + tool calls + usage) |
POST /v1/responses |
OpenAI Responses API(typed SSE events) |
POST /v1/messages |
Anthropic(流式 + 非流式 + tool_use + usage) |
POST /v1/messages/count_tokens |
Anthropic |
底层每次请求在 PromptQL 新建一个 thread(无状态语义),发首条消息触发 agent,轮询 thread_events 拿回复,再转成各家格式。
- 多账号池:
account/<name>.json存每个 PromptQL 账号的hasura-luxcookie + project 信息。网关启动加载全部,每次请求 round-robin 轮换一个账号,分散速率/配额限制;某账号认证失败自动标记disabled并换号(返回 503,客户端重试即换号)。 - 配置:
config.toml(gitignored)放网关/行为/端点/注册机配置;账号凭据分离到account/。 - 注册机(
registrar/,独立包):协议式自动注册 prompt.ql.app 新账号——临邮 + Turnstile + OTP → 提取hasura-lux+ project → 写account/<name>.json。主程序不依赖它(uv sync --extra registrar才装)。
- 认证链:
hasura-luxcookie →POST auth.pro.ql.app/ddn/promptql/token拿 luxJWT →mutation EnrichToken换成主 GraphQL 的 Bearer JWT(~24h,自动刷新)。 - 发消息:
mutation start_thread(projectId, message, timezone, roomless=true)—— 一步创建 thread + 发首条消息 + 触发 agent。agentResponseConfig留空即触发 agent。 - 收回复:轮询
query QueryThreadEvents(thread_id, after_event_id),消费 event 流:main_agent.llm_response:含usage(input/output/cached/thinking tokens,真实计数)+thinking_text。main_agent.actions_parsed.actions[].final_response.message:给用户的最终文本。interaction_finished:终止。
- token 计数:优先用
llm_response.usage;无则 tiktoken 兜底。 - CoT / Thinking 透传:
llm_response.thinking_text会按各家格式返回;客户端传入的 thinking/reasoning 内容也会保留进 PromptQL prompt。
详见 app/promptql/ 与项目 memory。
PromptQL 的 agent 会在 llm_response.thinking_text 中返回推理过程。本网关把它透传给客户端,并保留客户端回传的历史 CoT:
| 接口 | 返回字段 / 格式 | 请求体中 CoT 的保留方式 |
|---|---|---|
POST /v1/chat/completions |
choices[0].message.reasoning_content(DeepSeek 兼容的事实标准) |
message 根字段 reasoning_content 会包装成 <reasoning>...</reasoning> 放进 prompt |
POST /v1/responses |
output 中增加 { "type": "reasoning", "summary": [{ "type": "summary_text", "text": "..." }] } |
input 中 type: "reasoning" 的 item 会保留进 prompt |
POST /v1/messages |
content 中增加 { "type": "thinking", "thinking": "...", "signature": "" } |
content 中 type: "thinking" 的 block 会包装成 <thinking>...</thinking> 放进 prompt;请求体支持 thinking 参数 |
流式响应同样会按上述格式 emit CoT 增量事件。
注意:Anthropic 官方返回的 thinking block 含 signature,但 PromptQL 上游未提供,因此网关返回的 signature 为空字符串。
PromptQL 的 agent 有很强的内置 system prompt 与 data/query assistant 身份,会拒绝「按 <tool_call> 围栏输出工具调用」这类直白指令(实测回复 "that's not how I operate"),甚至自带 wiki/data/code 工具自行回答。复杂场景(长 system + 多工具 + 多轮,如 Claude Code / Cline 驱动)下单一手段易被识破,故本网关用多层防御叠加命中率——参考 /data1/cursor2api 的工程方法,但全部针对 PromptQL 身份重写、且工具/客户端无关:
- system 软化包装(
app/system_sanitizer.py):客户端 system 不再用硬标签[system]直塞,而是移除计费/调试头垃圾行后,用柔和的背景框架承载——弱化「系统强制身份覆盖」色彩,降低 agent 身份对抗刺激。不动一个字的实质指令(身份声明、工具指令、强制措辞一律保留)。 - 认知重构角度集(
app/reframe_angles.py):顺应 agent 身份,在消息最前注入情景,让 agent「只是在生成一段表示工具调用的记录」(职责内)而非「执行工具」。默认 B「测试夹具」(probe 实测稳定命中者);新增 **F「结构化动作记录」/ G「路由标注」**更贴 agentactions_parsed本职,作重试轮换备选(scripts/probe_reframe.py --angles all验证后可替换默认)。 - 拒绝检测 + 自动重试(
app/orchestrator.py:stream_with_retry+app/refusal.py):整轮 buffer 回复 → 命中身份识破/拒绝措辞 → 换角度重建 prompt 重试(config.toml [promptql].tool_call_retries,默认 3),把单次命中率累积成多次。与deps._RetryingClient(账号级 503 换号)正交。 - 多 few-shot 续推:
extract_user_prompt把历史tool_calls/tool_use渲染成<tool_call>围栏送回 agent;build_directive默认按工具 namespace 分组注入多样 few-shot(参数从 schema 推断,不硬编码工具名),单轮也获锚定。 - 鲁棒解析(
app/tools.py:parse_tool_calls):JSON-aware 围栏扫描(免疫大 content 内}/</tool_call>字面量)+tolerant_parse(未闭合括号/控制字符/尾逗号容错)+ 字段名兼容(name|tool、arguments|parameters|input)+ 拒绝感知 + 同名同参数去重。
场景决定的命中率(重要局限)——认知重构有效性高度依赖客户端 system 的「身份对抗强度」:
| 场景 | tool call 命中率(实测) |
|---|---|
| simple(无 system 身份声明、无强制措辞、单工具) | gpt-5.5/sonnet/deepseek 等 ~100%,glm-5.2 ~66%,Opus ~0%(识破) |
complex(含 You are Claude Code 身份声明 + You must call tools 强制措辞,如 Claude Code / Cline 驱动) |
全模型 ~0% 识破——gpt-5.5/sonnet/deepseek 全部亮身份拒绝(实测账号额度耗尽前已确认) |
记忆里 probe_models 的"非 Opus 模型 ~100%"是 simple 场景结论,不可外推到真实 agent 框架驱动的复杂场景。
根因:PromptQL agent 的 data/query assistant 自我身份感很强,遇到「外部身份覆盖 + 强制工具调用」组合时主动纠正而非配合。这是固有硬墙——防御纵深的工程层已验证生效(orchestrator 检测到拒绝 → 角度轮换 B→F→G→D 全部触发,见网关 stderr 日志),但 B/F/G/D 四个角度重试均被识破,单靠 prompt 工程(认知重构 + 软化包装)突破不了复杂 system 的身份对抗硬墙。
故当前对复杂 system 客户端的 tool calling 不能保证,未命中回退普通文本。后续方向(plan 的可选后续):调研 PromptQL 原生工具通道(route_config 的 mcp_uri/chat_handler_uri、agent 的 http/playground_graphql 原生工具审批 QueryPendingApprovalRequests),评估能否把客户端工具原生注册进 agent——让它用本职机制合规调用,彻底绕过身份对抗。需抓包逆向,能否成功尚未确定。
/v1/models 返回实地从 prompt.ql.app 抓取的 10 个模型(模型选择 dialog 各选项 button 的 data-testid 即 llmConfigId;模型列表为前端 bundle 硬编码,后端无查询接口):
gpt-5.5(默认)/ claude-opus-4-8 / claude-sonnet-4-5-20250929 / deepseek-v4-pro / gemini-3.1-pro-preview / gemini-3.5-flash / glm-5.2 / kimi-k2.6 / kimi-k2.7-code / minimax-m3
客户端传的 model 经 normalize_model 归一化(支持 id、显示名、模糊匹配)后映射到 llmConfigId,通过 start_thread 的 llmConfigId 参数真正切换底层模型。未知 model 回退默认 gpt-5.5。映射表见 app/adapters/__init__.py:MODEL_CATALOG。
复制 config.toml.example 为 config.toml 并填值(config.toml 已 gitignore):
[gateway]
host = "0.0.0.0"
port = 8088
api_key = "" # 客户端访问网关的 key(Authorization: Bearer <key>);留空则不校验
[promptql]
timezone = "Asia/Shanghai"
agent_response_config = "" # 空=触发 agent
poll_interval = 1.2
request_timeout = 120.0
token_refresh_margin = 300
auth_token_url = "https://auth.pro.ql.app/ddn/promptql/token"
graphql_url = "https://data.prompt.ql.app/promptql/playground-v2-hge/v1/graphql"
[registry]
account_dir = "account" # 账号凭据目录(account/*.json,gitignored)
# ===== 以下仅注册机用 =====
[email] # Cloudflare Temp Email:https://github.com/dreamhunter2333/cloudflare_temp_email
base_url = "https://your-mail-service.example.com"
admin_auth = "" # 后台管理员密码(x-admin-auth)
custom_auth = "" # 后台自定义鉴权(x-custom-auth)
domain = "your-domain.com" # 已绑定的收件域名(不带 @)
[turnstile]
method = "semi" # semi(默认)/ cdp / api,详见下方「注册机」
headless = false # semi 策略:prompt.ql.app 无头过不了,建议 false
# ===== 管理后台 =====
[admin]
auth_key = "" # /admin/* 管理端点鉴权(Authorization: Bearer <key> 或 ?auth_key=<key>);留空=关闭 admin 端点账号凭据放 account/<name>.json(gitignored):
{
"name": "main",
"source_email": "abc@your-domain.com",
"hasura_lux": "<auth.pro.ql.app 的 hasura-lux cookie 全值>",
"project_id": "<uuid>",
"project_name": "p-<uuid>",
"created_at": "2026-07-02T14:22:33",
"disabled": false
}从旧 .env 迁移(已有 HASURA_LUX/PROJECT_ID):
uv run python scripts/migrate_env_to_toml.py # 幂等:生成 account/main.json + config.tomluv sync
uv run uvicorn app.main:app --host 0.0.0.0 --port 8088Docker:
docker build -t promptql2api .
docker run -p 8088:8088 \
-v $(pwd)/config.toml:/app/config.toml \
-v $(pwd)/account:/app/account \
promptql2api启动时若无可用账号,会提示「请运行注册机或迁移 .env」。
注册机是独立可选组件(重依赖 curl-cffi/playwright/camoufox,主程序不依赖):
# 1. 装注册机依赖 + 浏览器
uv sync --extra registrar
uv run playwright install chromium
# 2. 在 config.toml 填 [email](你的临邮服务)+ [turnstile]
# 3. 注册(默认 semi 策略)
uv run python -m registrar -n 5 -w 2 --proxy http://host:port
uv run python -m registrar --count 0 --workers 3 # 无限运行直到 Ctrl+CCLI:-n/--count(0=无限)、-w/--workers(并发线程)、--proxy、--turnstile-method(覆盖 config)、--config。
注册成功自动写 account/<name>.json(用临邮 local-part 命名,重名加序号),网关下次启动即加载。
prompt.ql.app 的 Turnstile 严格反自动化(registrar/PROTOCOL.md 有完整逆向):服务端强校验 captcha_token(空值 → 400 Captcha verification is required),且 playwright chromium / camoufox firefox 等自动化浏览器(headless、非无头都)过不了——只有真实日常浏览器或人类交互能过。故 solver 抽象为三种可配置策略:
method |
原理 | 适用 |
|---|---|---|
semi(默认) |
playwright 弹浏览器到登录页,等 widget 自动过或人手动点一下;检测到 token 后全自动继续 | 交互式跑,需桌面 |
cdp |
connect_over_cdp 连你已开的 debug chrome(--remote-debugging-port=9222),真实指纹自动过 |
有日常 chrome |
api |
第三方打码(CapSolver AntiTurnstileTaskProxyLess),无浏览器 | 付费、全自动、无桌面 |
由 config.toml [turnstile].method 或 CLI --turnstile-method 切换。注册协议(otp/send +captcha_token、otp/verify {email, otp, nonce}、查 ddn_projects)见 registrar/PROTOCOL.md。
from openai import OpenAI
c = OpenAI(base_url="http://localhost:8088/v1", api_key="any")
r = c.chat.completions.create(model="gpt-5.5", messages=[{"role": "user", "content": "你好"}])
print(r.choices[0].message.content)from anthropic import Anthropic
c = Anthropic(base_url="http://localhost:8088", api_key="any")
m = c.messages.create(model="gpt-5.5", max_tokens=1024,
messages=[{"role": "user", "content": "你好"}])
print(m.content[0].text)在 config.toml 配置 [admin].auth_key 后,可启用 /admin/* 管理端点:
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /admin/accounts |
列出账号摘要(不暴露 cookie/project_id) |
| GET | /admin/accounts/{name} |
查看单个账号完整字段 |
| POST | /admin/accounts |
上传/新增账号,请求体同 account/<name>.json |
| DELETE | /admin/accounts/{name} |
删除账号 |
| POST | /admin/reload |
重新从磁盘加载账号池 |
鉴权方式(二选一):
curl -H "Authorization: Bearer $ADMIN_KEY" http://localhost:8088/admin/accounts
# 或
curl "http://localhost:8088/admin/accounts?auth_key=$ADMIN_KEY"上传账号示例:
curl -X POST "http://localhost:8088/admin/accounts?auth_key=$ADMIN_KEY" \
-H "content-type: application/json" \
-d '{
"name": "main",
"source_email": "user@example.com",
"hasura_lux": "...",
"project_id": "...",
"project_name": "p-...",
"created_at": "2026-07-02T14:22:33",
"disabled": false
}'项目提供 scripts/promptql_account_uploader.user.js:
- 安装 Tampermonkey Beta(仅 Beta 版支持读取 httpOnly cookie;稳定版读不到
hasura-lux)。Violentmonkey 无GM_cookie,会走下面的手动兜底。 - 将
scripts/promptql_account_uploader.user.js添加为新脚本。脚本已声明@match https://auth.pro.ql.app/*,这是GM_cookie跨域读取hasura-lux的授权前提。 - 在脚本菜单中设置
ADMIN_URL(默认http://localhost:8088)与ADMIN_AUTH_KEY。 - 登录
https://prompt.ql.app,点击右下角「上交账号」按钮即可自动提取hasura-luxcookie、查询 project 并上传。
关于
hasura-lux:它是pro.ql.app域的 httpOnly cookie(domain=pro.ql.app,故auth.pro.ql.app、data.pro.ql.app等子域请求都带它;但prompt.ql.app、data.prompt.ql.app等非 pro.ql.app 域不带)。脚本通过GM_cookie.list({ url: 'https://auth.pro.ql.app/' })跨域读取,需 Tampermonkey Beta 且首次运行授权 cookie 权限。若自动读取失败(稳定版 / 未授权 / 跨域被拒),脚本会弹窗引导:DevTools → Application → Cookies →https://auth.pro.ql.app(或任一 pro.ql.app 子域)→ 复制hasura-lux的 Value 粘贴即可(值会缓存在本机便于续期)。
关于跨域上传:上传走
GM_xmlhttpRequest(非页面fetch),因此可跨源、跨 HTTP↔HTTPS 访问内网/本地网关(如http://192.168.x.x:8089),不受浏览器 CORS 预检与混合内容(Mixed Content)拦截。脚本声明@connect *兜底任意ADMIN_URL,首次连接目标主机时 Tampermonkey 会弹窗请求授权,点允许即可。
- Turnstile 反自动化:注册机的 Turnstile 必须真实求解(semi/cdp/api),无法协议层绕过(服务端强校验 + 前端无旁路 + 无 password/signup 端点)。
- tool calling 依赖模型:默认 gpt-5.5 下认知重构 ~100% 生效;唯独 claude-opus-4-8 会识破(~0%)。未命中回退普通文本。
- 流式为「伪流式」:PromptQL 的 agent 文本是整块返回(每个事件带完整文本,非逐 token delta)。
- 每次请求新建 thread:会产生 PromptQL thread 残留。
- usage 含缓存命中:网关取首次非零 usage 作为返回。
- 不支持 vision/音频/部分 OpenAI 参数。
uv sync --extra dev
uv run pytest -q # 95 个测试(adapters/tools/account/config/events/refusal/system_sanitizer/orchestrator)
uv run python scripts/probe.py # 抓包探针app/
config.py Settings(pydantic + tomllib,无账号凭据)
account.py Account + AccountPool(round-robin + mark_disabled)
admin.py /admin/* 管理端点(账号上传/删除/重载)
deps.py get_client 走账号池 + _RetryingClient(认证失败 → 503 换号)
main.py FastAPI 入口(lifespan 加载账号池)
tools.py tool-call 认知重构 + 多级鲁棒解析
refusal.py 拒绝/身份识破检测(orchestrator 重试依据)
system_sanitizer.py system 软化包装 + 垃圾行移除(不动实质内容)
orchestrator.py stream_with_retry(整轮 buffer + 拒绝检测 + 角度轮换重试)
reframe_angles.py 认知重构角度集(A-G,B 默认,F/G 顺应 actions_parsed 本职)
reframe_angles.py 认知重构角度集(A-G,B 默认)
tokens.py usage 汇总 + tiktoken 兜底
promptql/
auth.py cookie → luxJWT → Bearer JWT(per-account,缓存+刷新)
client.py start_thread / QueryThreadEvents 轮询
events.py event_data → 统一 IR
adapters/
openai_models.py / openai_chat.py / openai_responses.py / anthropic_messages.py
registrar/ 全自动注册机(独立包,主程序不 import)
cli.py argparse 入口(python -m registrar)
pipeline.py 注册编排:临邮→Turnstile→otp/send→收码→otp/verify→查 project
email_client.py Cloudflare Temp Email(create_email / poll_code)
turnstile.py Turnstile solver(semi/cdp/api 三策略)
http_client.py curl-cffi(impersonate chrome + 429 退避)
models.py RegistrarConfig(读 config [email]/[turnstile])
PROTOCOL.md 注册协议逆向依据
account/ 账号凭据 *.json(gitignored)
config.toml 运行配置(gitignored)
config.toml.example 配置模板(传 git)
scripts/migrate_env_to_toml.py .env → config.toml + account/main.json(幂等)
scripts/probe.py 抓包探针
scripts/probe_models.py 模型 tool-call 命中率探针
scripts/probe_reframe.py 认知重构角度探针
scripts/promptql_account_uploader.user.js 油猴脚本:自动上交 PromptQL 账号
tests/ events / tools / adapters / account / config / admin(新增)
MIT © 2026 Null