fix: route claudeqq notifications through k3s
This commit is contained in:
+88
-13
@@ -94,6 +94,7 @@ const providerGatewayWsEgressProxyUrl = "http://127.0.0.1:18789";
|
||||
const nativeK3sInstallVersion = "v1.34.1+k3s1";
|
||||
const nativeK3sImage = "rancher/k3s:v1.34.1-k3s1";
|
||||
const nativeK3sCtrAddress = "/run/k3s/containerd/containerd.sock";
|
||||
const unideskRepoUrl = "https://github.com/pikasTech/unidesk";
|
||||
|
||||
function isHelpArg(value: string | undefined): boolean {
|
||||
return value === "help" || value === "--help" || value === "-h";
|
||||
@@ -180,6 +181,16 @@ function repoSlug(repo: string): string | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
function isFullGitSha(value: string): boolean {
|
||||
return /^[0-9a-f]{40}$/iu.test(value);
|
||||
}
|
||||
|
||||
function isUnideskRepo(repo: string): boolean {
|
||||
const desiredSlug = repoSlug(repo);
|
||||
const unideskSlug = repoSlug(unideskRepoUrl);
|
||||
return desiredSlug !== null && desiredSlug === unideskSlug;
|
||||
}
|
||||
|
||||
function sshUrlForSlug(slug: string | null): string | null {
|
||||
if (slug === null) return null;
|
||||
const [host = "", ...pathParts] = slug.split("/");
|
||||
@@ -208,7 +219,7 @@ function providerSourceRepoUrl(repo: string): string {
|
||||
|
||||
function localResolvedCommitForDesired(desired: DeployManifestService): string | null {
|
||||
const desiredSlug = repoSlug(desired.repo);
|
||||
if (desiredSlug === null || desired.commitId.length !== 40) return null;
|
||||
if (desiredSlug === null || !isFullGitSha(desired.commitId)) return null;
|
||||
const localOrigin = runCommand(["git", "remote", "get-url", "origin"], repoRoot);
|
||||
const localOriginUrl = localOrigin.exitCode === 0 ? localOrigin.stdout.trim() : "";
|
||||
if (localOriginUrl.length === 0 || repoSlug(localOriginUrl) !== desiredSlug) return null;
|
||||
@@ -235,6 +246,7 @@ function runResolveGit(args: string[], cwd: string): ReturnType<typeof runComman
|
||||
function resolveDesiredCommit(desired: DeployManifestService): DeployManifestService {
|
||||
const localCommit = localResolvedCommitForDesired(desired);
|
||||
if (localCommit !== null) return { ...desired, commitId: localCommit };
|
||||
if (isFullGitSha(desired.commitId) && !isUnideskRepo(desired.repo)) return { ...desired, commitId: desired.commitId.toLowerCase() };
|
||||
|
||||
const cacheDir = repoResolveCacheDir(desired.repo);
|
||||
mkdirSync(cacheDir, { recursive: true });
|
||||
@@ -412,12 +424,32 @@ function targetExportDir(service: UniDeskMicroserviceConfig, runId: string): str
|
||||
|
||||
function targetWorkDir(service: UniDeskMicroserviceConfig): string {
|
||||
if (service.deployment.mode === "k3sctl-managed") return k3sDeployDir;
|
||||
if (targetIsMain(service) && service.repository.url === "https://github.com/pikasTech/unidesk") {
|
||||
if (targetIsMain(service) && service.repository.url === unideskRepoUrl) {
|
||||
return rootPath(".state", "deploy", "work", safeId(service.id));
|
||||
}
|
||||
return service.development.worktreePath;
|
||||
}
|
||||
|
||||
function sourceWorkDir(service: UniDeskMicroserviceConfig): string {
|
||||
if (service.deployment.mode === "k3sctl-managed" && service.repository.url !== unideskRepoUrl) {
|
||||
return `${remoteDeployRoot}/work/${safeId(service.id)}`;
|
||||
}
|
||||
return targetWorkDir(service);
|
||||
}
|
||||
|
||||
function sourceRootSubdir(service: UniDeskMicroserviceConfig): string {
|
||||
const claudeqqPrefix = "claudeqq/";
|
||||
if (service.id === "claudeqq" && service.repository.url !== unideskRepoUrl && service.repository.dockerfile.startsWith(claudeqqPrefix)) {
|
||||
return "claudeqq";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function sourceBuildContext(service: UniDeskMicroserviceConfig): string {
|
||||
const subdir = sourceRootSubdir(service);
|
||||
return subdir.length > 0 ? `${sourceWorkDir(service)}/${subdir}` : sourceWorkDir(service);
|
||||
}
|
||||
|
||||
function buildImageTag(service: UniDeskMicroserviceConfig): string {
|
||||
if (service.deployment.mode === "k3sctl-managed") return `unidesk-${service.id}:d601`;
|
||||
if (targetIsMain(service)) {
|
||||
@@ -438,12 +470,12 @@ function directComposeEnvFile(service: UniDeskMicroserviceConfig): string {
|
||||
}
|
||||
|
||||
function directBuildContextOverride(service: UniDeskMicroserviceConfig): string {
|
||||
if (targetIsMain(service) && service.repository.url === "https://github.com/pikasTech/unidesk") return targetWorkDir(service);
|
||||
if (targetIsMain(service) && service.repository.url === unideskRepoUrl) return targetWorkDir(service);
|
||||
return "";
|
||||
}
|
||||
|
||||
function directDockerfileOverride(service: UniDeskMicroserviceConfig): string {
|
||||
if (targetIsMain(service) && service.repository.url === "https://github.com/pikasTech/unidesk") return service.repository.dockerfile;
|
||||
if (targetIsMain(service) && service.repository.url === unideskRepoUrl) return service.repository.dockerfile;
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -453,8 +485,17 @@ function k8sManifestPath(service: UniDeskMicroserviceConfig): string {
|
||||
return composeFile.replace(/\.k3s\.json$/u, ".k8s.yaml");
|
||||
}
|
||||
|
||||
function sourceDockerfilePath(service: UniDeskMicroserviceConfig): string {
|
||||
const claudeqqPrefix = "claudeqq/";
|
||||
if (service.id === "claudeqq" && service.repository.url !== unideskRepoUrl && service.repository.dockerfile.startsWith(claudeqqPrefix)) {
|
||||
return service.repository.dockerfile.slice(claudeqqPrefix.length);
|
||||
}
|
||||
return service.repository.dockerfile;
|
||||
}
|
||||
|
||||
function sourceProxyPrelude(service: UniDeskMicroserviceConfig): string {
|
||||
if (targetIsMain(service)) return "";
|
||||
const strictHostKeyChecking = isUnideskRepo(service.repository.url) ? "yes" : "accept-new";
|
||||
return [
|
||||
`build_proxy=${shellQuote(providerGatewayWsEgressProxyUrl)}`,
|
||||
"export HTTP_PROXY=\"$build_proxy\" HTTPS_PROXY=\"$build_proxy\" ALL_PROXY=\"$build_proxy\"",
|
||||
@@ -521,7 +562,7 @@ while True:
|
||||
"UNIDESK_GIT_SSH_PROXY",
|
||||
"chmod 700 \"$git_ssh_proxy\"",
|
||||
"export UNIDESK_GIT_SSH_HTTP_PROXY=\"$build_proxy\"",
|
||||
"export GIT_SSH_COMMAND=\"ssh -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$HOME/.ssh/known_hosts -i $HOME/.ssh/id_ed25519 -o 'ProxyCommand=$git_ssh_proxy %h %p'\"",
|
||||
`export GIT_SSH_COMMAND="ssh -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=${strictHostKeyChecking} -o UserKnownHostsFile=$HOME/.ssh/known_hosts -i $HOME/.ssh/id_ed25519 -o 'ProxyCommand=$git_ssh_proxy %h %p'"`,
|
||||
"curl -fsSI --max-time 20 -x \"$build_proxy\" https://github.com >/dev/null",
|
||||
"echo target_source_proxy=provider-gateway-ws-egress:$build_proxy",
|
||||
"echo target_build_proxy=provider-gateway-ws-egress:$build_proxy",
|
||||
@@ -541,7 +582,7 @@ function buildCachePrelude(dockerfileVariable: string): string[] {
|
||||
}
|
||||
|
||||
function prepareSourceScript(service: UniDeskMicroserviceConfig, desired: DeployManifestService, exportDir: string): string {
|
||||
if (targetIsMain(service) && desired.repo === "https://github.com/pikasTech/unidesk") {
|
||||
if (targetIsMain(service) && desired.repo === unideskRepoUrl) {
|
||||
return [
|
||||
"set -euo pipefail",
|
||||
`repo=${shellQuote(repoRoot)}`,
|
||||
@@ -577,11 +618,15 @@ function prepareSourceScript(service: UniDeskMicroserviceConfig, desired: Deploy
|
||||
}
|
||||
|
||||
function syncSourceScript(service: UniDeskMicroserviceConfig, exportDir: string): string {
|
||||
const workDir = targetWorkDir(service);
|
||||
const workDir = sourceWorkDir(service);
|
||||
const buildContext = sourceBuildContext(service);
|
||||
const dockerfilePath = sourceDockerfilePath(service);
|
||||
return [
|
||||
"set -euo pipefail",
|
||||
`export_dir=${shellQuote(exportDir)}`,
|
||||
`work_dir=${shellQuote(workDir)}`,
|
||||
`build_context=${shellQuote(buildContext)}`,
|
||||
`dockerfile_path=${shellQuote(dockerfilePath)}`,
|
||||
"mkdir -p \"$work_dir\"",
|
||||
[
|
||||
"rsync -a --delete",
|
||||
@@ -593,27 +638,51 @@ function syncSourceScript(service: UniDeskMicroserviceConfig, exportDir: string)
|
||||
"\"$export_dir/\"",
|
||||
"\"$work_dir/\"",
|
||||
].join(" "),
|
||||
`test -f "$work_dir/${service.repository.dockerfile}"`,
|
||||
"printf 'synced deploy worktree to %s\\n' \"$work_dir\"",
|
||||
"test -f \"$build_context/$dockerfile_path\"",
|
||||
"printf 'synced deploy worktree to %s\\nbuild_context=%s\\n' \"$work_dir\" \"$build_context\"",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function syncK8sControlManifestsScript(service: UniDeskMicroserviceConfig): string {
|
||||
if (service.deployment.mode !== "k3sctl-managed" || isUnideskRepo(service.repository.url)) return "";
|
||||
const manifests = [service.repository.composeFile, k8sManifestPath(service)];
|
||||
const commands = [
|
||||
"set -euo pipefail",
|
||||
`target_root=${shellQuote(targetWorkDir(service))}`,
|
||||
"mkdir -p \"$target_root\"",
|
||||
];
|
||||
for (const relativePath of manifests) {
|
||||
const absolutePath = rootPath(relativePath);
|
||||
if (!existsSync(absolutePath)) throw new Error(`${service.id} k3s control manifest missing: ${relativePath}`);
|
||||
const encoded = Buffer.from(readFileSync(absolutePath, "utf8"), "utf8").toString("base64");
|
||||
commands.push(
|
||||
`relative_path=${shellQuote(relativePath)}`,
|
||||
`target_file="$target_root/$relative_path"`,
|
||||
"mkdir -p \"$(dirname \"$target_file\")\"",
|
||||
`printf %s ${shellQuote(encoded)} | base64 -d > "$target_file"`,
|
||||
"printf 'synced_k3s_control_manifest=%s\\n' \"$target_file\"",
|
||||
);
|
||||
}
|
||||
return commands.join("\n");
|
||||
}
|
||||
|
||||
function buildImageScript(service: UniDeskMicroserviceConfig, desired: DeployManifestService, resolvedCommit: string): string {
|
||||
const image = buildImageTag(service);
|
||||
const workDir = targetWorkDir(service);
|
||||
const dockerfile = `${workDir}/${service.repository.dockerfile}`;
|
||||
const buildContext = sourceBuildContext(service);
|
||||
const dockerfilePath = sourceDockerfilePath(service);
|
||||
const dockerfile = `${buildContext}/${dockerfilePath}`;
|
||||
const labelArgs = [
|
||||
"--label", `unidesk.ai/service-id=${service.id}`,
|
||||
"--label", `unidesk.ai/source-repo=${desired.repo}`,
|
||||
"--label", `unidesk.ai/source-commit=${resolvedCommit}`,
|
||||
"--label", `unidesk.ai/dockerfile=${service.repository.dockerfile}`,
|
||||
"--label", `unidesk.ai/dockerfile=${dockerfilePath}`,
|
||||
];
|
||||
const commonArgs = [
|
||||
"--progress=plain",
|
||||
...labelArgs,
|
||||
"-t", image,
|
||||
"-f", dockerfile,
|
||||
workDir,
|
||||
buildContext,
|
||||
];
|
||||
const proxyBuildArgs = targetIsMain(service)
|
||||
? []
|
||||
@@ -1399,6 +1468,12 @@ async function applyOneService(config: UniDeskConfig, service: UniDeskMicroservi
|
||||
const sync = await step(config, service, "sync-source", syncSourceScript(service, exportDir), targetIsMain(service) ? repoRoot : "/home/ubuntu", 90_000, false);
|
||||
if (!pushStep(steps, sync)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps };
|
||||
|
||||
const controlManifestSyncScript = syncK8sControlManifestsScript(service);
|
||||
if (controlManifestSyncScript.length > 0) {
|
||||
const controlManifestSync = await step(config, service, "sync-k3s-control-manifests", controlManifestSyncScript, targetIsMain(service) ? repoRoot : "/home/ubuntu", 90_000, !targetIsMain(service));
|
||||
if (!pushStep(steps, controlManifestSync)) return { ok: false, serviceId: service.id, startedAt, finishedAt: nowIso(), resolvedCommit, before, steps };
|
||||
}
|
||||
|
||||
const buildScript = service.deployment.mode === "unidesk-direct"
|
||||
? buildDirectImageScript(service, desired, resolvedCommit)
|
||||
: buildImageScript(service, desired, resolvedCommit);
|
||||
|
||||
@@ -152,7 +152,7 @@ export function writeComposeEnv(config: UniDeskConfig, freshLogPrefix: boolean):
|
||||
UNIDESK_OA_EVENT_FLOW_PORT: runtimeSecret("UNIDESK_OA_EVENT_FLOW_PORT") || "4255",
|
||||
UNIDESK_OA_EVENT_FLOW_BIND_HOST: runtimeSecretWithDefault("UNIDESK_OA_EVENT_FLOW_BIND_HOST", restrictedHostBind, "127.0.0.1"),
|
||||
UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_ENABLED") || "true",
|
||||
UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL") || "http://backend-core:8080/api/microservices/claudeqq/proxy",
|
||||
UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_BASE_URL") || "http://claudeqq.unidesk.svc.cluster.local:3290",
|
||||
UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_TARGET_TYPE") || "private",
|
||||
UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_USER_ID: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_USER_ID") || "645275593",
|
||||
UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_GROUP_ID: runtimeSecret("UNIDESK_CODE_QUEUE_NOTIFY_CLAUDEQQ_GROUP_ID"),
|
||||
|
||||
+17
-2
@@ -1124,6 +1124,7 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
}>;
|
||||
} }).body;
|
||||
const k3sctlCodeQueueService = k3sctlControlPlaneBody?.services?.find((service) => service.id === "code-queue");
|
||||
const k3sctlClaudeqqService = k3sctlControlPlaneBody?.services?.find((service) => service.id === "claudeqq");
|
||||
const k3sctlD518Instance = k3sctlCodeQueueService?.instances?.find((instance) => instance.id === "D518");
|
||||
const filebrowserHealthBody = (filebrowserHealth as { body?: { status?: string } }).body;
|
||||
const filebrowserD601HealthBody = (filebrowserD601Health as { body?: { status?: string } }).body;
|
||||
@@ -1152,7 +1153,15 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
addSelectedCheck(checks, options, "microservice:catalog-findjob", (microservices as { ok?: boolean }).ok === true && findjob?.providerId === "D601" && findjob.backend?.public === false, { microservices });
|
||||
addSelectedCheck(checks, options, "microservice:catalog-pipeline", (microservices as { ok?: boolean }).ok === true && pipeline?.providerId === "D601" && pipeline.backend?.public === false && pipeline.runtime?.container?.name === "pipeline-v2-control", { microservices });
|
||||
addSelectedCheck(checks, options, "microservice:catalog-met-nonlinear", (microservices as { ok?: boolean }).ok === true && metNonlinear?.providerId === "D601" && metNonlinear.backend?.public === false && metNonlinear.runtime?.container?.name === "met-nonlinear-ts", { microservices });
|
||||
addSelectedCheck(checks, options, "microservice:catalog-claudeqq", (microservices as { ok?: boolean }).ok === true && claudeqq?.providerId === "D601" && claudeqq.backend?.public === false && claudeqq.runtime?.container?.name === "claudeqq-backend", { microservices });
|
||||
addSelectedCheck(checks, options, "microservice:catalog-claudeqq",
|
||||
(microservices as { ok?: boolean }).ok === true
|
||||
&& claudeqq?.providerId === "D601"
|
||||
&& claudeqq.backend?.public === false
|
||||
&& claudeqq.backend?.proxyMode === "k3sctl-adapter-http"
|
||||
&& claudeqq.deployment?.mode === "k3sctl-managed"
|
||||
&& claudeqq.runtime?.orchestrator === "k3sctl"
|
||||
&& claudeqq.runtime?.container === null,
|
||||
{ microservices });
|
||||
addSelectedCheck(checks, options, "microservice:catalog-todo-note", (microservices as { ok?: boolean }).ok === true && todoNote?.providerId === config.providerGateway.id && todoNote.backend?.public === false && todoNote.runtime?.container?.name === "todo-note-backend", { microservices });
|
||||
addSelectedCheck(checks, options, "microservice:catalog-oa-event-flow", (microservices as { ok?: boolean }).ok === true && oaEventFlow?.providerId === config.providerGateway.id && oaEventFlow.backend?.public === false && oaEventFlow.runtime?.container?.name === "oa-event-flow-backend", { microservices });
|
||||
addSelectedCheck(checks, options, "microservice:catalog-code-queue",
|
||||
@@ -1181,6 +1190,11 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
&& k3sctlCodeQueueService?.servingHealthy === true
|
||||
&& k3sctlCodeQueueService?.active?.id === "D601"
|
||||
&& k3sctlCodeQueueService?.active?.healthy === true
|
||||
&& k3sctlClaudeqqService?.status === "healthy"
|
||||
&& k3sctlClaudeqqService?.topologyComplete === true
|
||||
&& k3sctlClaudeqqService?.servingHealthy === true
|
||||
&& k3sctlClaudeqqService?.active?.id === "D601"
|
||||
&& k3sctlClaudeqqService?.active?.healthy === true
|
||||
&& (k3sctlCodeQueueService?.presentNodeIds ?? []).includes("D601")
|
||||
&& (k3sctlCodeQueueService?.presentNodeIds ?? []).includes("D518")
|
||||
&& (k3sctlCodeQueueService?.missingNodeIds ?? []).length === 0
|
||||
@@ -1192,6 +1206,7 @@ async function serviceChecks(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
noFallback: k3sctlControlPlaneBody?.noFallback,
|
||||
kubeApiProxy: k3sctlControlPlaneBody?.kubeApiProxy,
|
||||
service: k3sctlCodeQueueService,
|
||||
claudeqq: k3sctlClaudeqqService,
|
||||
});
|
||||
addSelectedCheck(checks, options, "microservice:catalog-filebrowser", (microservices as { ok?: boolean }).ok === true
|
||||
&& filebrowser?.providerId === "D518"
|
||||
@@ -2831,7 +2846,7 @@ async function frontendCheck(config: UniDeskConfig, urls: PublicUrls, checks: E2
|
||||
addSelectedCheck(checks, options, "frontend:code-queue-judge-wrap",
|
||||
codexJudgeWrapMetrics.checked === true && codexJudgeWrapMetrics.ok === true,
|
||||
{ codexJudgeWrapMetrics });
|
||||
addSelectedCheck(checks, options, "frontend:claudeqq-integrated-visible", claudeqqTextLower.includes("claudeqq 工作台") && claudeqqText.includes("D601") && claudeqqText.includes("QQ 事件订阅") && claudeqqText.includes("消息推送") && claudeqqText.includes("事件缓存") && claudeqqText.includes("主用户私聊账号") && claudeqqText.includes("645275593") && claudeqqTextLower.includes("napcat 容器登录") && claudeqqText.includes("已登录") && /health\s+ok/i.test(claudeqqText) && claudeqqText.includes("仅 UniDesk frontend 代理访问") && !claudeqqText.includes("{\n"), { claudeqqTextPreview: claudeqqText.slice(0, 1400) });
|
||||
addSelectedCheck(checks, options, "frontend:claudeqq-integrated-visible", claudeqqTextLower.includes("claudeqq 工作台") && claudeqqText.includes("D601") && claudeqqText.includes("D601 k3s Service") && claudeqqText.includes("k3s://unidesk/claudeqq") && claudeqqText.includes("QQ 事件订阅") && claudeqqText.includes("消息推送") && claudeqqText.includes("事件缓存") && claudeqqText.includes("主用户私聊账号") && claudeqqText.includes("645275593") && claudeqqTextLower.includes("napcat 容器登录") && claudeqqText.includes("已登录") && /health\s+ok/i.test(claudeqqText) && claudeqqText.includes("仅 UniDesk frontend 代理访问") && !claudeqqText.includes("{\n"), { claudeqqTextPreview: claudeqqText.slice(0, 1400) });
|
||||
addSelectedCheck(checks, options, "frontend:url-route-deeplink", routeInitialPath === "/app/pipeline/" && routeDockerPath === "/nodes/docker/" && routeBackPath === "/app/pipeline/" && routeOverviewPath === "/ops/status/" && routeCodexPath === "/app/code-queue/" && routeDeepLinkText.toLowerCase().includes("pipeline v2 工作台".toLowerCase()) && routeOverviewText.includes("核心指标") && routeCodexShellMetrics.appShell === true && routeCodexShellMetrics.standalone === false && routeCodexShellMetrics.topbar === true && routeCodexShellMetrics.codexPage === true && String(routeCodexShellMetrics.railText || "").includes("用户服务") && String(routeCodexShellMetrics.tabsText || "").includes("Code Queue"), { routeInitialPath, routeDockerPath, routeBackIntermediatePath, routeBackPath, routeOverviewPath, routeCodexPath, routeCodexShellMetrics, routeDeepLinkPreview: routeDeepLinkText.slice(0, 1200), routeOverviewPreview: routeOverviewText.slice(0, 800) });
|
||||
addSelectedCheck(checks, options, "frontend:pipeline-integrated-visible",
|
||||
pipelineTextLower.includes("pipeline v2 工作台".toLowerCase())
|
||||
|
||||
Reference in New Issue
Block a user