Files
maxerton 4dd2e58b86 trello-tools: окно подтверждения на протоколе 2026-07-28, ошибки — модели
Claude Code теперь согласует со stdio-серверами MCP 2026-07-28, где
сервер не может сам слать запросы клиенту: ctx.elicit падал с
NoBackChannelError, и trello_create_card не создавал карточки ни в
терминале, ни в Desktop. Подтверждение переведено на Resolve/Elicit —
SDK спрашивает через input_required или elicitation/create, смотря по
версии протокола.

SDK прячет от модели текст любых исключений, кроме ToolError, поэтому
подсказки про незаданные ключи и 404 до неё не доходили.
TrelloToolsServer отдаёт их текстом (HTTP-ошибки — без query, там
ключи Trello).

Ещё:
- get_dev_task: перед реализацией ТЗ прочитать код, пересказать суть
  и предложить варианты или задать вопросы;
- QA-документы по умолчанию на украинском;
- README и подсказки в ошибках: в Claude Desktop /plugin configure нет,
  ключи задаются из терминала.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 16:53:19 +00:00

167 lines
6.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Клиент qa-docs-service."""
from __future__ import annotations
import os
import httpx
class QaServiceConfigError(RuntimeError):
"""Не заданы обязательные переменные окружения для qa-docs-service."""
def _config() -> tuple[str, str]:
url = os.environ.get("QA_SERVICE_URL")
api_key = os.environ.get("QA_SERVICE_API_KEY")
if not url or not api_key:
raise QaServiceConfigError(
"QA_SERVICE_URL / QA_SERVICE_API_KEY не заданы (плагин: в терминале запусти claude и введи /plugin configure trello-tools@gp-tools — в Claude Desktop эта команда не работает, после настройки его нужно перезапустить; локально: .env)"
)
return url.rstrip("/"), api_key
def slug_from(slug_or_url: str) -> str:
"""Принимает и slug, и полную ссылку вида .../docs/<slug>."""
return slug_or_url.strip().rstrip("/").rsplit("/", 1)[-1]
async def publish_doc(
title: str,
markdown: str,
card_url: str | None = None,
problem: str | None = None,
) -> dict:
"""Публикует новый QA-документ, возвращает {"slug": ..., "url": ...}."""
base_url, api_key = _config()
async with httpx.AsyncClient() as client:
response = await client.post(
f"{base_url}/docs",
headers={"X-API-Key": api_key},
json={
"title": title,
"markdown": markdown,
"card_url": card_url,
"problem": problem,
},
)
response.raise_for_status()
return response.json()
async def update_doc(
slug: str,
title: str,
markdown: str,
card_url: str | None = None,
problem: str | None = None,
) -> dict:
"""Перезаписывает уже опубликованный документ по slug (та же ссылка).
Возвращает {"slug": ..., "url": ...}; 404 (через raise_for_status), если
под этим slug ничего не опубликовано.
"""
base_url, api_key = _config()
async with httpx.AsyncClient() as client:
response = await client.put(
f"{base_url}/docs/{slug_from(slug)}",
headers={"X-API-Key": api_key},
json={
"title": title,
"markdown": markdown,
"card_url": card_url,
"problem": problem,
},
)
response.raise_for_status()
return response.json()
async def get_doc(slug: str) -> dict:
"""Скачивает опубликованный документ: исходный markdown, метаданные и
отметки тестировщиков по сценариям (поле scenarios).
404 (через raise_for_status), если под этим slug ничего не опубликовано.
"""
base_url, api_key = _config()
async with httpx.AsyncClient() as client:
response = await client.get(
f"{base_url}/docs/{slug_from(slug)}/raw",
headers={"X-API-Key": api_key},
)
response.raise_for_status()
return response.json()
def _tasks_config() -> tuple[str, dict]:
"""URL сервиса и заголовки для /tasks.
Подходит и основной ключ (у тебя), и QA_TASKS_TOKEN (у Claude на
фронте/бэке, которому доступны только задания).
"""
url = os.environ.get("QA_SERVICE_URL")
token = os.environ.get("QA_SERVICE_API_KEY") or os.environ.get("QA_TASKS_TOKEN")
if not url or not token:
raise QaServiceConfigError(
"QA_SERVICE_URL и QA_SERVICE_API_KEY или QA_TASKS_TOKEN не заданы (плагин: в терминале запусти claude и введи /plugin configure trello-tools@gp-tools — в Claude Desktop эта команда не работает, после настройки его нужно перезапустить; локально: .env)"
)
return url.rstrip("/"), {"Authorization": f"Bearer {token}"}
async def publish_task(
target: str, title: str, markdown: str, card_url: str | None = None
) -> dict:
"""Публикует задание для фронта/бэка, возвращает {slug, url, target, ...}."""
base_url, headers = _tasks_config()
async with httpx.AsyncClient() as client:
response = await client.post(
f"{base_url}/tasks",
headers=headers,
json={"title": title, "markdown": markdown, "target": target, "card_url": card_url},
)
response.raise_for_status()
return response.json()
async def update_task(
slug: str, target: str, title: str, markdown: str, card_url: str | None = None
) -> dict:
"""Перезаписывает задание по slug (та же ссылка)."""
base_url, headers = _tasks_config()
async with httpx.AsyncClient() as client:
response = await client.put(
f"{base_url}/tasks/{slug_from(slug)}",
headers=headers,
json={"title": title, "markdown": markdown, "target": target, "card_url": card_url},
)
response.raise_for_status()
return response.json()
async def get_task(slug: str) -> dict:
"""Задание целиком: {slug, url, title, target, card_url, markdown, ...}."""
base_url, headers = _tasks_config()
async with httpx.AsyncClient() as client:
response = await client.get(
f"{base_url}/tasks/{slug_from(slug)}/raw", headers=headers
)
response.raise_for_status()
return response.json()
async def list_tasks(card_url: str | None = None, target: str | None = None) -> list[dict]:
"""Задания без текста, новые первыми; фильтры по карточке и адресату."""
base_url, headers = _tasks_config()
params = {key: value for key, value in (("card", card_url), ("target", target)) if value}
async with httpx.AsyncClient() as client:
response = await client.get(f"{base_url}/tasks", headers=headers, params=params)
response.raise_for_status()
return response.json()