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
现象
在「添加被控」向导第三步(安装脚本)中,
--token参数没有被替换成真实 token,而是显示字面量占位符{TOKEN}:在多个不同的主控上都能复现,怀疑是最新版本引入的问题。
根因排查
登录其中一台主控(对应上面的
server-id)后,在nodeget-server容器日志中找到了这次操作对应的报错,target_username和截图里的 agent-id 完全一致:链路:
AddAgentDialog.vue/ShowAgentCommandDialog.vue在展示安装命令前调用reGenerateToken(),向主控预生成 agent token。generateToken.ts中 agent token 的username固定为`[agent]:${nodeUuid}`(含冒号),这段逻辑自 2026-05-30 起没有变化。crates/ng-token/src/generate_token.rs新增了校验:username 不能包含:或|,原因是TokenOrAuth::from_full_token用这两个字符区分key:secret与username|password两种鉴权格式,含冒号的 username 会在之后用username|password方式登录时被误解析,因此主控选择在创建时 fail-fast 拒绝。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-check和eslint:改动前后对比确认已有的类型报错([extensionRoute].vue等路由参数类型问题)是仓库里既有、与本改动无关的问题;下面这两个改动的文件本身没有新增报错。diff(供参考,我没有仓库推送权限,所以以 issue 形式提交而不是 PR)
如果需要,我可以把这份 diff 整理成 PR 分支,麻烦哪位有权限的同学 fork 后帮忙提一下,或者告诉我要不要给我加一下写权限。