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 MAX_AGENT_AVATAR_SOURCE_BYTES = 50 * 1024 * 1024;
const AGENT_AVATAR_RENDER_SIZE = 512; const AGENT_AVATAR_RENDER_SIZE = 512;
const AGENT_AVATAR_OUTPUT_QUALITY = 0.86; 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_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 DEFAULT_OPS_AGENT_MCP_ENDPOINT = "https://ops-agents.nodedc.ru/mcp";
const codexAgentService = new WorkspaceCodexAgentService(); const codexAgentService = new WorkspaceCodexAgentService();
@ -143,29 +142,16 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
); );
const connectionGuideMcpEndpoint = getMcpEndpoint(setupCards.find((card) => card.setup)?.setup); const connectionGuideMcpEndpoint = getMcpEndpoint(setupCards.find((card) => card.setup)?.setup);
const connectionGuideConfigSnippet = buildCodexConfigSnippet(connectionGuideMcpEndpoint); const connectionGuideConfigSnippet = buildCodexConfigSnippet(connectionGuideMcpEndpoint);
const connectionGuideOpsAgentMd = buildOpsAgentMarkdown(connectionGuideMcpEndpoint);
const handleCopy = async (value: string, label: string) => { const handleCopy = async (value: string, label: string) => {
await navigator.clipboard.writeText(value); await navigator.clipboard.writeText(value);
setToast({ setToast({
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS,
title: `${label} скопирован`, 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 handleCreateAvatarChange = async (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]; const file = event.target.files?.[0];
event.target.value = ""; event.target.value = "";
@ -603,7 +589,9 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
<div className="grid gap-4"> <div className="grid gap-4">
{agentTokens.map((token) => { {agentTokens.map((token) => {
const revealedToken = revealedTokens[token.id]; const revealedToken = revealedTokens[token.id];
const tokenValue = revealedToken ?? maskToken(token); const tokenValue = revealedToken
? stripCodexAgentTokenPrefix(revealedToken)
: maskToken(token);
return ( return (
<div key={token.id} className="nodedc-settings-field p-4"> <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"> <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> <span className="truncate">{tokenValue}</span>
</code> </code>
<button {revealedToken && (
type="button" <button
aria-label="Скопировать токен" type="button"
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" aria-label="Скопировать токен"
disabled={!revealedToken} 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={() => revealedToken && void handleCopy(revealedToken, "Токен")} onClick={() =>
> void handleCopy(stripCodexAgentTokenPrefix(revealedToken), "Токен")
<Copy className="size-4" /> }
</button> >
<Copy className="size-4" />
</button>
)}
</div> </div>
{revealedToken && (
<p className="mt-3 text-12 leading-5 text-tertiary">
Сохраните токен в надежное место. После обновления страницы полный токен будет
недоступен.
</p>
)}
</div> </div>
); );
})} })}
@ -670,8 +667,7 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
<CodexConnectionGuide <CodexConnectionGuide
configSnippet={connectionGuideConfigSnippet} configSnippet={connectionGuideConfigSnippet}
mcpEndpoint={connectionGuideMcpEndpoint} mcpEndpoint={connectionGuideMcpEndpoint}
onCopyConfig={() => void handleCopy(connectionGuideConfigSnippet, "config.toml")} onCopyConfig={() => void handleCopy(connectionGuideConfigSnippet, "MCP-блок")}
onDownloadAgentsMd={() => handleDownload(connectionGuideOpsAgentMd, OPS_AGENT_FILENAME)}
/> />
</section> </section>
)} )}
@ -687,7 +683,6 @@ type TCodexConnectionGuideProps = {
configSnippet: string; configSnippet: string;
mcpEndpoint: string; mcpEndpoint: string;
onCopyConfig: () => void; onCopyConfig: () => void;
onDownloadAgentsMd: () => void;
}; };
function CodexConnectionGuide(props: TCodexConnectionGuideProps) { function CodexConnectionGuide(props: TCodexConnectionGuideProps) {
@ -715,44 +710,40 @@ function CodexConnectionGuide(props: TCodexConnectionGuideProps) {
</div> </div>
<div className="nodedc-settings-input min-h-36 px-4 py-4 text-13 leading-5 text-secondary"> <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> <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> </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>
<div className="nodedc-settings-input min-h-36 px-4 py-4 text-13 leading-5 text-secondary"> <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> <p>
Скачайте <code>{OPS_AGENT_FILENAME}</code>. Если в проекте уже есть <code>AGENTS.md</code>, добавьте Сохраните <code>config.toml</code> и перезапустите локальный Codex. После старта попросите Codex проверить
содержимое Ops Agent.md в начало текущего файла. Если файла правил нет положите Ops Agent.md в корень доступные проекты через <code>tasker_list_projects</code>.
проекта.
</p> </p>
<Button <p className="mt-3">Правила работы и grants Codex получит сам через MCP по токену.</p>
variant="primary"
size="sm"
className="nodedc-settings-save-button mt-3"
onClick={props.onDownloadAgentsMd}
>
Скачать Ops Agent.md
</Button>
</div> </div>
</div> </div>
<div className="nodedc-settings-field p-4"> <div className="nodedc-settings-field p-4">
<div className="mb-2 text-12 font-semibold tracking-wide text-tertiary uppercase">config.toml block</div> <div className="mb-2 text-12 font-semibold tracking-wide text-tertiary uppercase">config.toml block</div>
<textarea <p className="mb-3 text-13 leading-5 text-secondary">
readOnly Добавьте этот блок в конец существующего <code>config.toml</code>. Не заменяйте файл целиком. Если блок{" "}
className="nodedc-settings-input font-mono h-44 w-full resize-y px-3 py-3 text-12" <code>[mcp_servers.{CODEX_MCP_SERVER_NAME}]</code> уже есть замените только этот блок и его{" "}
value={props.configSnippet} <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"> <div className="mt-3 flex flex-wrap gap-2">
<Button variant="primary" size="sm" className="nodedc-settings-save-button" onClick={props.onCopyConfig}> <Button variant="primary" size="sm" className="nodedc-settings-save-button" onClick={props.onCopyConfig}>
Скопировать config.toml Скопировать MCP-блок
</Button> </Button>
</div> </div>
</div> </div>
@ -767,68 +758,15 @@ function getMcpEndpoint(setup?: TCodexAgentSetupPacket): string {
function buildCodexConfigSnippet(endpoint: string): string { function buildCodexConfigSnippet(endpoint: string): string {
return `[mcp_servers.${CODEX_MCP_SERVER_NAME}] return `[mcp_servers.${CODEX_MCP_SERVER_NAME}]
url = "${endpoint}" url = "${endpoint}"
bearer_token_env_var = "${CODEX_TOKEN_ENV_VAR}"
enabled = true enabled = true
required = true required = true
startup_timeout_sec = 20 startup_timeout_sec = 20
tool_timeout_sec = 60`; tool_timeout_sec = 60
}
function buildOpsAgentMarkdown(endpoint: string): string { [mcp_servers.${CODEX_MCP_SERVER_NAME}.headers]
return `# NODE.DC Ops Agent Rules Authorization = "Bearer ndcag_ВАШ_УНИКАЛЬНЫЙ_ТОКЕН"
Accept = "application/json, text/event-stream"
MCP endpoint: ${endpoint} "MCP-Protocol-Version" = "2025-06-18"`;
## 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.
`;
} }
function getAgentDraftName(agentDraftNames: Record<string, string>, agent: TCodexAgent): string { 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)}`; 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 { function getAvatarSrc(avatarUrl?: string | null): string | null {
if (!avatarUrl) return null; if (!avatarUrl) return null;
if (/^(data:|blob:|https?:\/\/)/.test(avatarUrl)) return avatarUrl; if (/^(data:|blob:|https?:\/\/)/.test(avatarUrl)) return avatarUrl;