feat: 新增服务商 + 文件上传优化 + AskQuestion 改进
新增功能: - 新增字节火山方舟 (Volcengine Ark) 快速配置预设 - 支持用户输入模型名称,映射到聊天输入框模型选择器 - 模型名称存入 extra_env 的 ANTHROPIC_MODEL - 新增阿里云百炼 (Aliyun Bailian) 快速配置预设 - 内置 7 个模型供用户直接选择(Qwen 3.5 Plus、Kimi K2.5、GLM-5 等) - 无需额外配置模型名称 - 两个服务商均使用 @lobehub/icons 品牌图标 (Volcengine, Bailian) - ProviderManager 图标识别规则新增 volces.com/dashscope 等 URL 匹配 - PresetConnectDialog 新增 model_names 字段类型,渲染模型名称输入框 - models route 动态读取 extra_env 中的 ANTHROPIC_MODEL 展示到模型选择器 修复 & 优化: - 移除文件上传的类型限制 (ACCEPTED_FILE_TYPES) 和大小限制 (MAX_FILE_SIZE) 现在支持所有文件类型和任意大小,由 API 端处理限制 - AskQuestion 按钮样式对齐权限确认按钮 (h-8 px-3 text-sm font-medium) - AskQuestion 组件渲染位置从工具调用区域移到流式文本下方 解决长文本输出后用户找不到选择组件的问题 - Anthropic Third-party API 预设的 provider_type 从 "custom" 改为 "anthropic" 修复第三方 Anthropic 兼容 API 无法正确生效的问题 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
d872f91478
commit
68bb6b60bb
8 files changed
+102
-39
No files matched your search
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "codepilot",
|
||||
"version": "0.19.2",
|
||||
"version": "0.20.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "codepilot",
|
||||
"version": "0.19.2",
|
||||
"version": "0.20.0",
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^3.0.47",
|
||||
"@ai-sdk/google": "^3.0.31",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "codepilot",
|
||||
"version": "0.19.2",
|
||||
"version": "0.20.0",
|
||||
"private": true,
|
||||
"author": {
|
||||
"name": "op7418",
|
||||
|
||||
@@ -51,6 +51,15 @@ const PROVIDER_MODEL_LABELS: Record<string, { value: string; label: string }[]>
|
||||
{ value: 'opus', label: 'Opus 4.6' },
|
||||
{ value: 'haiku', label: 'Haiku 4.5' },
|
||||
],
|
||||
'https://coding.dashscope.aliyuncs.com/apps/anthropic': [
|
||||
{ value: 'qwen3.5-plus', label: 'Qwen 3.5 Plus' },
|
||||
{ value: 'qwen3-coder-next', label: 'Qwen 3 Coder Next' },
|
||||
{ value: 'qwen3-coder-plus', label: 'Qwen 3 Coder Plus' },
|
||||
{ value: 'kimi-k2.5', label: 'Kimi K2.5' },
|
||||
{ value: 'glm-5', label: 'GLM-5' },
|
||||
{ value: 'glm-4.7', label: 'GLM-4.7' },
|
||||
{ value: 'MiniMax-M2.5', label: 'MiniMax-M2.5' },
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -90,7 +99,19 @@ export async function GET() {
|
||||
for (const provider of providers) {
|
||||
if (MEDIA_PROVIDER_TYPES.has(provider.provider_type)) continue;
|
||||
const matched = PROVIDER_MODEL_LABELS[provider.base_url];
|
||||
const rawModels = matched || DEFAULT_MODELS;
|
||||
let rawModels = matched || DEFAULT_MODELS;
|
||||
|
||||
// For providers with ANTHROPIC_MODEL in extra_env (e.g. Volcengine Ark),
|
||||
// show the configured model name in the selector
|
||||
if (!matched) {
|
||||
try {
|
||||
const envObj = JSON.parse(provider.extra_env || '{}');
|
||||
if (envObj.ANTHROPIC_MODEL) {
|
||||
rawModels = [{ value: envObj.ANTHROPIC_MODEL, label: envObj.ANTHROPIC_MODEL }];
|
||||
}
|
||||
} catch { /* use default */ }
|
||||
}
|
||||
|
||||
const models = deduplicateModels(rawModels);
|
||||
|
||||
groups.push({
|
||||
|
||||
@@ -81,18 +81,6 @@ const IMAGE_AGENT_SYSTEM_PROMPT = `你是一个图像生成助手。当用户请
|
||||
- 如果用户要求修改上一张生成的图片,必须加 "useLastGenerated": true
|
||||
- 在输出结构化块之前,可以先简要说明你的理解和计划`;
|
||||
|
||||
// Accepted file types for upload
|
||||
const ACCEPTED_FILE_TYPES = [
|
||||
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
|
||||
'application/pdf',
|
||||
'text/*',
|
||||
'.md', '.json', '.csv', '.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.rs',
|
||||
].join(',');
|
||||
|
||||
// Max file sizes
|
||||
const MAX_IMAGE_SIZE = 5 * 1024 * 1024; // 5MB
|
||||
const MAX_DOC_SIZE = 10 * 1024 * 1024; // 10MB
|
||||
const MAX_FILE_SIZE = MAX_DOC_SIZE; // Use larger limit; we validate per-type in conversion
|
||||
|
||||
interface MessageInputProps {
|
||||
onSend: (content: string, files?: FileAttachment[], systemPromptAppend?: string, displayOverride?: string) => void;
|
||||
@@ -631,12 +619,7 @@ export function MessageInput({
|
||||
file.filename || 'file',
|
||||
file.mediaType || 'application/octet-stream',
|
||||
);
|
||||
// Enforce per-type size limits
|
||||
const isImage = attachment.type.startsWith('image/');
|
||||
const sizeLimit = isImage ? MAX_IMAGE_SIZE : MAX_DOC_SIZE;
|
||||
if (attachment.size <= sizeLimit) {
|
||||
attachments.push(attachment);
|
||||
}
|
||||
attachments.push(attachment);
|
||||
} catch {
|
||||
// Skip files that fail conversion
|
||||
}
|
||||
@@ -1082,9 +1065,8 @@ export function MessageInput({
|
||||
{/* PromptInput replaces the old input area */}
|
||||
<PromptInput
|
||||
onSubmit={handleSubmit}
|
||||
accept={ACCEPTED_FILE_TYPES}
|
||||
accept=""
|
||||
multiple
|
||||
maxFileSize={MAX_FILE_SIZE}
|
||||
>
|
||||
{/* Bridge: listens for file tree "+" button events */}
|
||||
<FileTreeAttachmentBridge />
|
||||
|
||||
@@ -219,7 +219,7 @@ function AskUserQuestionUI({
|
||||
<button
|
||||
key={opt.label}
|
||||
onClick={() => toggleOption(qIdx, opt.label, q.multiSelect)}
|
||||
className={`rounded-lg border px-3 py-1.5 text-xs transition-colors ${
|
||||
className={`rounded-lg border h-8 px-3 text-sm font-medium transition-colors ${
|
||||
isSelected
|
||||
? 'border-primary bg-primary/10 text-primary'
|
||||
: 'border-border bg-background text-foreground hover:bg-muted'
|
||||
@@ -236,7 +236,7 @@ function AskUserQuestionUI({
|
||||
{/* Other option */}
|
||||
<button
|
||||
onClick={() => toggleOther(qIdx, q.multiSelect)}
|
||||
className={`rounded-lg border px-3 py-1.5 text-xs transition-colors ${
|
||||
className={`rounded-lg border h-8 px-3 text-sm font-medium transition-colors ${
|
||||
useOther[qIdx]
|
||||
? 'border-primary bg-primary/10 text-primary'
|
||||
: 'border-border bg-background text-foreground hover:bg-muted'
|
||||
@@ -261,7 +261,7 @@ function AskUserQuestionUI({
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={!hasAnswer}
|
||||
className="rounded-lg bg-primary px-4 py-1.5 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-40"
|
||||
className="rounded-lg bg-primary h-8 px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-40"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
@@ -439,17 +439,6 @@ export function StreamingMessage({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Permission approval — AskUserQuestion gets a dedicated UI */}
|
||||
{pendingPermission?.toolName === 'AskUserQuestion' && !permissionResolved && (
|
||||
<AskUserQuestionUI
|
||||
toolInput={pendingPermission.toolInput as Record<string, unknown>}
|
||||
onSubmit={(decision, updatedInput) => onPermissionResponse?.(decision, updatedInput)}
|
||||
/>
|
||||
)}
|
||||
{pendingPermission?.toolName === 'AskUserQuestion' && permissionResolved && (
|
||||
<p className="py-1 text-xs text-green-600 dark:text-green-400">Answer submitted</p>
|
||||
)}
|
||||
|
||||
{/* Permission approval — ExitPlanMode gets a dedicated UI */}
|
||||
{pendingPermission?.toolName === 'ExitPlanMode' && !permissionResolved && (
|
||||
<ExitPlanModeUI
|
||||
@@ -576,6 +565,17 @@ export function StreamingMessage({
|
||||
return stripped ? <MessageResponse>{stripped}</MessageResponse> : null;
|
||||
})()}
|
||||
|
||||
{/* Permission approval — AskUserQuestion rendered after text so it stays in view */}
|
||||
{pendingPermission?.toolName === 'AskUserQuestion' && !permissionResolved && (
|
||||
<AskUserQuestionUI
|
||||
toolInput={pendingPermission.toolInput as Record<string, unknown>}
|
||||
onSubmit={(decision, updatedInput) => onPermissionResponse?.(decision, updatedInput)}
|
||||
/>
|
||||
)}
|
||||
{pendingPermission?.toolName === 'AskUserQuestion' && permissionResolved && (
|
||||
<p className="py-1 text-xs text-green-600 dark:text-green-400">Answer submitted</p>
|
||||
)}
|
||||
|
||||
{/* Loading indicator when no content yet */}
|
||||
{isStreaming && !content && toolUses.length === 0 && !pendingPermission && (
|
||||
<div className="py-2">
|
||||
|
||||
@@ -37,6 +37,7 @@ import { ProviderForm } from "./ProviderForm";
|
||||
import type { ProviderFormData } from "./ProviderForm";
|
||||
import type { ApiProvider } from "@/types";
|
||||
import { useTranslation } from "@/hooks/useTranslation";
|
||||
import type { TranslationKey } from "@/i18n";
|
||||
import Anthropic from "@lobehub/icons/es/Anthropic";
|
||||
import OpenRouter from "@lobehub/icons/es/OpenRouter";
|
||||
import Zhipu from "@lobehub/icons/es/Zhipu";
|
||||
@@ -46,6 +47,8 @@ import Minimax from "@lobehub/icons/es/Minimax";
|
||||
import Aws from "@lobehub/icons/es/Aws";
|
||||
import Bedrock from "@lobehub/icons/es/Bedrock";
|
||||
import Google from "@lobehub/icons/es/Google";
|
||||
import Volcengine from "@lobehub/icons/es/Volcengine";
|
||||
import Bailian from "@lobehub/icons/es/Bailian";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Brand icon resolver
|
||||
@@ -62,6 +65,10 @@ function getProviderIcon(name: string, baseUrl: string): ReactNode {
|
||||
if (url.includes("kimi.com") || lower.includes("kimi")) return <Kimi size={18} />;
|
||||
if (url.includes("moonshot") || lower.includes("moonshot")) return <Moonshot size={18} />;
|
||||
if (url.includes("minimax") || lower.includes("minimax")) return <Minimax size={18} />;
|
||||
if (url.includes("volces.com") || url.includes("volcengine") || lower.includes("volcengine") || lower.includes("火山") || lower.includes("doubao") || lower.includes("豆包"))
|
||||
return <Volcengine size={18} />;
|
||||
if (url.includes("dashscope") || lower.includes("bailian") || lower.includes("百炼") || lower.includes("aliyun"))
|
||||
return <Bailian size={18} />;
|
||||
if (lower.includes("bedrock")) return <Bedrock size={18} />;
|
||||
if (lower.includes("vertex") || lower.includes("google")) return <Google size={18} />;
|
||||
if (lower.includes("aws")) return <Aws size={18} />;
|
||||
@@ -118,7 +125,7 @@ const QUICK_PRESETS: QuickPreset[] = [
|
||||
description: "Anthropic-compatible API — provide URL and Key",
|
||||
descriptionZh: "Anthropic 兼容第三方 API — 填写地址和密钥",
|
||||
icon: <Anthropic size={18} />,
|
||||
provider_type: "custom",
|
||||
provider_type: "anthropic",
|
||||
base_url: "",
|
||||
extra_env: '{"ANTHROPIC_API_KEY":""}',
|
||||
fields: ["name", "api_key", "base_url", "model_names"],
|
||||
@@ -211,6 +218,28 @@ const QUICK_PRESETS: QuickPreset[] = [
|
||||
extra_env: '{"API_TIMEOUT_MS":"3000000","CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":"1","ANTHROPIC_API_KEY":""}',
|
||||
fields: ["api_key"],
|
||||
},
|
||||
{
|
||||
key: "volcengine",
|
||||
name: "Volcengine Ark",
|
||||
description: "Volcengine Ark Coding Plan — Doubao, GLM, DeepSeek, Kimi",
|
||||
descriptionZh: "字节火山方舟 Coding Plan — 豆包、GLM、DeepSeek、Kimi",
|
||||
icon: <Volcengine size={18} />,
|
||||
provider_type: "custom",
|
||||
base_url: "https://ark.cn-beijing.volces.com/api/coding",
|
||||
extra_env: '{"ANTHROPIC_AUTH_TOKEN":""}',
|
||||
fields: ["api_key", "model_names"],
|
||||
},
|
||||
{
|
||||
key: "bailian",
|
||||
name: "Aliyun Bailian",
|
||||
description: "Aliyun Bailian Coding Plan — Qwen, GLM, Kimi, MiniMax",
|
||||
descriptionZh: "阿里云百炼 Coding Plan — 通义千问、GLM、Kimi、MiniMax",
|
||||
icon: <Bailian size={18} />,
|
||||
provider_type: "custom",
|
||||
base_url: "https://coding.dashscope.aliyuncs.com/apps/anthropic",
|
||||
extra_env: '{"ANTHROPIC_AUTH_TOKEN":""}',
|
||||
fields: ["api_key"],
|
||||
},
|
||||
{
|
||||
key: "bedrock",
|
||||
name: "AWS Bedrock",
|
||||
@@ -294,6 +323,7 @@ function PresetConnectDialog({
|
||||
const [baseUrl, setBaseUrl] = useState("");
|
||||
const [name, setName] = useState("");
|
||||
const [extraEnv, setExtraEnv] = useState("{}");
|
||||
const [modelName, setModelName] = useState("");
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -307,6 +337,7 @@ function PresetConnectDialog({
|
||||
setBaseUrl(preset.base_url);
|
||||
setName(preset.name);
|
||||
setExtraEnv(preset.extra_env);
|
||||
setModelName("");
|
||||
setError(null);
|
||||
setSaving(false);
|
||||
setShowAdvanced(false);
|
||||
@@ -329,6 +360,15 @@ function PresetConnectDialog({
|
||||
} catch { /* use as-is */ }
|
||||
}
|
||||
|
||||
// Inject model name into extra_env if model_names field is used
|
||||
if (preset.fields.includes("model_names") && modelName.trim()) {
|
||||
try {
|
||||
const envObj = JSON.parse(finalExtraEnv);
|
||||
envObj["ANTHROPIC_MODEL"] = modelName.trim();
|
||||
finalExtraEnv = JSON.stringify(envObj);
|
||||
} catch { /* use as-is */ }
|
||||
}
|
||||
|
||||
// Validate extra_env JSON
|
||||
try {
|
||||
JSON.parse(finalExtraEnv);
|
||||
@@ -410,6 +450,24 @@ function PresetConnectDialog({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Model name — for providers that need user-specified model */}
|
||||
{preset.fields.includes("model_names") && (
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-muted-foreground">{t('provider.modelName' as TranslationKey)}</Label>
|
||||
<Input
|
||||
value={modelName}
|
||||
onChange={(e) => setModelName(e.target.value)}
|
||||
placeholder="ark-code-latest"
|
||||
className="text-sm font-mono"
|
||||
/>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
{isZh
|
||||
? '在服务商控制台配置的模型名称,如 ark-code-latest、doubao-seed-2.0-code'
|
||||
: 'Model name configured in provider console, e.g. ark-code-latest'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Extra env — bedrock/vertex/custom always shown */}
|
||||
{preset.fields.includes("extra_env") && (
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -128,6 +128,7 @@ const en = {
|
||||
'provider.providerType': 'Provider Type',
|
||||
'provider.apiKey': 'API Key',
|
||||
'provider.baseUrl': 'Base URL',
|
||||
'provider.modelName': 'Model Name',
|
||||
'provider.advancedOptions': 'Advanced Options',
|
||||
'provider.extraEnvVars': 'Extra Environment Variables',
|
||||
'provider.notes': 'Notes',
|
||||
|
||||
@@ -125,6 +125,7 @@ const zh: Record<TranslationKey, string> = {
|
||||
'provider.providerType': '服务商类型',
|
||||
'provider.apiKey': 'API 密钥',
|
||||
'provider.baseUrl': '基础 URL',
|
||||
'provider.modelName': '模型名称',
|
||||
'provider.advancedOptions': '高级选项',
|
||||
'provider.extraEnvVars': '额外环境变量',
|
||||
'provider.notes': '备注',
|
||||
|
||||
Reference in new issue
Block a user