Files
cc-switch/deplink.html
T
Jason 502d681b73 docs: point the deep link generator to ccswitch.io and link the site owner guide
The manual linked the generator at farion1231.github.io, but GitHub Pages
was never enabled for the repository, so the link returned 404. The page
is now hosted on the website at ccswitch.io/deplink.html; the manual
(zh/en/ja), the four READMEs and the site owner guide point there.

The generator inlines its favicon and logo so the same file works both in
the repository and on the website. Manual 5.3 gains a pointer to the site
owner guide, and its usage steps list the Skill repository type.
2026-10-12 23:26:43 +08:00

2267 lines
157 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CC Switch 深链接生成器</title>
<!-- 图标内联:页面同时挂在仓库和官网(ccswitch.io/deplink.html),不依赖相对路径 -->
<link rel="icon" href="data:image/png;base64,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">
<script>
// 先于样式应用保存的外观,避免深色模式下闪一下白屏
try {
var savedTheme = localStorage.getItem('ccswitch-deeplink-theme');
if (savedTheme === 'light' || savedTheme === 'dark') document.documentElement.setAttribute('data-theme', savedTheme);
} catch (e) { }
</script>
<style>
/* 令牌照抄 docs/design-system.html(v7),与 App 的 src/index.css 一致:
中性面 + 主题橙只用在操作上,深色模式跟随系统或手动切换 */
:root {
--action-bg: #f97316;
--action-hover: #ea620a;
--action-fg: #ffffff;
--action-text: #c2410c;
--action-soft: #fff1e6;
--ring: #f97316;
--bg-app: #ffffff;
--bg-sidebar: #f5f5f7;
--bg-subtle: #efeff2;
--bg-selected: #e4e4ea;
--bg-card: #ffffff;
--border: #e4e4e9;
--border-strong: #cfcfd6;
--text-1: #18181b;
--text-2: #52525b;
--text-3: #65656e;
--inverse-bg: #18181b;
--inverse-fg: #ffffff;
--success: #16a34a;
--success-text: #007634;
--success-soft: #e8f7ee;
--warning: #d97706;
--warning-text: #a64a00;
--warning-soft: #fef3c7;
--danger: #dc2626;
--danger-text: #b91c1c;
--danger-soft: #fee2e2;
--agent-claude: #c2613f;
--agent-codex: #3f3f46;
--agent-gemini: #3367d6;
--agent-opencode: #0f766e;
--agent-pi: #7c3aed;
--shadow-sm: 0 1px 2px rgba(16, 24, 40, .06);
--shadow-md: 0 4px 12px rgba(16, 24, 40, .08), 0 1px 3px rgba(16, 24, 40, .06);
--shadow-lg: 0 20px 40px rgba(16, 24, 40, .16), 0 4px 12px rgba(16, 24, 40, .08);
--r-control: 6px;
--r-panel: 10px;
--font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "PingFang SC", "Hiragino Sans GB",
"Segoe UI Variable Text", "Segoe UI", "Microsoft YaHei UI", "Microsoft YaHei", Roboto, "Helvetica Neue", Arial, system-ui, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, "SF Mono", Consolas, "Liberation Mono", Menlo, monospace;
--dur: 150ms;
color-scheme: light;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--action-bg: #ea580c; --action-hover: #f06d1f; --action-text: #fb923c; --action-soft: rgba(249, 115, 21, 0.12); --ring: #d77a3c;
--bg-app: #1c1c1e; --bg-sidebar: #161618; --bg-subtle: #26262a; --bg-selected: #313136; --bg-card: #2e2e33;
--border: #3e3e45; --border-strong: #4a4a52;
--text-1: #e4e4e7; --text-2: #ababb3; --text-3: #9a9aa2;
--inverse-bg: #f4f4f5; --inverse-fg: #18181b;
--success: #4ade80; --success-text: #4ade80; --success-soft: #0f2b1a;
--warning: #fbbf24; --warning-text: #fcd34d; --warning-soft: #33260a;
--danger: #f87171; --danger-text: #fca5a5; --danger-soft: #3a1414;
--agent-claude: #e08a6a; --agent-codex: #d4d4d8; --agent-gemini: #8ab4f8; --agent-opencode: #5eead4; --agent-pi: #c4b5fd;
--shadow-sm: 0 0 0 1px rgba(255, 255, 255, .08), 0 1px 2px rgba(0, 0, 0, .4);
--shadow-md: 0 4px 12px rgba(0, 0, 0, .45), 0 1px 3px rgba(0, 0, 0, .4);
--shadow-lg: 0 20px 40px rgba(0, 0, 0, .5), 0 4px 12px rgba(0, 0, 0, .4);
color-scheme: dark;
}
}
:root[data-theme="dark"] {
--action-bg: #ea580c; --action-hover: #f06d1f; --action-text: #fb923c; --action-soft: rgba(249, 115, 21, 0.12); --ring: #d77a3c;
--bg-app: #1c1c1e; --bg-sidebar: #161618; --bg-subtle: #26262a; --bg-selected: #313136; --bg-card: #2e2e33;
--border: #3e3e45; --border-strong: #4a4a52;
--text-1: #e4e4e7; --text-2: #ababb3; --text-3: #9a9aa2;
--inverse-bg: #f4f4f5; --inverse-fg: #18181b;
--success: #4ade80; --success-text: #4ade80; --success-soft: #0f2b1a;
--warning: #fbbf24; --warning-text: #fcd34d; --warning-soft: #33260a;
--danger: #f87171; --danger-text: #fca5a5; --danger-soft: #3a1414;
--agent-claude: #e08a6a; --agent-codex: #d4d4d8; --agent-gemini: #8ab4f8; --agent-opencode: #5eead4; --agent-pi: #c4b5fd;
--shadow-sm: 0 0 0 1px rgba(255, 255, 255, .08), 0 1px 2px rgba(0, 0, 0, .4);
--shadow-md: 0 4px 12px rgba(0, 0, 0, .45), 0 1px 3px rgba(0, 0, 0, .4);
--shadow-lg: 0 20px 40px rgba(0, 0, 0, .5), 0 4px 12px rgba(0, 0, 0, .4);
color-scheme: dark;
}
* { box-sizing: border-box; }
html { font-family: var(--font-sans); -webkit-font-smoothing: antialiased; scroll-behavior: smooth; }
body { margin: 0; background: var(--bg-app); color: var(--text-1); font-size: 13px; line-height: 20px; }
h1, h2, h3, h4 { margin: 0; color: var(--text-1); }
p { margin: 0; }
code { font-family: var(--font-mono); font-size: 12px; background: var(--bg-subtle); border-radius: 4px; padding: 1px 5px; color: var(--text-1); }
a { color: var(--action-text); text-decoration: none; }
a:hover { text-decoration: underline; text-underline-offset: 3px; }
:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
svg.i { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; flex-shrink: 0; }
svg.i.sm { width: 14px; height: 14px; }
/* ---------- 外壳:照 App 的侧栏 + 内容 ---------- */
.shell { display: grid; grid-template-columns: 208px minmax(0, 1fr); min-height: 100vh; }
.rail {
position: sticky; top: 0; align-self: start; height: 100vh; overflow-y: auto;
background: var(--bg-sidebar); border-right: 1px solid var(--border);
padding: 18px 8px 14px; display: flex; flex-direction: column;
}
.brand { display: flex; align-items: center; gap: 8px; padding: 0 8px; font-size: 15px; font-weight: 600; }
.brand img { width: 22px; height: 22px; border-radius: 6px; }
.rail .tag { padding: 2px 8px 10px 38px; font-size: 11px; color: var(--text-3); }
.rail nav { display: flex; flex-direction: column; gap: 1px; }
.rail .grp { padding: 14px 8px 4px; font-size: 11px; font-weight: 500; color: var(--text-3); }
.rail nav a {
display: flex; align-items: center; height: 28px; padding: 0 8px; border-radius: var(--r-control);
color: var(--text-2); font-size: 13px; white-space: nowrap; transition: background-color var(--dur), color var(--dur);
}
.rail nav a:hover { background: var(--bg-subtle); color: var(--text-1); text-decoration: none; }
.rail nav a.on { background: var(--bg-selected); color: var(--text-1); font-weight: 500; }
.rail .foot { margin-top: auto; padding: 16px 8px 0; display: flex; flex-direction: column; gap: 10px; }
.rail .foot a { font-size: 12px; color: var(--text-2); display: inline-flex; align-items: center; gap: 4px; }
.main { min-width: 0; padding: 0 40px 96px; }
.main > * { max-width: 860px; }
.masthead { padding: 44px 0 28px; border-bottom: 1px solid var(--border); }
.eyebrow { font: 500 12px var(--font-mono); color: var(--action-text); }
.masthead h1 { font-size: 30px; line-height: 38px; letter-spacing: -.01em; margin: 10px 0 10px; font-weight: 700; }
.masthead > p { max-width: 64ch; font-size: 14px; line-height: 22px; color: var(--text-2); }
.masthead .notice { margin-top: 20px; }
section { padding: 36px 0; border-bottom: 1px solid var(--border); scroll-margin-top: 8px; }
section:last-child { border-bottom: 0; }
section > h2, details.fold > summary h2 { font-size: 20px; line-height: 28px; font-weight: 600; }
/* 阅读优先级低的节默认折叠,标题一行可点开 */
details.fold > summary { display: flex; align-items: baseline; flex-wrap: wrap; gap: 4px 12px; cursor: pointer; list-style: none; }
details.fold > summary::-webkit-details-marker { display: none; }
details.fold > summary::after { content: "展开"; margin-left: auto; font-size: 12px; color: var(--action-text); }
details.fold[open] > summary::after { content: "收起"; }
details.fold > summary:hover h2 { color: var(--action-text); }
.fold-hint { font-size: 13px; color: var(--text-2); }
details.fold > summary + * { margin-top: 12px; }
.lede { max-width: 70ch; margin-top: 6px; font-size: 13px; line-height: 21px; color: var(--text-2); }
.lede + * { margin-top: 20px; }
h3.sub { font-size: 15px; line-height: 22px; font-weight: 600; margin: 32px 0 12px; }
h3.sub:first-of-type { margin-top: 24px; }
/* ---------- 按钮 ---------- */
.btn {
display: inline-flex; align-items: center; justify-content: center; gap: 6px; white-space: nowrap;
height: 32px; padding: 0 14px; border-radius: var(--r-control); border: 1px solid transparent;
font: 500 13px/20px var(--font-sans); cursor: pointer; background: transparent; color: var(--text-1);
text-decoration: none; transition: background-color var(--dur), color var(--dur), scale var(--dur);
}
.btn:hover { text-decoration: none; }
.btn:active { scale: .96; }
.btn.compact { height: 28px; padding: 0 12px; }
.btn.solid { background: var(--action-bg); color: var(--action-fg); font-weight: 600; }
.btn.solid:hover { background: var(--action-hover); }
.btn.neutral { border-color: var(--border-strong); background: var(--bg-card); }
.btn.neutral:hover { background: var(--bg-subtle); }
.btn.copied, .btn.copied:hover { border-color: transparent; background: var(--success-soft); color: var(--success-text); }
.actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
/* ---------- 分段控件(选中项是浮起的白块) ---------- */
.seg { display: inline-flex; align-items: stretch; height: 36px; padding: 3px; gap: 2px; border-radius: 10px; background: var(--bg-subtle); max-width: 100%; overflow-x: auto; }
.seg button {
display: inline-flex; align-items: center; justify-content: center; padding: 0 14px; border: 0; border-radius: 7px;
background: transparent; color: var(--text-2); font: 500 13px var(--font-sans); cursor: pointer; white-space: nowrap;
transition: color var(--dur), background-color var(--dur);
}
.seg button:hover { color: var(--text-1); }
.seg button[aria-pressed="true"] { color: var(--text-1); background: var(--bg-card); box-shadow: var(--shadow-sm); font-weight: 600; }
.seg.sm { height: 28px; border-radius: 8px; }
.seg.sm button { padding: 0 10px; border-radius: 5px; font-size: 12px; }
/* ---------- 通知条 ---------- */
.notice { display: flex; align-items: flex-start; gap: 10px; border-radius: var(--r-panel); padding: 10px 14px; font-size: 13px; line-height: 20px; }
.notice > svg { margin-top: 2px; }
.notice .grow { flex: 1; min-width: 0; }
.notice ul { margin: 0; padding-left: 18px; }
.notice li + li { margin-top: 4px; }
.notice code { background: var(--bg-card); }
.notice.neutral { background: var(--bg-subtle); } .notice.neutral > svg { color: var(--text-2); }
.notice.warning { background: var(--warning-soft); } .notice.warning > svg { color: var(--warning-text); }
/* ---------- 卡片 / 徽章 ---------- */
.card { border: 1px solid var(--border); border-radius: var(--r-panel); background: var(--bg-card); padding: 20px; }
.chip { display: inline-flex; align-items: center; gap: 5px; height: 20px; padding: 0 8px; border-radius: 9999px; font-size: 11px; line-height: 16px; font-weight: 500; white-space: nowrap; }
.chip.app { border: 1px solid var(--border-strong); color: var(--text-2); }
.chip.app .d { width: 6px; height: 6px; border-radius: 50%; background: var(--dot, var(--text-3)); }
.chip.action { background: var(--action-soft); color: var(--action-text); }
/* ---------- 表单 ---------- */
.fsec { padding: 20px 0; border-top: 1px solid var(--border); }
.fsec:first-child { padding-top: 0; border-top: 0; }
.fsec > h4 { font-size: 13px; font-weight: 600; margin-bottom: 4px; }
.fsec > .hint { margin-bottom: 12px; }
.fsec > h4 + .fields, .fsec > h4 + div > .fields { margin-top: 12px; }
.fields { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px 16px; }
.fields.three { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.fields > .full { grid-column: 1 / -1; }
.fields + .fields, .fields + div > .fields, div + .fields { margin-top: 14px; }
.field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.field > label, .field > .label { font-size: 12px; font-weight: 500; color: var(--text-1); }
.field .opt { font-weight: 400; color: var(--text-3); }
.hint { font-size: 12px; line-height: 18px; color: var(--text-2); }
.hint:empty { display: none; }
.input {
height: 32px; width: 100%; padding: 0 10px; border: 1px solid var(--border-strong); border-radius: var(--r-control);
background: var(--bg-card); color: var(--text-1); font: 13px var(--font-sans); outline: none;
transition: border-color var(--dur), box-shadow var(--dur);
}
textarea.input { height: auto; min-height: 64px; padding: 8px 10px; line-height: 20px; resize: vertical; }
.input.mono { font-family: var(--font-mono); font-size: 12px; line-height: 18px; }
select.input {
appearance: none; -webkit-appearance: none; padding-right: 30px; cursor: pointer;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23888890' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
background-repeat: no-repeat; background-position: right 9px center; background-size: 14px;
}
.input::placeholder { color: var(--text-3); }
.input:focus { border-color: var(--ring); box-shadow: 0 0 0 3px color-mix(in srgb, var(--ring) 22%, transparent); }
.subpanel { margin-top: 14px; padding: 14px; border-radius: var(--r-panel); background: var(--bg-sidebar); border: 1px solid var(--border); }
.subpanel > .hint:first-child { margin-bottom: 12px; }
.form-foot { padding-top: 20px; border-top: 1px solid var(--border); }
/* 生成结果 */
.result { margin-top: 16px; padding: 14px; border-radius: var(--r-panel); background: var(--bg-sidebar); border: 1px solid var(--border); }
.result > .label { font-size: 12px; font-weight: 500; color: var(--text-2); margin-bottom: 8px; }
.linkbox {
padding: 10px 12px; margin-bottom: 12px; border: 1px solid var(--border); border-radius: var(--r-control);
background: var(--bg-card); font: 12px/18px var(--font-mono); color: var(--text-1); word-break: break-all; cursor: text;
max-height: 180px; overflow-y: auto; white-space: pre-wrap;
}
/* ---------- 代码块 ---------- */
.code { margin: 14px 0 0; border: 1px solid var(--border); border-radius: 8px; background: var(--bg-sidebar); overflow: hidden; }
.code figcaption { padding: 6px 12px; font-size: 11px; line-height: 16px; color: var(--text-3); border-bottom: 1px solid var(--border); }
.code pre { margin: 0; padding: 12px; font: 12px/18px var(--font-mono); color: var(--text-1); white-space: pre-wrap; word-break: break-word; }
.code pre + figcaption { border-top: 1px solid var(--border); }
.code .foot { padding: 8px 12px; font-size: 12px; line-height: 18px; color: var(--text-2); border-top: 1px solid var(--border); }
/* ---------- 示例卡片 ---------- */
.ex-list { display: flex; flex-direction: column; gap: 12px; }
.ex { border: 1px solid var(--border); border-radius: var(--r-panel); background: var(--bg-card); padding: 16px 18px 18px; transition: border-color var(--dur), box-shadow var(--dur); }
.ex:hover { border-color: var(--border-strong); box-shadow: var(--shadow-sm); }
.ex-head { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
.ex-head h3 { font-size: 14px; line-height: 22px; font-weight: 600; }
.ex-desc { margin-top: 6px; color: var(--text-2); line-height: 21px; }
.params { display: grid; grid-template-columns: 40px minmax(0, 1fr); gap: 4px 10px; margin: 12px 0 0; font-size: 12px; line-height: 20px; }
.params dt { color: var(--text-3); }
.params dd { margin: 0; color: var(--text-1); }
.ex-note { margin-top: 10px; font-size: 12px; color: var(--text-2); display: flex; gap: 6px; align-items: flex-start; }
.ex-note svg { margin-top: 2px; color: var(--warning-text); }
.ex .actions { margin-top: 14px; }
.rules { margin: 14px 0 0; padding-left: 18px; color: var(--text-2); line-height: 21px; max-width: 76ch; }
.rules li + li { margin-top: 6px; }
.rules strong { color: var(--text-1); font-weight: 600; }
/* ---------- 解析结果 ---------- */
.pgroup { margin-top: 16px; }
.pgroup > h4 { font-size: 12px; font-weight: 600; color: var(--text-2); margin-bottom: 8px; }
.kv { display: grid; grid-template-columns: 150px minmax(0, 1fr); margin: 0; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; background: var(--bg-card); }
.kv dt, .kv dd { margin: 0; padding: 7px 12px; border-top: 1px solid var(--border); font-size: 12px; line-height: 18px; }
.kv dt:first-of-type, .kv dt:first-of-type + dd { border-top: 0; }
.kv dt { color: var(--text-2); background: var(--bg-sidebar); word-break: break-all; }
.kv dd { font-family: var(--font-mono); word-break: break-all; }
.kv dd .plain { font-family: var(--font-sans); }
.kv dd .sub { display: block; color: var(--text-2); }
.flag { font-family: var(--font-sans); color: var(--text-2); margin-left: 6px; }
.flag.danger, .kv dd.danger, .kv dt.danger { color: var(--danger-text); }
.flag.ok { color: var(--success-text); }
.flag.warn, .kv dd.warn, .kv dt.warn { color: var(--warning-text); }
.kv dd > .flag:first-child { margin-left: 0; }
.perror { margin-top: 16px; padding: 10px 14px; border-radius: var(--r-panel); background: var(--danger-soft); color: var(--danger-text); font-size: 13px; }
details.raw { margin-top: 8px; }
details.raw summary { cursor: pointer; font-size: 12px; color: var(--text-2); }
details.raw summary:hover { color: var(--text-1); }
.grid2 { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
.grid2 .card { display: flex; flex-direction: column; gap: 12px; padding: 16px; }
.grid2 .card h3 { font-size: 14px; font-weight: 600; }
.faq { margin: 20px 0 0; display: grid; grid-template-columns: 140px minmax(0, 1fr); gap: 12px 20px; }
.faq dt { font-weight: 600; }
.faq dd { margin: 0; color: var(--text-2); line-height: 21px; }
/* Toast(照 App 的 sonner 样式) */
#toaster { position: fixed; bottom: 18px; left: 50%; translate: -50% 0; z-index: 60; display: flex; flex-direction: column; gap: 8px; width: min(420px, calc(100vw - 32px)); pointer-events: none; }
.toast { display: flex; align-items: flex-start; gap: 10px; padding: 10px 14px 10px 12px; border: 1px solid var(--border); border-radius: var(--r-panel); background: var(--bg-card); color: var(--text-1); box-shadow: var(--shadow-lg); font-size: 13px; line-height: 20px; animation: tin 200ms cubic-bezier(.4, 0, .2, 1); pointer-events: auto; }
.toast > svg { margin-top: 2px; color: var(--danger); }
.toast.info > svg { color: var(--text-2); }
@keyframes tin { from { opacity: 0; transform: translateY(8px); } }
@media (max-width: 860px) {
.shell { display: block; }
.rail {
position: sticky; z-index: 20; height: auto; flex-direction: row; align-items: center; gap: 8px;
padding: 8px 16px; border-right: 0; border-bottom: 1px solid var(--border); overflow-x: auto;
}
.brand span, .rail .tag, .rail .grp, .rail .foot a { display: none; }
.brand { padding: 0; }
.rail nav { flex-direction: row; }
.rail .foot { margin: 0 0 0 auto; padding: 0; }
.main { padding: 0 16px 72px; }
section { scroll-margin-top: 56px; }
.masthead { padding-top: 28px; }
.masthead h1 { font-size: 24px; line-height: 32px; }
.fields, .fields.three, .grid2 { grid-template-columns: minmax(0, 1fr); }
.kv { grid-template-columns: 110px minmax(0, 1fr); }
.faq { grid-template-columns: minmax(0, 1fr); gap: 4px; }
.faq dd { margin-bottom: 10px; }
}
</style>
</head>
<body>
<svg width="0" height="0" style="position: absolute" aria-hidden="true">
<symbol id="i-copy" viewBox="0 0 24 24"><rect width="14" height="14" x="8" y="8" rx="2" ry="2" /><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" /></symbol>
<symbol id="i-check" viewBox="0 0 24 24"><path d="M20 6 9 17l-5-5" /></symbol>
<symbol id="i-info" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" /><path d="M12 16v-4" /><path d="M12 8h.01" /></symbol>
<symbol id="i-alert" viewBox="0 0 24 24"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3" /><path d="M12 9v4" /><path d="M12 17h.01" /></symbol>
<symbol id="i-x" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" /><path d="m15 9-6 6" /><path d="m9 9 6 6" /></symbol>
<symbol id="i-import" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" /><path d="m7 10 5 5 5-5" /><path d="M12 15V3" /></symbol>
<symbol id="i-external" viewBox="0 0 24 24"><path d="M15 3h6v6" /><path d="M10 14 21 3" /><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" /></symbol>
</svg>
<div class="shell">
<aside class="rail">
<div class="brand"><img src="data:image/png;base64,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" alt=""><span>CC Switch</span></div>
<div class="tag">深链接工具</div>
<nav>
<div class="grp">工具</div>
<a href="#generator">生成器</a>
<a href="#parser">解析器</a>
<a href="#base64">Base64 编解码</a>
<div class="grp">示例</div>
<a href="#ex-provider">供应商</a>
<a href="#ex-usage">用量查询脚本</a>
<a href="#ex-mcp">MCP</a>
<a href="#ex-prompt">提示词</a>
<a href="#ex-skill">Skill 仓库</a>
<div class="grp">帮助</div>
<a href="#faq">常见问题</a>
</nav>
<div class="foot">
<div class="seg sm" id="themeSeg" role="group" aria-label="外观">
<button type="button" data-theme-value="system" aria-pressed="true">自动</button>
<button type="button" data-theme-value="light" aria-pressed="false">浅色</button>
<button type="button" data-theme-value="dark" aria-pressed="false">深色</button>
</div>
<a href="https://github.com/farion1231/cc-switch/blob/main/docs/guides/deeplink-site-integration-guide-zh.md" target="_blank" rel="noopener">站长接入指南<svg class="i sm"><use href="#i-external" /></svg></a>
</div>
</aside>
<main class="main">
<header class="masthead" id="top">
<div class="eyebrow">ccswitch://v1/import</div>
<h1>深链接生成器</h1>
<p>生成 CC Switch 的导入链接:用户点一下,就能把供应商、MCP、提示词或 Skill 仓库导入 CC Switch。也可以把收到的链接贴进解析器,看看里面写了什么。</p>
<div class="notice neutral">
<svg class="i"><use href="#i-info" /></svg>
<div class="grow">
<ul>
<li>本页按 <strong>CC Switch v4.0.10</strong> 的规则生成链接。<code>apiFormat</code>、<code>iconUrl</code> 参数和 <code>app=pi</code> 需要 v4.0.10 及以上:更早的版本会忽略 <code>apiFormat</code>(一律用应用的默认格式)和 <code>iconUrl</code>,并拒绝导入 Pi 供应商。</li>
<li>「导入后立即切换 / 添加 / 启用」由用户在确认框里的开关决定,链接里的 <code>enabled</code> 只是开关的初始值,一般不用写。</li>
<li>完整说明见 <a href="https://github.com/farion1231/cc-switch/blob/main/docs/guides/deeplink-site-integration-guide-zh.md" target="_blank" rel="noopener">站长深链接接入指南</a>。示例里的域名和 Key 都是假的,导入后无法实际使用。</li>
</ul>
</div>
</div>
</header>
<!-- 生成器 -->
<section id="generator">
<h2>生成器</h2>
<p class="lede">填好表单生成链接。点「在 CC Switch 中打开」可以在本机直接试导入。</p>
<div class="seg" id="genSeg" role="tablist" aria-label="资源类型">
<button type="button" data-panel="gen-provider" aria-pressed="true">供应商</button>
<button type="button" data-panel="gen-mcp" aria-pressed="false">MCP</button>
<button type="button" data-panel="gen-prompt" aria-pressed="false">提示词</button>
<button type="button" data-panel="gen-skill" aria-pressed="false">Skill 仓库</button>
</div>
<!-- 供应商生成器 -->
<div class="card gen-panel" id="gen-provider" data-generate="generateLink" style="margin-top: 16px;">
<div class="fsec">
<h4>基本信息</h4>
<div class="fields">
<div class="field">
<label for="app">应用</label>
<select class="input" id="app" onchange="updateModelFields()">
<option value="claude">Claude Code</option>
<option value="codex">Codex</option>
<option value="gemini">Gemini CLI</option>
<option value="grokbuild">Grok Build</option>
<option value="opencode">OpenCode</option>
<option value="openclaw">OpenClaw</option>
<option value="hermes">Hermes</option>
<option value="pi">Pi</option>
</select>
<span class="hint">Claude Desktop、MiniMax Code 的供应商暂不支持深链接导入</span>
</div>
<div class="field">
<label for="apiFormat">接口格式 <span class="opt">apiFormat</span></label>
<select class="input" id="apiFormat"></select>
<span class="hint" id="apiFormatHint"></span>
</div>
<div class="field full">
<label for="name">供应商名称</label>
<input class="input" type="text" id="name" placeholder="例如:Example AI">
<span class="hint">必填。地址和 API Key 也必须给出,写在 URL 参数或配置文件里都行。</span>
</div>
<div class="field full">
<span class="label">填写方式</span>
<div>
<div class="seg sm" id="modeSeg" role="group" aria-label="填写方式">
<button type="button" data-mode="url" aria-pressed="true">URL 参数</button>
<button type="button" data-mode="config" aria-pressed="false">配置文件</button>
</div>
</div>
<input type="hidden" id="importMode" value="url">
<span class="hint">URL 参数:地址、Key 直接写在链接里。配置文件:放一份 Base64 编码的 JSON 或 TOML,适合要带额外设置的情况。</span>
</div>
</div>
</div>
<div class="fsec">
<h4>连接</h4>
<!-- URL 参数模式 -->
<div id="urlModeFields">
<div class="fields">
<div class="field full">
<label for="endpoint">API 地址</label>
<input class="input" type="url" id="endpoint" placeholder="https://api.example.com/v1">
<span class="hint">Anthropic 格式在 Claude Code 里不带 /v1,OpenAI 两种格式一般带 /v1;OpenCode、OpenClaw、Hermes、Pi 以在该应用里实测能通的写法为准。</span>
</div>
<div class="field full">
<label for="apiKey">API Key</label>
<input class="input" type="text" id="apiKey" placeholder="sk-xxxxx 或 AIzaSyXXXXX">
</div>
<div class="field">
<label for="extraEndpoints">备用地址 <span class="opt">可选,逗号分隔</span></label>
<input class="input" type="text" id="extraEndpoints" placeholder="https://api2.example.com/v1, …">
<span class="hint">导入后加为自定义端点</span>
</div>
<div class="field">
<label for="homepage">官网 <span class="opt">可选</span></label>
<input class="input" type="url" id="homepage" placeholder="https://example.com">
</div>
</div>
</div>
<!-- 配置文件模式 -->
<div id="configModeFields" style="display: none;">
<div id="generalConfigGroup">
<div class="field">
<label for="configJson" id="configJsonLabel">配置文件(JSON)</label>
<textarea class="input mono" id="configJson" rows="10"></textarea>
<span class="hint">生成时自动 Base64 编码</span>
</div>
</div>
<!-- Codex / Grok Build 的 TOML 配置 -->
<div id="codexConfigGroup" style="display: none;">
<div id="codexAuthGroup" style="margin-bottom: 14px;">
<div class="field">
<label for="codexAuthJson">认证信息(JSON)</label>
<textarea class="input mono" id="codexAuthJson" rows="5"></textarea>
<span class="hint">包含 API Key 的认证信息对象</span>
</div>
</div>
<div class="field">
<label for="codexConfigToml" id="tomlConfigLabel">Codex 配置文件 (TOML)</label>
<textarea class="input mono" id="codexConfigToml" rows="10"></textarea>
<span class="hint" id="tomlConfigHint">config.toml 的原始内容。只收选中的那张 [model_providers.&lt;id&gt;] 表和顶层的模型相关字段</span>
</div>
</div>
<div class="subpanel">
<p class="hint">用 URL 参数覆盖配置文件里的 Key 和地址。留空就完全用配置文件;两边都有时以 URL 参数为准。</p>
<div class="fields">
<div class="field">
<label for="overrideApiKey">API Key <span class="opt">可选</span></label>
<input class="input" type="text" id="overrideApiKey" placeholder="覆盖配置文件中的 Key">
</div>
<div class="field">
<label for="overrideEndpoint">API 地址 <span class="opt">可选</span></label>
<input class="input" type="url" id="overrideEndpoint" placeholder="覆盖配置文件中的地址">
</div>
</div>
</div>
</div>
</div>
<div class="fsec">
<h4>模型</h4>
<div class="fields">
<div class="field full">
<label for="model" id="modelLabel">默认模型(可选)</label>
<input class="input" type="text" id="model" placeholder="例如:example-chat">
<span class="hint" id="modelHint"></span>
</div>
</div>
<div id="claudeFields">
<div class="subpanel">
<p class="hint">Claude Code 三档模型。中转站提供的是原版 Claude 模型时全部留空,Claude Code 会用自带的模型列表;只有 DeepSeek、Kimi 这类非原生模型才需要写上。</p>
<div class="fields three">
<div class="field">
<label for="haikuModel">Haiku</label>
<input class="input" type="text" id="haikuModel" placeholder="example-fast">
<span class="hint"><code>ANTHROPIC_DEFAULT_HAIKU_MODEL</code></span>
</div>
<div class="field">
<label for="sonnetModel">Sonnet</label>
<input class="input" type="text" id="sonnetModel" placeholder="example-chat">
<span class="hint"><code>ANTHROPIC_DEFAULT_SONNET_MODEL</code></span>
</div>
<div class="field">
<label for="opusModel">Opus</label>
<input class="input" type="text" id="opusModel" placeholder="example-pro">
<span class="hint"><code>ANTHROPIC_DEFAULT_OPUS_MODEL</code></span>
</div>
</div>
</div>
</div>
</div>
<div class="fsec">
<h4>展示与确认</h4>
<div class="fields">
<div class="field">
<label for="icon">图标 <span class="opt">可选</span></label>
<input class="input" type="text" id="icon" placeholder="例如:openai、anthropic、google">
<span class="hint">CC Switch 内置的图标名,只能是小写字母、数字和 . _ -</span>
</div>
<div class="field">
<label for="iconUrl">自己的图标 <span class="opt">iconUrl</span></label>
<input class="input" type="url" id="iconUrl" placeholder="https://example.com/logo.png">
<span class="hint">内置图标里没有你的品牌时用。下载失败退回上面的内置图标</span>
</div>
<p class="hint full">iconUrl 要求:https、域名(不能是 IP)、默认端口;公开可访问(不带 Cookie 和 Referer);5 秒内下完、不超过 256 KB;<code>Content-Type</code> 是 PNG、JPEG、WebP 或 SVG 且与文件一致。建议用方形图。</p>
<div class="field">
<label for="enabled">确认框开关的初始值 <span class="opt">enabled</span></label>
<select class="input" id="enabled">
<option value="">不指定(推荐,按应用默认)</option>
<option value="true">打开</option>
<option value="false">关闭</option>
</select>
</div>
<p class="hint full">确认框里有「导入后立即切换 / 添加到应用配置」开关,用户可以改。Claude Code、Codex、Gemini CLI、Grok Build 默认关闭;OpenCode、OpenClaw、Hermes、Pi 默认打开。</p>
<div class="field full">
<label for="notes">备注 <span class="opt">可选</span></label>
<textarea class="input" id="notes" rows="2" placeholder="例如:公司专用账号"></textarea>
</div>
</div>
</div>
<div class="fsec">
<h4>用量查询 <span class="opt" style="font-weight: 400; color: var(--text-3);">可选</span></h4>
<p class="hint">带上查询脚本,导入后供应商卡片上会直接显示余额。写法见下方<a href="#ex-usage">用量查询脚本示例</a>。</p>
<div class="fields">
<div class="field full">
<span class="label">查询脚本</span>
<div class="actions">
<button type="button" class="btn neutral compact" onclick="fillUsageTemplate('generic')">填入通用余额模板</button>
<button type="button" class="btn neutral compact" onclick="fillUsageTemplate('newapi')">填入 NewAPI 模板</button>
<button type="button" class="btn neutral compact" onclick="fillUsageTemplate('sub2api')">填入 Sub2API 模板</button>
</div>
<textarea class="input mono" id="usageScript" rows="10" placeholder="点上面的按钮填入模板,或者粘贴自己的脚本"></textarea>
<span class="hint">占位符:<code>{{baseUrl}}</code> <code>{{apiKey}}</code> <code>{{accessToken}}</code> <code>{{userId}}</code></span>
</div>
<div class="field">
<label for="usageEnabled">启用用量查询</label>
<select class="input" id="usageEnabled">
<option value="">不指定(带脚本即启用)</option>
<option value="true">启用</option>
<option value="false">禁用</option>
</select>
</div>
<div class="field">
<label for="usageAutoInterval">自动查询间隔(分钟)</label>
<input class="input" type="number" id="usageAutoInterval" placeholder="30" min="0">
<span class="hint">0 表示不自动查询</span>
</div>
<div class="field">
<label for="usageBaseUrl">查询地址 <span class="opt">usageBaseUrl</span></label>
<input class="input" type="url" id="usageBaseUrl" placeholder="不填就用供应商地址">
<span class="hint">脚本里的请求必须与它同源</span>
</div>
<div class="field">
<label for="usageApiKey">查询专用 Key <span class="opt">可选</span></label>
<input class="input" type="text" id="usageApiKey" placeholder="不填就用供应商的 Key">
</div>
<div class="field">
<label for="usageAccessToken">Access Token <span class="opt">可选</span></label>
<input class="input" type="text" id="usageAccessToken" placeholder="某些接口需要的访问令牌">
</div>
<div class="field">
<label for="usageUserId">User ID <span class="opt">可选</span></label>
<input class="input" type="text" id="usageUserId" placeholder="某些接口需要的用户 ID">
</div>
</div>
</div>
<div class="form-foot">
<button type="button" class="btn solid" onclick="generateLink()">生成链接</button>
<div id="result" class="result" style="display: none;">
<div class="label">深链接</div>
<div class="linkbox" id="linkText" onclick="selectText(this)"></div>
<div class="actions">
<a id="testLink" class="btn solid compact"><svg class="i sm"><use href="#i-import" /></svg>在 CC Switch 中打开</a>
<button type="button" class="btn neutral compact" onclick="copyText(document.getElementById('linkText').textContent, this)"><svg class="i sm"><use href="#i-copy" /></svg><span>复制链接</span></button>
</div>
</div>
</div>
</div>
<!-- MCP 生成器 -->
<div class="card gen-panel" id="gen-mcp" data-generate="generateMcpLink" style="margin-top: 16px; display: none;">
<div class="fsec">
<div class="fields">
<div class="field full">
<label for="mcpApps">目标应用</label>
<input class="input" type="text" id="mcpApps" placeholder="例如:claude,codex">
<span class="hint">多个应用用逗号分隔。可选值:claude、codex、gemini、grokbuild、opencode、hermes、pi、mcode(openclaw 会被忽略)</span>
</div>
<div class="field full">
<label for="mcpConfig">MCP 配置(JSON)</label>
<textarea class="input mono" id="mcpConfig" rows="9" placeholder='{
"mcpServers": {
"server-name": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-xxx"],
"type": "stdio"
}
}
}'></textarea>
<span class="hint">导入即写入所选应用的配置文件;已存在同 ID 的服务器只勾上新的应用,不覆盖配置。</span>
</div>
</div>
</div>
<div class="form-foot">
<button type="button" class="btn solid" onclick="generateMcpLink()">生成链接</button>
<div id="mcpResult" class="result" style="display: none;">
<div class="label">深链接</div>
<div class="linkbox" id="mcpUrl" onclick="selectText(this)"></div>
<div class="actions">
<a id="mcpImportBtn" class="btn solid compact"><svg class="i sm"><use href="#i-import" /></svg>在 CC Switch 中打开</a>
<button type="button" class="btn neutral compact" onclick="copyText(document.getElementById('mcpUrl').textContent, this)"><svg class="i sm"><use href="#i-copy" /></svg><span>复制链接</span></button>
</div>
</div>
</div>
</div>
<!-- 提示词生成器 -->
<div class="card gen-panel" id="gen-prompt" data-generate="generatePromptLink" style="margin-top: 16px; display: none;">
<div class="fsec">
<div class="fields">
<div class="field">
<label for="promptApp">目标应用</label>
<select class="input" id="promptApp">
<option value="claude">Claude Code</option>
<option value="codex">Codex</option>
<option value="gemini">Gemini CLI</option>
<option value="grokbuild">Grok Build</option>
<option value="opencode">OpenCode</option>
<option value="pi">Pi</option>
<option value="mcode">MiniMax Code</option>
</select>
<span class="hint">OpenClaw、Hermes 的提示词由工作区编辑器和记忆管理负责,不建议用深链接分发</span>
</div>
<div class="field">
<label for="promptName">名称</label>
<input class="input" type="text" id="promptName" placeholder="例如:代码审查专家">
</div>
<div class="field full">
<label for="promptContent">内容</label>
<textarea class="input" id="promptContent" rows="7" placeholder="# 角色定义
你是一位专业的…"></textarea>
<span class="hint">Markdown,生成时自动 Base64 编码</span>
</div>
<div class="field">
<label for="promptDescription">描述 <span class="opt">可选</span></label>
<input class="input" type="text" id="promptDescription" placeholder="例如:专注代码质量">
</div>
<div class="field">
<label for="promptEnabled">「导入后立即启用」的初始值</label>
<select class="input" id="promptEnabled">
<option value="">不指定(开关默认关闭,推荐)</option>
<option value="true">打开(确认后替换当前的提示词文件)</option>
</select>
<span class="hint">用户可以在确认框里改</span>
</div>
</div>
</div>
<div class="form-foot">
<button type="button" class="btn solid" onclick="generatePromptLink()">生成链接</button>
<div id="promptResult" class="result" style="display: none;">
<div class="label">深链接</div>
<div class="linkbox" id="promptUrl" onclick="selectText(this)"></div>
<div class="actions">
<a id="promptImportBtn" class="btn solid compact"><svg class="i sm"><use href="#i-import" /></svg>在 CC Switch 中打开</a>
<button type="button" class="btn neutral compact" onclick="copyText(document.getElementById('promptUrl').textContent, this)"><svg class="i sm"><use href="#i-copy" /></svg><span>复制链接</span></button>
</div>
</div>
</div>
</div>
<!-- Skill 生成器 -->
<div class="card gen-panel" id="gen-skill" data-generate="generateSkillLink" style="margin-top: 16px; display: none;">
<div class="fsec">
<div class="fields">
<div class="field">
<label for="skillRepo">GitHub 仓库</label>
<input class="input" type="text" id="skillRepo" placeholder="owner/repo-name">
<span class="hint">必须是 GitHub 上的公开仓库</span>
</div>
<div class="field">
<label for="skillBranch">分支</label>
<input class="input" type="text" id="skillBranch" placeholder="main" value="main">
</div>
<p class="hint full">添加的是仓库,用户还要在 Skills 管理界面自己选择安装哪些 Skill。</p>
</div>
</div>
<div class="form-foot">
<button type="button" class="btn solid" onclick="generateSkillLink()">生成链接</button>
<div id="skillResult" class="result" style="display: none;">
<div class="label">深链接</div>
<div class="linkbox" id="skillUrl" onclick="selectText(this)"></div>
<div class="actions">
<a id="skillImportBtn" class="btn solid compact"><svg class="i sm"><use href="#i-import" /></svg>在 CC Switch 中打开</a>
<button type="button" class="btn neutral compact" onclick="copyText(document.getElementById('skillUrl').textContent, this)"><svg class="i sm"><use href="#i-copy" /></svg><span>复制链接</span></button>
</div>
</div>
</div>
</div>
</section>
<!-- 解析器 -->
<section id="parser">
<h2>解析器</h2>
<p class="lede">贴一条 <code>ccswitch://</code> 链接,查看它会导入什么。会被 CC Switch 拒绝或忽略的参数会标出来。</p>
<div class="card" data-generate="parseDeepLink">
<div class="field">
<label for="parseUrl">深链接</label>
<textarea class="input mono" id="parseUrl" rows="3" placeholder="ccswitch://v1/import?resource=provider&amp;app=claude&amp;…"></textarea>
</div>
<div class="actions" style="margin-top: 12px;">
<button type="button" class="btn solid" onclick="parseDeepLink()">解析</button>
</div>
<div id="parseResult" style="display: none;">
<div id="parseBasicInfo"></div>
<div id="parseUrlParams"></div>
<div id="parseConfigContent"></div>
</div>
</div>
</section>
<!-- Base64 -->
<section id="base64">
<details class="fold">
<summary><h2>Base64 编解码</h2><span class="fold-hint">生成器会自动编码;手动检查链接里的配置时再用</span></summary>
<p class="lede">按 UTF-8 编码,中文没问题。编码结果可以直接放进链接的 <code>config</code> 或 <code>content</code> 参数(生成器会自动做这一步);解码用来检查链接里的配置对不对。</p>
<div class="grid2">
<div class="card">
<h3>编码</h3>
<textarea class="input mono" id="encodeInput" rows="6" placeholder="输入要编码的文本,JSON、TOML、Markdown 都可以"></textarea>
<div class="actions"><button type="button" class="btn neutral" onclick="encodeToBase64()">编码为 Base64</button></div>
<div id="encodeResult" style="display: none;">
<div class="linkbox" id="encodeOutput"></div>
<div class="actions">
<button type="button" class="btn neutral compact" onclick="copyText(document.getElementById('encodeOutput').textContent, this)"><svg class="i sm"><use href="#i-copy" /></svg><span>复制结果</span></button>
</div>
</div>
</div>
<div class="card">
<h3>解码</h3>
<textarea class="input mono" id="decodeInput" rows="6" placeholder="粘贴 Base64,例如 eyJlbnYiOnsiaGVsbG8iOiJ3b3JsZCJ9fQ=="></textarea>
<div class="actions"><button type="button" class="btn neutral" onclick="decodeFromBase64()">解码为文本</button></div>
<div id="decodeResult" style="display: none;">
<div class="linkbox" id="decodeOutput"></div>
<div class="actions">
<button type="button" class="btn neutral compact" onclick="copyText(document.getElementById('decodeOutput').textContent, this)"><svg class="i sm"><use href="#i-copy" /></svg><span>复制结果</span></button>
<span id="jsonFormat" style="display: none;"><button type="button" class="btn neutral compact" onclick="formatJson()">格式化 JSON</button></span>
</div>
</div>
</div>
</div>
</details>
</section>
<!-- 供应商示例 -->
<section id="ex-provider">
<h2>供应商示例</h2>
<p class="lede">每条都能直接点开试导入。链接都按规则做过 URL 转义,可以照着改。</p>
<div class="ex-list">
<article class="ex">
<div class="ex-head"><span class="chip app" style="--dot: var(--agent-claude)"><i class="d"></i>Claude Code</span><h3>完整 JSON 配置(非原生模型)</h3></div>
<p class="ex-desc">通过 Base64 编码的 JSON 配置导入。中转站提供的是原版 Claude 模型时,模型字段全部留空即可;只有 DeepSeek、Kimi 这类非原生模型才需要像这样写上四档模型。</p>
<dl class="params">
<dt>必需</dt><dd><code>resource=provider</code> <code>app=claude</code> <code>name</code>,地址和 Key(URL 参数或 config 里)</dd>
<dt>可选</dt><dd><code>config</code>(Base64 JSON)<code>configFormat=json</code></dd>
</dl>
<figure class="code">
<figcaption>解码后的 config</figcaption>
<pre>{
"env": {
"ANTHROPIC_AUTH_TOKEN": "sk-test-key123",
"ANTHROPIC_BASE_URL": "https://api.example.com",
"ANTHROPIC_MODEL": "example-chat",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "example-fast",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "example-chat",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "example-pro"
}
}</pre>
</figure>
<div class="actions">
<a class="btn solid compact" href="ccswitch://v1/import?resource=provider&amp;app=claude&amp;name=Example%20AI&amp;configFormat=json&amp;config=eyJlbnYiOnsiQU5USFJPUElDX0FVVEhfVE9LRU4iOiJzay10ZXN0LWtleTEyMyIsIkFOVEhST1BJQ19CQVNFX1VSTCI6Imh0dHBzOi8vYXBpLmV4YW1wbGUuY29tIiwiQU5USFJPUElDX01PREVMIjoiZXhhbXBsZS1jaGF0IiwiQU5USFJPUElDX0RFRkFVTFRfSEFJS1VfTU9ERUwiOiJleGFtcGxlLWZhc3QiLCJBTlRIUk9QSUNfREVGQVVMVF9TT05ORVRfTU9ERUwiOiJleGFtcGxlLWNoYXQiLCJBTlRIUk9QSUNfREVGQVVMVF9PUFVTX01PREVMIjoiZXhhbXBsZS1wcm8ifX0%3D"><svg class="i sm"><use href="#i-import" /></svg>在 CC Switch 中打开</a>
<button type="button" class="btn neutral compact" data-link="ccswitch://v1/import?resource=provider&amp;app=claude&amp;name=Example%20AI&amp;configFormat=json&amp;config=eyJlbnYiOnsiQU5USFJPUElDX0FVVEhfVE9LRU4iOiJzay10ZXN0LWtleTEyMyIsIkFOVEhST1BJQ19CQVNFX1VSTCI6Imh0dHBzOi8vYXBpLmV4YW1wbGUuY29tIiwiQU5USFJPUElDX01PREVMIjoiZXhhbXBsZS1jaGF0IiwiQU5USFJPUElDX0RFRkFVTFRfSEFJS1VfTU9ERUwiOiJleGFtcGxlLWZhc3QiLCJBTlRIUk9QSUNfREVGQVVMVF9TT05ORVRfTU9ERUwiOiJleGFtcGxlLWNoYXQiLCJBTlRIUk9QSUNfREVGQVVMVF9PUFVTX01PREVMIjoiZXhhbXBsZS1wcm8ifX0%3D" onclick="copyDeepLink(this.dataset.link, this)"><svg class="i sm"><use href="#i-copy" /></svg><span>复制链接</span></button>
</div>
</article>
<article class="ex">
<div class="ex-head"><span class="chip app" style="--dot: var(--agent-claude)"><i class="d"></i>Claude Code</span><h3>配置文件 + URL 参数覆盖</h3></div>
<p class="ex-desc">配置文件放地址和额外的环境变量,Key 用 URL 参数给出。两边都有时以 URL 参数为准。</p>
<dl class="params">
<dt>必需</dt><dd><code>name</code> <code>config</code></dd>
<dt>可选</dt><dd><code>apiKey</code>(覆盖配置文件里的值)</dd>
</dl>
<figure class="code">
<figcaption>解码后的 config</figcaption>
<pre>{
"env": {
"ANTHROPIC_BASE_URL": "https://api.example.com",
"API_TIMEOUT_MS": "600000"
}
}</pre>
<div class="foot">URL 参数:<code>apiKey=sk-test-key123</code></div>
</figure>
<div class="actions">
<a class="btn solid compact" href="ccswitch://v1/import?resource=provider&amp;app=claude&amp;name=Example%20AI&amp;apiKey=sk-test-key123&amp;configFormat=json&amp;config=eyJlbnYiOnsiQU5USFJPUElDX0JBU0VfVVJMIjoiaHR0cHM6Ly9hcGkuZXhhbXBsZS5jb20iLCJBUElfVElNRU9VVF9NUyI6IjYwMDAwMCJ9fQ%3D%3D"><svg class="i sm"><use href="#i-import" /></svg>在 CC Switch 中打开</a>
<button type="button" class="btn neutral compact" data-link="ccswitch://v1/import?resource=provider&amp;app=claude&amp;name=Example%20AI&amp;apiKey=sk-test-key123&amp;configFormat=json&amp;config=eyJlbnYiOnsiQU5USFJPUElDX0JBU0VfVVJMIjoiaHR0cHM6Ly9hcGkuZXhhbXBsZS5jb20iLCJBUElfVElNRU9VVF9NUyI6IjYwMDAwMCJ9fQ%3D%3D" onclick="copyDeepLink(this.dataset.link, this)"><svg class="i sm"><use href="#i-copy" /></svg><span>复制链接</span></button>
</div>
</article>
<article class="ex">
<div class="ex-head"><span class="chip app" style="--dot: var(--agent-codex)"><i class="d"></i>Codex</span><h3>附带 config.toml</h3></div>
<p class="ex-desc">Codex 的 config 是 <code>{"auth": {...}, "config": "&lt;config.toml 原文&gt;"}</code>。CC Switch 只收选中的那张 <code>[model_providers.&lt;id&gt;]</code> 表和顶层的模型相关字段,导入后表名统一改成 <code>custom</code>。</p>
<dl class="params">
<dt>必需</dt><dd><code>name</code> <code>config</code></dd>
<dt>可选</dt><dd><code>configFormat=json</code>(也可以用 <code>configFormat=toml</code> 直接放 config.toml 原文)</dd>
</dl>
<figure class="code">
<figcaption>auth</figcaption>
<pre>{
"OPENAI_API_KEY": "sk-test-key123"
}</pre>
<figcaption>config(TOML)</figcaption>
<pre>model_provider = "example"
model = "example-coder"
[model_providers.example]
name = "Example AI"
base_url = "https://api.example.com/v1"
wire_api = "responses"</pre>
</figure>
<div class="actions">
<a class="btn solid compact" href="ccswitch://v1/import?resource=provider&amp;app=codex&amp;name=Example%20AI&amp;configFormat=json&amp;config=eyJhdXRoIjp7Ik9QRU5BSV9BUElfS0VZIjoic2stdGVzdC1rZXkxMjMifSwiY29uZmlnIjoibW9kZWxfcHJvdmlkZXIgPSBcImV4YW1wbGVcIlxubW9kZWwgPSBcImV4YW1wbGUtY29kZXJcIlxuXG5bbW9kZWxfcHJvdmlkZXJzLmV4YW1wbGVdXG5uYW1lID0gXCJFeGFtcGxlIEFJXCJcbmJhc2VfdXJsID0gXCJodHRwczovL2FwaS5leGFtcGxlLmNvbS92MVwiXG53aXJlX2FwaSA9IFwicmVzcG9uc2VzXCIifQ%3D%3D"><svg class="i sm"><use href="#i-import" /></svg>在 CC Switch 中打开</a>
<button type="button" class="btn neutral compact" data-link="ccswitch://v1/import?resource=provider&amp;app=codex&amp;name=Example%20AI&amp;configFormat=json&amp;config=eyJhdXRoIjp7Ik9QRU5BSV9BUElfS0VZIjoic2stdGVzdC1rZXkxMjMifSwiY29uZmlnIjoibW9kZWxfcHJvdmlkZXIgPSBcImV4YW1wbGVcIlxubW9kZWwgPSBcImV4YW1wbGUtY29kZXJcIlxuXG5bbW9kZWxfcHJvdmlkZXJzLmV4YW1wbGVdXG5uYW1lID0gXCJFeGFtcGxlIEFJXCJcbmJhc2VfdXJsID0gXCJodHRwczovL2FwaS5leGFtcGxlLmNvbS92MVwiXG53aXJlX2FwaSA9IFwicmVzcG9uc2VzXCIifQ%3D%3D" onclick="copyDeepLink(this.dataset.link, this)"><svg class="i sm"><use href="#i-copy" /></svg><span>复制链接</span></button>
</div>
</article>
<article class="ex">
<div class="ex-head"><span class="chip app" style="--dot: var(--agent-gemini)"><i class="d"></i>Gemini CLI</span><h3>Gemini 配置</h3></div>
<p class="ex-desc">Gemini 的 config 是扁平的键值对,只读 <code>GEMINI_API_KEY</code>、<code>GOOGLE_GEMINI_BASE_URL</code>、<code>GEMINI_MODEL</code> 三个键。</p>
<dl class="params">
<dt>必需</dt><dd><code>name</code> <code>config</code></dd>
<dt>可选</dt><dd><code>configFormat=json</code></dd>
</dl>
<figure class="code">
<figcaption>解码后的 config</figcaption>
<pre>{
"GEMINI_API_KEY": "AIzaSy-test-key123",
"GOOGLE_GEMINI_BASE_URL": "https://api.example.com"
}</pre>
</figure>
<div class="actions">
<a class="btn solid compact" href="ccswitch://v1/import?resource=provider&amp;app=gemini&amp;name=Example%20Gemini&amp;configFormat=json&amp;config=eyJHRU1JTklfQVBJX0tFWSI6IkFJemFTeS10ZXN0LWtleTEyMyIsIkdPT0dMRV9HRU1JTklfQkFTRV9VUkwiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSJ9"><svg class="i sm"><use href="#i-import" /></svg>在 CC Switch 中打开</a>
<button type="button" class="btn neutral compact" data-link="ccswitch://v1/import?resource=provider&amp;app=gemini&amp;name=Example%20Gemini&amp;configFormat=json&amp;config=eyJHRU1JTklfQVBJX0tFWSI6IkFJemFTeS10ZXN0LWtleTEyMyIsIkdPT0dMRV9HRU1JTklfQkFTRV9VUkwiOiJodHRwczovL2FwaS5leGFtcGxlLmNvbSJ9" onclick="copyDeepLink(this.dataset.link, this)"><svg class="i sm"><use href="#i-copy" /></svg><span>复制链接</span></button>
</div>
</article>
<article class="ex">
<div class="ex-head"><span class="chip app" style="--dot: var(--agent-opencode)"><i class="d"></i>OpenCode</span><h3>用 apiFormat 声明 Anthropic 格式</h3></div>
<p class="ex-desc">OpenCode、OpenClaw、Hermes、Pi 自己支持多种接口格式,<code>apiFormat</code> 会换算成它们配置里的协议字段,客户端直接用这种格式请求接口。不写时用 OpenAI Chat Completions。</p>
<dl class="params">
<dt>必需</dt><dd><code>name</code> <code>endpoint</code> <code>apiKey</code></dd>
<dt>可选</dt><dd><code>apiFormat</code> <code>model</code>(OpenCode / OpenClaw / Hermes 不写就没有可选的模型)</dd>
</dl>
<figure class="code">
<figcaption>链接参数</figcaption>
<pre>app=opencode
endpoint=https://api.example.com/v1
apiFormat=anthropic → OpenCode 的 npm = @ai-sdk/anthropic
model=example-chat</pre>
</figure>
<p class="ex-note"><svg class="i sm"><use href="#i-alert" /></svg><span>需要 v4.0.10 及以上。更早的版本会忽略 apiFormat,按 Chat Completions 导入。</span></p>
<div class="actions">
<a class="btn solid compact" href="ccswitch://v1/import?resource=provider&amp;app=opencode&amp;name=Example%20AI&amp;endpoint=https%3A%2F%2Fapi.example.com%2Fv1&amp;apiKey=sk-test-key123&amp;apiFormat=anthropic&amp;model=example-chat"><svg class="i sm"><use href="#i-import" /></svg>在 CC Switch 中打开</a>
<button type="button" class="btn neutral compact" data-link="ccswitch://v1/import?resource=provider&amp;app=opencode&amp;name=Example%20AI&amp;endpoint=https%3A%2F%2Fapi.example.com%2Fv1&amp;apiKey=sk-test-key123&amp;apiFormat=anthropic&amp;model=example-chat" onclick="copyDeepLink(this.dataset.link, this)"><svg class="i sm"><use href="#i-copy" /></svg><span>复制链接</span></button>
</div>
</article>
<article class="ex">
<div class="ex-head"><span class="chip app" style="--dot: var(--agent-pi)"><i class="d"></i>Pi</span><h3>Pi 供应商(必须写模型)</h3></div>
<p class="ex-desc">Pi 的供应商节点必须带模型,链接里没写 <code>model</code> 会被拒绝。<code>apiFormat</code> 换算成 Pi 的 <code>api</code> 字段。</p>
<dl class="params">
<dt>必需</dt><dd><code>name</code> <code>endpoint</code> <code>apiKey</code> <code>model</code></dd>
<dt>可选</dt><dd><code>apiFormat</code></dd>
</dl>
<figure class="code">
<figcaption>链接参数</figcaption>
<pre>app=pi
endpoint=https://api.example.com/v1
apiFormat=openai_chat → Pi 的 api = openai-completions
model=example-chat</pre>
</figure>
<p class="ex-note"><svg class="i sm"><use href="#i-alert" /></svg><span>需要 v4.0.10 及以上,更早的版本会拒绝导入。</span></p>
<div class="actions">
<a class="btn solid compact" href="ccswitch://v1/import?resource=provider&amp;app=pi&amp;name=Example%20AI&amp;endpoint=https%3A%2F%2Fapi.example.com%2Fv1&amp;apiKey=sk-test-key123&amp;apiFormat=openai_chat&amp;model=example-chat"><svg class="i sm"><use href="#i-import" /></svg>在 CC Switch 中打开</a>
<button type="button" class="btn neutral compact" data-link="ccswitch://v1/import?resource=provider&amp;app=pi&amp;name=Example%20AI&amp;endpoint=https%3A%2F%2Fapi.example.com%2Fv1&amp;apiKey=sk-test-key123&amp;apiFormat=openai_chat&amp;model=example-chat" onclick="copyDeepLink(this.dataset.link, this)"><svg class="i sm"><use href="#i-copy" /></svg><span>复制链接</span></button>
</div>
</article>
<article class="ex">
<div class="ex-head"><span class="chip app" style="--dot: var(--agent-claude)"><i class="d"></i>Claude Code</span><h3>带上自己的图标</h3></div>
<p class="ex-desc">内置图标里没有你的品牌时,用 <code>iconUrl</code> 给一个图片地址。确认框一打开就下载,导入后作为卡片图标;下载失败时退回 <code>icon</code>,没写 <code>icon</code> 就显示名称首字母。这条示例借用 CC Switch 自己的图标。</p>
<dl class="params">
<dt>必需</dt><dd><code>name</code> <code>endpoint</code> <code>apiKey</code></dd>
<dt>可选</dt><dd><code>iconUrl</code> <code>icon</code>(下载失败时的退路)</dd>
</dl>
<figure class="code">
<figcaption>链接参数</figcaption>
<pre>iconUrl=https://raw.githubusercontent.com/farion1231/cc-switch/main/src-tauri/icons/128x128.png</pre>
</figure>
<p class="ex-note"><svg class="i sm"><use href="#i-alert" /></svg><span>需要 v4.0.10 及以上,更早的版本会忽略 iconUrl。图片会被重新画成 96×96 的 PNG 存进 CC Switch,之后不再访问这个地址。</span></p>
<div class="actions">
<a class="btn solid compact" href="ccswitch://v1/import?resource=provider&amp;app=claude&amp;name=Example%20AI&amp;endpoint=https%3A%2F%2Fapi.example.com&amp;apiKey=sk-test-key123&amp;iconUrl=https%3A%2F%2Fraw.githubusercontent.com%2Ffarion1231%2Fcc-switch%2Fmain%2Fsrc-tauri%2Ficons%2F128x128.png"><svg class="i sm"><use href="#i-import" /></svg>在 CC Switch 中打开</a>
<button type="button" class="btn neutral compact" data-link="ccswitch://v1/import?resource=provider&amp;app=claude&amp;name=Example%20AI&amp;endpoint=https%3A%2F%2Fapi.example.com&amp;apiKey=sk-test-key123&amp;iconUrl=https%3A%2F%2Fraw.githubusercontent.com%2Ffarion1231%2Fcc-switch%2Fmain%2Fsrc-tauri%2Ficons%2F128x128.png" onclick="copyDeepLink(this.dataset.link, this)"><svg class="i sm"><use href="#i-copy" /></svg><span>复制链接</span></button>
</div>
</article>
</div>
</section>
<!-- 用量查询脚本示例 -->
<section id="ex-usage">
<h2>用量查询脚本示例</h2>
<p class="lede">让用户导入后,直接在供应商卡片上看到余额。</p>
<ul class="rules">
<li>脚本是一段 JavaScript 表达式,返回 <code>{ request, extractor }</code>:<code>request</code> 描述发什么请求,<code>extractor(response)</code> 从响应 JSON 里取数。请求由 CC Switch 代发,脚本本身没有网络能力。</li>
<li>占位符:<code>{{apiKey}}</code>(<code>usageApiKey</code>,没给时用供应商 Key)、<code>{{baseUrl}}</code>(<code>usageBaseUrl</code>,没给时用供应商地址)、<code>{{accessToken}}</code>、<code>{{userId}}</code>。</li>
<li><strong>每次只发一个请求,必须是 HTTPS,且与 <code>{{baseUrl}}</code> 同源</strong>;响应必须是 JSON。余额接口在另一个域名时,用 <code>usageBaseUrl</code> 指过去。</li>
<li><code>extractor</code> 返回对象或对象数组,字段都可选:<code>remaining</code>、<code>used</code>、<code>total</code>(数字)、<code>unit</code>、<code>planName</code>、<code>extra</code>(字符串)、<code>isValid</code>(布尔)、<code>invalidMessage</code>(字符串)。<strong>数字字段如果接口返回的是字符串,要先用 <code>Number()</code> 转。</strong></li>
<li>带了 <code>usageScript</code> 就默认启用(v4.0.10 起);要兼容更早的版本,同时写上 <code>usageEnabled=true</code>。<code>usageAutoInterval</code> 是自动查询间隔(分钟),别设得太短。</li>
</ul>
<div class="ex-list" style="margin-top: 20px;">
<article class="ex">
<div class="ex-head"><span class="chip app" style="--dot: var(--agent-claude)"><i class="d"></i>Claude Code</span><h3>通用余额接口</h3></div>
<p class="ex-desc">用 API Key 查余额。按你的接口改 <code>url</code> 和 <code>extractor</code> 里的字段名即可。Key 失效时返回 <code>isValid: false</code>,卡片会显示失效原因。</p>
<dl class="params">
<dt>必需</dt><dd><code>usageScript</code>(Base64,推荐 URL-safe 写法)</dd>
<dt>可选</dt><dd><code>usageEnabled</code> <code>usageApiKey</code> <code>usageBaseUrl</code> <code>usageAutoInterval</code></dd>
</dl>
<figure class="code">
<figcaption>查询脚本(链接里是它的 Base64)</figcaption>
<pre>({
request: {
url: "{{baseUrl}}/user/balance",
method: "GET",
headers: {
"Authorization": "Bearer {{apiKey}}"
}
},
extractor: function (response) {
if (response.error) {
return { isValid: false, invalidMessage: String(response.error.message || response.error) };
}
return {
remaining: Number(response.balance),
unit: "USD"
};
}
})</pre>
<div class="foot">假设接口返回 <code>{"balance": 12.5}</code>,卡片显示剩余 12.5 USD。<code>{{baseUrl}}</code> 是供应商地址原样,地址带 <code>/v1</code> 时拼出来也带。</div>
</figure>
<div class="actions">
<a class="btn solid compact" href="ccswitch://v1/import?resource=provider&amp;app=claude&amp;name=Example+AI&amp;endpoint=https%3A%2F%2Fapi.example.com&amp;apiKey=sk-test-key123&amp;usageScript=KHsKICByZXF1ZXN0OiB7CiAgICB1cmw6ICJ7e2Jhc2VVcmx9fS91c2VyL2JhbGFuY2UiLAogICAgbWV0aG9kOiAiR0VUIiwKICAgIGhlYWRlcnM6IHsKICAgICAgIkF1dGhvcml6YXRpb24iOiAiQmVhcmVyIHt7YXBpS2V5fX0iCiAgICB9CiAgfSwKICBleHRyYWN0b3I6IGZ1bmN0aW9uIChyZXNwb25zZSkgewogICAgaWYgKHJlc3BvbnNlLmVycm9yKSB7CiAgICAgIHJldHVybiB7IGlzVmFsaWQ6IGZhbHNlLCBpbnZhbGlkTWVzc2FnZTogU3RyaW5nKHJlc3BvbnNlLmVycm9yLm1lc3NhZ2UgfHwgcmVzcG9uc2UuZXJyb3IpIH07CiAgICB9CiAgICByZXR1cm4gewogICAgICByZW1haW5pbmc6IE51bWJlcihyZXNwb25zZS5iYWxhbmNlKSwKICAgICAgdW5pdDogIlVTRCIKICAgIH07CiAgfQp9KQ"><svg class="i sm"><use href="#i-import" /></svg>在 CC Switch 中打开</a>
<button type="button" class="btn neutral compact" data-link="ccswitch://v1/import?resource=provider&amp;app=claude&amp;name=Example+AI&amp;endpoint=https%3A%2F%2Fapi.example.com&amp;apiKey=sk-test-key123&amp;usageScript=KHsKICByZXF1ZXN0OiB7CiAgICB1cmw6ICJ7e2Jhc2VVcmx9fS91c2VyL2JhbGFuY2UiLAogICAgbWV0aG9kOiAiR0VUIiwKICAgIGhlYWRlcnM6IHsKICAgICAgIkF1dGhvcml6YXRpb24iOiAiQmVhcmVyIHt7YXBpS2V5fX0iCiAgICB9CiAgfSwKICBleHRyYWN0b3I6IGZ1bmN0aW9uIChyZXNwb25zZSkgewogICAgaWYgKHJlc3BvbnNlLmVycm9yKSB7CiAgICAgIHJldHVybiB7IGlzVmFsaWQ6IGZhbHNlLCBpbnZhbGlkTWVzc2FnZTogU3RyaW5nKHJlc3BvbnNlLmVycm9yLm1lc3NhZ2UgfHwgcmVzcG9uc2UuZXJyb3IpIH07CiAgICB9CiAgICByZXR1cm4gewogICAgICByZW1haW5pbmc6IE51bWJlcihyZXNwb25zZS5iYWxhbmNlKSwKICAgICAgdW5pdDogIlVTRCIKICAgIH07CiAgfQp9KQ" onclick="copyDeepLink(this.dataset.link, this)"><svg class="i sm"><use href="#i-copy" /></svg><span>复制链接</span></button>
</div>
</article>
<article class="ex">
<div class="ex-head"><span class="chip app" style="--dot: var(--agent-codex)"><i class="d"></i>Codex</span><h3>NewAPI / OneAPI 系的站</h3></div>
<p class="ex-desc">这类站的余额接口用系统访问令牌和用户 ID,不用 API Key。供应商地址带 <code>/v1</code>,余额接口在根路径下,所以用 <code>usageBaseUrl</code> 指向根地址。</p>
<dl class="params">
<dt>必需</dt><dd><code>usageScript</code> <code>usageAccessToken</code> <code>usageUserId</code></dd>
<dt>可选</dt><dd><code>usageBaseUrl</code>(这里是 https://api.example.com)<code>usageAutoInterval</code>(这里是 30)</dd>
</dl>
<figure class="code">
<figcaption>查询脚本(链接里是它的 Base64)</figcaption>
<pre>({
request: {
url: "{{baseUrl}}/api/user/self",
method: "GET",
headers: {
"Authorization": "Bearer {{accessToken}}",
"New-Api-User": "{{userId}}"
}
},
extractor: function (response) {
if (response.success &amp;&amp; response.data) {
return {
planName: response.data.group,
remaining: response.data.quota / 500000,
used: response.data.used_quota / 500000,
unit: "USD"
};
}
return { isValid: false, invalidMessage: String(response.message || "查询失败") };
}
})</pre>
<div class="foot"><code>500000</code> 是 NewAPI 默认的额度换算比例,改过的话按自己的值写。系统访问令牌的权限比 API Key 大,只在用户本人登录后生成的链接里放。</div>
</figure>
<div class="actions">
<a class="btn solid compact" href="ccswitch://v1/import?resource=provider&amp;app=codex&amp;name=Example+AI&amp;endpoint=https%3A%2F%2Fapi.example.com%2Fv1&amp;apiKey=sk-test-key123&amp;usageScript=KHsKICByZXF1ZXN0OiB7CiAgICB1cmw6ICJ7e2Jhc2VVcmx9fS9hcGkvdXNlci9zZWxmIiwKICAgIG1ldGhvZDogIkdFVCIsCiAgICBoZWFkZXJzOiB7CiAgICAgICJBdXRob3JpemF0aW9uIjogIkJlYXJlciB7e2FjY2Vzc1Rva2VufX0iLAogICAgICAiTmV3LUFwaS1Vc2VyIjogInt7dXNlcklkfX0iCiAgICB9CiAgfSwKICBleHRyYWN0b3I6IGZ1bmN0aW9uIChyZXNwb25zZSkgewogICAgaWYgKHJlc3BvbnNlLnN1Y2Nlc3MgJiYgcmVzcG9uc2UuZGF0YSkgewogICAgICByZXR1cm4gewogICAgICAgIHBsYW5OYW1lOiByZXNwb25zZS5kYXRhLmdyb3VwLAogICAgICAgIHJlbWFpbmluZzogcmVzcG9uc2UuZGF0YS5xdW90YSAvIDUwMDAwMCwKICAgICAgICB1c2VkOiByZXNwb25zZS5kYXRhLnVzZWRfcXVvdGEgLyA1MDAwMDAsCiAgICAgICAgdW5pdDogIlVTRCIKICAgICAgfTsKICAgIH0KICAgIHJldHVybiB7IGlzVmFsaWQ6IGZhbHNlLCBpbnZhbGlkTWVzc2FnZTogU3RyaW5nKHJlc3BvbnNlLm1lc3NhZ2UgfHwgIuafpeivouWksei0pSIpIH07CiAgfQp9KQ&amp;usageBaseUrl=https%3A%2F%2Fapi.example.com&amp;usageAccessToken=test-access-token&amp;usageUserId=1&amp;usageAutoInterval=30"><svg class="i sm"><use href="#i-import" /></svg>在 CC Switch 中打开</a>
<button type="button" class="btn neutral compact" data-link="ccswitch://v1/import?resource=provider&amp;app=codex&amp;name=Example+AI&amp;endpoint=https%3A%2F%2Fapi.example.com%2Fv1&amp;apiKey=sk-test-key123&amp;usageScript=KHsKICByZXF1ZXN0OiB7CiAgICB1cmw6ICJ7e2Jhc2VVcmx9fS9hcGkvdXNlci9zZWxmIiwKICAgIG1ldGhvZDogIkdFVCIsCiAgICBoZWFkZXJzOiB7CiAgICAgICJBdXRob3JpemF0aW9uIjogIkJlYXJlciB7e2FjY2Vzc1Rva2VufX0iLAogICAgICAiTmV3LUFwaS1Vc2VyIjogInt7dXNlcklkfX0iCiAgICB9CiAgfSwKICBleHRyYWN0b3I6IGZ1bmN0aW9uIChyZXNwb25zZSkgewogICAgaWYgKHJlc3BvbnNlLnN1Y2Nlc3MgJiYgcmVzcG9uc2UuZGF0YSkgewogICAgICByZXR1cm4gewogICAgICAgIHBsYW5OYW1lOiByZXNwb25zZS5kYXRhLmdyb3VwLAogICAgICAgIHJlbWFpbmluZzogcmVzcG9uc2UuZGF0YS5xdW90YSAvIDUwMDAwMCwKICAgICAgICB1c2VkOiByZXNwb25zZS5kYXRhLnVzZWRfcXVvdGEgLyA1MDAwMDAsCiAgICAgICAgdW5pdDogIlVTRCIKICAgICAgfTsKICAgIH0KICAgIHJldHVybiB7IGlzVmFsaWQ6IGZhbHNlLCBpbnZhbGlkTWVzc2FnZTogU3RyaW5nKHJlc3BvbnNlLm1lc3NhZ2UgfHwgIuafpeivouWksei0pSIpIH07CiAgfQp9KQ&amp;usageBaseUrl=https%3A%2F%2Fapi.example.com&amp;usageAccessToken=test-access-token&amp;usageUserId=1&amp;usageAutoInterval=30" onclick="copyDeepLink(this.dataset.link, this)"><svg class="i sm"><use href="#i-copy" /></svg><span>复制链接</span></button>
</div>
</article>
<article class="ex">
<div class="ex-head"><span class="chip app" style="--dot: var(--agent-claude)"><i class="d"></i>Claude Code</span><h3>Sub2API 系的站</h3></div>
<p class="ex-desc">Sub2API 自带给 CC Switch 用的 <code>/v1/usage</code> 接口,用 API Key 认证,不需要访问令牌。钱包余额、订阅分组、Key 单独设的额度三种情况都返回 <code>remaining</code>,脚本按原样取出来。</p>
<dl class="params">
<dt>必需</dt><dd><code>usageScript</code></dd>
<dt>可选</dt><dd><code>usageAutoInterval</code>(这里是 30)</dd>
</dl>
<figure class="code">
<figcaption>查询脚本(链接里是它的 Base64)</figcaption>
<pre>({
request: {
url: "{{baseUrl}}".replace(/\/+$/, "").replace(/\/v1$/, "") + "/v1/usage",
method: "GET",
headers: {
"Authorization": "Bearer {{apiKey}}",
"User-Agent": "cc-switch/1.0"
}
},
extractor: function (response) {
var quota = response.quota || {};
var result = {
isValid: response.isValid !== false,
unit: response.unit || quota.unit || "USD"
};
if (typeof response.planName === "string") result.planName = response.planName;
if (typeof response.remaining === "number" &amp;&amp; response.remaining &gt;= 0) {
result.remaining = response.remaining;
}
if (quota.limit &gt; 0) {
result.used = quota.used;
result.total = quota.limit;
}
if (!result.isValid) {
result.invalidMessage = response.status
? String(response.status)
: "查询失败";
}
return result;
},
})</pre>
<div class="foot">Codex、Grok Build 的供应商地址带 <code>/v1</code>,Claude Code 的不带,脚本先去掉末尾的 <code>/v1</code> 再拼接,所以各个应用通用,也不用写 <code>usageBaseUrl</code>。响应里还有 <code>rate_limits</code>(5 小时 / 1 天 / 7 天窗口)和 <code>subscription</code>(日 / 周 / 月限额),想在卡片上分行显示,可以让 <code>extractor</code> 返回数组。</div>
</figure>
<div class="actions">
<a class="btn solid compact" href="ccswitch://v1/import?resource=provider&amp;app=claude&amp;name=Example+AI&amp;endpoint=https%3A%2F%2Fapi.example.com&amp;apiKey=sk-test-key123&amp;usageScript=KHsKICByZXF1ZXN0OiB7CiAgICB1cmw6ICJ7e2Jhc2VVcmx9fSIucmVwbGFjZSgvXC8rJC8sICIiKS5yZXBsYWNlKC9cL3YxJC8sICIiKSArICIvdjEvdXNhZ2UiLAogICAgbWV0aG9kOiAiR0VUIiwKICAgIGhlYWRlcnM6IHsKICAgICAgIkF1dGhvcml6YXRpb24iOiAiQmVhcmVyIHt7YXBpS2V5fX0iLAogICAgICAiVXNlci1BZ2VudCI6ICJjYy1zd2l0Y2gvMS4wIgogICAgfQogIH0sCiAgZXh0cmFjdG9yOiBmdW5jdGlvbiAocmVzcG9uc2UpIHsKICAgIHZhciBxdW90YSA9IHJlc3BvbnNlLnF1b3RhIHx8IHt9OwogICAgdmFyIHJlc3VsdCA9IHsKICAgICAgaXNWYWxpZDogcmVzcG9uc2UuaXNWYWxpZCAhPT0gZmFsc2UsCiAgICAgIHVuaXQ6IHJlc3BvbnNlLnVuaXQgfHwgcXVvdGEudW5pdCB8fCAiVVNEIgogICAgfTsKICAgIGlmICh0eXBlb2YgcmVzcG9uc2UucGxhbk5hbWUgPT09ICJzdHJpbmciKSByZXN1bHQucGxhbk5hbWUgPSByZXNwb25zZS5wbGFuTmFtZTsKICAgIGlmICh0eXBlb2YgcmVzcG9uc2UucmVtYWluaW5nID09PSAibnVtYmVyIiAmJiByZXNwb25zZS5yZW1haW5pbmcgPj0gMCkgewogICAgICByZXN1bHQucmVtYWluaW5nID0gcmVzcG9uc2UucmVtYWluaW5nOwogICAgfQogICAgaWYgKHF1b3RhLmxpbWl0ID4gMCkgewogICAgICByZXN1bHQudXNlZCA9IHF1b3RhLnVzZWQ7CiAgICAgIHJlc3VsdC50b3RhbCA9IHF1b3RhLmxpbWl0OwogICAgfQogICAgaWYgKCFyZXN1bHQuaXNWYWxpZCkgewogICAgICByZXN1bHQuaW52YWxpZE1lc3NhZ2UgPSByZXNwb25zZS5zdGF0dXMKICAgICAgICA%2FIFN0cmluZyhyZXNwb25zZS5zdGF0dXMpCiAgICAgICAgOiAi5p%2Bl6K%2Bi5aSx6LSlIjsKICAgIH0KICAgIHJldHVybiByZXN1bHQ7CiAgfSwKfSk%3D&amp;usageAutoInterval=30"><svg class="i sm"><use href="#i-import" /></svg>在 CC Switch 中打开</a>
<button type="button" class="btn neutral compact" data-link="ccswitch://v1/import?resource=provider&amp;app=claude&amp;name=Example+AI&amp;endpoint=https%3A%2F%2Fapi.example.com&amp;apiKey=sk-test-key123&amp;usageScript=KHsKICByZXF1ZXN0OiB7CiAgICB1cmw6ICJ7e2Jhc2VVcmx9fSIucmVwbGFjZSgvXC8rJC8sICIiKS5yZXBsYWNlKC9cL3YxJC8sICIiKSArICIvdjEvdXNhZ2UiLAogICAgbWV0aG9kOiAiR0VUIiwKICAgIGhlYWRlcnM6IHsKICAgICAgIkF1dGhvcml6YXRpb24iOiAiQmVhcmVyIHt7YXBpS2V5fX0iLAogICAgICAiVXNlci1BZ2VudCI6ICJjYy1zd2l0Y2gvMS4wIgogICAgfQogIH0sCiAgZXh0cmFjdG9yOiBmdW5jdGlvbiAocmVzcG9uc2UpIHsKICAgIHZhciBxdW90YSA9IHJlc3BvbnNlLnF1b3RhIHx8IHt9OwogICAgdmFyIHJlc3VsdCA9IHsKICAgICAgaXNWYWxpZDogcmVzcG9uc2UuaXNWYWxpZCAhPT0gZmFsc2UsCiAgICAgIHVuaXQ6IHJlc3BvbnNlLnVuaXQgfHwgcXVvdGEudW5pdCB8fCAiVVNEIgogICAgfTsKICAgIGlmICh0eXBlb2YgcmVzcG9uc2UucGxhbk5hbWUgPT09ICJzdHJpbmciKSByZXN1bHQucGxhbk5hbWUgPSByZXNwb25zZS5wbGFuTmFtZTsKICAgIGlmICh0eXBlb2YgcmVzcG9uc2UucmVtYWluaW5nID09PSAibnVtYmVyIiAmJiByZXNwb25zZS5yZW1haW5pbmcgPj0gMCkgewogICAgICByZXN1bHQucmVtYWluaW5nID0gcmVzcG9uc2UucmVtYWluaW5nOwogICAgfQogICAgaWYgKHF1b3RhLmxpbWl0ID4gMCkgewogICAgICByZXN1bHQudXNlZCA9IHF1b3RhLnVzZWQ7CiAgICAgIHJlc3VsdC50b3RhbCA9IHF1b3RhLmxpbWl0OwogICAgfQogICAgaWYgKCFyZXN1bHQuaXNWYWxpZCkgewogICAgICByZXN1bHQuaW52YWxpZE1lc3NhZ2UgPSByZXNwb25zZS5zdGF0dXMKICAgICAgICA%2FIFN0cmluZyhyZXNwb25zZS5zdGF0dXMpCiAgICAgICAgOiAi5p%2Bl6K%2Bi5aSx6LSlIjsKICAgIH0KICAgIHJldHVybiByZXN1bHQ7CiAgfSwKfSk%3D&amp;usageAutoInterval=30" onclick="copyDeepLink(this.dataset.link, this)"><svg class="i sm"><use href="#i-copy" /></svg><span>复制链接</span></button>
</div>
</article>
</div>
</section>
<!-- MCP 示例 -->
<section id="ex-mcp">
<details class="fold">
<summary><h2>MCP 示例</h2><span class="fold-hint">和 API 服务关系不大,顺带分发配套工具时再看</span></summary>
<div class="ex-list" style="margin-top: 16px;">
<article class="ex">
<div class="ex-head"><span class="chip app"><i class="d"></i>MCP</span><h3>一次导入多个 MCP 服务器</h3></div>
<p class="ex-desc">导入 Context7 和 Sequential-thinking 两个服务器,并在 Claude Code 和 Codex 里启用。</p>
<dl class="params">
<dt>必需</dt><dd><code>resource=mcp</code> <code>apps</code> <code>config</code>(Base64)</dd>
<dt>apps</dt><dd>claude、codex、gemini、grokbuild、opencode、hermes、pi、mcode(openclaw 会被忽略)</dd>
</dl>
<figure class="code">
<figcaption>解码后的 config</figcaption>
<pre>{
"mcpServers": {
"context7": {
"command": "npx",
"args": ["-y", "@upstash/context7-mcp"],
"env": {}
},
"sequential-thinking": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-sequential-thinking"],
"env": {}
}
}
}</pre>
<div class="foot">已存在同 ID 的服务器:只给它勾上新的应用,不覆盖配置内容。导入即写入客户端配置文件,<code>enabled</code> 参数目前不起作用。</div>
</figure>
<div class="actions">
<a class="btn solid compact" href="ccswitch://v1/import?resource=mcp&amp;apps=claude%2Ccodex&amp;config=eyJtY3BTZXJ2ZXJzIjp7ImNvbnRleHQ3Ijp7ImNvbW1hbmQiOiJucHgiLCJhcmdzIjpbIi15IiwiQHVwc3Rhc2gvY29udGV4dDctbWNwIl0sImVudiI6e319LCJzZXF1ZW50aWFsLXRoaW5raW5nIjp7ImNvbW1hbmQiOiJucHgiLCJhcmdzIjpbIi15IiwiQG1vZGVsY29udGV4dHByb3RvY29sL3NlcnZlci1zZXF1ZW50aWFsLXRoaW5raW5nIl0sImVudiI6e319fX0%3D"><svg class="i sm"><use href="#i-import" /></svg>在 CC Switch 中打开</a>
<button type="button" class="btn neutral compact" data-link="ccswitch://v1/import?resource=mcp&amp;apps=claude%2Ccodex&amp;config=eyJtY3BTZXJ2ZXJzIjp7ImNvbnRleHQ3Ijp7ImNvbW1hbmQiOiJucHgiLCJhcmdzIjpbIi15IiwiQHVwc3Rhc2gvY29udGV4dDctbWNwIl0sImVudiI6e319LCJzZXF1ZW50aWFsLXRoaW5raW5nIjp7ImNvbW1hbmQiOiJucHgiLCJhcmdzIjpbIi15IiwiQG1vZGVsY29udGV4dHByb3RvY29sL3NlcnZlci1zZXF1ZW50aWFsLXRoaW5raW5nIl0sImVudiI6e319fX0%3D" onclick="copyDeepLink(this.dataset.link, this)"><svg class="i sm"><use href="#i-copy" /></svg><span>复制链接</span></button>
</div>
</article>
</div>
</details>
</section>
<!-- 提示词示例 -->
<section id="ex-prompt">
<details class="fold">
<summary><h2>提示词示例</h2><span class="fold-hint">和 API 服务关系不大,顺带分发配套工具时再看</span></summary>
<div class="ex-list" style="margin-top: 16px;">
<article class="ex">
<div class="ex-head"><span class="chip app" style="--dot: var(--agent-claude)"><i class="d"></i>Claude Code</span><h3>代码审查专家</h3></div>
<p class="ex-desc">为 Claude Code 导入一条提示词。确认框里有「导入后立即启用」开关,默认关闭;用户打开后会替换当前的提示词文件(如 CLAUDE.md)。</p>
<dl class="params">
<dt>必需</dt><dd><code>resource=prompt</code> <code>app</code> <code>name</code> <code>content</code>(Base64 Markdown)</dd>
<dt>可选</dt><dd><code>description</code> <code>enabled</code>(开关的初始值,一般不要写)</dd>
</dl>
<figure class="code">
<figcaption>解码后的 content</figcaption>
<pre># 代码审查专家
你是一位有经验的代码审查员。审查代码时,先指出会导致错误的问题,再给出修改建议。</pre>
</figure>
<div class="actions">
<a class="btn solid compact" href="ccswitch://v1/import?resource=prompt&amp;app=claude&amp;name=%E4%BB%A3%E7%A0%81%E5%AE%A1%E6%9F%A5%E4%B8%93%E5%AE%B6&amp;content=IyDku6PnoIHlrqHmn6XkuJPlrrYKCuS9oOaYr%2BS4gOS9jeaciee7j%2BmqjOeahOS7o%2BeggeWuoeafpeWRmOOAguWuoeafpeS7o%2BeggeaXtu%2B8jOWFiOaMh%2BWHuuS8muWvvOiHtOmUmeivr%2BeahOmXrumimO%2B8jOWGjee7meWHuuS%2FruaUueW7uuiuruOAgg%3D%3D&amp;description=%E4%B8%93%E6%B3%A8%E4%BB%A3%E7%A0%81%E8%B4%A8%E9%87%8F"><svg class="i sm"><use href="#i-import" /></svg>在 CC Switch 中打开</a>
<button type="button" class="btn neutral compact" data-link="ccswitch://v1/import?resource=prompt&amp;app=claude&amp;name=%E4%BB%A3%E7%A0%81%E5%AE%A1%E6%9F%A5%E4%B8%93%E5%AE%B6&amp;content=IyDku6PnoIHlrqHmn6XkuJPlrrYKCuS9oOaYr%2BS4gOS9jeaciee7j%2BmqjOeahOS7o%2BeggeWuoeafpeWRmOOAguWuoeafpeS7o%2BeggeaXtu%2B8jOWFiOaMh%2BWHuuS8muWvvOiHtOmUmeivr%2BeahOmXrumimO%2B8jOWGjee7meWHuuS%2FruaUueW7uuiuruOAgg%3D%3D&amp;description=%E4%B8%93%E6%B3%A8%E4%BB%A3%E7%A0%81%E8%B4%A8%E9%87%8F" onclick="copyDeepLink(this.dataset.link, this)"><svg class="i sm"><use href="#i-copy" /></svg><span>复制链接</span></button>
</div>
</article>
</div>
</details>
</section>
<!-- Skill 示例 -->
<section id="ex-skill">
<details class="fold">
<summary><h2>Skill 仓库示例</h2><span class="fold-hint">和 API 服务关系不大,顺带分发配套工具时再看</span></summary>
<div class="ex-list" style="margin-top: 16px;">
<article class="ex">
<div class="ex-head"><span class="chip app"><i class="d"></i>Skills</span><h3>添加 Skill 仓库</h3></div>
<p class="ex-desc">把一个 GitHub 公开仓库加进 Skill 仓库列表。添加的是仓库,用户还要在 Skills 管理界面自己选择安装哪些 Skill。</p>
<dl class="params">
<dt>必需</dt><dd><code>resource=skill</code> <code>repo</code>(owner/name)</dd>
<dt>可选</dt><dd><code>branch</code>(默认 main)</dd>
</dl>
<figure class="code">
<figcaption>链接参数</figcaption>
<pre>repo=example/claude-skills
branch=main</pre>
</figure>
<div class="actions">
<a class="btn solid compact" href="ccswitch://v1/import?resource=skill&amp;repo=example%2Fclaude-skills&amp;branch=main"><svg class="i sm"><use href="#i-import" /></svg>在 CC Switch 中打开</a>
<button type="button" class="btn neutral compact" data-link="ccswitch://v1/import?resource=skill&amp;repo=example%2Fclaude-skills&amp;branch=main" onclick="copyDeepLink(this.dataset.link, this)"><svg class="i sm"><use href="#i-copy" /></svg><span>复制链接</span></button>
</div>
</article>
</div>
</details>
</section>
<!-- 常见问题 -->
<section id="faq">
<h2>常见问题</h2>
<dl class="faq">
<dt>怎么做成我网站上的按钮</dt>
<dd>链接里有用户的 Key,不要直接写进按钮的 <code>href</code>。用户点击时,前端向你的后端要这个用户的链接,拿到后用 <code>location.href = link</code> 跳转;按钮旁写一句「点了没反应?先安装 CC Switch」并附上下载地址。一条链接只导入到一个应用,同时支持 Claude Code、Codex 等几个应用时,每个应用放一个按钮。后端接口和前端按钮的完整代码见 <a href="https://github.com/farion1231/cc-switch/blob/main/docs/guides/deeplink-site-integration-guide-zh.md#把链接放到按钮上" target="_blank" rel="noopener">站长接入指南</a>。</dd>
<dt>点了没反应</dt>
<dd>第一次点击时浏览器会询问是否打开 CC Switch,选「允许」或「打开」。macOS 上如果仍然打不开,到「系统设置 → 隐私与安全性」里允许 CC Switch。</dd>
<dt>导入时会发生什么</dt>
<dd>CC Switch 弹出确认框,API Key 打码显示。「导入后立即切换 / 添加 / 启用」由用户在确认框里的开关决定,链接只给初始值。</dd>
<dt>导入之后</dt>
<dd>可以在 CC Switch 里随时编辑或删除导入的内容。</dd>
<dt>iconUrl 的图标没显示</dt>
<dd>CC Switch 下载失败时不报错,直接退回内置图标或名称首字母。常见原因:地址要登录或开了防盗链、响应头 <code>Content-Type</code> 写成了 <code>application/octet-stream</code>、图片超过 256 KB、域名解析到内网地址。用 <code>curl -I</code> 看一下响应头,或者在 CC Switch 的日志里搜 <code>Skipped deep link icon</code>。</dd>
<dt>示例能直接用吗</dt>
<dd>不能。示例里的域名和 Key 都是假的,只用来演示格式。</dd>
</dl>
</section>
</main>
</div>
<div id="toaster" aria-live="polite"></div>
<script>
// UTF-8 字符串转 Base64
function utf8_to_b64(str) {
try {
const bytes = new TextEncoder().encode(str);
const binString = Array.from(bytes, (byte) => String.fromCharCode(byte)).join("");
return btoa(binString);
} catch (e) {
console.error("Base64 encode error:", e);
return window.btoa(unescape(encodeURIComponent(str)));
}
}
// Base64 转 UTF-8 字符串
function b64_to_utf8(str) {
try {
const binString = atob(str);
const bytes = Uint8Array.from(binString, (m) => m.codePointAt(0));
return new TextDecoder().decode(bytes);
} catch (e) {
console.error("Base64 decode error:", e);
return decodeURIComponent(escape(window.atob(str)));
}
}
// 各应用的默认接口格式和可选的 apiFormat(与 src-tauri/src/deeplink/provider.rs 的 ApiFormat 一致)
const API_FORMAT_LABELS = {
anthropic: 'Anthropic Messages',
openai_chat: 'OpenAI Chat Completions',
openai_responses: 'OpenAI Responses',
gemini_native: 'Gemini Native'
};
const ALL_FORMATS = ['anthropic', 'openai_chat', 'openai_responses', 'gemini_native'];
const APP_FORMATS = {
claude: { default: 'anthropic', allowed: ALL_FORMATS },
codex: { default: 'openai_responses', allowed: ['openai_responses', 'openai_chat', 'anthropic'] },
gemini: { default: 'gemini_native', allowed: ['gemini_native'] },
grokbuild: { default: 'openai_responses', allowed: ['openai_responses', 'openai_chat', 'anthropic'] },
opencode: { default: 'openai_chat', allowed: ALL_FORMATS },
openclaw: { default: 'openai_chat', allowed: ALL_FORMATS },
hermes: { default: 'openai_chat', allowed: ['openai_chat', 'openai_responses', 'anthropic'] },
pi: { default: 'openai_chat', allowed: ALL_FORMATS }
};
// 与 src-tauri/src/deeplink/parser.rs 的 icon 白名单一致,其他写法会被丢掉
const ICON_NAME_PATTERN = /^[a-z0-9._-]{1,64}$/;
// 与 src-tauri/src/deeplink/icon_fetch.rs 的 parse_icon_url 一致:形状不对的地址在解析时就被丢掉。
// 返回问题描述,合格时返回空字符串。下载阶段的检查(公网地址、大小、类型)这里做不了
function iconUrlProblem(raw) {
if (raw.length > 2048) return '超过 2048 个字符';
let url;
try {
url = new URL(raw);
} catch (e) {
return '不是有效的网址';
}
if (url.protocol !== 'https:') return '必须以 https:// 开头';
if (url.username || url.password) return '不能带账号密码';
if (url.port) return '不能带端口号';
const host = url.hostname.replace(/\.$/, '');
if (host.startsWith('[') || /^\d+\.\d+\.\d+\.\d+$/.test(host)) return '的主机必须是域名,不能是 IP';
if (!host.includes('.') || host === 'localhost' || host.endsWith('.localhost')) return '的主机必须是公开域名';
return '';
}
// 用量查询脚本模板(与页面上「用量查询脚本示例」一致)
const USAGE_TEMPLATES = {
generic: "({\n request: {\n url: \"{{baseUrl}}/user/balance\",\n method: \"GET\",\n headers: {\n \"Authorization\": \"Bearer {{apiKey}}\"\n }\n },\n extractor: function (response) {\n if (response.error) {\n return { isValid: false, invalidMessage: String(response.error.message || response.error) };\n }\n return {\n remaining: Number(response.balance),\n unit: \"USD\"\n };\n }\n})",
newapi: "({\n request: {\n url: \"{{baseUrl}}/api/user/self\",\n method: \"GET\",\n headers: {\n \"Authorization\": \"Bearer {{accessToken}}\",\n \"New-Api-User\": \"{{userId}}\"\n }\n },\n extractor: function (response) {\n if (response.success && response.data) {\n return {\n planName: response.data.group,\n remaining: response.data.quota / 500000,\n used: response.data.used_quota / 500000,\n unit: \"USD\"\n };\n }\n return { isValid: false, invalidMessage: String(response.message || \"查询失败\") };\n }\n})",
sub2api: "({\n request: {\n url: \"{{baseUrl}}\".replace(/\\/+$/, \"\").replace(/\\/v1$/, \"\") + \"/v1/usage\",\n method: \"GET\",\n headers: {\n \"Authorization\": \"Bearer {{apiKey}}\",\n \"User-Agent\": \"cc-switch/1.0\"\n }\n },\n extractor: function (response) {\n var quota = response.quota || {};\n var result = {\n isValid: response.isValid !== false,\n unit: response.unit || quota.unit || \"USD\"\n };\n if (typeof response.planName === \"string\") result.planName = response.planName;\n if (typeof response.remaining === \"number\" && response.remaining >= 0) {\n result.remaining = response.remaining;\n }\n if (quota.limit > 0) {\n result.used = quota.used;\n result.total = quota.limit;\n }\n if (!result.isValid) {\n result.invalidMessage = response.status\n ? String(response.status)\n : \"查询失败\";\n }\n return result;\n },\n})"
};
function fillUsageTemplate(kind) {
const textarea = document.getElementById('usageScript');
if (textarea.value.trim() && !confirm('替换当前的脚本?')) return;
textarea.value = USAGE_TEMPLATES[kind];
if (kind === 'newapi') {
notify('NewAPI 模板还需要填写下方的 Access Token(系统访问令牌)和 User ID;供应商地址带 /v1 时,把「查询地址」填成站点根地址。', 'info');
}
}
const ADDITIVE_APPS = ['opencode', 'openclaw', 'hermes', 'pi'];
const ROUTED_APPS = ['claude', 'codex', 'grokbuild'];
// 累加式应用读配置时会展开的写法:链接里带了会被 CC Switch 拒绝,这里提前提示
function findExpansionSyntax(app, values) {
for (const [field, value] of Object.entries(values)) {
if (!value) continue;
if (value.includes('${') || value.includes('{env:') || value.includes('{file:')) {
return field;
}
if (app === 'pi') {
if (field === 'API Key' && value.trimStart().startsWith('!')) return field;
if (/\$[A-Za-z_]/.test(value)) return field;
}
}
return null;
}
// 解析器把链接内容插进页面前先转义
function esc(value) {
return String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
// 切换导入模式
function toggleImportMode() {
const mode = document.getElementById('importMode').value;
const urlFields = document.getElementById('urlModeFields');
const configFields = document.getElementById('configModeFields');
if (mode === 'url') {
urlFields.style.display = 'block';
configFields.style.display = 'none';
} else {
urlFields.style.display = 'none';
configFields.style.display = 'block';
// 按当前应用切换 JSON / TOML 输入组,并填入示例配置
updateModelFields();
}
}
// 根据应用类型填充配置模板
function populateConfigTemplate() {
const app = document.getElementById('app').value;
const configTextarea = document.getElementById('configJson');
const codexAuthTextarea = document.getElementById('codexAuthJson');
const codexConfigTextarea = document.getElementById('codexConfigToml');
let template = '';
if (app === 'claude') {
template = `{
"env": {
"ANTHROPIC_AUTH_TOKEN": "sk-your-api-key-here",
"ANTHROPIC_BASE_URL": "https://api.example.com"
}
}`;
configTextarea.value = template;
} else if (app === 'codex') {
// Codex 分开填充
codexAuthTextarea.value = `{
"auth": {
"OPENAI_API_KEY": "sk-your-api-key-here"
}
}`;
codexConfigTextarea.value = `model_provider = "example"
model = "example-coder"
model_reasoning_effort = "high"
[model_providers.example]
name = "Example AI"
base_url = "https://api.example.com/v1"
wire_api = "responses"`;
} else if (app === 'grokbuild') {
codexConfigTextarea.value = `[models]
default = "example-grok"
[model."example-grok"]
model = "example-grok"
name = "Example Grok"
base_url = "https://api.example.com/v1"
api_key = "sk-your-api-key-here"
api_backend = "responses"`;
} else if (app === 'gemini') {
template = `{
"GEMINI_API_KEY": "AIzaSy-your-api-key-here",
"GOOGLE_GEMINI_BASE_URL": "https://api.example.com"
}`;
configTextarea.value = template;
} else {
// OpenCode / OpenClaw / Hermes / Pi:config 只取地址和 Key
template = `{
"baseUrl": "https://api.example.com/v1",
"apiKey": "sk-your-api-key-here"
}`;
configTextarea.value = template;
}
}
// 更新模型字段显示
function updateModelFields() {
const app = document.getElementById('app').value;
const claudeFields = document.getElementById('claudeFields');
const generalConfigGroup = document.getElementById('generalConfigGroup');
const codexConfigGroup = document.getElementById('codexConfigGroup');
const mode = document.getElementById('importMode').value;
// Claude 字段显示控制
if (app === 'claude') {
claudeFields.style.display = 'block';
} else {
claudeFields.style.display = 'none';
}
// 接口格式:只列这个应用支持的
const formats = APP_FORMATS[app];
const formatSelect = document.getElementById('apiFormat');
const previous = formatSelect.value;
formatSelect.innerHTML = `<option value="">不写(默认 ${API_FORMAT_LABELS[formats.default]})</option>` +
formats.allowed.map(f => `<option value="${f}">${f}(${API_FORMAT_LABELS[f]})</option>`).join('');
formatSelect.value = formats.allowed.includes(previous) ? previous : '';
let formatHint = '需要 v4.0.10 及以上,更早的版本会忽略这个参数。';
if (ROUTED_APPS.includes(app)) {
formatHint += '选了非原生格式,导入的卡片会带「需要路由」标记,用户要开启本地路由才能用。';
} else if (ADDITIVE_APPS.includes(app)) {
formatHint += '会换算成该应用配置里的协议字段,客户端直接用这种格式请求接口。';
}
document.getElementById('apiFormatHint').textContent = formatHint;
// 模型字段说明
const modelHints = {
claude: '原版 Claude 模型留空;非原生模型写上,并填下面的三档',
codex: '不写时用 gpt-5-codex',
gemini: '可选',
grokbuild: '不写时用 grok-4.5',
opencode: '不写就没有可选的模型',
openclaw: '不写就没有可选的模型',
hermes: '不写就没有可选的模型',
pi: '必填(v4.0.10 起支持 Pi),没写会被拒绝'
};
document.getElementById('modelLabel').textContent = app === 'pi' ? '默认模型 *' : '默认模型(可选)';
document.getElementById('modelHint').textContent = modelHints[app];
// 配置文件输入框控制
if (mode === 'config') {
if (app === 'codex' || app === 'grokbuild') {
generalConfigGroup.style.display = 'none';
codexConfigGroup.style.display = 'block';
document.getElementById('codexAuthGroup').style.display = app === 'codex' ? 'block' : 'none';
document.getElementById('tomlConfigLabel').textContent =
app === 'codex' ? 'Codex 配置文件 (TOML)' : 'Grok Build 配置文件 (TOML)';
document.getElementById('tomlConfigHint').textContent = app === 'codex'
? 'config.toml 的原始内容。只收选中的那张 [model_providers.<id>] 表和顶层的模型相关字段'
: '只导入 [models] default 指向的那张 [model."<名称>"] 表;没写 default 时文件里只能有一张表';
} else {
generalConfigGroup.style.display = 'block';
codexConfigGroup.style.display = 'none';
}
populateConfigTemplate();
}
}
function generateLink() {
const mode = document.getElementById('importMode').value;
const app = document.getElementById('app').value;
const name = document.getElementById('name').value.trim();
// 验证必填字段(只有名称是必填的)
if (!name) {
notify('请填写供应商名称');
return;
}
// 构建基础参数
const params = new URLSearchParams({
resource: 'provider',
app: app,
name: name
});
if (mode === 'url') {
// URL 参数模式
const homepage = document.getElementById('homepage').value.trim();
const endpoint = document.getElementById('endpoint').value.trim();
const apiKey = document.getElementById('apiKey').value.trim();
const model = document.getElementById('model').value.trim();
const enabled = document.getElementById('enabled').value;
const notes = document.getElementById('notes').value.trim();
// Claude 专用字段
const haikuModel = document.getElementById('haikuModel').value.trim();
const sonnetModel = document.getElementById('sonnetModel').value.trim();
const opusModel = document.getElementById('opusModel').value.trim();
// URL 模式下,至少需要 endpoint 和 apiKey
if (!endpoint || !apiKey) {
notify('用 URL 参数填写时,API 地址和 API Key 必填');
return;
}
// 验证 URL 格式
if (homepage) {
try {
new URL(homepage);
} catch (e) {
notify('官网地址要以 http:// 或 https:// 开头');
return;
}
}
try {
new URL(endpoint);
} catch (e) {
notify('API 地址要以 http:// 或 https:// 开头');
return;
}
// 添加参数
if (homepage) params.append('homepage', homepage);
// 合并主端点和备用端点
const extraEndpoints = document.getElementById('extraEndpoints').value.trim();
let fullEndpoint = endpoint;
if (extraEndpoints) {
const extras = extraEndpoints.split(',').map(e => e.trim()).filter(e => e);
if (extras.length > 0) {
fullEndpoint = endpoint + ',' + extras.join(',');
}
}
params.append('endpoint', fullEndpoint);
params.append('apiKey', apiKey);
if (model) params.append('model', model);
if (enabled) params.append('enabled', enabled);
if (notes) params.append('notes', notes);
// 添加 Claude 专用模型字段
if (app === 'claude') {
if (haikuModel) params.append('haikuModel', haikuModel);
if (sonnetModel) params.append('sonnetModel', sonnetModel);
if (opusModel) params.append('opusModel', opusModel);
}
} else {
// 配置文件模式
let configJson = '';
if (app === 'codex') {
// Codex 特殊处理:合并 Auth JSON 和 Config TOML
const authJson = document.getElementById('codexAuthJson').value.trim();
const configToml = document.getElementById('codexConfigToml').value.trim();
if (!authJson) {
notify('请填写 Codex 认证信息 (JSON)');
return;
}
if (!configToml) {
notify('请填写 Codex 配置文件 (TOML)');
return;
}
try {
const authObj = JSON.parse(authJson);
// 构造最终对象
const finalObj = {
...authObj,
config: configToml
};
configJson = JSON.stringify(finalObj);
} catch (e) {
notify('Codex 认证信息不是有效的 JSON 格式:' + e.message);
return;
}
} else if (app === 'grokbuild') {
// Grok Build:{"config": "<config.toml 原文>"}
const configToml = document.getElementById('codexConfigToml').value.trim();
if (!configToml) {
notify('请填写 Grok Build 配置文件 (TOML)');
return;
}
configJson = JSON.stringify({ config: configToml });
} else {
// 其他应用使用通用 JSON 输入框
configJson = document.getElementById('configJson').value.trim();
if (!configJson) {
notify('配置文件模式下,请填写配置文件内容');
return;
}
// 验证 JSON 格式
try {
JSON.parse(configJson);
} catch (e) {
notify('配置文件不是有效的 JSON 格式:' + e.message);
return;
}
}
const overrideApiKey = document.getElementById('overrideApiKey').value.trim();
const overrideEndpoint = document.getElementById('overrideEndpoint').value.trim();
const model = document.getElementById('model').value.trim();
const enabled = document.getElementById('enabled').value;
const notes = document.getElementById('notes').value.trim();
// Claude 专用字段
const haikuModel = document.getElementById('haikuModel').value.trim();
const sonnetModel = document.getElementById('sonnetModel').value.trim();
const opusModel = document.getElementById('opusModel').value.trim();
// Base64 编码配置文件
const configB64 = utf8_to_b64(configJson);
params.append('config', configB64);
params.append('configFormat', 'json');
// 添加覆盖参数
if (overrideApiKey) params.append('apiKey', overrideApiKey);
if (overrideEndpoint) {
try {
new URL(overrideEndpoint);
params.append('endpoint', overrideEndpoint);
} catch (e) {
notify('覆盖用的 API 地址格式不对');
return;
}
}
if (model) params.append('model', model);
if (enabled) params.append('enabled', enabled);
if (notes) params.append('notes', notes);
// 添加 Claude 专用模型字段
if (app === 'claude') {
if (haikuModel) params.append('haikuModel', haikuModel);
if (sonnetModel) params.append('sonnetModel', sonnetModel);
if (opusModel) params.append('opusModel', opusModel);
}
}
// 图标:两种填写方式共用
const icon = document.getElementById('icon').value.trim();
const iconUrl = document.getElementById('iconUrl').value.trim();
if (icon) {
if (!ICON_NAME_PATTERN.test(icon)) {
notify('内置图标名只能是小写字母、数字和 . _ -,CC Switch 会丢掉其他写法');
return;
}
params.append('icon', icon);
}
if (iconUrl) {
const problem = iconUrlProblem(iconUrl);
if (problem) {
notify('图标地址' + problem);
return;
}
params.append('iconUrl', iconUrl);
}
// 接口格式
const apiFormat = document.getElementById('apiFormat').value;
if (apiFormat) params.append('apiFormat', apiFormat);
// Pi 的供应商节点必须带模型
if (app === 'pi' && !params.get('model')) {
notify('Pi 供应商必须填写默认模型');
return;
}
// 累加式应用:拒绝客户端会展开的写法(命令、环境变量、本机文件)
if (ADDITIVE_APPS.includes(app)) {
const values = {
'供应商名称': name,
'API 端点': params.get('endpoint'),
'API Key': params.get('apiKey'),
'默认模型': params.get('model')
};
if (mode === 'config') {
try {
const cfg = JSON.parse(document.getElementById('configJson').value);
values['配置文件里的地址'] = cfg.baseUrl || cfg.base_url || (cfg.options && cfg.options.baseURL);
if (!values['API Key']) values['API Key'] = cfg.apiKey || cfg.api_key;
} catch (e) { /* 上面已校验过 JSON */ }
}
const field = findExpansionSyntax(app, values);
if (field) {
notify(`「${field}」里有会被 ${app} 展开成命令、环境变量或本机文件的写法(如 \${…}、{env:…}、{file:…}${app === 'pi' ? '、$NAME、以 ! 开头的 Key' : ''}),CC Switch 会拒绝导入这条链接。`);
return;
}
}
// 添加用量查询参数 (v3.9+)
const usageEnabled = document.getElementById('usageEnabled').value;
const usageBaseUrl = document.getElementById('usageBaseUrl').value.trim();
const usageApiKey = document.getElementById('usageApiKey').value.trim();
const usageScript = document.getElementById('usageScript').value.trim();
const usageAutoInterval = document.getElementById('usageAutoInterval').value.trim();
const usageAccessToken = document.getElementById('usageAccessToken').value.trim();
const usageUserId = document.getElementById('usageUserId').value.trim();
if (usageEnabled) params.append('usageEnabled', usageEnabled);
if (usageBaseUrl) params.append('usageBaseUrl', usageBaseUrl);
if (usageApiKey) params.append('usageApiKey', usageApiKey);
if (usageScript) {
// URL-safe Base64 编码
const scriptB64 = utf8_to_b64(usageScript)
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '');
params.append('usageScript', scriptB64);
}
if (usageAutoInterval) params.append('usageAutoInterval', usageAutoInterval);
if (usageAccessToken) params.append('usageAccessToken', usageAccessToken);
if (usageUserId) params.append('usageUserId', usageUserId);
const deepLink = `ccswitch://v1/import?${params.toString()}`;
// 显示结果
document.getElementById('linkText').textContent = deepLink;
document.getElementById('testLink').href = deepLink;
document.getElementById('result').style.display = 'block';
// 滚动到结果区域
document.getElementById('result').scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
}
// Base64 编码功能
function encodeToBase64() {
const input = document.getElementById('encodeInput').value;
if (!input.trim()) {
notify('请输入要编码的内容');
return;
}
try {
const encoded = utf8_to_b64(input);
document.getElementById('encodeOutput').textContent = encoded;
document.getElementById('encodeResult').style.display = 'block';
// 滚动到结果
document.getElementById('encodeResult').scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
} catch (e) {
notify('编码失败:' + e.message);
console.error('Encode error:', e);
}
}
// Base64 解码功能
function decodeFromBase64() {
const input = document.getElementById('decodeInput').value.trim();
if (!input) {
notify('请输入要解码的 Base64 内容');
return;
}
try {
const decoded = b64_to_utf8(input);
document.getElementById('decodeOutput').textContent = decoded;
document.getElementById('decodeResult').style.display = 'block';
// 检查是否是 JSON,如果是则显示格式化按钮
try {
JSON.parse(decoded);
document.getElementById('jsonFormat').style.display = 'block';
} catch {
document.getElementById('jsonFormat').style.display = 'none';
}
// 滚动到结果
document.getElementById('decodeResult').scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
} catch (e) {
notify('解码失败:' + e.message + '。请确认输入的是 Base64');
console.error('Decode error:', e);
}
}
// 格式化 JSON
function formatJson() {
try {
const text = document.getElementById('decodeOutput').textContent;
const obj = JSON.parse(text);
const formatted = JSON.stringify(obj, null, 2);
document.getElementById('decodeOutput').textContent = formatted;
} catch (e) {
notify('JSON 格式化失败:' + e.message);
}
}
// ==================== 深链接生成器函数 ====================
// 生成 MCP 深链接
function generateMcpLink() {
const apps = document.getElementById('mcpApps').value.trim();
const config = document.getElementById('mcpConfig').value.trim();
if (!apps) {
notify('请填写目标应用');
return;
}
if (!config) {
notify('请填写 MCP 配置');
return;
}
try {
// 验证 JSON 格式
const jsonObj = JSON.parse(config);
if (!jsonObj.mcpServers) {
notify('配置必须包含 mcpServers 字段');
return;
}
// Base64 编码配置
const configB64 = utf8_to_b64(config);
// 构建深链接
const url = `ccswitch://v1/import?resource=mcp&apps=${encodeURIComponent(apps)}&config=${encodeURIComponent(configB64)}`;
// 显示结果
document.getElementById('mcpUrl').textContent = url;
document.getElementById('mcpImportBtn').href = url;
document.getElementById('mcpResult').style.display = 'block';
// 滚动到结果
document.getElementById('mcpResult').scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
} catch (e) {
notify('JSON 格式错误:' + e.message);
}
}
// 生成 Prompt 深链接
function generatePromptLink() {
const app = document.getElementById('promptApp').value;
const name = document.getElementById('promptName').value.trim();
const content = document.getElementById('promptContent').value.trim();
const description = document.getElementById('promptDescription').value.trim();
const enabled = document.getElementById('promptEnabled').value;
if (!name) {
notify('请填写提示词名称');
return;
}
if (!content) {
notify('请填写提示词内容');
return;
}
// Base64 编码内容
const contentB64 = utf8_to_b64(content);
// 构建深链接
let url = `ccswitch://v1/import?resource=prompt&app=${app}&name=${encodeURIComponent(name)}&content=${encodeURIComponent(contentB64)}`;
if (description) {
url += `&description=${encodeURIComponent(description)}`;
}
if (enabled === 'true') {
url += '&enabled=true';
}
// 显示结果
document.getElementById('promptUrl').textContent = url;
document.getElementById('promptImportBtn').href = url;
document.getElementById('promptResult').style.display = 'block';
// 滚动到结果
document.getElementById('promptResult').scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
}
// 生成 Skill 深链接
function generateSkillLink() {
const repo = document.getElementById('skillRepo').value.trim();
const branch = document.getElementById('skillBranch').value.trim() || 'main';
if (!repo) {
notify('请填写 GitHub 仓库');
return;
}
// 验证仓库格式
if (!repo.includes('/')) {
notify('仓库格式应为: owner/repo-name');
return;
}
// 构建深链接
const url = `ccswitch://v1/import?resource=skill&repo=${encodeURIComponent(repo)}&branch=${encodeURIComponent(branch)}`;
// 显示结果
document.getElementById('skillUrl').textContent = url;
document.getElementById('skillImportBtn').href = url;
document.getElementById('skillResult').style.display = 'block';
// 滚动到结果
document.getElementById('skillResult').scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
}
// 选中文本(点击 URL 时)
function selectText(element) {
const range = document.createRange();
range.selectNodeContents(element);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
}
// ---------- 提示与复制 ----------
function notify(message, kind = 'error') {
const toaster = document.getElementById('toaster');
const toast = document.createElement('div');
toast.className = 'toast' + (kind === 'info' ? ' info' : '');
toast.innerHTML = `<svg class="i"><use href="#${kind === 'info' ? 'i-info' : 'i-x'}" /></svg><div></div>`;
toast.lastChild.textContent = message;
toaster.appendChild(toast);
setTimeout(() => toast.remove(), kind === 'info' ? 7000 : 5000);
}
function copyText(text, button) {
navigator.clipboard.writeText(text).then(() => {
if (!button) return;
const label = button.querySelector('span') || button;
if (!button.dataset.label) button.dataset.label = label.textContent;
label.textContent = '已复制';
button.classList.add('copied');
clearTimeout(button._copiedTimer);
button._copiedTimer = setTimeout(() => {
label.textContent = button.dataset.label;
button.classList.remove('copied');
}, 1600);
}).catch(err => {
console.error('复制失败:', err);
notify('复制失败,请手动选中复制');
});
}
// 示例卡片上的「复制链接」
function copyDeepLink(url, button) {
copyText(url, button);
}
// 深链接解析器
function parseDeepLink() {
const urlInput = document.getElementById('parseUrl').value.trim();
if (!urlInput) {
notify('请先粘贴一条深链接');
return;
}
try {
const url = new URL(urlInput);
if (url.protocol !== 'ccswitch:') {
notify('不是深链接:必须以 ccswitch:// 开头');
return;
}
const version = url.hostname;
const path = url.pathname;
const params = new URLSearchParams(url.search);
const paramsObj = {};
params.forEach((value, key) => {
paramsObj[key] = value;
});
// 一行键值;value 已转义成 HTML
const row = (key, valueHtml, cls = '') =>
`<dt class="${cls}">${esc(key)}</dt><dd class="${cls}">${valueHtml}</dd>`;
const group = (title, rows) => rows
? `<div class="pgroup"><h4>${esc(title)}</h4><dl class="kv">${rows}</dl></div>`
: '';
const mask = (value) => esc(String(value).substring(0, 10)) + '****';
const flag = (text, cls = '') => `<span class="flag ${cls}">${esc(text)}</span>`;
// 基本信息
const basicInfoHtml = group('基本信息',
row('协议版本', esc(version)) +
row('路径', esc(path)) +
row('资源类型', esc(paramsObj.resource || '-')) +
row('应用', esc(paramsObj.app || paramsObj.apps || '-')) +
row('名称', `<span class="plain">${esc(paramsObj.name || '-')}</span>`));
// URL 参数
let paramRows = '';
const regularParams = ['homepage', 'apiKey', 'apiFormat', 'model', 'icon', 'iconUrl', 'notes', 'enabled', 'description', 'repo', 'branch'];
regularParams.forEach(key => {
if (!paramsObj[key]) return;
const value = paramsObj[key];
let shown = key === 'apiKey' ? mask(value) : esc(value);
let cls = '';
if (key === 'apiFormat' && paramsObj.app) {
const formats = APP_FORMATS[paramsObj.app];
if (!API_FORMAT_LABELS[value]) {
shown += flag('不认识的取值,会被拒绝', 'danger');
cls = 'danger';
} else if (formats && !formats.allowed.includes(value)) {
shown += flag(`${paramsObj.app} 不支持这种格式,会被拒绝`, 'danger');
cls = 'danger';
} else {
shown += flag(`${API_FORMAT_LABELS[value]},v4.0.10 起生效`);
}
}
if (key === 'enabled') {
shown += flag('确认框开关的初始值');
}
if (key === 'icon' && !ICON_NAME_PATTERN.test(value)) {
shown += flag('不是内置图标名的写法,会被丢掉', 'warn');
cls = 'warn';
}
if (key === 'iconUrl') {
const problem = iconUrlProblem(value);
if (problem) {
shown += flag('地址' + problem + ',会被丢掉', 'warn');
cls = 'warn';
} else {
shown += flag('打开确认框时下载,v4.0.10 起生效');
}
}
paramRows += row(key, shown, cls);
});
if (paramsObj.resource === 'provider' && paramsObj.app === 'pi' && !paramsObj.model) {
paramRows += row('model', `<span class="plain">Pi 供应商没写模型,会被拒绝</span>`, 'danger');
}
['skills_path', 'directory'].forEach(key => {
if (paramsObj[key]) {
paramRows += row(key, esc(paramsObj[key]) + flag('CC Switch 会忽略这个参数'));
}
});
// endpoint 支持逗号分隔的多个地址,第一个是主地址
if (paramsObj.endpoint) {
const endpoints = paramsObj.endpoint.split(',').map(e => e.trim()).filter(e => e);
if (endpoints.length === 1) {
paramRows += row('endpoint', esc(endpoints[0]));
} else {
const list = endpoints.map((ep, idx) =>
`<span class="sub">${esc(ep)}${flag(idx === 0 ? '主地址' : `备用 ${idx}`)}</span>`).join('');
paramRows += row('endpoint', list);
}
}
['haikuModel', 'sonnetModel', 'opusModel'].forEach(key => {
if (paramsObj[key]) paramRows += row(key, esc(paramsObj[key]));
});
const urlParamsHtml = group('URL 参数', paramRows);
// 配置文件
let configHtml = '';
if (paramsObj.config) {
try {
// 兼容没转义的 + 被当成空格、URL-safe 写法
let configB64 = paramsObj.config.replace(/ /g, '+').replace(/-/g, '+').replace(/_/g, '/');
while (configB64.length % 4) configB64 += '=';
const decoded = b64_to_utf8(configB64);
const isToml = (paramsObj.configFormat || '').toLowerCase() === 'toml';
const configObj = isToml ? null : JSON.parse(decoded);
const codeBlock = (caption, text) =>
`<figure class="code"><figcaption>${esc(caption)}</figcaption><pre>${esc(text)}</pre></figure>`;
const kvRows = (obj) => Object.keys(obj || {}).map(key => {
let value = obj[key];
if (typeof value !== 'string') value = JSON.stringify(value);
return row(key, /TOKEN|KEY|SECRET/i.test(key) ? mask(value) : esc(value));
}).join('');
let body = '';
if (isToml) {
body = codeBlock('config.toml', decoded);
} else if (paramsObj.resource === 'mcp') {
body = codeBlock('mcpServers', JSON.stringify(configObj.mcpServers || configObj, null, 2));
} else if (paramsObj.app === 'claude') {
body = `<dl class="kv">${kvRows(configObj.env || {}) || row('env', '<span class="plain">(空)</span>')}</dl>`;
} else if (paramsObj.app === 'codex' || paramsObj.app === 'grokbuild') {
if (configObj.auth) body += `<dl class="kv">${kvRows(configObj.auth)}</dl>`;
if (typeof configObj.config === 'string') body += codeBlock('config.toml', configObj.config);
} else {
body = `<dl class="kv">${kvRows(configObj)}</dl>`;
}
if (configObj) {
body += `<details class="raw"><summary>查看原始 JSON</summary>${codeBlock('config', JSON.stringify(configObj, null, 2))}</details>`;
}
configHtml = `<div class="pgroup"><h4>配置文件(${isToml ? 'TOML' : 'JSON'})</h4>${body}</div>`;
} catch (e) {
configHtml = `<div class="perror">配置文件解析失败:${esc(e.message)}</div>`;
}
}
// 提示词内容
if (paramsObj.resource === 'prompt' && paramsObj.content) {
try {
const content = b64_to_utf8(paramsObj.content.replace(/ /g, '+'));
configHtml += `<div class="pgroup"><h4>提示词内容</h4><figure class="code" style="margin-top: 0;"><pre>${esc(content)}</pre></figure></div>`;
} catch (e) {
configHtml += `<div class="perror">提示词内容解码失败:${esc(e.message)}</div>`;
}
}
// 用量查询
let usageHtml = '';
if (paramsObj.usageEnabled || paramsObj.usageScript || paramsObj.usageBaseUrl) {
let usageState;
if (paramsObj.usageEnabled === 'true') {
usageState = flag('启用', 'ok');
} else if (paramsObj.usageEnabled === 'false') {
usageState = flag('不启用');
} else if (paramsObj.usageScript) {
usageState = flag('启用', 'ok') + flag('没写 usageEnabled,带脚本默认启用(v4.0.10 起;更早的版本不启用)');
} else {
usageState = flag('不启用(没带脚本)');
}
let usageRows = row('状态', usageState);
if (paramsObj.usageBaseUrl) usageRows += row('usageBaseUrl', esc(paramsObj.usageBaseUrl));
if (paramsObj.usageApiKey) usageRows += row('usageApiKey', mask(paramsObj.usageApiKey));
if (paramsObj.usageAutoInterval) usageRows += row('usageAutoInterval', esc(paramsObj.usageAutoInterval) + flag('分钟'));
if (paramsObj.usageAccessToken) usageRows += row('usageAccessToken', mask(paramsObj.usageAccessToken));
if (paramsObj.usageUserId) usageRows += row('usageUserId', esc(paramsObj.usageUserId));
let scriptHtml = '';
if (paramsObj.usageScript) {
try {
// URL-safe Base64,补齐 padding
let scriptB64 = paramsObj.usageScript.replace(/-/g, '+').replace(/_/g, '/');
while (scriptB64.length % 4) scriptB64 += '=';
const scriptContent = b64_to_utf8(scriptB64);
scriptHtml = `<figure class="code"><figcaption>查询脚本</figcaption><pre>${esc(scriptContent)}</pre></figure>`;
} catch (e) {
scriptHtml = `<div class="perror">脚本解码失败:${esc(e.message)}</div>`;
}
}
usageHtml = `<div class="pgroup"><h4>用量查询</h4><dl class="kv">${usageRows}</dl>${scriptHtml}</div>`;
}
document.getElementById('parseBasicInfo').innerHTML = basicInfoHtml;
document.getElementById('parseUrlParams').innerHTML = urlParamsHtml;
document.getElementById('parseConfigContent').innerHTML = configHtml + usageHtml;
document.getElementById('parseResult').style.display = 'block';
document.getElementById('parseResult').scrollIntoView({ behavior: 'smooth', block: 'nearest' });
} catch (e) {
notify('深链接解析失败:' + e.message);
console.error('Parse error:', e);
}
}
// ---------- 页面交互 ----------
function setTheme(value) {
if (value === 'system') {
document.documentElement.removeAttribute('data-theme');
} else {
document.documentElement.setAttribute('data-theme', value);
}
document.querySelectorAll('#themeSeg button').forEach(b => {
b.setAttribute('aria-pressed', String(b.dataset.themeValue === value));
});
try { localStorage.setItem('ccswitch-deeplink-theme', value); } catch (e) { /* 存不了就只对本次生效 */ }
}
function showGenerator(panelId) {
document.querySelectorAll('.gen-panel').forEach(p => {
p.style.display = p.id === panelId ? 'block' : 'none';
});
document.querySelectorAll('#genSeg button').forEach(b => {
b.setAttribute('aria-pressed', String(b.dataset.panel === panelId));
});
}
function setImportMode(mode) {
document.getElementById('importMode').value = mode;
document.querySelectorAll('#modeSeg button').forEach(b => {
b.setAttribute('aria-pressed', String(b.dataset.mode === mode));
});
toggleImportMode();
}
document.addEventListener('DOMContentLoaded', function () {
let savedTheme = 'system';
try { savedTheme = localStorage.getItem('ccswitch-deeplink-theme') || 'system'; } catch (e) { /* 忽略 */ }
setTheme(savedTheme);
document.querySelectorAll('#themeSeg button').forEach(b => {
b.addEventListener('click', () => setTheme(b.dataset.themeValue));
});
document.querySelectorAll('#genSeg button').forEach(b => {
b.addEventListener('click', () => showGenerator(b.dataset.panel));
});
document.querySelectorAll('#modeSeg button').forEach(b => {
b.addEventListener('click', () => setImportMode(b.dataset.mode));
});
// 单行输入框里按回车:触发所在面板的生成按钮
document.querySelectorAll('input').forEach(input => {
input.addEventListener('keydown', function (e) {
if (e.key !== 'Enter' || e.isComposing) return;
const panel = e.target.closest('[data-generate]');
if (!panel) return;
e.preventDefault();
window[panel.dataset.generate]();
});
});
// 侧栏高亮当前所在的一节
const links = Array.from(document.querySelectorAll('.rail nav a'));
const sections = links.map(a => document.querySelector(a.getAttribute('href'))).filter(Boolean);
const markActive = () => {
let current = sections[0];
sections.forEach(s => {
if (s.getBoundingClientRect().top <= 120) current = s;
});
if (window.innerHeight + window.scrollY >= document.body.scrollHeight - 4) {
current = sections[sections.length - 1];
}
links.forEach(a => a.classList.toggle('on', a.getAttribute('href') === '#' + current.id));
// 窄屏下侧栏变成横向导航条,把当前项滚进可见范围
const rail = document.querySelector('.rail');
const active = rail.querySelector('nav a.on');
if (active && rail.scrollWidth > rail.clientWidth) {
const left = active.offsetLeft - rail.clientWidth / 2 + active.offsetWidth / 2;
rail.scrollTo({ left: Math.max(0, left) });
}
};
// 折叠的节:从侧栏或带锚点的地址跳过来时自动展开
const openFold = (hash) => {
const fold = hash && hash.length > 1 && document.querySelector(hash + ' > details.fold');
if (fold) fold.open = true;
};
links.forEach(a => a.addEventListener('click', () => openFold(a.getAttribute('href'))));
window.addEventListener('hashchange', () => openFold(location.hash));
openFold(location.hash);
window.addEventListener('scroll', markActive, { passive: true });
markActive();
updateModelFields();
});
</script>
</body>
</html>