Skip to content

Commit a04a1fc

Browse files
authored
Merge pull request #62 from YuJunZhiXue/trae
Trae
2 parents c9ed466 + 26d3752 commit a04a1fc

39 files changed

Lines changed: 2819 additions & 860 deletions

.dockerignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,5 @@ test_*.py
6767
.dockerignore
6868
Dockerfile*
6969
docker-compose*.yml
70+
**/*.tsbuildinfo
71+
frontend/node_modules/.tmp/

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,4 @@ fix_*.py
4343
# local cleanup
4444
tests/
4545
docs/superpowers/
46+
*.tsbuildinfo

Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,19 +55,19 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
5555
&& rm -rf /var/lib/apt/lists/*
5656

5757
COPY backend/requirements.txt /tmp/requirements.txt
58-
RUN pip install -r /tmp/requirements.txt
58+
RUN python -m pip install --upgrade pip && python -m pip install -r /tmp/requirements.txt
5959

6060
# Download Camoufox browser at build time so runtime hosts do not need to fetch it again.
6161
RUN python -m camoufox fetch
6262

6363
COPY backend/ ./backend/
6464
COPY start.py ./
65-
COPY --from=frontend-builder /app/dist ./frontend/dist
6665
RUN mkdir -p /workspace/data /workspace/logs /workspace/frontend
66+
COPY --from=frontend-builder /app/dist ./frontend/dist
6767

6868
EXPOSE 7860
6969

7070
HEALTHCHECK --interval=30s --timeout=10s --start-period=120s --retries=3 \
7171
CMD curl -fsS "http://127.0.0.1:${PORT:-7860}/healthz" || exit 1
7272

73-
CMD ["sh", "-c", "python -m uvicorn backend.main:app --host 0.0.0.0 --port ${PORT:-7860} --workers ${WORKERS:-1}"]
73+
CMD ["sh", "-c", "exec python -m uvicorn backend.main:app --host 0.0.0.0 --port ${PORT:-7860} --workers ${WORKERS:-1}"]

backend/adapter/cli_proxy.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from backend.adapter.standard_request import StandardRequest, CLAUDE_CODE_OPENAI_PROFILE
99
from backend.core.config import resolve_model
1010
from backend.services.prompt_builder import messages_to_prompt
11+
from backend.services.workspace_context import derive_workspace_root
1112
from backend.toolcall.normalize import build_tool_name_registry
1213

1314
log = logging.getLogger("qwen2api.cli_proxy")
@@ -32,6 +33,8 @@ def from_openai(req_data: dict, *, client_profile: str = CLAUDE_CODE_OPENAI_PROF
3233
StandardRequest: 统一的标准请求对象
3334
"""
3435
model_name = req_data.get("model", "gpt-4o")
36+
workspace_root = derive_workspace_root(req_data)
37+
req_data = {**req_data, "_workspace_root": workspace_root}
3538
prompt_result = messages_to_prompt(req_data, client_profile=client_profile)
3639

3740
tools = prompt_result.tools
@@ -53,6 +56,7 @@ def from_openai(req_data: dict, *, client_profile: str = CLAUDE_CODE_OPENAI_PROF
5356
tool_names=tool_names,
5457
tool_name_registry=build_tool_name_registry(tool_names),
5558
tool_enabled=prompt_result.tool_enabled,
59+
workspace_root=workspace_root,
5660
)
5761

5862
@staticmethod
@@ -68,6 +72,8 @@ def from_anthropic(req_data: dict, *, client_profile: str = CLAUDE_CODE_OPENAI_P
6872
StandardRequest: 统一的标准请求对象
6973
"""
7074
model_name = req_data.get("model", "claude-3-5-sonnet")
75+
workspace_root = derive_workspace_root(req_data)
76+
req_data = {**req_data, "_workspace_root": workspace_root}
7177
prompt_result = messages_to_prompt(req_data, client_profile=client_profile)
7278

7379
tools = prompt_result.tools
@@ -89,6 +95,7 @@ def from_anthropic(req_data: dict, *, client_profile: str = CLAUDE_CODE_OPENAI_P
8995
tool_names=tool_names,
9096
tool_name_registry=build_tool_name_registry(tool_names),
9197
tool_enabled=prompt_result.tool_enabled,
98+
workspace_root=workspace_root,
9299
)
93100

94101
@staticmethod

backend/adapter/standard_request.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,6 @@ class StandardRequest:
3636
persistent_session: bool = False
3737
session_message_hashes: list[str] = field(default_factory=list)
3838
session_chat_invalidated: bool = False
39+
workspace_root: str | None = None
40+
retry_blocked_tools: list[str] = field(default_factory=list)
41+
retry_read_blocklist: list[str] = field(default_factory=list)

backend/api/admin.py

Lines changed: 13 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -187,27 +187,25 @@ async def register_new_account(request: Request):
187187

188188
@router.post("/verify", dependencies=[Depends(verify_admin)])
189189
async def verify_all_accounts(request: Request):
190-
"""验证所有账号的有效性 (完全复原单文件逻辑)"""
190+
"""逐个到 chat.qwen.ai 官网验证账号;token 失效时自动刷新。"""
191191
from backend.core.account_pool import AccountPool
192192
from backend.services.qwen_client import QwenClient
193-
import logging
194193

195-
log = logging.getLogger("qwen2api.admin")
196194
pool: AccountPool = request.app.state.account_pool
197195
client: QwenClient = request.app.state.qwen_client
198196

199197
results = []
200198
for acc in pool.accounts:
201-
is_valid = await client.verify_token(acc.token)
202-
if not is_valid and acc.password:
203-
log.info(f"[校验] {acc.email} token失效,尝试自动刷新...")
204-
is_valid = await client.auth_resolver.refresh_token(acc)
205-
206-
acc.valid = is_valid
207-
results.append({"email": acc.email, "valid": is_valid, "refreshed": not is_valid})
208-
209-
await pool.save() # 直接保存全部状态,不调用 mark_invalid 以免熔断影响测试
210-
return {"ok": True, "results": results}
199+
results.append(await client.verify_account(acc))
200+
201+
summary = {
202+
"total": len(results),
203+
"valid": sum(1 for item in results if item.get("valid")),
204+
"refreshed": sum(1 for item in results if item.get("refreshed")),
205+
"banned": sum(1 for item in results if item.get("status_code") == "banned"),
206+
"failed": sum(1 for item in results if not item.get("valid")),
207+
}
208+
return {"ok": True, "results": results, "summary": summary, "concurrency": 1}
211209

212210
@router.post("/accounts/{email}/activate", dependencies=[Depends(verify_admin)])
213211
async def activate_account(email: str, request: Request):
@@ -238,28 +236,18 @@ async def activate_account(email: str, request: Request):
238236

239237
@router.post("/accounts/{email}/verify", dependencies=[Depends(verify_admin)])
240238
async def verify_account(email: str, request: Request):
241-
"""单独验证某个账号的有效性 (完全复原单文件逻辑)"""
239+
"""单独到 chat.qwen.ai 官网验证账号;token 失效时自动刷新。"""
242240
from backend.services.qwen_client import QwenClient
243241
from backend.core.account_pool import AccountPool
244-
import logging
245242

246-
log = logging.getLogger("qwen2api.admin")
247243
pool: AccountPool = request.app.state.account_pool
248244
client: QwenClient = request.app.state.qwen_client
249245

250246
acc = next((a for a in pool.accounts if a.email == email), None)
251247
if not acc:
252248
raise HTTPException(status_code=404, detail="Account not found")
253249

254-
is_valid = await client.verify_token(acc.token)
255-
if not is_valid and acc.password:
256-
log.info(f"[校验] {acc.email} token失效,尝试自动刷新...")
257-
is_valid = await client.auth_resolver.refresh_token(acc)
258-
259-
acc.valid = is_valid
260-
await pool.save() # 直接保存,不调用 mark_invalid 以免熔断影响正常测试
261-
262-
return {"email": acc.email, "valid": is_valid}
250+
return await client.verify_account(acc)
263251

264252
@router.delete("/accounts/{email}", dependencies=[Depends(verify_admin)])
265253
async def delete_account(email: str, request: Request):

backend/api/anthropic.py

Lines changed: 79 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,35 @@
3232
plan_persistent_session_turn,
3333
)
3434
from backend.services.token_calc import count_tokens
35+
from backend.services.workspace_context import derive_workspace_root
3536
from backend.toolcall.normalize import build_tool_name_registry
3637

3738
log = logging.getLogger("qwen2api.anthropic")
3839
router = APIRouter()
3940

4041

42+
def _tool_input_preview(input_data, *, limit: int = 260) -> str:
43+
try:
44+
raw = json.dumps(input_data if input_data is not None else {}, ensure_ascii=False, sort_keys=True)
45+
except (TypeError, ValueError):
46+
raw = repr(input_data)
47+
return " ".join(raw.split())[:limit] + ("...[truncated]" if len(raw) > limit else "")
48+
49+
50+
def _log_response_tool_blocks(stage: str, blocks: list[dict]) -> None:
51+
for idx, block in enumerate(blocks, start=1):
52+
if not isinstance(block, dict) or block.get("type") != "tool_use":
53+
continue
54+
log.info(
55+
"[ANT-ToolOut] stage=%s index=%s id=%s name=%s input=%s",
56+
stage,
57+
idx,
58+
block.get("id", "-"),
59+
block.get("name", "-"),
60+
_tool_input_preview(block.get("input", {})),
61+
)
62+
63+
4164
class _AnthropicStreamState:
4265
def __init__(self, *, msg_id: str, model_name: str, prompt: str):
4366
self.msg_id = msg_id
@@ -125,8 +148,9 @@ def clear_answer_text(self) -> None:
125148

126149

127150
def _build_standard_request(req_data: dict) -> StandardRequest:
128-
"""使用 CLIProxy 进行协议转换"""
151+
"""浣跨敤 CLIProxy 杩涜鍗忚杞崲"""
129152
standard_request = CLIProxy.from_anthropic(req_data, client_profile=CLAUDE_CODE_OPENAI_PROFILE)
153+
standard_request.workspace_root = derive_workspace_root(req_data)
130154
CLIProxy.log_conversion("anthropic", standard_request.response_model, len(standard_request.prompt), len(standard_request.tools))
131155
return standard_request
132156

@@ -149,7 +173,7 @@ async def _run_anthropic_attempt(
149173
max_attempts: int,
150174
):
151175
update_request_context(stream_attempt=stream_attempt + 1)
152-
execution = await collect_completion_run(client, standard_request, current_prompt)
176+
execution = await collect_completion_run(client, standard_request, current_prompt, history_messages=history_messages)
153177
retry = evaluate_retry_directive(
154178
request=standard_request,
155179
current_prompt=current_prompt,
@@ -198,9 +222,9 @@ async def anthropic_count_tokens(request: Request):
198222
prompt_result = messages_to_prompt(req_data, client_profile=CLAUDE_CODE_OPENAI_PROFILE)
199223
base_tokens = count_tokens(prompt_result.prompt)
200224
# Context Pressure Inflation:
201-
# Claude Code 假设 context window=200K,到 ~80%(160K) 触发自动压缩。
202-
# Qwen 实际上游 window 只有 ~150K,到 ~120K 时就开始挤压输出预算。
203-
# 虚增 input_tokens 1.35x CC 提前触发压缩,避免爆 window。
225+
# Claude Code 鍋囪 context window=200K锛屽埌 ~80%(160K) 瑙﹀彂鑷姩鍘嬬缉銆?
226+
# 浣?Qwen 瀹為檯涓婃父 window 鍙湁 ~150K锛屽埌 ~120K 鏃跺氨寮€濮嬫尋鍘嬭緭鍑洪绠椼€?
227+
# 铏氬 input_tokens 1.35x 璁?CC 鎻愬墠瑙﹀彂鍘嬬缉锛岄伩鍏嶇垎 window銆?
204228
inflation = 1.35
205229
inflated = int(base_tokens * inflation)
206230
return JSONResponse({"input_tokens": inflated})
@@ -288,7 +312,17 @@ async def generate():
288312
async with app.state.session_locks.hold(session_key):
289313
standard_request, effective_payload, model_name, qwen_model, prompt, msg_id = await prepare_locked_request(req_data)
290314
update_request_context(requested_model=model_name, resolved_model=qwen_model)
291-
log.info(f"[ANT] model={qwen_model}, stream={standard_request.stream}, tool_enabled={standard_request.tool_enabled}, tools={[t.get('name') for t in standard_request.tools]}, prompt_len={len(prompt)}")
315+
tool_names = [t.get('name') for t in standard_request.tools]
316+
log.info(
317+
"[ANT] model=%s stream=%s tool_enabled=%s tools=%s mcp_tools=%s workspace=%s prompt_len=%s",
318+
qwen_model,
319+
standard_request.stream,
320+
standard_request.tool_enabled,
321+
tool_names,
322+
[name for name in tool_names if isinstance(name, str) and name.startswith("mcp__")],
323+
standard_request.workspace_root or "-",
324+
len(prompt),
325+
)
292326
history_messages = original_history_messages
293327
current_prompt = prompt
294328
max_attempts = request_max_attempts(standard_request)
@@ -329,6 +363,7 @@ async def on_delta(evt, text_chunk, _):
329363
max_continuation=2,
330364
warmup_chars=64,
331365
guard_chars=96,
366+
history_messages=history_messages,
332367
)
333368
retry = evaluate_retry_directive(
334369
request=standard_request,
@@ -341,11 +376,11 @@ async def on_delta(evt, text_chunk, _):
341376
)
342377
if retry.retry:
343378
reused_persistent_chat = bool(standard_request.persistent_session and standard_request.upstream_chat_id)
344-
# 如果正在复用会话,重试时保留会话,避免删除后重建导致上下文丢失
379+
# 濡傛灉姝e湪澶嶇敤浼氳瘽锛岄噸璇曟椂淇濈暀浼氳瘽锛岄伩鍏嶅垹闄ゅ悗閲嶅缓瀵艰嚧涓婁笅鏂囦涪澶?
345380
preserve_chat = reused_persistent_chat
346381
await cleanup_runtime_resources(client, execution.acc, execution.chat_id, preserve_chat=preserve_chat)
347382
if reused_persistent_chat:
348-
# 保留 upstream_chat_id,在同一会话中重试
383+
# 淇濈暀 upstream_chat_id锛屽湪鍚屼竴浼氳瘽涓噸璇?
349384
# standard_request.session_chat_invalidated = True
350385
# standard_request.upstream_chat_id = None
351386
current_prompt = build_retry_rebase_prompt(standard_request, reason=retry.reason)
@@ -357,13 +392,28 @@ async def on_delta(evt, text_chunk, _):
357392
if not stream_state.pending_chunks:
358393
stream_state.pending_chunks.append(_message_start_event(msg_id, model_name, current_prompt, execution.state.answer_text))
359394

360-
stream_state.close_current_block()
361-
directive = build_tool_directive(standard_request, execution.state)
395+
directive = build_tool_directive(standard_request, execution.state, history_messages=history_messages)
396+
if (
397+
directive.stop_reason != "tool_use"
398+
and not stream_state.answer_text_buffer
399+
and execution.state.answer_text
400+
):
401+
# ToolSieve may hold short normal replies until stream end to
402+
# avoid leaking partial tool markup. If no live text delta was
403+
# emitted, replay the finalized visible answer here.
404+
stream_state.buffer_answer_text(execution.state.answer_text)
405+
visible_answer_length = _visible_answer_text_length(
406+
directive=directive,
407+
execution=execution,
408+
stream_state=stream_state,
409+
)
362410
if directive.stop_reason == "tool_use":
363411
stream_state.clear_answer_text()
412+
stream_state.close_current_block()
364413
stream_state.current_block = {"type": None, "index": None, "tool_call_id": None}
365414
else:
366415
stream_state.flush_answer_text()
416+
stream_state.close_current_block()
367417
expected_tool_ids = {
368418
block.get("id")
369419
for block in directive.tool_blocks
@@ -381,11 +431,8 @@ async def on_delta(evt, text_chunk, _):
381431
)
382432
stream_state.close_current_block()
383433

384-
visible_answer_length = _visible_answer_text_length(
385-
directive=directive,
386-
execution=execution,
387-
stream_state=stream_state,
388-
)
434+
_log_response_tool_blocks("stream_response", directive.tool_blocks)
435+
389436
stop_reason = "tool_use" if expected_tool_ids else "end_turn"
390437
stream_state.pending_chunks.append(stream_presenter.anthropic_message_delta(stop_reason, visible_answer_length))
391438
stream_state.pending_chunks.append(stream_presenter.anthropic_message_stop())
@@ -435,7 +482,17 @@ async def on_delta(evt, text_chunk, _):
435482
async with app.state.session_locks.hold(session_key):
436483
standard_request, effective_payload, model_name, qwen_model, prompt, msg_id = await prepare_locked_request(req_data)
437484
update_request_context(requested_model=model_name, resolved_model=qwen_model)
438-
log.info(f"[ANT] model={qwen_model}, stream={standard_request.stream}, tool_enabled={standard_request.tool_enabled}, tools={[t.get('name') for t in standard_request.tools]}, prompt_len={len(prompt)}")
485+
tool_names = [t.get('name') for t in standard_request.tools]
486+
log.info(
487+
"[ANT] model=%s stream=%s tool_enabled=%s tools=%s mcp_tools=%s workspace=%s prompt_len=%s",
488+
qwen_model,
489+
standard_request.stream,
490+
standard_request.tool_enabled,
491+
tool_names,
492+
[name for name in tool_names if isinstance(name, str) and name.startswith("mcp__")],
493+
standard_request.workspace_root or "-",
494+
len(prompt),
495+
)
439496
history_messages = original_history_messages
440497
current_prompt = prompt
441498
max_attempts = request_max_attempts(standard_request)
@@ -451,11 +508,11 @@ async def on_delta(evt, text_chunk, _):
451508
)
452509
if retry.retry:
453510
reused_persistent_chat = bool(standard_request.persistent_session and standard_request.upstream_chat_id)
454-
# 如果正在复用会话,重试时保留会话,避免删除后重建导致上下文丢失
511+
# 濡傛灉姝e湪澶嶇敤浼氳瘽锛岄噸璇曟椂淇濈暀浼氳瘽锛岄伩鍏嶅垹闄ゅ悗閲嶅缓瀵艰嚧涓婁笅鏂囦涪澶?
455512
preserve_chat = reused_persistent_chat
456513
await cleanup_runtime_resources(client, execution.acc, execution.chat_id, preserve_chat=preserve_chat)
457514
if reused_persistent_chat:
458-
# 保留 upstream_chat_id,在同一会话中重试
515+
# 淇濈暀 upstream_chat_id锛屽湪鍚屼竴浼氳瘽涓噸璇?
459516
# standard_request.session_chat_invalidated = True
460517
# standard_request.upstream_chat_id = None
461518
current_prompt = build_retry_rebase_prompt(standard_request, reason=retry.reason)
@@ -464,7 +521,8 @@ async def on_delta(evt, text_chunk, _):
464521
await _reacquire_bound_account_if_needed(client=client, standard_request=standard_request)
465522
continue
466523

467-
directive = build_tool_directive(standard_request, execution.state)
524+
directive = build_tool_directive(standard_request, execution.state, history_messages=history_messages)
525+
_log_response_tool_blocks("json_response", directive.tool_blocks)
468526
content_blocks: list[dict] = []
469527
if execution.state.reasoning_text:
470528
content_blocks.append({"type": "thinking", "thinking": execution.state.reasoning_text})
@@ -511,3 +569,5 @@ async def on_delta(evt, text_chunk, _):
511569
if stream_attempt == max_attempts - 1:
512570
await clear_invalidated_session_chat(app=app, request=standard_request)
513571
raise HTTPException(status_code=500, detail=str(e))
572+
573+

0 commit comments

Comments
 (0)