@cc-x/cc-x 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.en.md +339 -0
- package/README.md +349 -0
- package/dist/actions.js +33 -0
- package/dist/config/presets.js +104 -0
- package/dist/config/store.js +235 -0
- package/dist/config/types.js +16 -0
- package/dist/env/default.js +42 -0
- package/dist/env/persist-unix.js +72 -0
- package/dist/env/persist-windows.js +49 -0
- package/dist/env/session.js +44 -0
- package/dist/i18n/index.js +53 -0
- package/dist/i18n/messages.js +143 -0
- package/dist/index.js +139 -0
- package/dist/ui/edit.js +149 -0
- package/dist/ui/format.js +21 -0
- package/dist/ui/menus.js +133 -0
- package/dist/ui/pickers.js +93 -0
- package/dist/ui/select.js +152 -0
- package/dist/ui/text.js +102 -0
- package/dist/utils/ansi.js +41 -0
- package/dist/utils/display.js +28 -0
- package/package.json +56 -0
- package/presets.json +36 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* cc-x —— Claude Code API 切换器(命令:xx)
|
|
4
|
+
*
|
|
5
|
+
* 入口:解析 CLI 参数 → 分派到 CLI 路径(--list / xx <name> / -s)或交互菜单(TUI)。
|
|
6
|
+
*
|
|
7
|
+
* 铁律:绝不写 Claude Code 配置文件;API 切换只动 7 个受管环境变量。详见 CLAUDE.md / plan §2。
|
|
8
|
+
*/
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
10
|
+
import { Command, Option } from 'commander';
|
|
11
|
+
import { launchSession, warnIfNoKey } from './actions.js';
|
|
12
|
+
import { loadStore, peekStoreLang, resolveStorePaths, StoreError } from './config/store.js';
|
|
13
|
+
import { loadPresets } from './config/presets.js';
|
|
14
|
+
import { setDefault } from './env/default.js';
|
|
15
|
+
import { providerDisplayName, resolveLang, setLang, T } from './i18n/index.js';
|
|
16
|
+
import { noteSuffix, stateLabel } from './ui/format.js';
|
|
17
|
+
import { openMenu } from './ui/menus.js';
|
|
18
|
+
import { padDisplay } from './utils/display.js';
|
|
19
|
+
const require = createRequire(import.meta.url);
|
|
20
|
+
const pkg = require('../package.json');
|
|
21
|
+
/** 从 argv 里轻量预读一个带值参数(`--name v` 或 `--name=v`),用于 parse 前定语言。 */
|
|
22
|
+
function peekArg(argv, name) {
|
|
23
|
+
for (let i = 0; i < argv.length; i++) {
|
|
24
|
+
const a = argv[i];
|
|
25
|
+
if (a === name)
|
|
26
|
+
return argv[i + 1];
|
|
27
|
+
if (a && a.startsWith(`${name}=`))
|
|
28
|
+
return a.slice(name.length + 1);
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
function main() {
|
|
33
|
+
// help/version 在 commander parse 时就打印,故须**在建命令前**定好语言:
|
|
34
|
+
// --lang > providers.json 的 lang(只读探测,不生成文件)> 环境 > 默认 zh。
|
|
35
|
+
const argv = process.argv;
|
|
36
|
+
const earlyLang = peekArg(argv, '--lang');
|
|
37
|
+
const lang = earlyLang === 'en' ? 'en' : earlyLang === 'zh' ? 'zh' : undefined;
|
|
38
|
+
const storeLang = peekStoreLang(resolveStorePaths(peekArg(argv, '--store-dir')));
|
|
39
|
+
setLang(resolveLang(lang, storeLang));
|
|
40
|
+
const program = new Command();
|
|
41
|
+
program
|
|
42
|
+
.name('xx')
|
|
43
|
+
.description(T('cli.desc'))
|
|
44
|
+
.helpOption('-h, --help', T('cli.opt.help'))
|
|
45
|
+
.version(pkg.version, '-v, --version', T('cli.opt.version'))
|
|
46
|
+
.argument('[name]', T('cli.arg.name'))
|
|
47
|
+
.option('-s, --session', T('cli.opt.session'))
|
|
48
|
+
.option('-l, --list', T('cli.opt.list'))
|
|
49
|
+
.option('--store-dir <dir>', T('cli.opt.storeDir'))
|
|
50
|
+
.addOption(
|
|
51
|
+
// [P1] 严格校验:拼错(如 proces)直接报错退出,不静默回退到危险的持久化路径。
|
|
52
|
+
new Option('--default-scope <scope>', T('cli.opt.defaultScope')).choices(['user', 'process']).default('user'))
|
|
53
|
+
.addOption(new Option('--lang <lang>', T('cli.opt.lang')).choices(['zh', 'en']))
|
|
54
|
+
.action(async (name, raw) => {
|
|
55
|
+
await dispatch(name, normalizeOpts(raw));
|
|
56
|
+
});
|
|
57
|
+
void program.parseAsync();
|
|
58
|
+
}
|
|
59
|
+
function normalizeOpts(raw) {
|
|
60
|
+
const scope = raw.defaultScope === 'process' ? 'process' : 'user';
|
|
61
|
+
const lang = raw.lang === 'en' ? 'en' : raw.lang === 'zh' ? 'zh' : undefined;
|
|
62
|
+
return { ...raw, defaultScope: scope, lang };
|
|
63
|
+
}
|
|
64
|
+
/** 按参数把请求分派到对应路径。 */
|
|
65
|
+
async function dispatch(name, opts) {
|
|
66
|
+
const paths = resolveStorePaths(opts.storeDir);
|
|
67
|
+
let store;
|
|
68
|
+
try {
|
|
69
|
+
store = loadStore(paths);
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
if (e instanceof StoreError) {
|
|
73
|
+
// store 不可用,按 --lang/环境定语言(拿不到 store.lang)。绝不重建、不动用户数据。
|
|
74
|
+
setLang(resolveLang(opts.lang));
|
|
75
|
+
const head = e.kind === 'read' ? 'error.storeRead' : e.kind === 'format' ? 'error.storeFormat' : 'error.storeCorrupt';
|
|
76
|
+
console.error(` ${T(head, e.file)}`);
|
|
77
|
+
console.error(` ${T('error.storeCorruptHint')}`);
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
throw e;
|
|
82
|
+
}
|
|
83
|
+
// 语言:--lang > providers.json lang > 环境 > 默认 zh。在产出任何文案前先定好。
|
|
84
|
+
setLang(resolveLang(opts.lang, store.lang));
|
|
85
|
+
if (opts.list) {
|
|
86
|
+
runList(store);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (name) {
|
|
90
|
+
const target = store.providers.find((p) => p.name === name);
|
|
91
|
+
if (!target) {
|
|
92
|
+
console.error(` ${T('error.notFound', name)}`);
|
|
93
|
+
console.error(` ${T('error.existing', store.providers.map((p) => p.name).join(', '))}`);
|
|
94
|
+
process.exitCode = 1;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (opts.session)
|
|
98
|
+
launchSession(target);
|
|
99
|
+
else
|
|
100
|
+
runDefault(paths, store, target, opts.defaultScope);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
await openMenu(paths, store, opts.defaultScope, pkg.version, loadPresets(opts.storeDir));
|
|
104
|
+
}
|
|
105
|
+
/** `--list`:列出所有配置及状态。官方档显示名走 i18n(评审①),其余原样。 */
|
|
106
|
+
function runList(store) {
|
|
107
|
+
const cur = store.providers.find((p) => p.name === store.current);
|
|
108
|
+
console.log('');
|
|
109
|
+
console.log(` ${T('list.default', cur ? providerDisplayName(cur) : store.current)}`);
|
|
110
|
+
for (const p of store.providers) {
|
|
111
|
+
const mark = p.name === store.current ? '▶' : ' ';
|
|
112
|
+
console.log(` ${mark} ${padDisplay(providerDisplayName(p), 18)}[${stateLabel(p)}]${noteSuffix(p)}`);
|
|
113
|
+
}
|
|
114
|
+
console.log('');
|
|
115
|
+
}
|
|
116
|
+
/** 设为默认:写用户环境变量(或 dry-run)+ 更新 store.current。 */
|
|
117
|
+
function runDefault(paths, store, p, scope) {
|
|
118
|
+
warnIfNoKey(p);
|
|
119
|
+
const name = providerDisplayName(p);
|
|
120
|
+
const r = setDefault(paths, store, p, scope);
|
|
121
|
+
if (r.dryRun) {
|
|
122
|
+
console.log(` ${T('default.done', name)}`);
|
|
123
|
+
console.log(` ${T('default.dryRun')}`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (r.windows && !r.windows.ok) {
|
|
127
|
+
console.error(` ${T('default.failed', r.windows.error ?? '')}`);
|
|
128
|
+
process.exitCode = 1;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (r.unix?.unsupported) {
|
|
132
|
+
console.error(` ${T('default.fishUnsupported')}`);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
console.log(` ${T('default.done', name)}`);
|
|
136
|
+
if (r.unix)
|
|
137
|
+
console.log(` ${T('default.unixWrote', r.unix.file)}`);
|
|
138
|
+
}
|
|
139
|
+
main();
|
package/dist/ui/edit.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 三级 · 编辑表单(对齐现版 Edit-Form):一屏显示全部字段,选序号改单项。
|
|
3
|
+
*
|
|
4
|
+
* 新需求(plan §7):密钥行默认掩码 `********`,提供「👁 显示/隐藏密钥明文」开关——
|
|
5
|
+
* 仅影响本表单的**显示**,不改数据、不持久化;默认隐藏防肩窥。输入态(readValue secret)另算。
|
|
6
|
+
*/
|
|
7
|
+
import { buildProviderEnv, getProviderEnvMap, reconcileBuiltin, resolveUniqueName, } from '../config/store.js';
|
|
8
|
+
import { T } from '../i18n/index.js';
|
|
9
|
+
import { pickAuth, pickBaseUrl, pickEffort, pickProvider, pickProviderUrl } from './pickers.js';
|
|
10
|
+
import { selectMenu } from './select.js';
|
|
11
|
+
import { readText, readValue } from './text.js';
|
|
12
|
+
function fromProvider(p) {
|
|
13
|
+
const m = getProviderEnvMap(p);
|
|
14
|
+
const usesApiKey = Boolean(m.ANTHROPIC_API_KEY && m.ANTHROPIC_API_KEY.trim());
|
|
15
|
+
return {
|
|
16
|
+
name: p.name,
|
|
17
|
+
note: p.note ?? '',
|
|
18
|
+
base: m.ANTHROPIC_BASE_URL ?? '',
|
|
19
|
+
auth: usesApiKey ? 'API_KEY' : 'AUTH_TOKEN',
|
|
20
|
+
token: (usesApiKey ? m.ANTHROPIC_API_KEY : m.ANTHROPIC_AUTH_TOKEN) ?? '',
|
|
21
|
+
opus: m.ANTHROPIC_DEFAULT_OPUS_MODEL ?? '',
|
|
22
|
+
sonnet: m.ANTHROPIC_DEFAULT_SONNET_MODEL ?? '',
|
|
23
|
+
haiku: m.ANTHROPIC_DEFAULT_HAIKU_MODEL ?? '',
|
|
24
|
+
effort: m.CLAUDE_CODE_EFFORT_LEVEL ?? '',
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** 编辑 `prov`(就地修改);保存返回 true,放弃返回 false。 */
|
|
28
|
+
export async function editForm(prov, store, catalog) {
|
|
29
|
+
const W = fromProvider(prov);
|
|
30
|
+
let showSecret = false;
|
|
31
|
+
let start = 0;
|
|
32
|
+
for (;;) {
|
|
33
|
+
const v = (x) => (x === '' ? T('empty.paren') : x);
|
|
34
|
+
const keyDisp = W.token === '' ? T('empty.paren') : showSecret ? W.token : '********';
|
|
35
|
+
const rows = [
|
|
36
|
+
{ action: 'provider', label: `${T('edit.field.provider')}: ${v(W.name)}` },
|
|
37
|
+
{ action: 'note', label: `${T('edit.field.note')}: ${v(W.note)}` },
|
|
38
|
+
{ action: 'base', label: `${T('edit.field.base')}: ${v(W.base)}` },
|
|
39
|
+
{ action: 'auth', label: `${T('edit.field.auth')}: ${W.auth}` },
|
|
40
|
+
{ action: 'key', label: `${T('edit.field.key')}: ${keyDisp}` },
|
|
41
|
+
{ action: 'opus', label: `${T('edit.field.opus')}: ${v(W.opus)}` },
|
|
42
|
+
{ action: 'sonnet', label: `${T('edit.field.sonnet')}: ${v(W.sonnet)}` },
|
|
43
|
+
{ action: 'haiku', label: `${T('edit.field.haiku')}: ${v(W.haiku)}` },
|
|
44
|
+
{ action: 'effort', label: `${T('edit.field.effort')}: ${v(W.effort)}` },
|
|
45
|
+
{ action: 'toggle', label: showSecret ? T('edit.toggleSecretHide') : T('edit.toggleSecretShow') },
|
|
46
|
+
{ action: 'sep', label: '' },
|
|
47
|
+
{ action: 'save', label: T('edit.save') },
|
|
48
|
+
{ action: 'discard', label: T('edit.discard') },
|
|
49
|
+
];
|
|
50
|
+
const sel = await selectMenu({ title: T('edit.title'), items: rows.map((r) => r.label), start, hint: T('edit.hint') });
|
|
51
|
+
if (sel < 0)
|
|
52
|
+
return false; // Esc / q = 放弃
|
|
53
|
+
start = sel;
|
|
54
|
+
switch (rows[sel]?.action) {
|
|
55
|
+
case 'provider': {
|
|
56
|
+
const pp = await pickProvider(catalog, W.name);
|
|
57
|
+
if (pp === 'custom') {
|
|
58
|
+
const name = await readText(` ${T('edit.customName')}`);
|
|
59
|
+
if (name && name.trim())
|
|
60
|
+
W.name = name.trim();
|
|
61
|
+
}
|
|
62
|
+
else if (pp) {
|
|
63
|
+
W.name = pp.name;
|
|
64
|
+
W.auth = pp.auth;
|
|
65
|
+
W.base = await pickProviderUrl(pp, W.base);
|
|
66
|
+
if (pp.models.opus)
|
|
67
|
+
W.opus = pp.models.opus;
|
|
68
|
+
if (pp.models.sonnet)
|
|
69
|
+
W.sonnet = pp.models.sonnet;
|
|
70
|
+
if (pp.models.haiku)
|
|
71
|
+
W.haiku = pp.models.haiku;
|
|
72
|
+
if (pp.effort)
|
|
73
|
+
W.effort = pp.effort;
|
|
74
|
+
}
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
case 'note': {
|
|
78
|
+
const note = await readText(` ${T('edit.noteInput')}`);
|
|
79
|
+
if (note === '-')
|
|
80
|
+
W.note = '';
|
|
81
|
+
else if (note && note.trim())
|
|
82
|
+
W.note = note.trim();
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
case 'base':
|
|
86
|
+
W.base = await pickBaseUrl(W.base, store, catalog);
|
|
87
|
+
break;
|
|
88
|
+
case 'auth':
|
|
89
|
+
W.auth = await pickAuth(W.auth);
|
|
90
|
+
break;
|
|
91
|
+
case 'key': {
|
|
92
|
+
const r = await readValue(T('edit.field.key').trim(), W.token, true);
|
|
93
|
+
if (r.changed)
|
|
94
|
+
W.token = r.value;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
case 'opus': {
|
|
98
|
+
const r = await readValue(T('edit.field.opus').trim(), W.opus);
|
|
99
|
+
if (r.changed)
|
|
100
|
+
W.opus = r.value;
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
case 'sonnet': {
|
|
104
|
+
const r = await readValue(T('edit.field.sonnet').trim(), W.sonnet);
|
|
105
|
+
if (r.changed)
|
|
106
|
+
W.sonnet = r.value;
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
case 'haiku': {
|
|
110
|
+
const r = await readValue(T('edit.field.haiku').trim(), W.haiku);
|
|
111
|
+
if (r.changed)
|
|
112
|
+
W.haiku = r.value;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
case 'effort':
|
|
116
|
+
W.effort = await pickEffort(W.effort);
|
|
117
|
+
break;
|
|
118
|
+
case 'toggle':
|
|
119
|
+
showSecret = !showSecret; // 仅切换显示,不改数据、不持久化
|
|
120
|
+
break;
|
|
121
|
+
case 'save': {
|
|
122
|
+
if (W.name.trim() === '') {
|
|
123
|
+
console.log(` ${T('edit.nameEmpty')}`);
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
const fields = {
|
|
127
|
+
ANTHROPIC_BASE_URL: W.base,
|
|
128
|
+
ANTHROPIC_DEFAULT_OPUS_MODEL: W.opus,
|
|
129
|
+
ANTHROPIC_DEFAULT_SONNET_MODEL: W.sonnet,
|
|
130
|
+
ANTHROPIC_DEFAULT_HAIKU_MODEL: W.haiku,
|
|
131
|
+
CLAUDE_CODE_EFFORT_LEVEL: W.effort,
|
|
132
|
+
};
|
|
133
|
+
if (W.auth === 'API_KEY')
|
|
134
|
+
fields.ANTHROPIC_API_KEY = W.token;
|
|
135
|
+
else
|
|
136
|
+
fields.ANTHROPIC_AUTH_TOKEN = W.token;
|
|
137
|
+
prov.name = resolveUniqueName(store, W.name.trim(), prov);
|
|
138
|
+
prov.env = buildProviderEnv(fields);
|
|
139
|
+
prov.note = W.note;
|
|
140
|
+
reconcileBuiltin(prov); // [P1] 官方档被配成第三方后清掉 builtin 身份
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
case 'discard':
|
|
144
|
+
return false;
|
|
145
|
+
default:
|
|
146
|
+
break; // sep
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 列表/菜单行的文案格式化(--list 与 TUI 菜单共用,避免重复)。
|
|
3
|
+
*/
|
|
4
|
+
import { getProviderState } from '../config/store.js';
|
|
5
|
+
import { T } from '../i18n/index.js';
|
|
6
|
+
/** 配置的状态文案(语义枚举 → 当前语言;effort 原样附加)。 */
|
|
7
|
+
export function stateLabel(p) {
|
|
8
|
+
const s = getProviderState(p);
|
|
9
|
+
const base = s.key === 'official'
|
|
10
|
+
? T('state.login')
|
|
11
|
+
: s.key === 'noKey'
|
|
12
|
+
? T('state.noKey')
|
|
13
|
+
: s.key === 'apiKey'
|
|
14
|
+
? T('state.apiKey')
|
|
15
|
+
: T('state.hasKey');
|
|
16
|
+
return s.effort ? `${base} · effort=${s.effort}` : base;
|
|
17
|
+
}
|
|
18
|
+
/** 备注后缀(有备注才显示)。 */
|
|
19
|
+
export function noteSuffix(p) {
|
|
20
|
+
return p.note ? ` — ${p.note}` : '';
|
|
21
|
+
}
|
package/dist/ui/menus.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 三级菜单:主菜单 ↔ 动作菜单 ↔ 编辑表单(M4)。
|
|
3
|
+
*
|
|
4
|
+
* 主菜单:列表 + 排序(Shift+↑↓/PgUp/PgDn)+ 记忆选中 + 新增 + 语言切换 + 退出。
|
|
5
|
+
* 动作菜单:本次启用 / 设为默认(绿条 toast)/ 编辑 / 删除(二次确认)/ 返回。
|
|
6
|
+
* 编辑表单见 ui/edit.ts(含密钥明文切换)。
|
|
7
|
+
*/
|
|
8
|
+
import { createInterface } from 'node:readline';
|
|
9
|
+
import { launchSession } from '../actions.js';
|
|
10
|
+
import { isOfficial, reconcileCurrent, saveStore } from '../config/store.js';
|
|
11
|
+
import { setDefault } from '../env/default.js';
|
|
12
|
+
import { getLang, providerDisplayName, setLang, T } from '../i18n/index.js';
|
|
13
|
+
import { padDisplay } from '../utils/display.js';
|
|
14
|
+
import { editForm } from './edit.js';
|
|
15
|
+
import { noteSuffix, stateLabel } from './format.js';
|
|
16
|
+
import { selectMenu } from './select.js';
|
|
17
|
+
async function readLine(prompt) {
|
|
18
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
19
|
+
const ans = await new Promise((res) => rl.question(prompt, res));
|
|
20
|
+
rl.close();
|
|
21
|
+
return ans.trim();
|
|
22
|
+
}
|
|
23
|
+
/** 一级 · 主菜单。布局:[profiles…] '' 新增 语言 '' 退出。 */
|
|
24
|
+
export async function openMenu(paths, store, scope, version, catalog) {
|
|
25
|
+
let sel = 0;
|
|
26
|
+
for (;;) {
|
|
27
|
+
const n = store.providers.length;
|
|
28
|
+
const buildItems = () => {
|
|
29
|
+
const labels = store.providers.map((p) => {
|
|
30
|
+
const dft = p.name === store.current ? T('menu.default') : '';
|
|
31
|
+
return `${padDisplay(providerDisplayName(p), 16)}${padDisplay(dft, 8)}[${stateLabel(p)}]${noteSuffix(p)}`;
|
|
32
|
+
});
|
|
33
|
+
return [...labels, '', T('menu.newProfile'), T('menu.language'), '', T('menu.exit')];
|
|
34
|
+
};
|
|
35
|
+
const onMove = (from, to) => {
|
|
36
|
+
const ps = store.providers;
|
|
37
|
+
const a = ps[from];
|
|
38
|
+
const b = ps[to];
|
|
39
|
+
if (a && b) {
|
|
40
|
+
ps[from] = b;
|
|
41
|
+
ps[to] = a;
|
|
42
|
+
saveStore(paths, store);
|
|
43
|
+
}
|
|
44
|
+
return buildItems();
|
|
45
|
+
};
|
|
46
|
+
sel = await selectMenu({
|
|
47
|
+
title: T('menu.mainTitle', version),
|
|
48
|
+
items: buildItems(),
|
|
49
|
+
colors: { [n + 1]: 'yellow' },
|
|
50
|
+
start: sel,
|
|
51
|
+
movableCount: n,
|
|
52
|
+
onMove,
|
|
53
|
+
hint: T('menu.mainHint'),
|
|
54
|
+
});
|
|
55
|
+
if (sel < 0 || sel === n + 4)
|
|
56
|
+
return; // 退出 / Esc / q
|
|
57
|
+
if (sel === n + 1) {
|
|
58
|
+
// 新增配置
|
|
59
|
+
const prov = { name: '', env: {} };
|
|
60
|
+
if (await editForm(prov, store, catalog)) {
|
|
61
|
+
store.providers.push(prov);
|
|
62
|
+
saveStore(paths, store);
|
|
63
|
+
sel = store.providers.length - 1; // 光标落到新配置
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else if (sel === n + 2) {
|
|
67
|
+
// 语言切换:即时切并写回 store.lang
|
|
68
|
+
const next = getLang() === 'zh' ? 'en' : 'zh';
|
|
69
|
+
setLang(next);
|
|
70
|
+
store.lang = next;
|
|
71
|
+
saveStore(paths, store);
|
|
72
|
+
}
|
|
73
|
+
else if (sel < n) {
|
|
74
|
+
const target = store.providers[sel];
|
|
75
|
+
if (target)
|
|
76
|
+
await actionMenu(paths, store, target, scope, catalog);
|
|
77
|
+
if (sel >= store.providers.length)
|
|
78
|
+
sel = Math.max(0, store.providers.length - 1); // 删除后夹取
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** 二级 · 动作菜单(循环停留;返回/删除已确认才回一级)。 */
|
|
83
|
+
async function actionMenu(paths, store, p, scope, catalog) {
|
|
84
|
+
let sel = 0;
|
|
85
|
+
let flash;
|
|
86
|
+
for (;;) {
|
|
87
|
+
const dft = p.name === store.current ? T('menu.default') : '';
|
|
88
|
+
const title = `${T('action.titlePrefix')}${providerDisplayName(p)}${dft}${noteSuffix(p)} [${stateLabel(p)}]`;
|
|
89
|
+
const items = [T('action.session'), T('action.setDefault'), T('action.edit'), T('action.delete'), T('action.back')];
|
|
90
|
+
sel = await selectMenu({ title, items, start: sel, ...(flash ? { status: flash } : {}), hint: T('action.hint') });
|
|
91
|
+
flash = undefined;
|
|
92
|
+
if (sel === 0) {
|
|
93
|
+
launchSession(p);
|
|
94
|
+
}
|
|
95
|
+
else if (sel === 1) {
|
|
96
|
+
flash = applyDefault(paths, store, p, scope);
|
|
97
|
+
}
|
|
98
|
+
else if (sel === 2) {
|
|
99
|
+
const old = p.name;
|
|
100
|
+
if (await editForm(p, store, catalog)) {
|
|
101
|
+
if (store.current === old)
|
|
102
|
+
store.current = p.name; // 改了名/供应商时同步默认指向
|
|
103
|
+
saveStore(paths, store);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else if (sel === 3) {
|
|
107
|
+
if (isOfficial(p))
|
|
108
|
+
console.log(` ${T('action.deleteOfficialWarn')}`);
|
|
109
|
+
const ans = await readLine(` ${T('action.deleteConfirm', providerDisplayName(p))}`);
|
|
110
|
+
if (ans === 'y' || ans === 'Y') {
|
|
111
|
+
store.providers = store.providers.filter((x) => x !== p);
|
|
112
|
+
reconcileCurrent(store);
|
|
113
|
+
saveStore(paths, store);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
return; // 返回 / q / Esc
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** 设为默认并返回一行 toast 文案。 */
|
|
123
|
+
function applyDefault(paths, store, p, scope) {
|
|
124
|
+
const name = providerDisplayName(p);
|
|
125
|
+
const r = setDefault(paths, store, p, scope);
|
|
126
|
+
if (r.dryRun)
|
|
127
|
+
return `${T('default.done', name)} ${T('default.dryRun')}`;
|
|
128
|
+
if (r.windows && !r.windows.ok)
|
|
129
|
+
return T('default.failed', r.windows.error ?? '');
|
|
130
|
+
if (r.unix?.unsupported)
|
|
131
|
+
return T('default.fishUnsupported');
|
|
132
|
+
return T('default.done', name);
|
|
133
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 编辑表单里的各 picker(供应商 / API 地址 / 认证字段 / effort),对齐现版 Pick-*。
|
|
3
|
+
*/
|
|
4
|
+
import { getProviderEnvMap } from '../config/store.js';
|
|
5
|
+
import { T } from '../i18n/index.js';
|
|
6
|
+
import { padDisplay } from '../utils/display.js';
|
|
7
|
+
import { selectMenu } from './select.js';
|
|
8
|
+
import { readText } from './text.js';
|
|
9
|
+
/** 选供应商:从目录选一个 / 自定义手填名 / 不改。 */
|
|
10
|
+
export async function pickProvider(catalog, current) {
|
|
11
|
+
const names = catalog.map((p) => p.name);
|
|
12
|
+
const items = [...names, T('pick.provider.custom'), T('pick.noChange')];
|
|
13
|
+
const sel = await selectMenu({
|
|
14
|
+
title: T('pick.provider.title', current || T('pick.provider.none')),
|
|
15
|
+
items,
|
|
16
|
+
hint: T('pick.hint'),
|
|
17
|
+
});
|
|
18
|
+
if (sel < 0 || sel === items.length - 1)
|
|
19
|
+
return null;
|
|
20
|
+
if (sel === names.length)
|
|
21
|
+
return 'custom';
|
|
22
|
+
return catalog[sel] ?? null;
|
|
23
|
+
}
|
|
24
|
+
/** 供应商有多个地址时让用户选一个;只有一个直接用,无地址保持原值。 */
|
|
25
|
+
export async function pickProviderUrl(preset, current) {
|
|
26
|
+
const urls = preset.urls;
|
|
27
|
+
if (urls.length === 0)
|
|
28
|
+
return current;
|
|
29
|
+
if (urls.length === 1)
|
|
30
|
+
return urls[0]?.url ?? current;
|
|
31
|
+
const labels = urls.map((u) => `${padDisplay(u.label, 12)} ${u.url || T('empty.paren')}`);
|
|
32
|
+
const items = [...labels, T('pick.noChange')];
|
|
33
|
+
const sel = await selectMenu({ title: T('pick.providerUrl.title', preset.name), items, hint: T('pick.hint') });
|
|
34
|
+
if (sel < 0 || sel === items.length - 1)
|
|
35
|
+
return current;
|
|
36
|
+
return urls[sel]?.url ?? current;
|
|
37
|
+
}
|
|
38
|
+
/** 选 API 地址:目录所有 url + 已有配置用过的 url + 手动输入 + 不修改。 */
|
|
39
|
+
export async function pickBaseUrl(current, store, catalog) {
|
|
40
|
+
const entries = [];
|
|
41
|
+
const seen = new Set();
|
|
42
|
+
for (const p of catalog) {
|
|
43
|
+
for (const u of p.urls) {
|
|
44
|
+
const tag = p.urls.length > 1 ? `${p.name}/${u.label}` : p.name;
|
|
45
|
+
entries.push({ label: `${padDisplay(tag, 20)} ${u.url || T('empty.paren')}`, url: u.url });
|
|
46
|
+
seen.add(u.url);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
for (const prov of store.providers) {
|
|
50
|
+
const u = getProviderEnvMap(prov).ANTHROPIC_BASE_URL;
|
|
51
|
+
if (u && u.trim() && !seen.has(u)) {
|
|
52
|
+
seen.add(u);
|
|
53
|
+
entries.push({ label: `${padDisplay(T('pick.base.existing', prov.name), 20)} ${u}`, url: u });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const items = [...entries.map((e) => e.label), T('pick.manual'), T('pick.noChange')];
|
|
57
|
+
const sel = await selectMenu({ title: T('pick.base.title', current || T('empty.paren')), items, hint: T('pick.hint') });
|
|
58
|
+
if (sel < 0 || sel === items.length - 1)
|
|
59
|
+
return current;
|
|
60
|
+
if (sel < entries.length)
|
|
61
|
+
return entries[sel]?.url ?? current;
|
|
62
|
+
const v = await readText(` ${T('pick.base.manualInput')}`);
|
|
63
|
+
if (v === undefined || v === '')
|
|
64
|
+
return current;
|
|
65
|
+
if (v === '-')
|
|
66
|
+
return '';
|
|
67
|
+
return v.trim();
|
|
68
|
+
}
|
|
69
|
+
/** 选认证字段:AUTH_TOKEN / API_KEY / 不改。 */
|
|
70
|
+
export async function pickAuth(current) {
|
|
71
|
+
const items = [T('pick.auth.token'), T('pick.auth.apikey'), T('pick.noChange')];
|
|
72
|
+
const sel = await selectMenu({ title: T('pick.auth.title', current), items, hint: T('pick.hint') });
|
|
73
|
+
if (sel === 0)
|
|
74
|
+
return 'AUTH_TOKEN';
|
|
75
|
+
if (sel === 1)
|
|
76
|
+
return 'API_KEY';
|
|
77
|
+
return current;
|
|
78
|
+
}
|
|
79
|
+
const EFFORT_OPTS = ['low', 'medium', 'high', 'xhigh', 'max', 'auto'];
|
|
80
|
+
/** 选 effort 思考档:low…auto / 留空 / 不改。 */
|
|
81
|
+
export async function pickEffort(current) {
|
|
82
|
+
const items = [...EFFORT_OPTS, T('pick.effort.empty'), T('pick.noChange')];
|
|
83
|
+
const sel = await selectMenu({
|
|
84
|
+
title: T('pick.effort.title', current || T('empty.paren')),
|
|
85
|
+
items,
|
|
86
|
+
hint: T('pick.effort.hint'),
|
|
87
|
+
});
|
|
88
|
+
if (sel < 0 || sel === items.length - 1)
|
|
89
|
+
return current;
|
|
90
|
+
if (sel === EFFORT_OPTS.length)
|
|
91
|
+
return '';
|
|
92
|
+
return EFFORT_OPTS[sel] ?? current;
|
|
93
|
+
}
|