Архитектура

Содержание

Структура пакетов

main.go               Пятистрочная точка входа: package main → internal/cli.Execute()
internal/cli/         Команды CLI (пакет cli), по одному файлу на группу команд:
                      chat.go, accounts_cli.go, coding_cli.go, ...
pkg/client/           Библиотека на Go — по одному файлу на сервис API, без зависимостей от CLI/TUI
internal/accounts/    Хранилище учётных данных нескольких аккаунтов (~/.config/go-z-ai/accounts.json)
internal/coding/      Хранилище учётных данных GLM Coding Plan + генераторы конфигураций для инструментов
internal/usageview/   Чисто презентационные помощники (временные окна, тепловые карты, форматирование) —
                      используются и CLI, и TUI, поэтому их вывод не может разойтись
internal/tui/         Терминальный интерфейс на Bubble Tea, по одному подпакету на вкладку
internal/fileinput/   FileOrURL: URL пропускается как есть, локальный путь кодируется в base64 —
                      общий для `ocr parse` и вкладки media в TUI

pkg/client не имеет никаких зависимостей от чего-либо специфичного для CLI или TUI — он спроектирован для отдельного импорта (см. Руководство по библиотеке) и является единственным публичным пакетом. Всё, что находится под internal/, является реализацией, которую компилятор запрещает импортировать внешнему коду, поэтому слои CLI/TUI можно свободно рефакторить. И CLI, и TUI — тонкие обёртки над pkg/client. go install github.com/SamyRai/go-z-ai@latest по-прежнему собирает корневой main.go в бинарник go-z-ai.

Соглашения CLI

Обработчики команд, которым нужен клиент API, регистрируются как RunE: runWithClient(runX) и принимают уже разрешённый *client.Client как третий параметр — runWithClientinternal/cli/common.go) разрешает его один раз через getClient, так что обработчикам не нужно повторять преамбулу с разрешением и проверкой в каждом из них. Сам приоритет учётных данных живёт в resolveConfig (флаг → --accountZAI_API_KEY/KEY → активный аккаунт), он покрыт юнит-тестами в credentials_test.go.

Формат вывода единообразен: каждая команда, печатающая результат, регистрирует общий флаг --format через addFormatFlag и рендерит через emit(cmd, v, textFn), который выдаёт красивый JSON для --format json, а иначе запускает читаемый человеком textFn. Прогресс-логи на командах, поддерживающих JSON, идут в stderr, поэтому stdout остаётся валидным JSON.

Фасад запросов

Каждый метод сервиса проходит через один из трёх методов Client — сервисы никогда не создают собственный http.Client и не отправляют сырой запрос:

doRequest(ctx, method, endpoint, body, result)
  → doRequestBase(ctx, baseURL, method, endpoint, body, result)   // для нестандартных базовых URL
    → doRequestBaseKey(ctx, baseURL, apiKey, method, endpoint, body, result)  // для другой учётной записи

Именно здесь централизованно в одном месте сосредоточены повтор/экспоненциальная задержка, разбор ошибок и аутентификация для каждого эндпоинта. Сервис, которому нужен другой базовый URL (Agents) или другая учётная запись (Embeddings/Moderations, использующие ChinaAPIKey), вызывает следующее звено цепочки — он никогда не обходит фасад.

sendMultipart — параллельный путь для загрузок multipart/form-data (файлы, транскрипция аудио, разбор документов) — он не выполняет повторные попытки, поскольку повторная загрузка файла при временном сбое — это решение вызывающей стороны, а не безопасное поведение по умолчанию.

Дизайн повторных попыток и тайм-аутов

Почему некоторые сервисы обращаются к другому хосту

Сервис Базовый URL Почему
Embeddings, Moderations open.bigmodel.cn Единственная платформа, документирующая эти эндпоинты — индекс документации api.z.ai не упоминает ни один из них. Живая верификация подтвердила, что обычный ключ z.ai аутентифицируется одинаково на обеих платформах, поэтому Config.ChinaAPIKey по умолчанию откатывается к Config.APIKey.
Agents https://api.z.ai/api (голый корень, без /paas/v4) по умолчанию; https://open.bigmodel.cn/api при Config.Region = RegionChina Проверено на живой API — вложение /v1/agents под базовый URL chat-completions возвращает 404.
Всё остальное Config.BaseURL (/api/paas/v4 либо /api/coding/paas/v4 для аккаунтов GLM Coding Plan) Общий случай.

Выбор регионального шлюза (Config.Region)

Z.AI обслуживает то же семейство моделей GLM с двух региональных шлюзов: международного хоста api.z.ai и зеркала для материкового Китая open.bigmodel.cn. Большинство сервисов выбирают хост через Config.BaseURL (chat, files, tools и т. д.) либо через фиксированную константу (Embeddings/Moderations всегда используют китайский хост). Четыре сервиса — monitor (квота/использование), biz (информация об аккаунте), agents и определение типа аккаунта — раньше были жёстко привязаны к api.z.ai, из-за чего ключ glm_coding_plan_china не мог достучаться до эндпоинтов monitor/usage своего региона (и некорректно классифицировался командами accounts add/account detect).

Config.Region (RegionGlobal по умолчанию или RegionChina) выбирает хост только для этих четырёх регионно-зависимых сервисов. Он не переопределяет Config.BaseURL (поверхность chat) и не меняет хост Embeddings/Moderations. Из CLI используйте --region {global,china} или ZAI_REGION (псевдонимы: cn, bigmodel, west). Неизвестное значение откатывается к global, а не вызывает ошибку, поэтому опечатка никогда не блокирует постороннюю команду.

Хосты китайского зеркала для monitor/biz/agents/определения типа смоделированы зеркалированием структуры путей api.z.ai на open.bigmodel.cn и помечены NOT VERIFIED LIVE — китайская платформа прошла живую верификацию на предоставление той же OpenAPI-поверхности для /models и /chat/completions (см. BigModelBaseURL), но хосты monitor/biz/agents на стороне Китая пока не зафиксированы ни в одной cassette. Зафиксируйте их через ZAI_RECORD=1, если у вас есть китайский ключ с соответствующими правами.

Соглашение о живой верификации

Собственные SDK и документация Z.AI иногда расходятся между собой, а иногда — с тем, что реально возвращает живой API (эндпоинт, заявленный как необязательный, но возвращающий 400 без него; ошибка, встроенная в тело ответа 200; поле с разным типом в двух официальных SDK). Вместо того чтобы доверять одному источнику, новые сервисы здесь проверяются реальным вызовом API, а взаимодействие записывается как cassette go-vcr (pkg/client/testdata/cassettes/) и проигрывается в ModeReplayOnly, поэтому тестовый набор никогда не обращается к сети.

Два файла в pkg/client ведут работу по живой верификации, с разными ролями: live_replay_test.go содержит тесты Test*Live, которые проигрывают зафиксированные cassette как замороженные находки (проверки прав, аномалия 200-со-встроенным-сбоем, утверждение об одном ключе для нескольких хостов); это текущий журнал того, что подтверждено и почему это важно. live_verify_test.go содержит инструмент записи TestVerify* — каждый тест SKIP-ается, пока вы не захватите новую cassette успешного пути через ZAI_RECORD=1, после чего её проигрывает; это список дел для форм, ожидающих реального захвата.

Если вы расширяете сервис, см. Contributing § the live-verification convention прежде чем добавлять новую cassette.

Расширение таблицы структурированного поиска

Несколько типов в этой кодовой базе сопоставляют небольшое замкнутое множество заданных API значений с человекочитаемыми метаданными через таблицу конфигурации плюс функцию поиска, а не через цепочку if/switch-условий — quotaWindowConfigs/findWindowConfig из pkg/client/quota.go самый наглядный пример:

var quotaWindowConfigs = []QuotaWindowConfig{
    {Type: QuotaTypeTokensLimit, UnitCode: UnitCodeHourly, Number: 5, Description: "5-hour rolling token window"},
    {Type: QuotaTypeTokensLimit, UnitCode: UnitCodeWeekly, Number: 1, Description: "weekly token window"},
    {Type: QuotaTypeTimeLimit, UnitCode: UnitCodeMonthly, Number: 1, Description: "monthly MCP tools quota"},
}

Когда Z.AI добавляет новый тип окна, изменение носит аддитивный характер (добавить строку) и локализовано, с обобщённым запасным описанием для всего, чего ещё нет в таблице, а не жёстким сбоем. Тот же паттерн встречается при категоризации моделей (visionModelMarkers из pkg/client/models.go — единый источник истины, так что isTextModel/isVisionModel никогда не противоречат друг другу) и при разрешении типа аккаунта в эндпоинт (internal/coding/plans.go). Предпочитайте эту форму ещё одной условной ветке, когда вы добавляете новое распознаваемое значение к существующему понятию.

Безопасность файлов учётных данных

И internal/accounts (хранилище нескольких аккаунтов), и internal/coding (хранилище учётных данных GLM Coding Plan плюс все конфиги сторонних инструментов, которые оно пишет — Claude Code, OpenCode, Crush, Factory Droid, Cursor) пишут через атомарную запись во временный файл с последующим переименованием, а не прямой записью по месту. Сбой или kill в процессе записи оставляет исходный файл нетронутым, а не обрезанным — это важно именно здесь, потому что некоторые из них — реальные конфиги других программ, в которые выполняется слияние, а не файлы, которыми этот проект владеет безраздельно. Файлы, содержащие ключ, создаются с правами 0600; каталоги — 0700.

TUI

internal/tui — программа на Bubble Tea с одной вкладкой на подпакет (chat, models, usage, accounts, coding, media, tools). Каждая вкладка — независимая tea.Model со своими Update/View; internal/tui/root.go выполняет диспетчеризацию между ними. Длительная работа (вызов API, файловая операция) оборачивается в замыкание tea.Cmd, которое Bubble Tea запускает в собственной goroutine — код, вызываемый из tea.Cmd, не должен рассчитывать, что изменяемое состояние уровня пакета свободно от конкуренции (однажды это ударило нас с http.DefaultClient.Timeout; детали — в комментарии-документации к фиксу в internal/coding/validator.go).