diff --git a/src/components/agents/AddAgentDialog.vue b/src/components/agents/AddAgentDialog.vue index 57ce23b..d42afb5 100644 --- a/src/components/agents/AddAgentDialog.vue +++ b/src/components/agents/AddAgentDialog.vue @@ -34,6 +34,7 @@ import { useKv } from "@/composables/useKv"; import { useBackendExtra } from "@/composables/useBackendExtra"; import { useLifecycle } from "@/composables/useLifecycle"; import { reGenerateToken } from "@/components/agents/generateToken"; +import { toast } from "vue-sonner"; const open = defineModel("open", { required: true }); const emit = defineEmits<{ @@ -267,7 +268,12 @@ const canNext = computed(() => { const handleNext = async () => { if (step.value === 1) { // 预生成 token - generatedToken.value = (await reGenerateToken(nodeUuid.value)) || ""; + const newToken = await reGenerateToken(nodeUuid.value); + if (!newToken) { + toast.error("预生成 token 失败,可能会导致安装失败,请重试"); + return; + } + generatedToken.value = newToken; step.value = 2; loadCrons(); } else if (step.value === 2) { diff --git a/src/components/agents/generateToken.ts b/src/components/agents/generateToken.ts index fcccf38..4dcfbf2 100644 --- a/src/components/agents/generateToken.ts +++ b/src/components/agents/generateToken.ts @@ -3,12 +3,26 @@ import { getWsConnection } from "@/composables/useWsConnection"; import { AGENT_TEMPLATE_PERMISSIONS } from "@/components/token/tokenTemplates.ts"; import { generatePassword } from "@/lib/password"; import { makeRpcFunction } from "@/composables/useWsConnection"; +import { + decodeTokenUsername, + encodeTokenUsername, +} from "@/components/token/scopeCodec"; const { currentBackend } = useBackendStore(); +// 主控自 2026-07-29 起拒绝包含 ':' 或 '|' 的 username(这两个字符用于区分 +// "key:secret" / "username|password" 两种鉴权格式),因此发往后端时统一 URI 编码。 +export const agentUsername = (nodeUuid: string) => + encodeTokenUsername(`[agent]:${nodeUuid}`); + +// 展示层用:把后端存储的 username 安全解码为可读原文。 +export const displayTokenUsername = ( + username: string | null | undefined, +): string => (username ? decodeTokenUsername(username) : ""); + function makeTokenObject(nodeUuid: string) { return { - username: `[agent]:${nodeUuid}`, + username: agentUsername(nodeUuid), password: generatePassword(16), timestamp_from: null, timestamp_to: null, @@ -61,7 +75,7 @@ export async function reGenerateToken( secret?: string; }>("token_delete", { token: backend.value.token, - target_token: `[agent]:${nodeUuid}`, + target_token: agentUsername(nodeUuid), }); } catch {} @@ -78,13 +92,9 @@ export async function upgradeTokenLimit( if (!backend.value) return; const rpc = makeRpcFunction(); try { - // const tokenDetail = await rpc("token_edit", { - // "token":`[agent]:${nodeUuid}`, - // "supertoken":backend.value?.token || '' - // }) rpc("token_edit", { token: backend.value.token, - target_token: `[agent]:${nodeUuid}`, + target_token: agentUsername(nodeUuid), limit: makeTokenObject(nodeUuid).token_limit, }); } catch (e) { diff --git a/src/components/node/setting/NodeSettingTabDelete.vue b/src/components/node/setting/NodeSettingTabDelete.vue index e800ef4..2420a39 100644 --- a/src/components/node/setting/NodeSettingTabDelete.vue +++ b/src/components/node/setting/NodeSettingTabDelete.vue @@ -30,6 +30,7 @@ import { type splitConfig, } from "@/composables/useAgentConfig"; import { compareVersions } from "compare-versions"; +import { agentUsername } from "@/components/agents/generateToken"; const props = defineProps<{ uuid: string }>(); @@ -135,7 +136,7 @@ async function handleDelete() { // disable token, stop data report await rpc("token_delete", { token: currentBackend.value?.token, - target_token: `[agent]:${props.uuid}`, + target_token: agentUsername(props.uuid), }); } catch {} setStep(0, "done"); diff --git a/src/components/token/components/tokenDetailPreview.vue b/src/components/token/components/tokenDetailPreview.vue index d89c4f8..a4cd590 100644 --- a/src/components/token/components/tokenDetailPreview.vue +++ b/src/components/token/components/tokenDetailPreview.vue @@ -17,6 +17,7 @@ import { CollapsibleTrigger, } from "@/components/ui/collapsible"; import { Spinner } from "@/components/ui/spinner"; +import { displayTokenUsername } from "@/components/agents/generateToken"; type PermissionSection = { title: string; @@ -245,7 +246,7 @@ const rawSummary = computed(() => {
{{ displayText( - rawSummary.username ?? token.username, + displayTokenUsername(rawSummary.username ?? token.username), t("dashboard.token.detail.preview.notSet"), ) }} @@ -293,7 +294,7 @@ const rawSummary = computed(() => {
- + {{ t("dashboard.token.detail.preview.rawJsonTitle") }} - +
+  encodeURIComponent(username);
+
+export const decodeTokenUsername = (username: string): string => {
+  try {
+    return decodeURIComponent(username);
+  } catch {
+    return username;
+  }
+};
+
 export const createDefaultToken = (): Token => ({
   version: 1,
   timestamp_from: 0,
@@ -172,7 +186,7 @@ export const buildCredentialPayload = (
   const password = source.password.trim();
 
   return {
-    ...(username ? { username } : {}),
+    ...(username ? { username: encodeTokenUsername(username) } : {}),
     ...(password ? { password } : {}),
   };
 };
@@ -207,7 +221,7 @@ export const mapTokenDetailToForm = (detail: TokenDetail | null): Token => {
 
   return {
     version: detail.version ?? 1,
-    username: detail.username ?? "",
+    username: detail.username ? decodeTokenUsername(detail.username) : "",
     password: detail.password ?? "",
     timestamp_from: detail.timestamp_from ?? 0,
     timestamp_to: detail.timestamp_to ?? 0,
diff --git a/src/components/token/token-list/tokenListCard.vue b/src/components/token/token-list/tokenListCard.vue
index 6b5865a..a4ce0f2 100644
--- a/src/components/token/token-list/tokenListCard.vue
+++ b/src/components/token/token-list/tokenListCard.vue
@@ -42,6 +42,7 @@ import {
 } from "lucide-vue-next";
 import { useTokenListHook, type Token } from "@/composables/token/useTokenList";
 import { getPasswordChangeValidationError } from "@/composables/token/tokenSecret";
+import { displayTokenUsername } from "@/components/agents/generateToken";
 import TokenSuccessDialog from "../components/TokenSuccessDialog.vue";
 
 const useTokenList = useTokenListHook();
@@ -82,10 +83,18 @@ const normalizeSearchText = (value: string | null | undefined) =>
 
 const filteredTokens = computed(() => {
   const keyword = normalizeSearchText(debouncedSearchKeyword.value);
+  const encodedKeyword = normalizeSearchText(encodeURIComponent(keyword));
 
   return tokensList.value.filter((token) => {
+    const storedUsername = token.username ?? "";
     const matchesUsername =
-      !keyword || normalizeSearchText(token.username).includes(keyword);
+      !keyword ||
+      normalizeSearchText(displayTokenUsername(storedUsername)).includes(
+        keyword,
+      ) ||
+      normalizeSearchText(storedUsername)
+        .replace(/%([0-9a-f]{2})/gi, (m) => m.toUpperCase())
+        .includes(encodedKeyword.toUpperCase());
     const matchesTokenKey =
       !keyword || normalizeSearchText(token.token_key).includes(keyword);
 
@@ -351,7 +360,7 @@ watch(changePasswordOpen, (open) => {
         
           
             {{ token.version }}
-            {{ token.username }}
+            {{ displayTokenUsername(token.username) }}
             {{ token.token_key }}
             {{ token.token_limit?.length ?? 0 }}
             
@@ -500,7 +509,7 @@ watch(changePasswordOpen, (open) => {
               {{ t("dashboard.token.list.table.username") }}:
             
             
-              {{ selectedResetToken?.username || "-" }}
+              {{ displayTokenUsername(selectedResetToken?.username) || "-" }}
             
           
@@ -551,7 +560,7 @@ watch(changePasswordOpen, (open) => { {{ t("dashboard.token.list.table.username") }}: - {{ selectedPasswordToken?.username || "-" }} + {{ displayTokenUsername(selectedPasswordToken?.username) || "-" }}
diff --git a/src/composables/useThemeTokenPresets.ts b/src/composables/useThemeTokenPresets.ts index d8a56bd..6c536d7 100644 --- a/src/composables/useThemeTokenPresets.ts +++ b/src/composables/useThemeTokenPresets.ts @@ -4,6 +4,7 @@ import { getWsConnection } from "@/composables/useWsConnection"; import { createDefaultToken, DEFAULT_SCOPE, + encodeTokenUsername, serializeTokenPayload, } from "@/components/token/scopeCodec"; import { generatePassword } from "@/lib/password"; @@ -61,7 +62,10 @@ export function useThemeTokenPresets() { const listResult = await conn.call<{ tokens?: Array<{ token_key: string; username: string | null }>; }>("token_list_all_tokens", { token: fatherToken }); - const existing = listResult.tokens?.find((t) => t.username === username); + const existing = listResult.tokens?.find( + (t) => + t.username === encodeTokenUsername(username) || t.username === username, + ); if (existing) { await conn.call("token_delete", { token: fatherToken,