UI - TASKER CODEX: упрощение подключения и local preview

This commit is contained in:
DCCONSTRUCTIONS 2026-05-16 11:28:21 +03:00
parent ae1e425974
commit 8f87f03ee6
3 changed files with 96 additions and 111 deletions

View File

@ -0,0 +1,4 @@
services:
web:
volumes:
- ../plane-src/apps/web/build/client:/usr/share/nginx/html:ro

View File

@ -0,0 +1,36 @@
#!/bin/sh
set -eu
ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
SOURCE_DIR="$ROOT_DIR/plane-src"
APP_DIR="$ROOT_DIR/plane-app"
cd "$SOURCE_DIR"
VITE_API_BASE_URL="${VITE_API_BASE_URL-}" \
VITE_ADMIN_BASE_URL="${VITE_ADMIN_BASE_URL-}" \
VITE_ADMIN_BASE_PATH="${VITE_ADMIN_BASE_PATH:-/nodedcsudo}" \
VITE_SPACE_BASE_URL="${VITE_SPACE_BASE_URL-}" \
VITE_SPACE_BASE_PATH="${VITE_SPACE_BASE_PATH:-/spaces}" \
VITE_LIVE_BASE_URL="${VITE_LIVE_BASE_URL-}" \
VITE_LIVE_BASE_PATH="${VITE_LIVE_BASE_PATH:-/live}" \
VITE_WEB_BASE_URL="${VITE_WEB_BASE_URL-}" \
VITE_NODEDC_LAUNCHER_URL="${VITE_NODEDC_LAUNCHER_URL:-http://launcher.local.nodedc}" \
VITE_NODEDC_OIDC_LOGIN_ENABLED="${VITE_NODEDC_OIDC_LOGIN_ENABLED:-1}" \
pnpm --filter web build
cd "$APP_DIR"
/usr/local/bin/docker compose \
-p plane-app \
--env-file plane.env \
-f docker-compose.yaml \
-f docker-compose.local-web-build.yaml \
up -d --no-build --force-recreate web
/usr/local/bin/docker compose \
-p plane-app \
--env-file plane.env \
-f docker-compose.yaml \
-f docker-compose.local-web-build.yaml \
ps web

View File

@ -44,9 +44,8 @@ const AGENT_AVATAR_ACCEPT = "image/png,image/jpeg,image/webp,image/gif";
const MAX_AGENT_AVATAR_SOURCE_BYTES = 50 * 1024 * 1024;
const AGENT_AVATAR_RENDER_SIZE = 512;
const AGENT_AVATAR_OUTPUT_QUALITY = 0.86;
const OPS_AGENT_FILENAME = "OPS_AGENT.md";
const CODEX_AGENT_TOKEN_PREFIX = "ndcag_";
const CODEX_MCP_SERVER_NAME = "nodedc-ops-agent";
const CODEX_TOKEN_ENV_VAR = "NODEDC_OPS_AGENT_TOKEN";
const DEFAULT_OPS_AGENT_MCP_ENDPOINT = "https://ops-agents.nodedc.ru/mcp";
const codexAgentService = new WorkspaceCodexAgentService();
@ -143,29 +142,16 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
);
const connectionGuideMcpEndpoint = getMcpEndpoint(setupCards.find((card) => card.setup)?.setup);
const connectionGuideConfigSnippet = buildCodexConfigSnippet(connectionGuideMcpEndpoint);
const connectionGuideOpsAgentMd = buildOpsAgentMarkdown(connectionGuideMcpEndpoint);
const handleCopy = async (value: string, label: string) => {
await navigator.clipboard.writeText(value);
setToast({
type: TOAST_TYPE.SUCCESS,
title: `${label} скопирован`,
message: "Секрет не хранится в Ops Agent.md. Token нужно сохранить в локальном Codex отдельно.",
message: "Вставьте скопированный фрагмент в нужное место локальной настройки Codex.",
});
};
const handleDownload = (value: string, fileName: string) => {
const blob = new Blob([value], { type: "text/markdown;charset=utf-8" });
const objectUrl = URL.createObjectURL(blob);
const linkElement = document.createElement("a");
linkElement.href = objectUrl;
linkElement.download = fileName;
document.body.appendChild(linkElement);
linkElement.click();
linkElement.remove();
URL.revokeObjectURL(objectUrl);
};
const handleCreateAvatarChange = async (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
event.target.value = "";
@ -603,7 +589,9 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
<div className="grid gap-4">
{agentTokens.map((token) => {
const revealedToken = revealedTokens[token.id];
const tokenValue = revealedToken ?? maskToken(token);
const tokenValue = revealedToken
? stripCodexAgentTokenPrefix(revealedToken)
: maskToken(token);
return (
<div key={token.id} className="nodedc-settings-field p-4">
@ -614,16 +602,25 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
<code className="nodedc-settings-input flex h-12 w-full items-center overflow-hidden px-4 pr-14 text-12 text-primary">
<span className="truncate">{tokenValue}</span>
</code>
<button
type="button"
aria-label="Скопировать токен"
className="absolute top-1/2 right-1.5 grid size-9 -translate-y-1/2 place-items-center rounded-full bg-[rgb(var(--nodedc-accent-rgb))] text-[rgb(var(--nodedc-on-accent-rgb))] transition hover:opacity-90 disabled:bg-white/10 disabled:text-tertiary disabled:opacity-60"
disabled={!revealedToken}
onClick={() => revealedToken && void handleCopy(revealedToken, "Токен")}
>
<Copy className="size-4" />
</button>
{revealedToken && (
<button
type="button"
aria-label="Скопировать токен"
className="absolute top-1/2 right-1.5 grid size-9 -translate-y-1/2 place-items-center rounded-full bg-[rgb(var(--nodedc-accent-rgb))] text-[rgb(var(--nodedc-on-accent-rgb))] transition hover:opacity-90"
onClick={() =>
void handleCopy(stripCodexAgentTokenPrefix(revealedToken), "Токен")
}
>
<Copy className="size-4" />
</button>
)}
</div>
{revealedToken && (
<p className="mt-3 text-12 leading-5 text-tertiary">
Сохраните токен в надежное место. После обновления страницы полный токен будет
недоступен.
</p>
)}
</div>
);
})}
@ -670,8 +667,7 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
<CodexConnectionGuide
configSnippet={connectionGuideConfigSnippet}
mcpEndpoint={connectionGuideMcpEndpoint}
onCopyConfig={() => void handleCopy(connectionGuideConfigSnippet, "config.toml")}
onDownloadAgentsMd={() => handleDownload(connectionGuideOpsAgentMd, OPS_AGENT_FILENAME)}
onCopyConfig={() => void handleCopy(connectionGuideConfigSnippet, "MCP-блок")}
/>
</section>
)}
@ -687,7 +683,6 @@ type TCodexConnectionGuideProps = {
configSnippet: string;
mcpEndpoint: string;
onCopyConfig: () => void;
onDownloadAgentsMd: () => void;
};
function CodexConnectionGuide(props: TCodexConnectionGuideProps) {
@ -715,44 +710,40 @@ function CodexConnectionGuide(props: TCodexConnectionGuideProps) {
</div>
<div className="nodedc-settings-input min-h-36 px-4 py-4 text-13 leading-5 text-secondary">
<div className="mb-2 font-semibold text-primary">2. Сохраните токен</div>
<div className="mb-2 font-semibold text-primary">2. Скопируйте готовый MCP-блок</div>
<p>
Создайте пользовательскую переменную окружения <code>{CODEX_TOKEN_ENV_VAR}</code>, заменив токен из примера
на уникальный токен конкретного агента.
Скопируйте блок ниже и вставьте его в конец <code>config.toml</code>. Если такой блок уже есть замените
только его.
</p>
<p className="mt-3">
В строке <code>Authorization</code> замените только <code>ВАШ_УНИКАЛЬНЫЙ_ТОКЕН</code> на значение из{" "}
<code>Agent token</code>. <code>Bearer</code> и <code>ndcag_</code> оставьте как есть.
</p>
<code className="mt-3 block rounded-2xl bg-black/20 px-3 py-3 text-12 break-all text-primary">
{CODEX_TOKEN_ENV_VAR}=ndcag_...
</code>
</div>
<div className="nodedc-settings-input min-h-36 px-4 py-4 text-13 leading-5 text-secondary">
<div className="mb-2 font-semibold text-primary">3. Добавьте Ops Agent.md</div>
<div className="mb-2 font-semibold text-primary">3. Перезапустите Codex</div>
<p>
Скачайте <code>{OPS_AGENT_FILENAME}</code>. Если в проекте уже есть <code>AGENTS.md</code>, добавьте
содержимое Ops Agent.md в начало текущего файла. Если файла правил нет положите Ops Agent.md в корень
проекта.
Сохраните <code>config.toml</code> и перезапустите локальный Codex. После старта попросите Codex проверить
доступные проекты через <code>tasker_list_projects</code>.
</p>
<Button
variant="primary"
size="sm"
className="nodedc-settings-save-button mt-3"
onClick={props.onDownloadAgentsMd}
>
Скачать Ops Agent.md
</Button>
<p className="mt-3">Правила работы и grants Codex получит сам через MCP по токену.</p>
</div>
</div>
<div className="nodedc-settings-field p-4">
<div className="mb-2 text-12 font-semibold tracking-wide text-tertiary uppercase">config.toml block</div>
<textarea
readOnly
className="nodedc-settings-input font-mono h-44 w-full resize-y px-3 py-3 text-12"
value={props.configSnippet}
/>
<p className="mb-3 text-13 leading-5 text-secondary">
Добавьте этот блок в конец существующего <code>config.toml</code>. Не заменяйте файл целиком. Если блок{" "}
<code>[mcp_servers.{CODEX_MCP_SERVER_NAME}]</code> уже есть замените только этот блок и его{" "}
<code>headers</code>.
</p>
<pre className="nodedc-settings-input font-mono w-full px-3 py-3 text-12 leading-5 break-all whitespace-pre-wrap text-primary">
{props.configSnippet}
</pre>
<div className="mt-3 flex flex-wrap gap-2">
<Button variant="primary" size="sm" className="nodedc-settings-save-button" onClick={props.onCopyConfig}>
Скопировать config.toml
Скопировать MCP-блок
</Button>
</div>
</div>
@ -767,68 +758,15 @@ function getMcpEndpoint(setup?: TCodexAgentSetupPacket): string {
function buildCodexConfigSnippet(endpoint: string): string {
return `[mcp_servers.${CODEX_MCP_SERVER_NAME}]
url = "${endpoint}"
bearer_token_env_var = "${CODEX_TOKEN_ENV_VAR}"
enabled = true
required = true
startup_timeout_sec = 20
tool_timeout_sec = 60`;
}
tool_timeout_sec = 60
function buildOpsAgentMarkdown(endpoint: string): string {
return `# NODE.DC Ops Agent Rules
MCP endpoint: ${endpoint}
## Startup
- Call \`tasker_get_agent_instructions\` before creating or changing Tasker cards.
- Call \`tasker_list_projects\` and \`tasker_get_project_context\` before writing into a project.
- Keep Tasker as the source of truth for project cards, checkers, status, labels, comments, and assignments.
## Write Safety
- Every write tool call must include a unique \`idempotency_key\`.
- Never delete or archive Tasker cards, comments, labels, projects, states, members, or workspaces.
- Do not call raw Tasker APIs. Use only the NODE.DC MCP tools.
- Only assign existing project members returned by project context.
- If a needed label is missing, call \`tasker_ensure_labels\` first and then use returned ids with \`tasker_set_issue_labels\`.
## Card Writing
- Keep card titles concise and operational.
- Put current architecture, planned architecture, implementation notes, and validation into structured text blocks.
- Every structured text block must put its visible heading into the block \`title\` field.
- Do not put headings inside block body. Wrong: body starts with \`## Текущая архитектура\`. Correct: \`title: "Текущая архитектура"\`, body contains only content.
- Put short verifiable work items into checker blocks with explicit \`title\` fields.
- After code work, update the related card with factual files touched and validation performed.
## Labels
- Before assigning a new marker/label, check project context labels.
- If the label does not exist, call \`tasker_ensure_labels\` with the granted \`project_id\`.
- Use only label ids returned by project context or \`tasker_ensure_labels\` when calling \`tasker_set_issue_labels\`.
## Effective Grants
- Grants are bound to the local agent token.
- Load effective workspace/project grants through \`tasker_get_agent_instructions\` and \`tasker_list_projects\` after connecting.
- Do not assume access to projects, labels, states, or members that were not returned by NODE.DC MCP tools.
## Available Tools
- \`tasker_get_agent_instructions\`: Return effective NODE.DC Tasker card-writing rules, grants, scopes, and mode expectations.
- \`tasker_list_projects\`: List Tasker projects granted to the current agent.
- \`tasker_get_project_context\`: Return states, labels, members, and card-writing context for one granted project.
- \`tasker_search_issues\`: Search work items inside one granted Tasker project.
- \`tasker_create_issue\`: Create a Tasker card with optional NODE.DC structured text/checker blocks.
- \`tasker_update_issue\`: Patch allowed issue fields without delete, archive, or project transfer.
- \`tasker_update_structured_blocks\`: Replace NODE.DC structured text/checker blocks in an issue detail layout.
- \`tasker_move_issue\`: Move an issue to an existing state in the same granted project.
- \`tasker_append_comment\`: Append a comment to a granted issue.
- \`tasker_ensure_labels\`: Create missing labels in a granted project and return label ids.
- \`tasker_set_issue_labels\`: Replace issue labels with existing labels from the granted project.
- \`tasker_assign_issue\`: Replace issue assignees with existing members of the granted project.
`;
[mcp_servers.${CODEX_MCP_SERVER_NAME}.headers]
Authorization = "Bearer ndcag_ВАШ_УНИКАЛЬНЫЙ_ТОКЕН"
Accept = "application/json, text/event-stream"
"MCP-Protocol-Version" = "2025-06-18"`;
}
function getAgentDraftName(agentDraftNames: Record<string, string>, agent: TCodexAgent): string {
@ -845,6 +783,13 @@ function maskToken(token: TCodexAgentToken): string {
return `${"×".repeat(16)}${token.token_suffix ?? token.id.slice(-8)}`;
}
function stripCodexAgentTokenPrefix(token: string): string {
const trimmedToken = token.trim();
return trimmedToken.startsWith(CODEX_AGENT_TOKEN_PREFIX)
? trimmedToken.slice(CODEX_AGENT_TOKEN_PREFIX.length)
: trimmedToken;
}
function getAvatarSrc(avatarUrl?: string | null): string | null {
if (!avatarUrl) return null;
if (/^(data:|blob:|https?:\/\/)/.test(avatarUrl)) return avatarUrl;