UI - TASKER CODEX: упрощение подключения и local preview
This commit is contained in:
parent
ae1e425974
commit
8f87f03ee6
|
|
@ -0,0 +1,4 @@
|
|||
services:
|
||||
web:
|
||||
volumes:
|
||||
- ../plane-src/apps/web/build/client:/usr/share/nginx/html:ro
|
||||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
{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 disabled:bg-white/10 disabled:text-tertiary disabled:opacity-60"
|
||||
disabled={!revealedToken}
|
||||
onClick={() => revealedToken && void handleCopy(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={() =>
|
||||
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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue