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 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;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue