FEAT - TASKER: управление Codex Agent API через Gateway proxy
This commit is contained in:
+233
-1
@@ -4,16 +4,40 @@
|
||||
* See the LICENSE file for details.
|
||||
*/
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { Bot, Check, KeyRound, Route, ShieldCheck } from "lucide-react";
|
||||
import useSWR from "swr";
|
||||
import { Button } from "@plane/propel/button";
|
||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||
// components
|
||||
import { SettingsHeading } from "@/components/settings/heading";
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store/use-workspace";
|
||||
// services
|
||||
import { ProjectService } from "@/services/project/project.service";
|
||||
import {
|
||||
WorkspaceCodexAgentService,
|
||||
type TCodexAgentCreateTokenResponse,
|
||||
} from "@/services/workspace-codex-agent.service";
|
||||
import { WorkspaceService } from "@/services/workspace.service";
|
||||
|
||||
const TASK_AUTHOR_SCOPES = [
|
||||
"workspace:read",
|
||||
"project:read",
|
||||
"project:member:add_existing",
|
||||
"issue:read",
|
||||
"issue:create",
|
||||
"issue:update",
|
||||
"issue:move",
|
||||
"issue:comment",
|
||||
"issue:label",
|
||||
"issue:assign",
|
||||
"issue:structured_blocks:write",
|
||||
];
|
||||
|
||||
const codexAgentService = new WorkspaceCodexAgentService();
|
||||
const projectService = new ProjectService();
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
type TProps = {
|
||||
@@ -23,12 +47,80 @@ type TProps = {
|
||||
|
||||
export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSettingsContent(props: TProps) {
|
||||
const { showHeading = true, workspaceSlug } = props;
|
||||
const [isCreatingAgent, setIsCreatingAgent] = useState(false);
|
||||
const [newAgentName, setNewAgentName] = useState("Local Codex");
|
||||
const [selectedProjectId, setSelectedProjectId] = useState("");
|
||||
const [tokenPacket, setTokenPacket] = useState<TCodexAgentCreateTokenResponse | null>(null);
|
||||
const { currentWorkspace } = useWorkspace();
|
||||
const { data: nodedcWorkspacePolicy, isLoading } = useSWR(
|
||||
workspaceSlug ? `NODEDC_WORKSPACE_POLICY_${workspaceSlug}` : null,
|
||||
() => workspaceService.getNodeDCWorkspacePolicy(workspaceSlug)
|
||||
);
|
||||
const isCodexAgentEntitled = nodedcWorkspacePolicy?.service_modules?.codex_agents === true;
|
||||
const {
|
||||
data: codexAgentsPayload,
|
||||
error: codexAgentsError,
|
||||
isLoading: areAgentsLoading,
|
||||
mutate: mutateCodexAgents,
|
||||
} = useSWR(isCodexAgentEntitled ? `CODEX_AGENT_API_AGENTS_${workspaceSlug}` : null, () =>
|
||||
codexAgentService.listAgents(workspaceSlug)
|
||||
);
|
||||
const { data: projects } = useSWR(isCodexAgentEntitled ? `CODEX_AGENT_API_PROJECTS_${workspaceSlug}` : null, () =>
|
||||
projectService.getProjectsLite(workspaceSlug)
|
||||
);
|
||||
const activeAgents = useMemo(
|
||||
() => (codexAgentsPayload?.agents ?? []).filter((agent) => agent.status !== "revoked"),
|
||||
[codexAgentsPayload?.agents]
|
||||
);
|
||||
const projectOptions = projects ?? [];
|
||||
const effectiveSelectedProjectId = selectedProjectId || projectOptions[0]?.id || "";
|
||||
|
||||
const handleCopy = async (value: string, label: string) => {
|
||||
await navigator.clipboard.writeText(value);
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: `${label} скопирован`,
|
||||
message: "Секреты не сохраняются в TASKER_AGENT.md; token нужно хранить отдельно в локальном Codex.",
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreateAgent = async () => {
|
||||
const displayName = newAgentName.trim();
|
||||
if (!displayName || !effectiveSelectedProjectId) return;
|
||||
|
||||
setIsCreatingAgent(true);
|
||||
try {
|
||||
const createResponse = await codexAgentService.createAgent(workspaceSlug, { display_name: displayName });
|
||||
await codexAgentService.upsertGrant(workspaceSlug, createResponse.agent.id, {
|
||||
project_id: effectiveSelectedProjectId,
|
||||
scopes: TASK_AUTHOR_SCOPES,
|
||||
mode: "voluntary",
|
||||
});
|
||||
const tokenResponse = await codexAgentService.createToken(workspaceSlug, createResponse.agent.id, {
|
||||
name: `${displayName} local token`,
|
||||
});
|
||||
setTokenPacket(tokenResponse);
|
||||
await mutateCodexAgents();
|
||||
setToast({
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
title: "Codex agent создан",
|
||||
message: "Token показан один раз. Скопируйте его в локальную конфигурацию Codex.",
|
||||
});
|
||||
} catch (error: any) {
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Не удалось создать Codex agent",
|
||||
message: error?.message ?? error?.error ?? "Проверьте entitlement, Gateway URL/token и выбранный проект.",
|
||||
});
|
||||
} finally {
|
||||
setIsCreatingAgent(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRevokeAgent = async (agentId: string) => {
|
||||
await codexAgentService.revokeAgent(workspaceSlug, agentId);
|
||||
await mutateCodexAgents();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-7">
|
||||
@@ -52,7 +144,8 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
||||
</div>
|
||||
<p className="mt-2 max-w-3xl text-13 leading-5 text-secondary">
|
||||
Доступ к модулю приходит из Launcher entitlement Operational Core → Codex Agent API. Если entitlement
|
||||
снят, этот раздел исчезает из настроек workspace и backend policy больше не возвращает активный модуль.
|
||||
снят, этот раздел исчезает из настроек workspace и backend policy больше не возвращает активный
|
||||
модуль.
|
||||
</p>
|
||||
</div>
|
||||
<div className="nodedc-external-readonly-value shrink-0">
|
||||
@@ -81,6 +174,145 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
|
||||
description="Пользовательский Codex подключается по MCP endpoint с agent token; token хранится только на стороне Gateway."
|
||||
/>
|
||||
</section>
|
||||
|
||||
{isCodexAgentEntitled && (
|
||||
<section className="nodedc-settings-card px-5 py-5">
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div className="min-w-0">
|
||||
<div className="text-15 font-semibold text-primary">Агенты workspace</div>
|
||||
<p className="mt-1 max-w-3xl text-13 leading-5 text-secondary">
|
||||
Tasker вызывает Agent Gateway только через backend proxy. Frontend не получает сервисный Gateway
|
||||
token.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-[minmax(12rem,1fr)_minmax(12rem,1fr)_auto]">
|
||||
<input
|
||||
className="nodedc-settings-input border-custom-border-200 bg-custom-background-100 h-10 min-w-0 rounded-lg border px-3 text-13 text-primary outline-none"
|
||||
value={newAgentName}
|
||||
onChange={(event) => setNewAgentName(event.target.value)}
|
||||
placeholder="Имя агента"
|
||||
/>
|
||||
<select
|
||||
className="nodedc-settings-input border-custom-border-200 bg-custom-background-100 h-10 min-w-0 rounded-lg border px-3 text-13 text-primary outline-none"
|
||||
value={effectiveSelectedProjectId}
|
||||
onChange={(event) => setSelectedProjectId(event.target.value)}
|
||||
>
|
||||
{projectOptions.map((project) => (
|
||||
<option key={project.id} value={project.id}>
|
||||
{project.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="lg"
|
||||
className="nodedc-settings-save-button min-w-[11rem]"
|
||||
disabled={!newAgentName.trim() || !effectiveSelectedProjectId}
|
||||
loading={isCreatingAgent}
|
||||
onClick={handleCreateAgent}
|
||||
>
|
||||
Создать агента
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{codexAgentsError && (
|
||||
<div className="border-red-500/30 bg-red-500/10 text-red-300 mt-4 rounded-xl border px-4 py-3 text-13">
|
||||
Gateway недоступен или не настроен. Проверьте `PLANE_NODEDC_AGENT_GATEWAY_URL` и
|
||||
`PLANE_NODEDC_AGENT_GATEWAY_TOKEN` в Tasker API runtime.
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-5 grid gap-3">
|
||||
{areAgentsLoading ? (
|
||||
<div className="text-13 text-secondary">Загрузка агентов...</div>
|
||||
) : activeAgents.length > 0 ? (
|
||||
activeAgents.map((agent) => (
|
||||
<div
|
||||
key={agent.id}
|
||||
className="border-custom-border-200 bg-custom-background-90 flex flex-col gap-3 rounded-xl border px-4 py-3 md:flex-row md:items-center md:justify-between"
|
||||
>
|
||||
<div className="min-w-0">
|
||||
<div className="text-14 font-semibold text-primary">{agent.display_name}</div>
|
||||
<div className="mt-1 text-12 text-secondary">
|
||||
status: {agent.status} · created: {new Date(agent.created_at).toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="nodedc-settings-chip"
|
||||
onClick={() => void handleRevokeAgent(agent.id)}
|
||||
>
|
||||
Отозвать
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="border-custom-border-200 rounded-xl border border-dashed px-4 py-6 text-center text-13 text-secondary">
|
||||
Агентов пока нет. Создайте агента, выберите проект и сразу получите token + TASKER_AGENT.md.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{tokenPacket && (
|
||||
<section className="nodedc-settings-card px-5 py-5">
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<div className="text-15 font-semibold text-primary">Одноразовый token и setup packet</div>
|
||||
<p className="mt-1 text-13 text-secondary">
|
||||
Token больше не будет доступен после закрытия этого блока. Markdown-инструкция не содержит секрет.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="nodedc-settings-chip"
|
||||
onClick={() => setTokenPacket(null)}
|
||||
>
|
||||
Скрыть
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-4 grid gap-4">
|
||||
<div className="border-custom-border-200 bg-custom-background-90 rounded-xl border p-4">
|
||||
<div className="mb-2 text-12 font-semibold tracking-wide text-tertiary uppercase">Agent token</div>
|
||||
<code className="bg-custom-background-80 block rounded-lg px-3 py-2 text-12 break-all text-primary">
|
||||
{tokenPacket.token}
|
||||
</code>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="nodedc-settings-chip mt-3"
|
||||
onClick={() => void handleCopy(tokenPacket.token, "Agent token")}
|
||||
>
|
||||
Copy token
|
||||
</Button>
|
||||
</div>
|
||||
{tokenPacket.setup?.agents_md && (
|
||||
<div className="border-custom-border-200 bg-custom-background-90 rounded-xl border p-4">
|
||||
<div className="mb-2 text-12 font-semibold tracking-wide text-tertiary uppercase">
|
||||
TASKER_AGENT.md
|
||||
</div>
|
||||
<textarea
|
||||
readOnly
|
||||
className="border-custom-border-200 bg-custom-background-80 font-mono h-64 w-full resize-y rounded-lg border px-3 py-2 text-12 text-primary outline-none"
|
||||
value={tokenPacket.setup.agents_md}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="nodedc-settings-chip mt-3"
|
||||
onClick={() => void handleCopy(tokenPacket.setup?.agents_md ?? "", "TASKER_AGENT.md")}
|
||||
>
|
||||
Copy TASKER_AGENT.md
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user