feat: add Ops AI Workspace npm setup command
This commit is contained in:
parent
763b90f3ba
commit
43e5f570e5
|
|
@ -10,6 +10,7 @@ from plane.app.views import (
|
||||||
AIWorkspaceExecutorEventsEndpoint,
|
AIWorkspaceExecutorEventsEndpoint,
|
||||||
AIWorkspaceExecutorListEndpoint,
|
AIWorkspaceExecutorListEndpoint,
|
||||||
AIWorkspaceExecutorSelectEndpoint,
|
AIWorkspaceExecutorSelectEndpoint,
|
||||||
|
AIWorkspaceExecutorSetupCommandEndpoint,
|
||||||
AIWorkspaceExecutorWindowsAgentEndpoint,
|
AIWorkspaceExecutorWindowsAgentEndpoint,
|
||||||
AIWorkspaceSettingsEndpoint,
|
AIWorkspaceSettingsEndpoint,
|
||||||
AIWorkspaceThreadDispatchEndpoint,
|
AIWorkspaceThreadDispatchEndpoint,
|
||||||
|
|
@ -55,6 +56,11 @@ urlpatterns = [
|
||||||
AIWorkspaceExecutorWindowsAgentEndpoint.as_view(),
|
AIWorkspaceExecutorWindowsAgentEndpoint.as_view(),
|
||||||
name="ai-workspace-executor-windows-agent",
|
name="ai-workspace-executor-windows-agent",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"workspaces/<str:slug>/ai-workspace/executors/<uuid:executor_id>/agent/setup-command/",
|
||||||
|
AIWorkspaceExecutorSetupCommandEndpoint.as_view(),
|
||||||
|
name="ai-workspace-executor-setup-command",
|
||||||
|
),
|
||||||
path(
|
path(
|
||||||
"workspaces/<str:slug>/ai-workspace/threads/",
|
"workspaces/<str:slug>/ai-workspace/threads/",
|
||||||
AIWorkspaceThreadListEndpoint.as_view(),
|
AIWorkspaceThreadListEndpoint.as_view(),
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,7 @@ from .ai_workspace import (
|
||||||
AIWorkspaceExecutorEventsEndpoint,
|
AIWorkspaceExecutorEventsEndpoint,
|
||||||
AIWorkspaceExecutorListEndpoint,
|
AIWorkspaceExecutorListEndpoint,
|
||||||
AIWorkspaceExecutorSelectEndpoint,
|
AIWorkspaceExecutorSelectEndpoint,
|
||||||
|
AIWorkspaceExecutorSetupCommandEndpoint,
|
||||||
AIWorkspaceExecutorWindowsAgentEndpoint,
|
AIWorkspaceExecutorWindowsAgentEndpoint,
|
||||||
AIWorkspaceSettingsEndpoint,
|
AIWorkspaceSettingsEndpoint,
|
||||||
AIWorkspaceThreadDispatchEndpoint,
|
AIWorkspaceThreadDispatchEndpoint,
|
||||||
|
|
|
||||||
|
|
@ -505,6 +505,51 @@ def ops_settings_payload(raw_payload, slug):
|
||||||
return payload
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def sync_ops_installer_context(request, slug, source):
|
||||||
|
request_data = request.data if isinstance(request.data, dict) else {}
|
||||||
|
ops_project_id = str(
|
||||||
|
request_data.get("opsProjectId")
|
||||||
|
or request_data.get("ops_project_id")
|
||||||
|
or request.query_params.get("ops_project_id")
|
||||||
|
or ""
|
||||||
|
).strip()
|
||||||
|
if not ops_project_id:
|
||||||
|
return None
|
||||||
|
|
||||||
|
ops_workspace_slug = str(
|
||||||
|
request_data.get("opsWorkspaceSlug")
|
||||||
|
or request_data.get("ops_workspace_slug")
|
||||||
|
or request.query_params.get("ops_workspace_slug")
|
||||||
|
or slug
|
||||||
|
or ""
|
||||||
|
).strip()
|
||||||
|
active_context = {
|
||||||
|
"surface": "ops",
|
||||||
|
"workspaceSlug": ops_workspace_slug,
|
||||||
|
"opsWorkspaceSlug": ops_workspace_slug,
|
||||||
|
"opsRouteWorkspaceSlug": slug,
|
||||||
|
"opsProjectId": ops_project_id,
|
||||||
|
}
|
||||||
|
sync_payload = ops_settings_payload(
|
||||||
|
{
|
||||||
|
"activeContext": active_context,
|
||||||
|
"enabledToolPacks": ["ops", "engine"],
|
||||||
|
"metadata": {
|
||||||
|
"source": source,
|
||||||
|
"updatedAt": timezone.now().isoformat(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
slug,
|
||||||
|
)
|
||||||
|
sync_payload, ops_grant_error = attach_ops_grant_for_active_context(request, slug, sync_payload)
|
||||||
|
if ops_grant_error is not None:
|
||||||
|
return ops_grant_error
|
||||||
|
sync_response = ai_workspace_request(request, "PATCH", "/settings", sync_payload)
|
||||||
|
if sync_response.status_code >= 400:
|
||||||
|
return sync_response
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class AIWorkspaceSettingsEndpoint(BaseAPIView):
|
class AIWorkspaceSettingsEndpoint(BaseAPIView):
|
||||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||||
def get(self, request, slug):
|
def get(self, request, slug):
|
||||||
|
|
@ -584,33 +629,9 @@ class AIWorkspaceExecutorWindowsAgentEndpoint(BaseAPIView):
|
||||||
params = {}
|
params = {}
|
||||||
if request.query_params.get("port"):
|
if request.query_params.get("port"):
|
||||||
params["port"] = request.query_params.get("port")
|
params["port"] = request.query_params.get("port")
|
||||||
ops_project_id = (request.query_params.get("ops_project_id") or "").strip()
|
sync_error = sync_ops_installer_context(request, slug, "ops-ai-workspace-installer")
|
||||||
if ops_project_id:
|
if sync_error is not None:
|
||||||
ops_workspace_slug = (request.query_params.get("ops_workspace_slug") or slug or "").strip()
|
return sync_error
|
||||||
active_context = {
|
|
||||||
"surface": "ops",
|
|
||||||
"workspaceSlug": ops_workspace_slug,
|
|
||||||
"opsWorkspaceSlug": ops_workspace_slug,
|
|
||||||
"opsRouteWorkspaceSlug": slug,
|
|
||||||
"opsProjectId": ops_project_id,
|
|
||||||
}
|
|
||||||
sync_payload = ops_settings_payload(
|
|
||||||
{
|
|
||||||
"activeContext": active_context,
|
|
||||||
"enabledToolPacks": ["ops", "engine"],
|
|
||||||
"metadata": {
|
|
||||||
"source": "ops-ai-workspace-installer",
|
|
||||||
"updatedAt": timezone.now().isoformat(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
slug,
|
|
||||||
)
|
|
||||||
sync_payload, ops_grant_error = attach_ops_grant_for_active_context(request, slug, sync_payload)
|
|
||||||
if ops_grant_error is not None:
|
|
||||||
return ops_grant_error
|
|
||||||
sync_response = ai_workspace_request(request, "PATCH", "/settings", sync_payload)
|
|
||||||
if sync_response.status_code >= 400:
|
|
||||||
return sync_response
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = requests.get(
|
response = requests.get(
|
||||||
|
|
@ -643,6 +664,27 @@ class AIWorkspaceExecutorWindowsAgentEndpoint(BaseAPIView):
|
||||||
return output
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
class AIWorkspaceExecutorSetupCommandEndpoint(BaseAPIView):
|
||||||
|
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||||
|
def post(self, request, slug, executor_id):
|
||||||
|
sync_error = sync_ops_installer_context(request, slug, "ops-ai-workspace-npm-setup")
|
||||||
|
if sync_error is not None:
|
||||||
|
return sync_error
|
||||||
|
|
||||||
|
request_data = request.data if isinstance(request.data, dict) else {}
|
||||||
|
payload = {}
|
||||||
|
port = request_data.get("port") or request.query_params.get("port")
|
||||||
|
if port:
|
||||||
|
payload["port"] = port
|
||||||
|
return ai_workspace_request(
|
||||||
|
request,
|
||||||
|
"POST",
|
||||||
|
f"/executors/{executor_id}/agent/setup-command",
|
||||||
|
payload,
|
||||||
|
timeout_override=15,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AIWorkspaceThreadListEndpoint(BaseAPIView):
|
class AIWorkspaceThreadListEndpoint(BaseAPIView):
|
||||||
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
|
||||||
def get(self, request, slug):
|
def get(self, request, slug):
|
||||||
|
|
|
||||||
|
|
@ -1547,6 +1547,29 @@ export function AIWorkspaceProductConsole({ open, workspaceSlug, onClose }: TAIW
|
||||||
[opsWorkspaceSlug, runRegistryAction, selectedProjectId, updateRegistry, workspaceSlug]
|
[opsWorkspaceSlug, runRegistryAction, selectedProjectId, updateRegistry, workspaceSlug]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const copySetupCommand = useCallback(
|
||||||
|
async (executorId: string, input: TAIWorkspaceExecutorInput, port: string) => {
|
||||||
|
await runRegistryAction(
|
||||||
|
"setup-command",
|
||||||
|
async () => {
|
||||||
|
const payload = await workspaceAIWorkspaceService.updateExecutor(workspaceSlug, executorId, input);
|
||||||
|
await updateRegistry(payload);
|
||||||
|
const setup = await workspaceAIWorkspaceService.createAgentSetupCommand(workspaceSlug, executorId, {
|
||||||
|
port,
|
||||||
|
opsWorkspaceSlug,
|
||||||
|
opsProjectId: selectedProjectId,
|
||||||
|
});
|
||||||
|
const command = String(setup.install?.command || "").trim();
|
||||||
|
if (!command) throw new Error("setup_command_empty");
|
||||||
|
if (!navigator.clipboard?.writeText) throw new Error("clipboard_unavailable");
|
||||||
|
await navigator.clipboard.writeText(command);
|
||||||
|
},
|
||||||
|
"npm setup command скопирована"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[opsWorkspaceSlug, runRegistryAction, selectedProjectId, updateRegistry, workspaceSlug]
|
||||||
|
);
|
||||||
|
|
||||||
const createThread = async () => {
|
const createThread = async () => {
|
||||||
try {
|
try {
|
||||||
setThreadError("");
|
setThreadError("");
|
||||||
|
|
@ -2236,6 +2259,7 @@ export function AIWorkspaceProductConsole({ open, workspaceSlug, onClose }: TAIW
|
||||||
onDelete={deleteExecutor}
|
onDelete={deleteExecutor}
|
||||||
onOpsWorkspaceChange={setSelectedOpsWorkspaceSlug}
|
onOpsWorkspaceChange={setSelectedOpsWorkspaceSlug}
|
||||||
onProjectChange={setSelectedProjectId}
|
onProjectChange={setSelectedProjectId}
|
||||||
|
onCopySetupCommand={copySetupCommand}
|
||||||
onDownloadAgent={downloadAgent}
|
onDownloadAgent={downloadAgent}
|
||||||
getWindowsAgentInstallerUrl={(executorId, options) =>
|
getWindowsAgentInstallerUrl={(executorId, options) =>
|
||||||
workspaceAIWorkspaceService.getWindowsAgentInstallerUrl(workspaceSlug, executorId, {
|
workspaceAIWorkspaceService.getWindowsAgentInstallerUrl(workspaceSlug, executorId, {
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ type TAIWorkspaceProductSettingsModalProps = {
|
||||||
onDelete: (executorId: string) => Promise<TAIWorkspaceExecutorListResponse | null>;
|
onDelete: (executorId: string) => Promise<TAIWorkspaceExecutorListResponse | null>;
|
||||||
onOpsWorkspaceChange: (workspaceSlug: string) => void;
|
onOpsWorkspaceChange: (workspaceSlug: string) => void;
|
||||||
onProjectChange: (projectId: string) => void;
|
onProjectChange: (projectId: string) => void;
|
||||||
|
onCopySetupCommand: (executorId: string, input: TAIWorkspaceExecutorInput, port: string) => Promise<void>;
|
||||||
onDownloadAgent: (executorId: string, input: TAIWorkspaceExecutorInput, port: string) => Promise<void>;
|
onDownloadAgent: (executorId: string, input: TAIWorkspaceExecutorInput, port: string) => Promise<void>;
|
||||||
getWindowsAgentInstallerUrl: (
|
getWindowsAgentInstallerUrl: (
|
||||||
executorId: string,
|
executorId: string,
|
||||||
|
|
@ -324,6 +325,7 @@ export function AIWorkspaceProductSettingsModal({
|
||||||
onDelete,
|
onDelete,
|
||||||
onOpsWorkspaceChange,
|
onOpsWorkspaceChange,
|
||||||
onProjectChange,
|
onProjectChange,
|
||||||
|
onCopySetupCommand,
|
||||||
onDownloadAgent,
|
onDownloadAgent,
|
||||||
getWindowsAgentInstallerUrl,
|
getWindowsAgentInstallerUrl,
|
||||||
}: TAIWorkspaceProductSettingsModalProps) {
|
}: TAIWorkspaceProductSettingsModalProps) {
|
||||||
|
|
@ -450,6 +452,11 @@ export function AIWorkspaceProductSettingsModal({
|
||||||
await onDownloadAgent(editingExecutor.id, toInput(draft), draft.agentPort || DEFAULT_WINDOWS_AGENT_PORT);
|
await onDownloadAgent(editingExecutor.id, toInput(draft), draft.agentPort || DEFAULT_WINDOWS_AGENT_PORT);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const copySetupCommand = async () => {
|
||||||
|
if (!editingExecutor) return;
|
||||||
|
await onCopySetupCommand(editingExecutor.id, toInput(draft), draft.agentPort || DEFAULT_WINDOWS_AGENT_PORT);
|
||||||
|
};
|
||||||
|
|
||||||
const activeCheckDetails = activeExecutor
|
const activeCheckDetails = activeExecutor
|
||||||
? [formatCheckTime(activeExecutor.lastSeenAt), activeExecutor.statusDetail].filter(Boolean).join(" · ")
|
? [formatCheckTime(activeExecutor.lastSeenAt), activeExecutor.statusDetail].filter(Boolean).join(" · ")
|
||||||
: "";
|
: "";
|
||||||
|
|
@ -696,18 +703,26 @@ export function AIWorkspaceProductSettingsModal({
|
||||||
placeholder="XXXX-XXXX-XXXX"
|
placeholder="XXXX-XXXX-XXXX"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="modal-btn ai-settings-download ai-settings-inline-download btn-primary"
|
||||||
|
disabled={isBusy}
|
||||||
|
onClick={() => void copySetupCommand()}
|
||||||
|
>
|
||||||
|
Copy npx
|
||||||
|
</button>
|
||||||
<a
|
<a
|
||||||
className="modal-btn ai-settings-download ai-settings-inline-download"
|
className="modal-btn ai-settings-download ai-settings-inline-download"
|
||||||
href={getWindowsAgentInstallerUrl(editingExecutor.id)}
|
href={getWindowsAgentInstallerUrl(editingExecutor.id)}
|
||||||
onClick={downloadAgent}
|
onClick={downloadAgent}
|
||||||
download
|
download
|
||||||
>
|
>
|
||||||
Скачать агент
|
Legacy .ps1
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
) : draft.connectionMode === "hub" ? (
|
) : draft.connectionMode === "hub" ? (
|
||||||
<div className="ai-settings-check-note">
|
<div className="ai-settings-check-note">
|
||||||
После сохранения общий AI Workspace создаст pairing code и installer агента.
|
После сохранения общий AI Workspace создаст pairing code и npm setup command.
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,21 @@ export type TAIWorkspaceBridgeEventsResponse = {
|
||||||
events: TAIWorkspaceBridgeEvent[];
|
events: TAIWorkspaceBridgeEvent[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TAIWorkspaceAgentSetupCommandResponse = {
|
||||||
|
ok: boolean;
|
||||||
|
executorId?: string;
|
||||||
|
install?: {
|
||||||
|
command?: string;
|
||||||
|
packageName?: string;
|
||||||
|
gatewayUrl?: string;
|
||||||
|
expiresAt?: string;
|
||||||
|
};
|
||||||
|
setupCode?: {
|
||||||
|
suffix?: string;
|
||||||
|
expiresAt?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type TAIWorkspaceOpsProject = {
|
export type TAIWorkspaceOpsProject = {
|
||||||
id: string;
|
id: string;
|
||||||
identifier?: string | null;
|
identifier?: string | null;
|
||||||
|
|
@ -284,6 +299,22 @@ export class WorkspaceAIWorkspaceService extends APIService {
|
||||||
return this.listExecutors(workspaceSlug);
|
return this.listExecutors(workspaceSlug);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createAgentSetupCommand(
|
||||||
|
workspaceSlug: string,
|
||||||
|
executorId: string,
|
||||||
|
options: { port?: string | number; opsWorkspaceSlug?: string; opsProjectId?: string } = {}
|
||||||
|
): Promise<TAIWorkspaceAgentSetupCommandResponse> {
|
||||||
|
return this.post(`/api/workspaces/${workspaceSlug}/ai-workspace/executors/${executorId}/agent/setup-command/`, {
|
||||||
|
port: String(options.port || "").trim() || undefined,
|
||||||
|
opsWorkspaceSlug: String(options.opsWorkspaceSlug || "").trim() || undefined,
|
||||||
|
opsProjectId: String(options.opsProjectId || "").trim() || undefined,
|
||||||
|
})
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data ?? error;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getWindowsAgentInstallerUrl(
|
getWindowsAgentInstallerUrl(
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
executorId: string,
|
executorId: string,
|
||||||
|
|
|
||||||
|
|
@ -1163,7 +1163,7 @@
|
||||||
|
|
||||||
.ai-settings-field-row--download {
|
.ai-settings-field-row--download {
|
||||||
align-items: end;
|
align-items: end;
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 1fr) minmax(96px, 112px) auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-settings-field > span {
|
.ai-settings-field > span {
|
||||||
|
|
@ -1409,7 +1409,7 @@
|
||||||
|
|
||||||
.ai-settings-actions .modal-btn:hover:not(:disabled),
|
.ai-settings-actions .modal-btn:hover:not(:disabled),
|
||||||
.ai-settings-empty-context .modal-btn:hover:not(:disabled),
|
.ai-settings-empty-context .modal-btn:hover:not(:disabled),
|
||||||
.ai-settings-inline-download:hover {
|
.ai-settings-inline-download:hover:not(:disabled) {
|
||||||
background: var(--nodedc-glass-control-hover);
|
background: var(--nodedc-glass-control-hover);
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
}
|
}
|
||||||
|
|
@ -1420,11 +1420,21 @@
|
||||||
color: var(--brand-contrast);
|
color: var(--brand-contrast);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ai-settings-inline-download.btn-primary {
|
||||||
|
background: var(--brand);
|
||||||
|
color: var(--brand-contrast);
|
||||||
|
}
|
||||||
|
|
||||||
.ai-settings-actions .modal-btn:disabled {
|
.ai-settings-actions .modal-btn:disabled {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
opacity: 0.48;
|
opacity: 0.48;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ai-settings-inline-download:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.48;
|
||||||
|
}
|
||||||
|
|
||||||
.ai-settings-context-pane > .ai-settings-actions,
|
.ai-settings-context-pane > .ai-settings-actions,
|
||||||
.ai-settings-details-pane > .ai-settings-actions {
|
.ai-settings-details-pane > .ai-settings-actions {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue