Skip to content

添加被控时 token 未填充,显示字面量 {TOKEN}(主控新增 username 校验导致 token_create 失败) #138

Description

@cold-sword

现象

在「添加被控」向导第三步(安装脚本)中,--token 参数没有被替换成真实 token,而是显示字面量占位符 {TOKEN}

bash <(curl -sL https://install.example.com) install-agent  \
  --agent-id "00000000-0000-0000-0000-000000000001" \
  --token "{TOKEN}" \
  --server-ws "wss://master.example.com/nodeget/rpc" \
  --server-id "00000000-0000-0000-0000-000000000002" \
  --server-name "Master-Demo"

在多个不同的主控上都能复现,怀疑是最新版本引入的问题。

根因排查

登录其中一台主控(对应上面的 server-id)后,在 nodeget-server 容器日志中找到了这次操作对应的报错,target_username 和截图里的 agent-id 完全一致:

ERROR rpc: request failed error=ErrorObject { code: ServerError(108), message: "Invalid input: Username cannot contain ':' or '|' characters", data: None } [token::create{token_key="..." username="" target_username=Some("[agent]:00000000-0000-0000-0000-000000000001")}]

链路:

  1. AddAgentDialog.vue / ShowAgentCommandDialog.vue 在展示安装命令前调用 reGenerateToken(),向主控预生成 agent token。
  2. generateToken.ts 中 agent token 的 username 固定为 `[agent]:${nodeUuid}`(含冒号),这段逻辑自 2026-05-30 起没有变化。
  3. NodeGet 主控PR #177(2026-07-29 合并)中,于 crates/ng-token/src/generate_token.rs 新增了校验:username 不能包含 :|,原因是 TokenOrAuth::from_full_token 用这两个字符区分 key:secretusername|password 两种鉴权格式,含冒号的 username 会在之后用 username|password 方式登录时被误解析,因此主控选择在创建时 fail-fast 拒绝。
  4. 于是每次 token_create 都以 108 错误失败,而前端在 generateToken.ts 里用 try/catch 把错误吞掉(只 console.error),generatedToken 保持空字符串,最终 installScript 计算属性 fallback 到字面量 "{TOKEN}"

也就是说这是主控端一次安全加固(禁止 username 含 :/|,避免和 key:secret / username|password 两种鉴权格式产生歧义)与前端约定的 agent token 命名格式([agent]:{uuid})之间的兼容性回归,跟单台机器的部署配置无关——任何跑了 2026-07-29 之后镜像的主控都会必现。目标机器上还跑着自动更新镜像的容器(what's-up-docker),这也是为什么多个主控几乎同时出现该问题。

建议修复(前端侧,已本地验证)

不建议放宽主控的校验(那是刻意的安全修复,防止认证歧义)。更合适的做法是前端把 agent token 的 username 分隔符从 : 换成不冲突的字符(如 _),并且在删除/更新 token 时同时兼容旧格式,因为线上已经有一批在这次改动之前创建、真实带冒号 username 的 agent token。

在本地跑过 pnpm run type-checkeslint:改动前后对比确认已有的类型报错([extensionRoute].vue 等路由参数类型问题)是仓库里既有、与本改动无关的问题;下面这两个改动的文件本身没有新增报错。

diff(供参考,我没有仓库推送权限,所以以 issue 形式提交而不是 PR)
diff --git a/src/components/agents/generateToken.ts b/src/components/agents/generateToken.ts
index fcccf38..db543e6 100644
--- a/src/components/agents/generateToken.ts
+++ b/src/components/agents/generateToken.ts
@@ -6,9 +6,18 @@ import { makeRpcFunction } from "@/composables/useWsConnection";
 
 const { currentBackend } = useBackendStore();
 
+// 当前 agent token 的 username 格式。
+// 注意:不能包含 ':' 或 '|',主控自 2026-07-29 起会拒绝创建含这两个字符的 username
+// (TokenOrAuth::from_full_token 用它们区分 "key:secret" / "username|password" 两种鉴权格式)。
+const AGENT_USERNAME_PREFIX = "[agent]_";
+export const agentUsername = (nodeUuid: string) =>
+  `${AGENT_USERNAME_PREFIX}${nodeUuid}`;
+// 2026-07-29 之前创建的 agent token 仍然是旧的 `[agent]:{uuid}` 格式,删除时需要兼容清理。
+export const legacyAgentUsername = (nodeUuid: string) => `[agent]:${nodeUuid}`;
+
 function makeTokenObject(nodeUuid: string) {
   return {
-    username: `[agent]:${nodeUuid}`,
+    username: agentUsername(nodeUuid),
     password: generatePassword(16),
     timestamp_from: null,
     timestamp_to: null,
@@ -55,15 +64,20 @@ export async function reGenerateToken(
 ) {
   if (!backend.value) return;
   try {
-    try {
-      await getWsConnection(backend.value.url).call<{
-        key?: string;
-        secret?: string;
-      }>("token_delete", {
-        token: backend.value.token,
-        target_token: `[agent]:${nodeUuid}`,
-      });
-    } catch {}
+    for (const targetToken of [
+      agentUsername(nodeUuid),
+      legacyAgentUsername(nodeUuid),
+    ]) {
+      try {
+        await getWsConnection(backend.value.url).call<{
+          key?: string;
+          secret?: string;
+        }>("token_delete", {
+          token: backend.value.token,
+          target_token: targetToken,
+        });
+      } catch {}
+    }
 
     return preGenerateToken(nodeUuid, backend);
   } catch (e) {
@@ -77,17 +91,19 @@ export async function upgradeTokenLimit(
 ) {
   if (!backend.value) return;
   const rpc = makeRpcFunction();
-  try {
-    // const tokenDetail = await rpc<Token>("token_edit", {
-    //     "token":`[agent]:${nodeUuid}`,
-    //     "supertoken":backend.value?.token || ''
-    // })
-    rpc("token_edit", {
-      token: backend.value.token,
-      target_token: `[agent]:${nodeUuid}`,
-      limit: makeTokenObject(nodeUuid).token_limit,
-    });
-  } catch (e) {
-    console.error("Token update failed:", e);
+  // 目标 token 可能是升级前(旧 `[agent]:` 格式)或升级后(新 `[agent]_` 格式)创建的,两者都尝试。
+  for (const targetToken of [
+    agentUsername(nodeUuid),
+    legacyAgentUsername(nodeUuid),
+  ]) {
+    try {
+      rpc("token_edit", {
+        token: backend.value.token,
+        target_token: targetToken,
+        limit: makeTokenObject(nodeUuid).token_limit,
+      });
+    } catch (e) {
+      console.error("Token update failed:", e);
+    }
   }
 }
diff --git a/src/components/node/setting/NodeSettingTabDelete.vue b/src/components/node/setting/NodeSettingTabDelete.vue
index e800ef4..51f8372 100644
--- a/src/components/node/setting/NodeSettingTabDelete.vue
+++ b/src/components/node/setting/NodeSettingTabDelete.vue
@@ -30,6 +30,10 @@ import {
   type splitConfig,
 } from "@/composables/useAgentConfig";
 import { compareVersions } from "compare-versions";
+import {
+  agentUsername,
+  legacyAgentUsername,
+} from "@/components/agents/generateToken";
 
 const props = defineProps<{ uuid: string }>();
 
@@ -131,13 +135,18 @@ async function handleDelete() {
     });
   } catch {}
 
-  try {
-    // disable token, stop data report
-    await rpc("token_delete", {
-      token: currentBackend.value?.token,
-      target_token: `[agent]:${props.uuid}`,
-    });
-  } catch {}
+  for (const targetToken of [
+    agentUsername(props.uuid),
+    legacyAgentUsername(props.uuid),
+  ]) {
+    try {
+      // disable token, stop data report
+      await rpc("token_delete", {
+        token: currentBackend.value?.token,
+        target_token: targetToken,
+      });
+    } catch {}
+  }
   setStep(0, "done");
 
   // Step 2: clean cron

如果需要,我可以把这份 diff 整理成 PR 分支,麻烦哪位有权限的同学 fork 后帮忙提一下,或者告诉我要不要给我加一下写权限。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions