feat: sync remaining channel and relay updates

This commit is contained in:
OpenClaw Task Bot
2026-04-27 08:56:38 +08:00
parent 0321101ff7
commit 58c0258362
18 changed files with 465 additions and 271 deletions

View File

@@ -27,7 +27,10 @@ import {
verifyJSON,
} from '../../../../helpers';
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
import { CHANNEL_OPTIONS, MODEL_FETCHABLE_CHANNEL_TYPES } from '../../../../constants';
import {
CHANNEL_OPTIONS,
MODEL_FETCHABLE_CHANNEL_TYPES,
} from '../../../../constants';
import {
SideSheet,
Space,
@@ -128,7 +131,7 @@ const DEPRECATED_DOUBAO_CODING_PLAN_BASE_URL = 'doubao-coding-plan';
// 支持并且已适配通过接口获取模型列表的渠道类型
const MODEL_FETCHABLE_TYPES = new Set([
1, 4, 14, 34, 17, 26, 27, 24, 47, 25, 20, 23, 31, 40, 42, 48, 43,
1, 4, 14, 34, 17, 26, 27, 24, 47, 25, 20, 23, 31, 40, 42, 48, 43, 58, 59, 60,
]);
function type2secretPrompt(type) {
@@ -277,7 +280,8 @@ const EditChannelModal = (props) => {
[inputs.upstream_model_update_last_detected_models],
);
const upstreamDetectedModelsPreview = useMemo(
() => upstreamDetectedModels.slice(0, UPSTREAM_DETECTED_MODEL_PREVIEW_LIMIT),
() =>
upstreamDetectedModels.slice(0, UPSTREAM_DETECTED_MODEL_PREVIEW_LIMIT),
[upstreamDetectedModels],
);
const upstreamDetectedModelsOmittedCount =
@@ -310,9 +314,7 @@ const EditChannelModal = (props) => {
return {
tagLabel: t('不更改'),
tagColor: 'grey',
preview: t(
'此项可选,用于覆盖请求参数。不支持覆盖 stream 参数',
),
preview: t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数'),
};
}
if (!verifyJSON(raw)) {
@@ -1612,7 +1614,7 @@ const EditChannelModal = (props) => {
return;
}
if (
localInputs.type === 45 &&
[45, 58, 59, 60].includes(localInputs.type) &&
(!localInputs.base_url || localInputs.base_url.trim() === '')
) {
showInfo(t('请输入API地址'));
@@ -2257,6 +2259,39 @@ const EditChannelModal = (props) => {
/>
)}
{inputs.type === 58 && (
<Banner
type='info'
closeIcon={null}
className='mb-4 rounded-xl'
description={t(
'该类型用于兼容上游把 OpenAI 官方兼容入口放在 /openai 的第三方站点。这里填写上游给官方 OpenAI 客户端或 Codex CLI 使用的 Base URL例如https://api.univibe.cc/openai下游客户端仍然使用你自己的 New API 地址加 /v1。',
)}
/>
)}
{inputs.type === 59 && (
<Banner
type='info'
closeIcon={null}
className='mb-4 rounded-xl'
description={t(
'该类型用于兼容 Claude Code 或 Anthropic 网关类上游。这里填写上游提供的 Anthropic Base URL例如https://gateway.example.com/anthropic转发时会保持 Anthropic 的 /v1/messages 路径,但按网关常见要求使用 Bearer 认证。',
)}
/>
)}
{inputs.type === 60 && (
<Banner
type='info'
closeIcon={null}
className='mb-4 rounded-xl'
description={t(
'该类型用于兼容 Gemini OpenAI 兼容入口。这里填写上游给 Gemini CLI 或 OpenAI 兼容 SDK 使用的 Base URL例如https://generativelanguage.googleapis.com/v1beta/openai下游客户端仍然使用你自己的 New API 地址加 /v1。',
)}
/>
)}
{inputs.type === 20 && (
<Form.Switch
field='is_enterprise_account'
@@ -3001,6 +3036,9 @@ const EditChannelModal = (props) => {
inputs.type !== 8 &&
inputs.type !== 22 &&
inputs.type !== 36 &&
inputs.type !== 58 &&
inputs.type !== 59 &&
inputs.type !== 60 &&
(inputs.type !== 45 || doubaoApiEditUnlocked) && (
<div>
<Form.Input
@@ -3057,6 +3095,66 @@ const EditChannelModal = (props) => {
</div>
)}
{inputs.type === 58 && (
<div>
<Form.Input
field='base_url'
label={t('上游 OpenAI / Codex Base URL')}
placeholder={t(
'请输入上游提供给官方 OpenAI 客户端或 Codex CLI 使用的 Base URL例如https://api.univibe.cc/openai',
)}
onChange={(value) =>
handleInputChange('base_url', value)
}
showClear
disabled={isIonetLocked}
extraText={t(
'此类型会把下游的 /v1/* 请求自动映射到上游的 /openai/*。这里不要填写 /v1保持上游原始 Base URL 即可。',
)}
/>
</div>
)}
{inputs.type === 59 && (
<div>
<Form.Input
field='base_url'
label={t('上游 Claude Code / Anthropic Base URL')}
placeholder={t(
'请输入上游提供给 Claude Code 使用的 Anthropic Base URL例如https://gateway.example.com/anthropic',
)}
onChange={(value) =>
handleInputChange('base_url', value)
}
showClear
disabled={isIonetLocked}
extraText={t(
'此类型保持 Anthropic 官方请求路径,不自动补 /v1 到 Base URL这里直接填写上游原始 Base URL 即可。',
)}
/>
</div>
)}
{inputs.type === 60 && (
<div>
<Form.Input
field='base_url'
label={t('上游 Gemini OpenAI Compat Base URL')}
placeholder={t(
'请输入 Gemini OpenAI 兼容 Base URL例如https://generativelanguage.googleapis.com/v1beta/openai',
)}
onChange={(value) =>
handleInputChange('base_url', value)
}
showClear
disabled={isIonetLocked}
extraText={t(
'此类型会把下游的 /v1/* 请求自动映射到上游的 /v1beta/openai/*。这里不要再额外填写 /v1。',
)}
/>
</div>
)}
{inputs.type === 45 && !doubaoApiEditUnlocked && (
<div>
<Form.Select
@@ -3478,79 +3576,81 @@ const EditChannelModal = (props) => {
/>
<Form.Switch
field='upstream_model_update_auto_sync_enabled'
label={t('是否自动同步上游模型更新')}
checkedText={t('开')}
uncheckedText={t('关')}
disabled={!inputs.upstream_model_update_check_enabled}
onChange={(value) =>
handleChannelOtherSettingsChange(
'upstream_model_update_auto_sync_enabled',
value,
)
}
extraText={t(
'开启后检测到新增模型会自动加入当前渠道模型列表',
)}
field='upstream_model_update_auto_sync_enabled'
label={t('是否自动同步上游模型更新')}
checkedText={t('开')}
uncheckedText={t('关')}
disabled={!inputs.upstream_model_update_check_enabled}
onChange={(value) =>
handleChannelOtherSettingsChange(
'upstream_model_update_auto_sync_enabled',
value,
)
}
extraText={t(
'开启后检测到新增模型会自动加入当前渠道模型列表',
)}
/>
<div className='text-xs text-gray-500 mb-3'>
{t('上次检测到可加入模型')}:&nbsp;
{upstreamDetectedModels.length === 0 ? (
t('暂无')
t('暂无')
) : (
<>
<Tooltip
position='topLeft'
content={
<div className='max-w-[640px] break-all text-xs leading-5'>
{upstreamDetectedModels.join(', ')}
</div>
}
>
<>
<Tooltip
position='topLeft'
content={
<div className='max-w-[640px] break-all text-xs leading-5'>
{upstreamDetectedModels.join(', ')}
</div>
}
>
<span className='cursor-help break-all'>
{upstreamDetectedModelsPreview.join(', ')}
</span>
</Tooltip>
<span className='ml-1 text-gray-400'>
</Tooltip>
<span className='ml-1 text-gray-400'>
{upstreamDetectedModelsOmittedCount > 0
? t('(共 {{total}} 个,省略 {{omit}} 个)', {
? t('(共 {{total}} 个,省略 {{omit}} 个)', {
total: upstreamDetectedModels.length,
omit: upstreamDetectedModelsOmittedCount,
})
: t('(共 {{total}} 个)', {
: t('(共 {{total}} 个)', {
total: upstreamDetectedModels.length,
})}
</span>
</>
</>
)}
</div>
<div className='mb-4'>
<div className='flex items-center justify-between gap-2 mb-1'>
<Text className='text-sm font-medium'>{t('参数覆盖')}</Text>
<Text className='text-sm font-medium'>
{t('参数覆盖')}
</Text>
<Space wrap>
<Button
size='small'
type='primary'
icon={<IconCode size={14} />}
onClick={() => setParamOverrideEditorVisible(true)}
size='small'
type='primary'
icon={<IconCode size={14} />}
onClick={() => setParamOverrideEditorVisible(true)}
>
{t('可视化编辑')}
</Button>
<Button
size='small'
onClick={() =>
applyParamOverrideTemplate('operations', 'fill')
}
size='small'
onClick={() =>
applyParamOverrideTemplate('operations', 'fill')
}
>
{t('填充新模板')}
</Button>
<Button
size='small'
onClick={() =>
applyParamOverrideTemplate('legacy', 'fill')
}
size='small'
onClick={() =>
applyParamOverrideTemplate('legacy', 'fill')
}
>
{t('填充旧模板')}
</Button>
@@ -3564,14 +3664,16 @@ const EditChannelModal = (props) => {
</Space>
</div>
<Text type='tertiary' size='small'>
{t('此项可选,用于覆盖请求参数。不支持覆盖 stream 参数')}
{t(
'此项可选,用于覆盖请求参数。不支持覆盖 stream 参数',
)}
</Text>
<div
className='mt-2 rounded-xl p-3'
style={{
backgroundColor: 'var(--semi-color-fill-0)',
border: '1px solid var(--semi-color-fill-2)',
}}
className='mt-2 rounded-xl p-3'
style={{
backgroundColor: 'var(--semi-color-fill-0)',
border: '1px solid var(--semi-color-fill-2)',
}}
>
<div className='flex items-center justify-between mb-2'>
<Tag color={paramOverrideMeta.tagColor}>
@@ -3579,17 +3681,19 @@ const EditChannelModal = (props) => {
</Tag>
<Space spacing={8}>
<Button
size='small'
icon={<IconCopy />}
type='tertiary'
onClick={copyParamOverrideJson}
size='small'
icon={<IconCopy />}
type='tertiary'
onClick={copyParamOverrideJson}
>
{t('复制')}
</Button>
<Button
size='small'
type='tertiary'
onClick={() => setParamOverrideEditorVisible(true)}
size='small'
type='tertiary'
onClick={() =>
setParamOverrideEditorVisible(true)
}
>
{t('编辑')}
</Button>
@@ -3602,80 +3706,80 @@ const EditChannelModal = (props) => {
</div>
<Form.TextArea
field='header_override'
label={t('请求头覆盖')}
placeholder={
t('此项可选,用于覆盖请求头参数') +
'\n' +
t('格式示例:') +
'\n{\n "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0",\n "Authorization": "Bearer {api_key}"\n}'
}
autosize
onChange={(value) =>
handleInputChange('header_override', value)
}
extraText={
<div className='flex flex-col gap-1'>
<div className='flex gap-2 flex-wrap items-center'>
<Text
className='!text-semi-color-primary cursor-pointer'
onClick={() =>
handleInputChange(
'header_override',
JSON.stringify(
{
'*': true,
're:^X-Trace-.*$': true,
'X-Foo': '{client_header:X-Foo}',
Authorization: 'Bearer {api_key}',
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0',
},
null,
2,
),
)
}
>
{t('填入模板')}
</Text>
<Text
className='!text-semi-color-primary cursor-pointer'
onClick={() =>
handleInputChange(
'header_override',
JSON.stringify(
{
'*': true,
},
null,
2,
),
)
}
>
{t('填入透传模版')}
</Text>
<Text
className='!text-semi-color-primary cursor-pointer'
onClick={() => formatJsonField('header_override')}
>
{t('格式化')}
</Text>
</div>
<div>
<Text type='tertiary' size='small'>
{t('支持变量:')}
</Text>
<div className='text-xs text-tertiary ml-2'>
<div>
{t('渠道密钥')}: {'{api_key}'}
</div>
field='header_override'
label={t('请求头覆盖')}
placeholder={
t('此项可选,用于覆盖请求头参数') +
'\n' +
t('格式示例:') +
'\n{\n "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0",\n "Authorization": "Bearer {api_key}"\n}'
}
autosize
onChange={(value) =>
handleInputChange('header_override', value)
}
extraText={
<div className='flex flex-col gap-1'>
<div className='flex gap-2 flex-wrap items-center'>
<Text
className='!text-semi-color-primary cursor-pointer'
onClick={() =>
handleInputChange(
'header_override',
JSON.stringify(
{
'*': true,
're:^X-Trace-.*$': true,
'X-Foo': '{client_header:X-Foo}',
Authorization: 'Bearer {api_key}',
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36 Edg/139.0.0.0',
},
null,
2,
),
)
}
>
{t('填入模板')}
</Text>
<Text
className='!text-semi-color-primary cursor-pointer'
onClick={() =>
handleInputChange(
'header_override',
JSON.stringify(
{
'*': true,
},
null,
2,
),
)
}
>
{t('填入透传模版')}
</Text>
<Text
className='!text-semi-color-primary cursor-pointer'
onClick={() => formatJsonField('header_override')}
>
{t('格式化')}
</Text>
</div>
<div>
<Text type='tertiary' size='small'>
{t('支持变量:')}
</Text>
<div className='text-xs text-tertiary ml-2'>
<div>
{t('渠道密钥')}: {'{api_key}'}
</div>
</div>
</div>
}
showClear
</div>
}
showClear
/>
<JSONEditor
key={`status_code_mapping-${isEdit ? channelId : 'new'}`}

View File

@@ -60,6 +60,7 @@ const ModelTestModal = ({
const streamToggleDisabled = [
'embeddings',
'image-generation',
'image-edit',
'jina-rerank',
'openai-response-compact',
].includes(selectedEndpointType);
@@ -96,6 +97,10 @@ const ModelTestModal = ({
value: 'image-generation',
label: t('图像生成') + ' (/v1/images/generations)',
},
{
value: 'image-edit',
label: 'OpenAI Image Edit (/v1/images/edits)',
},
{ value: 'embeddings', label: 'Embeddings (/v1/embeddings)' },
];

View File

@@ -50,6 +50,7 @@ const ENDPOINT_TEMPLATE = {
gemini: { path: '/v1beta/models/{model}:generateContent', method: 'POST' },
'jina-rerank': { path: '/v1/rerank', method: 'POST' },
'image-generation': { path: '/v1/images/generations', method: 'POST' },
'image-edit': { path: '/v1/images/edits', method: 'POST' },
};
const nameRuleOptions = [

View File

@@ -48,6 +48,7 @@ const ENDPOINT_TEMPLATE = {
gemini: { path: '/v1beta/models/{model}:generateContent', method: 'POST' },
'jina-rerank': { path: '/v1/rerank', method: 'POST' },
'image-generation': { path: '/v1/images/generations', method: 'POST' },
'image-edit': { path: '/v1/images/edits', method: 'POST' },
};
const EditPrefillGroupModal = ({

View File

@@ -189,11 +189,26 @@ export const CHANNEL_OPTIONS = [
color: 'blue',
label: 'Codex (OpenAI OAuth)',
},
{
value: 58,
color: 'cyan',
label: 'OpenAI Official / Codex CLI (/openai BaseURL)',
},
{
value: 59,
color: 'indigo',
label: 'Claude Code / Anthropic Gateway',
},
{
value: 60,
color: 'green',
label: 'Gemini CLI / OpenAI Compat',
},
];
// Channel types that support upstream model list fetching in UI.
export const MODEL_FETCHABLE_CHANNEL_TYPES = new Set([
1, 4, 14, 34, 17, 26, 27, 24, 47, 25, 20, 23, 31, 40, 42, 48, 43,
1, 4, 14, 34, 17, 26, 27, 24, 47, 25, 20, 23, 31, 40, 42, 48, 43, 58, 59, 60,
]);
export const MODEL_TABLE_PAGE_SIZE = 10;