mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 13:55:50 +00:00
* feat: acp-link 支持 --group 参数指定 channel group - 添加 --group CLI flag,校验格式 [a-zA-Z0-9_-]+ - 支持 ACP_RCS_GROUP 环境变量 fallback - 传递 channelGroupId 到 RcsUpstreamClient - 更新 README 文档说明 --group 和相关环境变量 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: RCS 后端 session 复用与 group 绑定 - storeFindEnvironmentByMachineName 匹配 offline 状态,防止重连创建重复 session - registerEnvironment 复用已有 session 而非每次新建 - EnvironmentResponse 返回 channel_group_id 字段 - 注册时将 session 绑定到 group ID,支持 web UI 按 group 查询 - apiKeyAuth 不再设置 uuid,由 uuidAuth 统一处理 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: Web UI Token Manager — 多 token 切换与 session 隔离 - 新增 useTokens hook 管理 localStorage token CRUD - 新增 TokenManagerDialog 弹窗组件(添加/编辑/删除/切换 token) - api client 支持Bearer token 认证,UUID 跟随 token 变化 - Navbar 添加 token 切换按钮 - 切换 token 时自动 reload,实现 session 数据隔离 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: 修复 useTokens useState 初始化函数签名错误 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
121 lines
3.4 KiB
TypeScript
121 lines
3.4 KiB
TypeScript
import { useState, useCallback } from "react";
|
|
|
|
export interface TokenEntry {
|
|
id: string;
|
|
token: string;
|
|
label: string;
|
|
}
|
|
|
|
const TOKENS_KEY = "rcs_tokens";
|
|
const ACTIVE_TOKEN_KEY = "rcs_uuid";
|
|
const DEFAULT_ID = "__default__";
|
|
|
|
function generateId(): string {
|
|
return `tk_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
}
|
|
|
|
/** Ensure the existing rcs_uuid is present as the default token entry */
|
|
function ensureDefault(tokens: TokenEntry[]): TokenEntry[] {
|
|
if (tokens.some((t) => t.id === DEFAULT_ID)) return tokens;
|
|
let uuid: string | null = null;
|
|
try {
|
|
uuid = localStorage.getItem("rcs_uuid");
|
|
} catch {
|
|
// ignore
|
|
}
|
|
if (!uuid) return tokens;
|
|
return [{ id: DEFAULT_ID, token: uuid, label: "Default" }, ...tokens];
|
|
}
|
|
|
|
function loadTokens(): TokenEntry[] {
|
|
let tokens: TokenEntry[] = [];
|
|
try {
|
|
const raw = localStorage.getItem(TOKENS_KEY);
|
|
if (raw) {
|
|
const parsed = JSON.parse(raw);
|
|
if (Array.isArray(parsed)) tokens = parsed;
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
return ensureDefault(tokens);
|
|
}
|
|
|
|
function loadActiveTokenId(tokens: TokenEntry[]): string {
|
|
// Try saved active token
|
|
try {
|
|
const saved = localStorage.getItem(ACTIVE_TOKEN_KEY);
|
|
if (saved && tokens.some((t) => t.id === saved)) return saved;
|
|
} catch {
|
|
// ignore
|
|
}
|
|
// Fall back to default (rcs_uuid) entry
|
|
const defaultEntry = tokens.find((t) => t.id === DEFAULT_ID);
|
|
if (defaultEntry) return defaultEntry.id;
|
|
// Fall back to first entry
|
|
return tokens[0]?.id ?? DEFAULT_ID;
|
|
}
|
|
|
|
export function useTokens() {
|
|
const [tokens, setTokens] = useState<TokenEntry[]>(loadTokens);
|
|
const [activeTokenId, setActiveTokenIdState] = useState<string>(() => loadActiveTokenId(loadTokens()));
|
|
|
|
const persistTokens = useCallback((next: TokenEntry[]) => {
|
|
setTokens(next);
|
|
try {
|
|
localStorage.setItem(TOKENS_KEY, JSON.stringify(next));
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}, []);
|
|
|
|
const setActiveTokenId = useCallback((id: string) => {
|
|
setActiveTokenIdState(id);
|
|
try {
|
|
localStorage.setItem(ACTIVE_TOKEN_KEY, id);
|
|
location.reload(); // Reload to ensure api client picks up new token from localStorage
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}, []);
|
|
|
|
const addToken = useCallback((token: string, label: string): string | null => {
|
|
const trimmed = token.trim();
|
|
if (!trimmed) return "Token is required";
|
|
const entry: TokenEntry = { id: generateId(), token: trimmed, label: label.trim() || trimmed.slice(0, 8) };
|
|
const next = [...tokens, entry];
|
|
persistTokens(next);
|
|
return null;
|
|
}, [tokens, persistTokens]);
|
|
|
|
const removeToken = useCallback((id: string) => {
|
|
if (id === DEFAULT_ID) return; // Cannot remove default
|
|
const next = tokens.filter((t) => t.id !== id);
|
|
persistTokens(next);
|
|
if (activeTokenId === id) {
|
|
setActiveTokenId(DEFAULT_ID);
|
|
}
|
|
}, [tokens, persistTokens, activeTokenId, setActiveTokenId]);
|
|
|
|
const updateToken = useCallback((id: string, label: string) => {
|
|
const next = tokens.map((t) => t.id === id ? { ...t, label } : t);
|
|
persistTokens(next);
|
|
}, [tokens, persistTokens]);
|
|
|
|
const activeToken = tokens.find((t) => t.id === activeTokenId) ?? tokens[0] ?? null;
|
|
const activeLabel = activeToken?.label ?? "Default";
|
|
const activeTokenValue = activeToken?.token ?? null;
|
|
|
|
return {
|
|
tokens,
|
|
activeTokenId,
|
|
activeToken,
|
|
activeLabel,
|
|
activeTokenValue,
|
|
setActiveTokenId,
|
|
addToken,
|
|
removeToken,
|
|
updateToken,
|
|
};
|
|
}
|