mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 16:25:51 +00:00
feat: /login 命令新增自定义 anthropic 终端登陆
This commit is contained in:
34
DEV-LOG.md
34
DEV-LOG.md
@@ -168,3 +168,37 @@ GrowthBook 功能开关系统原为 Anthropic 内部构建设计,硬编码 SDK
|
|||||||
注意:
|
注意:
|
||||||
- `USER_TYPE=ant` 启用 alt-screen 全屏模式,中心区域满屏是预期行为
|
- `USER_TYPE=ant` 启用 alt-screen 全屏模式,中心区域满屏是预期行为
|
||||||
- `global.d.ts` 中剩余未 stub 的全局函数(`getAntModels` 等)遇到 `X is not defined` 时按同样模式处理
|
- `global.d.ts` 中剩余未 stub 的全局函数(`getAntModels` 等)遇到 `X is not defined` 时按同样模式处理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## /login 添加 Custom Platform 选项 (2026-04-03)
|
||||||
|
|
||||||
|
在 `/login` 命令的登录方式选择列表中新增 "Custom Platform" 选项(位于第一位),允许用户直接在终端配置第三方 API 兼容服务的 Base URL、API Key 和三种模型映射,保存到 `~/.claude/settings.json`。
|
||||||
|
|
||||||
|
**修改文件:**
|
||||||
|
|
||||||
|
| 文件 | 变更 |
|
||||||
|
|------|------|
|
||||||
|
| `src/components/ConsoleOAuthFlow.tsx` | `OAuthStatus` 类型新增 `custom_platform` state(含 `baseUrl`、`apiKey`、`haikuModel`、`sonnetModel`、`opusModel`、`activeField`);`idle` case Select 选项新增 Custom Platform 并排第一位;新增 `custom_platform` case 渲染 5 字段表单(Tab/Shift+Tab 切换、focus 高亮、Enter 跳转/保存);Select onChange 处理 `custom_platform` 初始状态(从 `process.env` 预填当前值);`OAuthStatusMessageProps` 类型及调用处新增 `onDone` prop |
|
||||||
|
| `src/components/ConsoleOAuthFlow.tsx` | 新增 `updateSettingsForSource` import |
|
||||||
|
|
||||||
|
**UI 交互:**
|
||||||
|
- 5 个字段同屏:Base URL、API Key、Haiku Model、Sonnet Model、Opus Model
|
||||||
|
- 当前活动字段的标签用 `suggestion` 背景色 + `inverseText` 反色高亮
|
||||||
|
- Tab / Shift+Tab 在字段间切换,各自保留输入值
|
||||||
|
- 每个字段按 Enter 跳到下一个,最后一个字段 (Opus) 按 Enter 保存
|
||||||
|
- 模型字段自动从 `process.env` 读取当前配置作为预填值,无值则空
|
||||||
|
- 保存时调用 `updateSettingsForSource('userSettings', { env })` 写入 settings.json,同时更新 `process.env`
|
||||||
|
|
||||||
|
**保存的 settings.json env 字段:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ANTHROPIC_BASE_URL": "...",
|
||||||
|
"ANTHROPIC_AUTH_TOKEN": "...",
|
||||||
|
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "...",
|
||||||
|
"ANTHROPIC_DEFAULT_SONNET_MODEL": "...",
|
||||||
|
"ANTHROPIC_DEFAULT_OPUS_MODEL": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
非空字段才写入,保存后立即生效(`onDone()` 触发 `onChangeAPIKey()` 刷新 API 客户端)。
|
||||||
|
|||||||
35
README.md
35
README.md
@@ -31,6 +31,7 @@
|
|||||||
- [x] 关闭自动更新;
|
- [x] 关闭自动更新;
|
||||||
- [x] 添加自定义 sentry 错误上报支持 [文档](https://ccb.agent-aura.top/docs/internals/sentry-setup)
|
- [x] 添加自定义 sentry 错误上报支持 [文档](https://ccb.agent-aura.top/docs/internals/sentry-setup)
|
||||||
- [x] 添加自定义 GrowthBook 支持 (GB 也是开源的, 现在你可以配置一个自定义的遥控平台) [文档](https://ccb.agent-aura.top/docs/internals/growthbook-adapter)
|
- [x] 添加自定义 GrowthBook 支持 (GB 也是开源的, 现在你可以配置一个自定义的遥控平台) [文档](https://ccb.agent-aura.top/docs/internals/growthbook-adapter)
|
||||||
|
- [x] 自定义 login 模式, 大家可以用这个配置 Claude 的模型!
|
||||||
- [ ] V6 大规模重构石山代码, 全面模块分包
|
- [ ] V6 大规模重构石山代码, 全面模块分包
|
||||||
- [ ] V6 将会为全新分支, 届时 main 分支将会封存为历史版本
|
- [ ] V6 将会为全新分支, 届时 main 分支将会封存为历史版本
|
||||||
|
|
||||||
@@ -72,6 +73,40 @@ bun run build
|
|||||||
|
|
||||||
如果遇到 bug 请直接提一个 issues, 我们优先解决
|
如果遇到 bug 请直接提一个 issues, 我们优先解决
|
||||||
|
|
||||||
|
### 新人配置 /login
|
||||||
|
|
||||||
|
首次运行后,在 REPL 中输入 `/login` 命令进入登录配置界面,选择 **Custom Platform** 即可对接第三方 API 兼容服务(无需 Anthropic 官方账号)。
|
||||||
|
|
||||||
|
需要填写的字段:
|
||||||
|
|
||||||
|
| 字段 | 说明 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| Base URL | API 服务地址 | `https://api.example.com/v1` |
|
||||||
|
| API Key | 认证密钥 | `sk-xxx` |
|
||||||
|
| Haiku Model | 快速模型 ID | `claude-haiku-4-5-20251001` |
|
||||||
|
| Sonnet Model | 均衡模型 ID | `claude-sonnet-4-6` |
|
||||||
|
| Opus Model | 高性能模型 ID | `claude-opus-4-6` |
|
||||||
|
|
||||||
|
- **Tab / Shift+Tab** 切换字段,**Enter** 确认并跳到下一个,最后一个字段按 Enter 保存
|
||||||
|
- 模型字段会自动读取当前环境变量预填
|
||||||
|
- 配置保存到 `~/.claude/settings.json` 的 `env` 字段,保存后立即生效
|
||||||
|
|
||||||
|
也可以直接编辑 `~/.claude/settings.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"ANTHROPIC_BASE_URL": "https://api.example.com/v1",
|
||||||
|
"ANTHROPIC_AUTH_TOKEN": "sk-xxx",
|
||||||
|
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4-5-20251001",
|
||||||
|
"ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4-6",
|
||||||
|
"ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-4-6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 支持所有 Anthropic API 兼容服务(如 OpenRouter、AWS Bedrock 代理等),只要接口兼容 Messages API 即可。
|
||||||
|
|
||||||
## Feature Flags
|
## Feature Flags
|
||||||
|
|
||||||
所有功能开关通过 `FEATURE_<FLAG_NAME>=1` 环境变量启用,例如:
|
所有功能开关通过 `FEATURE_<FLAG_NAME>=1` 环境变量启用,例如:
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { sendNotification } from '../services/notifier.js';
|
|||||||
import { OAuthService } from '../services/oauth/index.js';
|
import { OAuthService } from '../services/oauth/index.js';
|
||||||
import { getOauthAccountInfo, validateForceLoginOrg } from '../utils/auth.js';
|
import { getOauthAccountInfo, validateForceLoginOrg } from '../utils/auth.js';
|
||||||
import { logError } from '../utils/log.js';
|
import { logError } from '../utils/log.js';
|
||||||
import { getSettings_DEPRECATED } from '../utils/settings/settings.js';
|
import { getSettings_DEPRECATED, updateSettingsForSource } from '../utils/settings/settings.js';
|
||||||
import { Select } from './CustomSelect/select.js';
|
import { Select } from './CustomSelect/select.js';
|
||||||
import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js';
|
import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js';
|
||||||
import { Spinner } from './Spinner.js';
|
import { Spinner } from './Spinner.js';
|
||||||
@@ -29,6 +29,15 @@ type OAuthStatus = {
|
|||||||
| {
|
| {
|
||||||
state: 'platform_setup';
|
state: 'platform_setup';
|
||||||
} // Show platform setup info (Bedrock/Vertex/Foundry)
|
} // Show platform setup info (Bedrock/Vertex/Foundry)
|
||||||
|
| {
|
||||||
|
state: 'custom_platform';
|
||||||
|
baseUrl: string;
|
||||||
|
apiKey: string;
|
||||||
|
haikuModel: string;
|
||||||
|
sonnetModel: string;
|
||||||
|
opusModel: string;
|
||||||
|
activeField: 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model';
|
||||||
|
} // Custom platform: configure API endpoint and model names
|
||||||
| {
|
| {
|
||||||
state: 'ready_to_start';
|
state: 'ready_to_start';
|
||||||
} // Flow started, waiting for browser to open
|
} // Flow started, waiting for browser to open
|
||||||
@@ -325,7 +334,7 @@ export function ConsoleOAuthFlow({
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>}
|
</Box>}
|
||||||
<Box paddingLeft={1} flexDirection="column" gap={1}>
|
<Box paddingLeft={1} flexDirection="column" gap={1}>
|
||||||
<OAuthStatusMessage oauthStatus={oauthStatus} mode={mode} startingMessage={startingMessage} forcedMethodMessage={forcedMethodMessage} showPastePrompt={showPastePrompt} pastedCode={pastedCode} setPastedCode={setPastedCode} cursorOffset={cursorOffset} setCursorOffset={setCursorOffset} textInputColumns={textInputColumns} handleSubmitCode={handleSubmitCode} setOAuthStatus={setOAuthStatus} setLoginWithClaudeAi={setLoginWithClaudeAi} />
|
<OAuthStatusMessage oauthStatus={oauthStatus} mode={mode} startingMessage={startingMessage} forcedMethodMessage={forcedMethodMessage} showPastePrompt={showPastePrompt} pastedCode={pastedCode} setPastedCode={setPastedCode} cursorOffset={cursorOffset} setCursorOffset={setCursorOffset} textInputColumns={textInputColumns} handleSubmitCode={handleSubmitCode} setOAuthStatus={setOAuthStatus} setLoginWithClaudeAi={setLoginWithClaudeAi} onDone={onDone} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>;
|
</Box>;
|
||||||
}
|
}
|
||||||
@@ -343,6 +352,7 @@ type OAuthStatusMessageProps = {
|
|||||||
handleSubmitCode: (value: string, url: string) => void;
|
handleSubmitCode: (value: string, url: string) => void;
|
||||||
setOAuthStatus: (status: OAuthStatus) => void;
|
setOAuthStatus: (status: OAuthStatus) => void;
|
||||||
setLoginWithClaudeAi: (value: boolean) => void;
|
setLoginWithClaudeAi: (value: boolean) => void;
|
||||||
|
onDone: () => void;
|
||||||
};
|
};
|
||||||
function OAuthStatusMessage(t0) {
|
function OAuthStatusMessage(t0) {
|
||||||
const $ = _c(51);
|
const $ = _c(51);
|
||||||
@@ -359,7 +369,8 @@ function OAuthStatusMessage(t0) {
|
|||||||
textInputColumns,
|
textInputColumns,
|
||||||
handleSubmitCode,
|
handleSubmitCode,
|
||||||
setOAuthStatus,
|
setOAuthStatus,
|
||||||
setLoginWithClaudeAi
|
setLoginWithClaudeAi,
|
||||||
|
onDone
|
||||||
} = t0;
|
} = t0;
|
||||||
switch (oauthStatus.state) {
|
switch (oauthStatus.state) {
|
||||||
case "idle":
|
case "idle":
|
||||||
@@ -402,7 +413,10 @@ function OAuthStatusMessage(t0) {
|
|||||||
}
|
}
|
||||||
let t6;
|
let t6;
|
||||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||||
t6 = [t4, t5, {
|
t6 = [{
|
||||||
|
label: <Text>Custom Platform ·{" "}<Text dimColor={true}>Configure your own API endpoint</Text>{"\n"}</Text>,
|
||||||
|
value: "custom_platform"
|
||||||
|
}, t4, t5, {
|
||||||
label: <Text>3rd-party platform ·{" "}<Text dimColor={true}>Amazon Bedrock, Microsoft Foundry, or Vertex AI</Text>{"\n"}</Text>,
|
label: <Text>3rd-party platform ·{" "}<Text dimColor={true}>Amazon Bedrock, Microsoft Foundry, or Vertex AI</Text>{"\n"}</Text>,
|
||||||
value: "platform"
|
value: "platform"
|
||||||
}];
|
}];
|
||||||
@@ -413,7 +427,18 @@ function OAuthStatusMessage(t0) {
|
|||||||
let t7;
|
let t7;
|
||||||
if ($[6] !== setLoginWithClaudeAi || $[7] !== setOAuthStatus) {
|
if ($[6] !== setLoginWithClaudeAi || $[7] !== setOAuthStatus) {
|
||||||
t7 = <Box><Select options={t6} onChange={value_0 => {
|
t7 = <Box><Select options={t6} onChange={value_0 => {
|
||||||
if (value_0 === "platform") {
|
if (value_0 === "custom_platform") {
|
||||||
|
logEvent("tengu_custom_platform_selected", {});
|
||||||
|
setOAuthStatus({
|
||||||
|
state: "custom_platform",
|
||||||
|
baseUrl: process.env.ANTHROPIC_BASE_URL ?? "",
|
||||||
|
apiKey: process.env.ANTHROPIC_AUTH_TOKEN ?? "",
|
||||||
|
haikuModel: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL ?? "",
|
||||||
|
sonnetModel: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL ?? "",
|
||||||
|
opusModel: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL ?? "",
|
||||||
|
activeField: "base_url"
|
||||||
|
});
|
||||||
|
} else if (value_0 === "platform") {
|
||||||
logEvent("tengu_oauth_platform_selected", {});
|
logEvent("tengu_oauth_platform_selected", {});
|
||||||
setOAuthStatus({
|
setOAuthStatus({
|
||||||
state: "platform_setup"
|
state: "platform_setup"
|
||||||
@@ -505,6 +530,115 @@ function OAuthStatusMessage(t0) {
|
|||||||
}
|
}
|
||||||
return t8;
|
return t8;
|
||||||
}
|
}
|
||||||
|
case "custom_platform":
|
||||||
|
{
|
||||||
|
type Field = 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model';
|
||||||
|
const FIELDS: Field[] = ['base_url', 'api_key', 'haiku_model', 'sonnet_model', 'opus_model'];
|
||||||
|
const cp = oauthStatus as { state: 'custom_platform'; activeField: Field; baseUrl: string; apiKey: string; haikuModel: string; sonnetModel: string; opusModel: string };
|
||||||
|
const { activeField, baseUrl, apiKey, haikuModel, sonnetModel, opusModel } = cp;
|
||||||
|
const displayValues: Record<Field, string> = { base_url: baseUrl, api_key: apiKey, haiku_model: haikuModel, sonnet_model: sonnetModel, opus_model: opusModel };
|
||||||
|
|
||||||
|
const [inputValue, setInputValue] = useState(() => displayValues[activeField]);
|
||||||
|
const [inputCursorOffset, setInputCursorOffset] = useState(() => displayValues[activeField].length);
|
||||||
|
|
||||||
|
// Build updated state with a given field changed
|
||||||
|
const buildState = useCallback((field: Field, value: string, newActive?: Field) => {
|
||||||
|
const s = { state: 'custom_platform' as const, activeField: newActive ?? activeField, baseUrl, apiKey, haikuModel, sonnetModel, opusModel };
|
||||||
|
switch (field) {
|
||||||
|
case 'base_url': return { ...s, baseUrl: value };
|
||||||
|
case 'api_key': return { ...s, apiKey: value };
|
||||||
|
case 'haiku_model': return { ...s, haikuModel: value };
|
||||||
|
case 'sonnet_model': return { ...s, sonnetModel: value };
|
||||||
|
case 'opus_model': return { ...s, opusModel: value };
|
||||||
|
}
|
||||||
|
}, [activeField, baseUrl, apiKey, haikuModel, sonnetModel, opusModel]);
|
||||||
|
|
||||||
|
// Tab switching: save current → update state → load target
|
||||||
|
const switchTo = useCallback((target: Field) => {
|
||||||
|
setOAuthStatus(buildState(activeField, inputValue, target));
|
||||||
|
setInputValue(displayValues[target] ?? '');
|
||||||
|
setInputCursorOffset((displayValues[target] ?? '').length);
|
||||||
|
}, [activeField, inputValue, displayValues, buildState, setOAuthStatus]);
|
||||||
|
|
||||||
|
const doSave = useCallback(() => {
|
||||||
|
const finalVals = { ...displayValues, [activeField]: inputValue };
|
||||||
|
const env: Record<string, string> = {};
|
||||||
|
if (finalVals.base_url) env.ANTHROPIC_BASE_URL = finalVals.base_url;
|
||||||
|
if (finalVals.api_key) env.ANTHROPIC_AUTH_TOKEN = finalVals.api_key;
|
||||||
|
if (finalVals.haiku_model) env.ANTHROPIC_DEFAULT_HAIKU_MODEL = finalVals.haiku_model;
|
||||||
|
if (finalVals.sonnet_model) env.ANTHROPIC_DEFAULT_SONNET_MODEL = finalVals.sonnet_model;
|
||||||
|
if (finalVals.opus_model) env.ANTHROPIC_DEFAULT_OPUS_MODEL = finalVals.opus_model;
|
||||||
|
const { error } = updateSettingsForSource('userSettings', { env } as any);
|
||||||
|
if (error) {
|
||||||
|
setOAuthStatus({ state: 'error', message: `Failed to save: ${error.message}`, toRetry: { state: 'custom_platform', baseUrl: '', apiKey: '', haikuModel: '', sonnetModel: '', opusModel: '', activeField: 'base_url' } });
|
||||||
|
} else {
|
||||||
|
for (const [k, v] of Object.entries(env)) process.env[k] = v;
|
||||||
|
setOAuthStatus({ state: 'success' });
|
||||||
|
void onDone();
|
||||||
|
}
|
||||||
|
}, [activeField, inputValue, displayValues, setOAuthStatus, onDone]);
|
||||||
|
|
||||||
|
const handleEnter = useCallback(() => {
|
||||||
|
const idx = FIELDS.indexOf(activeField);
|
||||||
|
// Update current field value in state
|
||||||
|
setOAuthStatus(buildState(activeField, inputValue));
|
||||||
|
if (idx === FIELDS.length - 1) {
|
||||||
|
doSave();
|
||||||
|
} else {
|
||||||
|
const next = FIELDS[idx + 1]!;
|
||||||
|
setInputValue(displayValues[next] ?? '');
|
||||||
|
setInputCursorOffset((displayValues[next] ?? '').length);
|
||||||
|
}
|
||||||
|
}, [activeField, inputValue, buildState, doSave, displayValues, setOAuthStatus]);
|
||||||
|
|
||||||
|
useKeybinding('tabs:next', () => {
|
||||||
|
const idx = FIELDS.indexOf(activeField);
|
||||||
|
if (idx < FIELDS.length - 1) {
|
||||||
|
setOAuthStatus(buildState(activeField, inputValue, FIELDS[idx + 1]));
|
||||||
|
setInputValue(displayValues[FIELDS[idx + 1]!] ?? '');
|
||||||
|
setInputCursorOffset((displayValues[FIELDS[idx + 1]!] ?? '').length);
|
||||||
|
}
|
||||||
|
}, { context: 'Tabs' });
|
||||||
|
useKeybinding('tabs:previous', () => {
|
||||||
|
const idx = FIELDS.indexOf(activeField);
|
||||||
|
if (idx > 0) {
|
||||||
|
setOAuthStatus(buildState(activeField, inputValue, FIELDS[idx - 1]));
|
||||||
|
setInputValue(displayValues[FIELDS[idx - 1]!] ?? '');
|
||||||
|
setInputCursorOffset((displayValues[FIELDS[idx - 1]!] ?? '').length);
|
||||||
|
}
|
||||||
|
}, { context: 'Tabs' });
|
||||||
|
useKeybinding('confirm:no', () => {
|
||||||
|
setOAuthStatus({ state: 'idle' });
|
||||||
|
}, { context: 'Confirmation' });
|
||||||
|
|
||||||
|
const columns = useTerminalSize().columns - 20;
|
||||||
|
|
||||||
|
const renderRow = (field: Field, label: string, opts?: { mask?: boolean; placeholder?: string }) => {
|
||||||
|
const active = activeField === field;
|
||||||
|
const val = displayValues[field];
|
||||||
|
return <Box>
|
||||||
|
<Text backgroundColor={active ? 'suggestion' : undefined} color={active ? 'inverseText' : undefined}>{` ${label} `}</Text>
|
||||||
|
<Text> </Text>
|
||||||
|
{active
|
||||||
|
? <TextInput value={inputValue} onChange={setInputValue} onSubmit={handleEnter} cursorOffset={inputCursorOffset} onChangeCursorOffset={setInputCursorOffset} columns={columns} mask={opts?.mask ? "*" : undefined} focus={true} />
|
||||||
|
: (val
|
||||||
|
? <Text color="success">{opts?.mask ? val.slice(0, 8) + '·'.repeat(Math.max(0, val.length - 8)) : val}</Text>
|
||||||
|
: null)}
|
||||||
|
</Box>;
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Box flexDirection="column" gap={1}>
|
||||||
|
<Text bold={true}>Custom Platform Setup</Text>
|
||||||
|
<Box flexDirection="column" gap={1}>
|
||||||
|
{renderRow('base_url', 'Base URL ')}
|
||||||
|
{renderRow('api_key', 'API Key ', { mask: true })}
|
||||||
|
{renderRow('haiku_model', 'Haiku ')}
|
||||||
|
{renderRow('sonnet_model', 'Sonnet ')}
|
||||||
|
{renderRow('opus_model', 'Opus ')}
|
||||||
|
</Box>
|
||||||
|
<Text dimColor>Tab to switch · Enter on last field to save · Esc to go back</Text>
|
||||||
|
</Box>;
|
||||||
|
}
|
||||||
case "waiting_for_login":
|
case "waiting_for_login":
|
||||||
{
|
{
|
||||||
let t1;
|
let t1;
|
||||||
|
|||||||
Reference in New Issue
Block a user