Add import-to-CCSwitch action for token rows on NewAPI-style consoles
// ==UserScript==
// @name NewAPI Token Import to CC Switch
// @namespace https://newapi.style/
// @version 0.14
// @description Add import-to-CCSwitch action for token rows on NewAPI-style consoles
// @author irisWirisW (https://github.com/irisWirisW)
// @license AGPL-3.0-or-later
// @homepageURL https://greasyfork.org/zh-CN/scripts/565602-newapi-token-import-to-cc-switch
// @supportURL https://greasyfork.org/zh-CN/scripts/565602-newapi-token-import-to-cc-switch/feedback
// @match *://*/console/*
// @run-at document-idle
// @grant GM_setClipboard
// @grant GM_notification
// ==/UserScript==
(() => {
"use strict";
const DEFAULT_APP = "codex";
const CODEX_MODEL = "gpt-5.2-codex";
const ROW_SELECTOR = "tr.semi-table-row[data-row-key]";
const ACTION_SELECTOR = 'td[aria-colindex="11"] .semi-space';
const BOUND_ATTR = "data-ccs-bound";
const TOKEN_PATH_REGEX = /^\/console\/token(?:\/|$)/;
const KEY_REGEX =
/^(sk-[A-Za-z0-9_-]{16,}|cr_[A-Za-z0-9_-]{16,}|rk-[A-Za-z0-9_-]{16,}|pk-[A-Za-z0-9_-]{16,})$/;
let modal = null;
let appField = null;
let nameField = null;
let endpointField = null;
let keyField = null;
let nameHintField = null;
let messageField = null;
let importBtn = null;
let copyBtn = null;
let actionMenu = null;
let actionMenuAnchor = null;
const style = document.createElement("style");
style.textContent = `
.ccs-newapi-btn {
margin-left: 4px;
border-color: rgba(59, 130, 246, 0.35) !important;
color: #60a5fa !important;
}
.ccs-newapi-modal {
--ccs-overlay: rgba(2, 6, 23, 0.72);
--ccs-panel: #0f172a;
--ccs-panel-border: rgba(148, 163, 184, 0.32);
--ccs-text: #e2e8f0;
--ccs-muted: #94a3b8;
--ccs-input-bg: #0b1220;
--ccs-input-border: #334155;
--ccs-input-text: #f8fafc;
--ccs-primary: #2563eb;
position: fixed;
inset: 0;
background: var(--ccs-overlay);
display: none;
align-items: center;
justify-content: center;
z-index: 99999;
}
.ccs-newapi-modal.open {
display: flex;
}
.ccs-newapi-panel {
width: min(560px, 92vw);
border-radius: 12px;
background: var(--ccs-panel);
border: 1px solid var(--ccs-panel-border);
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
color: var(--ccs-text);
padding: 16px;
font-size: 13px;
}
.ccs-newapi-title {
font-size: 16px;
font-weight: 700;
margin-bottom: 8px;
}
.ccs-newapi-msg {
color: var(--ccs-muted);
font-size: 12px;
margin-bottom: 10px;
}
.ccs-newapi-name-hint {
margin-top: 6px;
font-size: 12px;
color: #fbbf24;
line-height: 1.4;
display: none;
}
.ccs-newapi-panel label {
display: block;
font-weight: 600;
margin: 8px 0 4px;
}
.ccs-newapi-panel input,
.ccs-newapi-panel select {
width: 100%;
display: block;
box-sizing: border-box;
border-radius: 8px;
border: 1px solid var(--ccs-input-border);
background: var(--ccs-input-bg);
color: var(--ccs-input-text);
padding: 7px 10px;
font-size: 13px;
}
.ccs-newapi-actions {
margin-top: 12px;
display: flex;
justify-content: flex-end;
gap: 8px;
flex-wrap: wrap;
}
.ccs-newapi-actions button {
border-radius: 999px;
border: 1px solid #334155;
background: #111827;
color: #e2e8f0;
font-size: 12px;
font-weight: 700;
padding: 6px 12px;
cursor: pointer;
}
.ccs-newapi-actions .ccs-newapi-import {
background: var(--ccs-primary);
border-color: #1d4ed8;
color: #eff6ff;
}
.ccs-newapi-hidden-action {
display: none !important;
}
.ccs-newapi-split .ccs-newapi-btn {
margin-left: 0;
}
.ccs-newapi-more-menu {
position: fixed;
z-index: 100001;
min-width: 124px;
border-radius: 8px;
border: 1px solid #334155;
background: #111827;
box-shadow: 0 18px 44px rgba(0, 0, 0, 0.35);
padding: 4px;
display: none;
}
.ccs-newapi-more-menu.open {
display: block;
}
.ccs-newapi-more-menu__item {
width: 100%;
border: 0;
border-radius: 6px;
padding: 6px 10px;
background: transparent;
color: #e2e8f0;
text-align: left;
font-size: 12px;
font-weight: 600;
cursor: pointer;
}
.ccs-newapi-more-menu__item:hover {
background: rgba(148, 163, 184, 0.16);
}
.ccs-newapi-more-menu__item.danger {
color: #f87171;
}
thead th[aria-colindex="6"],
tr.semi-table-row td[aria-colindex="6"] {
width: 96px !important;
max-width: 96px !important;
}
tr.semi-table-row td[aria-colindex="6"] .semi-input-wrapper {
width: auto !important;
min-width: 0 !important;
max-width: none !important;
display: inline-flex !important;
align-items: center;
padding-left: 0 !important;
padding-right: 4px !important;
background: transparent !important;
border-color: transparent !important;
box-shadow: none !important;
}
tr.semi-table-row td[aria-colindex="6"] input.semi-input {
width: 0 !important;
min-width: 0 !important;
max-width: 0 !important;
padding: 0 !important;
margin: 0 !important;
border: 0 !important;
color: transparent !important;
-webkit-text-fill-color: transparent !important;
text-shadow: none !important;
opacity: 0 !important;
pointer-events: none;
user-select: none;
flex: 0 0 0 !important;
}
tr.semi-table-row td[aria-colindex="6"] .semi-input-suffix {
margin-left: 0 !important;
display: inline-flex !important;
gap: 0 !important;
}
tr.semi-table-row td[aria-colindex="6"] .semi-input-suffix button:not(:last-child) {
display: none !important;
}
`;
document.head.appendChild(style);
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function pageUrl() {
return window.location.href.split("#")[0];
}
function inferName(endpoint) {
try {
return new URL(endpoint).host;
} catch {
return "custom";
}
}
function getDefaultNameByApp(app, endpoint) {
if ((app || DEFAULT_APP) === "codex") {
return "custom";
}
return inferName(endpoint);
}
function updateNameHint(app, name) {
if (!nameHintField) return;
if ((app || DEFAULT_APP) !== "codex") {
nameHintField.style.display = "none";
nameHintField.textContent = "";
return;
}
const normalizedName = (name || "").trim();
const statusText =
normalizedName === "custom"
? "当前已设置为 custom。"
: "当前不是 custom,可能导致 thread 出错。";
nameHintField.style.display = "block";
nameHintField.textContent = `提示:Codex 导入名称建议使用 custom,不设置成 custom 可能导致 thread 出错。${statusText}`;
}
function buildDeepLink({ app, name, endpoint, apiKey }) {
const normalizedApp = app || DEFAULT_APP;
const params = new URLSearchParams({
resource: "provider",
app: normalizedApp,
name:
name && name.trim()
? name.trim()
: getDefaultNameByApp(normalizedApp, endpoint),
endpoint,
apiKey,
homepage: pageUrl(),
});
if ((app || DEFAULT_APP) === "codex" && CODEX_MODEL) {
params.set("model", CODEX_MODEL);
}
return `ccswitch://v1/import?${params.toString()}`;
}
function looksMasked(key) {
return key.includes("*");
}
function isLikelyKey(key) {
return KEY_REGEX.test((key || "").trim());
}
function getSiteName() {
const navTitle =
document.querySelector("header h4") ||
document.querySelector(".semi-typography-h4");
const navText = navTitle ? (navTitle.textContent || "").trim() : "";
if (navText) return navText;
const titleText = (document.title || "").split("|")[0].trim();
if (titleText) return titleText;
try {
return new URL(window.location.href).host;
} catch {
return "NewAPI Console";
}
}
function isTokenPage() {
return TOKEN_PATH_REGEX.test(window.location.pathname);
}
function cleanupInjected() {
closeActionMenu();
document.querySelectorAll('.ccs-newapi-split').forEach((split) => split.remove());
document
.querySelectorAll('.ccs-newapi-hidden-action')
.forEach((el) => el.classList.remove('ccs-newapi-hidden-action'));
document
.querySelectorAll('.ccs-newapi-btn')
.forEach((button) => {
if (!button.closest('.ccs-newapi-split')) button.remove();
});
document
.querySelectorAll(`tr.semi-table-row[${BOUND_ATTR}]`)
.forEach((row) => row.removeAttribute(BOUND_ATTR));
closeModal();
}
function getTokenInput(row) {
return row.querySelector('td[aria-colindex="6"] input.semi-input');
}
async function resolveApiKey(row) {
const input = getTokenInput(row);
let key = input ? (input.value || "").trim() : "";
if (key && !looksMasked(key) && isLikelyKey(key)) return key;
const eyeBtn = row.querySelector(
'td[aria-colindex="6"] button[aria-label*="toggle token visibility"]',
);
if (eyeBtn) {
eyeBtn.click();
await sleep(120);
key = input ? (input.value || "").trim() : key;
if (key && !looksMasked(key) && isLikelyKey(key)) return key;
}
const copyBtn = row.querySelector(
'td[aria-colindex="6"] button[aria-label*="copy token key"]',
);
if (copyBtn && navigator.clipboard && navigator.clipboard.readText) {
try {
copyBtn.click();
await sleep(160);
const copied = ((await navigator.clipboard.readText()) || "").trim();
if (copied && isLikelyKey(copied)) return copied;
} catch {
// Ignore and fallback to prompt.
}
}
return key;
}
function ensureModal() {
if (modal) return;
modal = document.createElement("div");
modal.className = "ccs-newapi-modal";
modal.innerHTML = `
<div class="ccs-newapi-panel" role="dialog" aria-modal="true">
<div class="ccs-newapi-title">导入到 CC Switch</div>
<div class="ccs-newapi-msg"></div>
<label>导入应用</label>
<select class="ccs-newapi-app">
<option value="codex">Codex</option>
<option value="claude">Claude Code</option>
</select>
<label>名称</label>
<input class="ccs-newapi-name" />
<div class="ccs-newapi-name-hint"></div>
<label>Endpoint URL</label>
<input class="ccs-newapi-endpoint" />
<label>API Key</label>
<input class="ccs-newapi-key" />
<div class="ccs-newapi-actions">
<button type="button" class="ccs-newapi-copy">复制导入链接</button>
<button type="button" class="ccs-newapi-import">导入到 CC Switch</button>
<button type="button" class="ccs-newapi-cancel">取消</button>
</div>
</div>
`;
document.body.appendChild(modal);
appField = modal.querySelector(".ccs-newapi-app");
nameField = modal.querySelector(".ccs-newapi-name");
endpointField = modal.querySelector(".ccs-newapi-endpoint");
keyField = modal.querySelector(".ccs-newapi-key");
nameHintField = modal.querySelector(".ccs-newapi-name-hint");
messageField = modal.querySelector(".ccs-newapi-msg");
importBtn = modal.querySelector(".ccs-newapi-import");
copyBtn = modal.querySelector(".ccs-newapi-copy");
const cancelBtn = modal.querySelector(".ccs-newapi-cancel");
cancelBtn.addEventListener("click", closeModal);
modal.addEventListener("click", (event) => {
if (event.target === modal) closeModal();
});
appField.addEventListener("change", () => {
if (!nameField) return;
const app = appField.value || DEFAULT_APP;
if (app === "codex" && !(nameField.value || "").trim()) {
nameField.value = "custom";
}
updateNameHint(app, nameField.value);
});
nameField.addEventListener("input", () => {
const app = (appField && appField.value) || DEFAULT_APP;
updateNameHint(app, nameField.value);
});
bindAutoSelect(nameField);
bindAutoSelect(endpointField);
bindAutoSelect(keyField);
}
function bindAutoSelect(input) {
if (!input) return;
const selectAll = () => {
input.focus();
input.select();
};
input.addEventListener("focus", () => setTimeout(selectAll, 0));
input.addEventListener("click", selectAll);
input.addEventListener("mouseup", (event) => event.preventDefault());
}
function closeModal() {
if (!modal) return;
modal.classList.remove("open");
}
function ensureActionMenu() {
if (actionMenu) return;
actionMenu = document.createElement("div");
actionMenu.className = "ccs-newapi-more-menu";
document.body.appendChild(actionMenu);
document.addEventListener(
"mousedown",
(event) => {
if (!actionMenu || !actionMenu.classList.contains("open")) return;
const target = event.target;
if (actionMenu.contains(target)) return;
if (actionMenuAnchor && actionMenuAnchor.contains(target)) return;
closeActionMenu();
},
true,
);
window.addEventListener("resize", closeActionMenu);
window.addEventListener("scroll", closeActionMenu, true);
}
function closeActionMenu() {
if (!actionMenu) return;
actionMenu.classList.remove("open");
actionMenu.innerHTML = "";
actionMenuAnchor = null;
}
function openActionMenu(anchor, buttons) {
const actions = buttons.filter((button) => button && button.isConnected);
if (!actions.length) {
closeActionMenu();
return;
}
ensureActionMenu();
actionMenu.innerHTML = "";
actions.forEach((button) => {
const label = (button.textContent || "").trim();
if (!label) return;
const item = document.createElement("button");
item.type = "button";
item.className = "ccs-newapi-more-menu__item";
if (button.classList.contains("semi-button-danger")) {
item.classList.add("danger");
}
item.textContent = label;
item.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
closeActionMenu();
button.click();
});
actionMenu.appendChild(item);
});
if (!actionMenu.children.length) {
closeActionMenu();
return;
}
actionMenu.classList.add("open");
actionMenuAnchor = anchor;
const rect = anchor.getBoundingClientRect();
const menuRect = actionMenu.getBoundingClientRect();
const left = Math.max(
8,
Math.min(rect.right - menuRect.width, window.innerWidth - menuRect.width - 8),
);
let top = rect.bottom + 6;
if (top + menuRect.height > window.innerHeight - 8) {
top = Math.max(8, rect.top - menuRect.height - 6);
}
actionMenu.style.left = `${left}px`;
actionMenu.style.top = `${top}px`;
}
function writeClipboard(text) {
if (typeof GM_setClipboard === "function") {
GM_setClipboard(text);
if (typeof GM_notification === "function") {
GM_notification({ text: "已复制导入链接", timeout: 1200 });
}
return;
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).catch(() => {});
}
}
function showModal({ name, endpoint, apiKey, message }) {
ensureModal();
appField.value = DEFAULT_APP;
nameField.value = name || getDefaultNameByApp(DEFAULT_APP, endpoint || `${window.location.origin}/v1`);
endpointField.value = endpoint || `${window.location.origin}/v1`;
keyField.value = apiKey || "";
messageField.textContent = message || "请确认参数后导入。";
updateNameHint(appField.value, nameField.value);
const buildCurrentLink = () =>
buildDeepLink({
app: appField.value,
name: nameField.value,
endpoint: endpointField.value,
apiKey: keyField.value,
});
importBtn.onclick = () => {
const endpointValue = (endpointField.value || "").trim();
const apiKeyValue = (keyField.value || "").trim();
if (!endpointValue || !apiKeyValue) {
messageField.textContent = "Endpoint 和 API Key 不能为空。";
return;
}
if (!isLikelyKey(apiKeyValue)) {
messageField.textContent =
"API Key 看起来不完整,请先手动确认或粘贴完整 key。";
return;
}
const appValue = (appField.value || DEFAULT_APP).trim();
const nameValue = (nameField.value || "").trim();
if (appValue === "codex" && nameValue !== "custom") {
const shouldContinue = window.confirm(
"提示:Codex 导入名称建议使用 custom,不设置成 custom 可能导致 thread 出错。是否继续导入?",
);
if (!shouldContinue) {
updateNameHint(appValue, nameValue);
return;
}
}
window.location.href = buildCurrentLink();
closeModal();
};
copyBtn.onclick = () => {
const endpointValue = (endpointField.value || "").trim();
const apiKeyValue = (keyField.value || "").trim();
if (!endpointValue || !apiKeyValue) {
messageField.textContent = "Endpoint 和 API Key 不能为空。";
return;
}
writeClipboard(buildCurrentLink());
};
modal.classList.add("open");
}
async function onImportClick(row) {
const name = getDefaultNameByApp(DEFAULT_APP, `${window.location.origin}/v1`);
const endpoint = `${window.location.origin}/v1`;
const key = await resolveApiKey(row);
const keyReady = key && isLikelyKey(key) && !looksMasked(key);
const message = keyReady
? "已自动读取 key,可直接导入。"
: "未能自动读取完整 key,请手动补全后导入。";
showModal({
name,
endpoint,
apiKey: key || "",
message,
});
}
function injectRowButton(row) {
if (!(row instanceof Element)) return;
const actionWrap = row.querySelector(ACTION_SELECTOR);
if (!actionWrap) return;
if (row.querySelector('.ccs-newapi-split')) {
row.setAttribute(BOUND_ATTR, "1");
return;
}
actionWrap
.querySelectorAll(':scope > .ccs-newapi-btn')
.forEach((button) => button.remove());
const originalSplit = actionWrap.querySelector(':scope > .semi-button-split');
if (!originalSplit) return;
const originalMoreBtn = originalSplit.querySelector('button:last-child');
const extraActionButtons = Array.from(
actionWrap.querySelectorAll(':scope > button:not(.ccs-newapi-btn)'),
);
const split = document.createElement("div");
split.className = "semi-button-split overflow-hidden ccs-newapi-split";
split.setAttribute("role", "group");
split.setAttribute("aria-label", "CC Switch 操作按钮组");
const importButton = document.createElement("button");
importButton.className =
"semi-button semi-button-tertiary semi-button-size-small semi-button-light semi-button-first ccs-newapi-btn";
importButton.type = "button";
importButton.innerHTML = '<span class="semi-button-content">导入CCSwitch</span>';
importButton.addEventListener("click", async (event) => {
event.preventDefault();
event.stopPropagation();
await onImportClick(row);
});
let moreButton = null;
if (originalMoreBtn) {
moreButton = originalMoreBtn.cloneNode(true);
moreButton.className =
"semi-button semi-button-tertiary semi-button-size-small semi-button-light semi-button-with-icon semi-button-with-icon-only semi-button-last ccs-newapi-more-btn";
moreButton.removeAttribute("aria-describedby");
moreButton.removeAttribute("data-popupid");
} else {
moreButton = document.createElement("button");
moreButton.type = "button";
moreButton.className =
"semi-button semi-button-tertiary semi-button-size-small semi-button-light semi-button-with-icon semi-button-with-icon-only semi-button-last ccs-newapi-more-btn";
moreButton.innerHTML =
'<span class="semi-button-content"><span aria-hidden="true">▼</span></span>';
}
moreButton.addEventListener("click", (event) => {
event.preventDefault();
event.stopPropagation();
openActionMenu(
moreButton,
extraActionButtons.filter((button) => button.classList.contains('ccs-newapi-hidden-action')),
);
});
split.appendChild(importButton);
split.appendChild(moreButton);
originalSplit.classList.add('ccs-newapi-hidden-action');
extraActionButtons.forEach((button) => button.classList.add('ccs-newapi-hidden-action'));
actionWrap.insertBefore(split, originalSplit);
row.setAttribute(BOUND_ATTR, "1");
}
function scanAndInject() {
if (!isTokenPage()) return;
document.querySelectorAll(ROW_SELECTOR).forEach((row) => injectRowButton(row));
}
let scanTimer = null;
let domObserver = null;
function startDomObserver() {
if (domObserver || !document.body) return;
domObserver = new MutationObserver(() => {
if (isTokenPage()) scheduleScan();
});
domObserver.observe(document.body, { childList: true, subtree: true });
}
function stopDomObserver() {
if (!domObserver) return;
domObserver.disconnect();
domObserver = null;
}
const scheduleScan = () => {
if (!isTokenPage()) {
cleanupInjected();
stopDomObserver();
return;
}
startDomObserver();
if (scanTimer) return;
scanTimer = setTimeout(() => {
scanTimer = null;
scanAndInject();
}, 100);
};
function getRouteKey() {
return `${window.location.pathname}${window.location.search}${window.location.hash}`;
}
let lastRouteKey = getRouteKey();
function onRouteChange(force = false) {
const routeKey = getRouteKey();
if (!force && routeKey === lastRouteKey) return;
lastRouteKey = routeKey;
scheduleScan();
}
function wrapHistoryMethod(methodName) {
const original = history[methodName];
if (typeof original !== "function" || original.__ccsWrapped) return;
function wrappedHistoryMethod(...args) {
const result = original.apply(this, args);
setTimeout(onRouteChange, 0);
return result;
}
wrappedHistoryMethod.__ccsWrapped = true;
history[methodName] = wrappedHistoryMethod;
}
function bindRouteWatcher() {
wrapHistoryMethod("pushState");
wrapHistoryMethod("replaceState");
window.addEventListener("popstate", onRouteChange);
window.addEventListener("hashchange", onRouteChange);
setInterval(onRouteChange, 400);
}
bindRouteWatcher();
onRouteChange(true);
})();