Skip to content

Commit 68bb6b6

Browse files
7418claude
andcommitted
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>
1 parent d872f91 commit 68bb6b6

File tree

8 files changed

+102
-39
lines changed

8 files changed

+102
-39
lines changed

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codepilot",
3-
"version": "0.19.2",
3+
"version": "0.20.0",
44
"private": true,
55
"author": {
66
"name": "op7418",

src/app/api/providers/models/route.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,15 @@ const PROVIDER_MODEL_LABELS: Record<string, { value: string; label: string }[]>
5151
{ value: 'opus', label: 'Opus 4.6' },
5252
{ value: 'haiku', label: 'Haiku 4.5' },
5353
],
54+
'https://coding.dashscope.aliyuncs.com/apps/anthropic': [
55+
{ value: 'qwen3.5-plus', label: 'Qwen 3.5 Plus' },
56+
{ value: 'qwen3-coder-next', label: 'Qwen 3 Coder Next' },
57+
{ value: 'qwen3-coder-plus', label: 'Qwen 3 Coder Plus' },
58+
{ value: 'kimi-k2.5', label: 'Kimi K2.5' },
59+
{ value: 'glm-5', label: 'GLM-5' },
60+
{ value: 'glm-4.7', label: 'GLM-4.7' },
61+
{ value: 'MiniMax-M2.5', label: 'MiniMax-M2.5' },
62+
],
5463
};
5564

5665
/**
@@ -90,7 +99,19 @@ export async function GET() {
9099
for (const provider of providers) {
91100
if (MEDIA_PROVIDER_TYPES.has(provider.provider_type)) continue;
92101
const matched = PROVIDER_MODEL_LABELS[provider.base_url];
93-
const rawModels = matched || DEFAULT_MODELS;
102+
let rawModels = matched || DEFAULT_MODELS;
103+
104+
// For providers with ANTHROPIC_MODEL in extra_env (e.g. Volcengine Ark),
105+
// show the configured model name in the selector
106+
if (!matched) {
107+
try {
108+
const envObj = JSON.parse(provider.extra_env || '{}');
109+
if (envObj.ANTHROPIC_MODEL) {
110+
rawModels = [{ value: envObj.ANTHROPIC_MODEL, label: envObj.ANTHROPIC_MODEL }];
111+
}
112+
} catch { /* use default */ }
113+
}
114+
94115
const models = deduplicateModels(rawModels);
95116

96117
groups.push({

src/components/chat/MessageInput.tsx

Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -81,18 +81,6 @@ const IMAGE_AGENT_SYSTEM_PROMPT = `你是一个图像生成助手。当用户请
8181
- 如果用户要求修改上一张生成的图片,必须加 "useLastGenerated": true
8282
- 在输出结构化块之前,可以先简要说明你的理解和计划`;
8383

84-
// Accepted file types for upload
85-
const ACCEPTED_FILE_TYPES = [
86-
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
87-
'application/pdf',
88-
'text/*',
89-
'.md', '.json', '.csv', '.ts', '.tsx', '.js', '.jsx', '.py', '.go', '.rs',
90-
].join(',');
91-
92-
// Max file sizes
93-
const MAX_IMAGE_SIZE = 5 * 1024 * 1024; // 5MB
94-
const MAX_DOC_SIZE = 10 * 1024 * 1024; // 10MB
95-
const MAX_FILE_SIZE = MAX_DOC_SIZE; // Use larger limit; we validate per-type in conversion
9684

9785
interface MessageInputProps {
9886
onSend: (content: string, files?: FileAttachment[], systemPromptAppend?: string, displayOverride?: string) => void;
@@ -631,12 +619,7 @@ export function MessageInput({
631619
file.filename || 'file',
632620
file.mediaType || 'application/octet-stream',
633621
);
634-
// Enforce per-type size limits
635-
const isImage = attachment.type.startsWith('image/');
636-
const sizeLimit = isImage ? MAX_IMAGE_SIZE : MAX_DOC_SIZE;
637-
if (attachment.size <= sizeLimit) {
638-
attachments.push(attachment);
639-
}
622+
attachments.push(attachment);
640623
} catch {
641624
// Skip files that fail conversion
642625
}
@@ -1082,9 +1065,8 @@ export function MessageInput({
10821065
{/* PromptInput replaces the old input area */}
10831066
<PromptInput
10841067
onSubmit={handleSubmit}
1085-
accept={ACCEPTED_FILE_TYPES}
1068+
accept=""
10861069
multiple
1087-
maxFileSize={MAX_FILE_SIZE}
10881070
>
10891071
{/* Bridge: listens for file tree "+" button events */}
10901072
<FileTreeAttachmentBridge />

src/components/chat/StreamingMessage.tsx

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ function AskUserQuestionUI({
219219
<button
220220
key={opt.label}
221221
onClick={() => toggleOption(qIdx, opt.label, q.multiSelect)}
222-
className={`rounded-lg border px-3 py-1.5 text-xs transition-colors ${
222+
className={`rounded-lg border h-8 px-3 text-sm font-medium transition-colors ${
223223
isSelected
224224
? 'border-primary bg-primary/10 text-primary'
225225
: 'border-border bg-background text-foreground hover:bg-muted'
@@ -236,7 +236,7 @@ function AskUserQuestionUI({
236236
{/* Other option */}
237237
<button
238238
onClick={() => toggleOther(qIdx, q.multiSelect)}
239-
className={`rounded-lg border px-3 py-1.5 text-xs transition-colors ${
239+
className={`rounded-lg border h-8 px-3 text-sm font-medium transition-colors ${
240240
useOther[qIdx]
241241
? 'border-primary bg-primary/10 text-primary'
242242
: 'border-border bg-background text-foreground hover:bg-muted'
@@ -261,7 +261,7 @@ function AskUserQuestionUI({
261261
<button
262262
onClick={handleSubmit}
263263
disabled={!hasAnswer}
264-
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"
264+
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"
265265
>
266266
Submit
267267
</button>
@@ -439,17 +439,6 @@ export function StreamingMessage({
439439
/>
440440
)}
441441

442-
{/* Permission approval — AskUserQuestion gets a dedicated UI */}
443-
{pendingPermission?.toolName === 'AskUserQuestion' && !permissionResolved && (
444-
<AskUserQuestionUI
445-
toolInput={pendingPermission.toolInput as Record<string, unknown>}
446-
onSubmit={(decision, updatedInput) => onPermissionResponse?.(decision, updatedInput)}
447-
/>
448-
)}
449-
{pendingPermission?.toolName === 'AskUserQuestion' && permissionResolved && (
450-
<p className="py-1 text-xs text-green-600 dark:text-green-400">Answer submitted</p>
451-
)}
452-
453442
{/* Permission approval — ExitPlanMode gets a dedicated UI */}
454443
{pendingPermission?.toolName === 'ExitPlanMode' && !permissionResolved && (
455444
<ExitPlanModeUI
@@ -576,6 +565,17 @@ export function StreamingMessage({
576565
return stripped ? <MessageResponse>{stripped}</MessageResponse> : null;
577566
})()}
578567

568+
{/* Permission approval — AskUserQuestion rendered after text so it stays in view */}
569+
{pendingPermission?.toolName === 'AskUserQuestion' && !permissionResolved && (
570+
<AskUserQuestionUI
571+
toolInput={pendingPermission.toolInput as Record<string, unknown>}
572+
onSubmit={(decision, updatedInput) => onPermissionResponse?.(decision, updatedInput)}
573+
/>
574+
)}
575+
{pendingPermission?.toolName === 'AskUserQuestion' && permissionResolved && (
576+
<p className="py-1 text-xs text-green-600 dark:text-green-400">Answer submitted</p>
577+
)}
578+
579579
{/* Loading indicator when no content yet */}
580580
{isStreaming && !content && toolUses.length === 0 && !pendingPermission && (
581581
<div className="py-2">

src/components/settings/ProviderManager.tsx

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import { ProviderForm } from "./ProviderForm";
3737
import type { ProviderFormData } from "./ProviderForm";
3838
import type { ApiProvider } from "@/types";
3939
import { useTranslation } from "@/hooks/useTranslation";
40+
import type { TranslationKey } from "@/i18n";
4041
import Anthropic from "@lobehub/icons/es/Anthropic";
4142
import OpenRouter from "@lobehub/icons/es/OpenRouter";
4243
import Zhipu from "@lobehub/icons/es/Zhipu";
@@ -46,6 +47,8 @@ import Minimax from "@lobehub/icons/es/Minimax";
4647
import Aws from "@lobehub/icons/es/Aws";
4748
import Bedrock from "@lobehub/icons/es/Bedrock";
4849
import Google from "@lobehub/icons/es/Google";
50+
import Volcengine from "@lobehub/icons/es/Volcengine";
51+
import Bailian from "@lobehub/icons/es/Bailian";
4952

5053
// ---------------------------------------------------------------------------
5154
// Brand icon resolver
@@ -62,6 +65,10 @@ function getProviderIcon(name: string, baseUrl: string): ReactNode {
6265
if (url.includes("kimi.com") || lower.includes("kimi")) return <Kimi size={18} />;
6366
if (url.includes("moonshot") || lower.includes("moonshot")) return <Moonshot size={18} />;
6467
if (url.includes("minimax") || lower.includes("minimax")) return <Minimax size={18} />;
68+
if (url.includes("volces.com") || url.includes("volcengine") || lower.includes("volcengine") || lower.includes("火山") || lower.includes("doubao") || lower.includes("豆包"))
69+
return <Volcengine size={18} />;
70+
if (url.includes("dashscope") || lower.includes("bailian") || lower.includes("百炼") || lower.includes("aliyun"))
71+
return <Bailian size={18} />;
6572
if (lower.includes("bedrock")) return <Bedrock size={18} />;
6673
if (lower.includes("vertex") || lower.includes("google")) return <Google size={18} />;
6774
if (lower.includes("aws")) return <Aws size={18} />;
@@ -118,7 +125,7 @@ const QUICK_PRESETS: QuickPreset[] = [
118125
description: "Anthropic-compatible API — provide URL and Key",
119126
descriptionZh: "Anthropic 兼容第三方 API — 填写地址和密钥",
120127
icon: <Anthropic size={18} />,
121-
provider_type: "custom",
128+
provider_type: "anthropic",
122129
base_url: "",
123130
extra_env: '{"ANTHROPIC_API_KEY":""}',
124131
fields: ["name", "api_key", "base_url", "model_names"],
@@ -211,6 +218,28 @@ const QUICK_PRESETS: QuickPreset[] = [
211218
extra_env: '{"API_TIMEOUT_MS":"3000000","CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC":"1","ANTHROPIC_API_KEY":""}',
212219
fields: ["api_key"],
213220
},
221+
{
222+
key: "volcengine",
223+
name: "Volcengine Ark",
224+
description: "Volcengine Ark Coding Plan — Doubao, GLM, DeepSeek, Kimi",
225+
descriptionZh: "字节火山方舟 Coding Plan — 豆包、GLM、DeepSeek、Kimi",
226+
icon: <Volcengine size={18} />,
227+
provider_type: "custom",
228+
base_url: "https://ark.cn-beijing.volces.com/api/coding",
229+
extra_env: '{"ANTHROPIC_AUTH_TOKEN":""}',
230+
fields: ["api_key", "model_names"],
231+
},
232+
{
233+
key: "bailian",
234+
name: "Aliyun Bailian",
235+
description: "Aliyun Bailian Coding Plan — Qwen, GLM, Kimi, MiniMax",
236+
descriptionZh: "阿里云百炼 Coding Plan — 通义千问、GLM、Kimi、MiniMax",
237+
icon: <Bailian size={18} />,
238+
provider_type: "custom",
239+
base_url: "https://coding.dashscope.aliyuncs.com/apps/anthropic",
240+
extra_env: '{"ANTHROPIC_AUTH_TOKEN":""}',
241+
fields: ["api_key"],
242+
},
214243
{
215244
key: "bedrock",
216245
name: "AWS Bedrock",
@@ -294,6 +323,7 @@ function PresetConnectDialog({
294323
const [baseUrl, setBaseUrl] = useState("");
295324
const [name, setName] = useState("");
296325
const [extraEnv, setExtraEnv] = useState("{}");
326+
const [modelName, setModelName] = useState("");
297327
const [showAdvanced, setShowAdvanced] = useState(false);
298328
const [error, setError] = useState<string | null>(null);
299329
const [saving, setSaving] = useState(false);
@@ -307,6 +337,7 @@ function PresetConnectDialog({
307337
setBaseUrl(preset.base_url);
308338
setName(preset.name);
309339
setExtraEnv(preset.extra_env);
340+
setModelName("");
310341
setError(null);
311342
setSaving(false);
312343
setShowAdvanced(false);
@@ -329,6 +360,15 @@ function PresetConnectDialog({
329360
} catch { /* use as-is */ }
330361
}
331362

363+
// Inject model name into extra_env if model_names field is used
364+
if (preset.fields.includes("model_names") && modelName.trim()) {
365+
try {
366+
const envObj = JSON.parse(finalExtraEnv);
367+
envObj["ANTHROPIC_MODEL"] = modelName.trim();
368+
finalExtraEnv = JSON.stringify(envObj);
369+
} catch { /* use as-is */ }
370+
}
371+
332372
// Validate extra_env JSON
333373
try {
334374
JSON.parse(finalExtraEnv);
@@ -410,6 +450,24 @@ function PresetConnectDialog({
410450
</div>
411451
)}
412452

453+
{/* Model name — for providers that need user-specified model */}
454+
{preset.fields.includes("model_names") && (
455+
<div className="space-y-2">
456+
<Label className="text-xs text-muted-foreground">{t('provider.modelName' as TranslationKey)}</Label>
457+
<Input
458+
value={modelName}
459+
onChange={(e) => setModelName(e.target.value)}
460+
placeholder="ark-code-latest"
461+
className="text-sm font-mono"
462+
/>
463+
<p className="text-[11px] text-muted-foreground">
464+
{isZh
465+
? '在服务商控制台配置的模型名称,如 ark-code-latest、doubao-seed-2.0-code'
466+
: 'Model name configured in provider console, e.g. ark-code-latest'}
467+
</p>
468+
</div>
469+
)}
470+
413471
{/* Extra env — bedrock/vertex/custom always shown */}
414472
{preset.fields.includes("extra_env") && (
415473
<div className="space-y-2">

src/i18n/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ const en = {
128128
'provider.providerType': 'Provider Type',
129129
'provider.apiKey': 'API Key',
130130
'provider.baseUrl': 'Base URL',
131+
'provider.modelName': 'Model Name',
131132
'provider.advancedOptions': 'Advanced Options',
132133
'provider.extraEnvVars': 'Extra Environment Variables',
133134
'provider.notes': 'Notes',

src/i18n/zh.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ const zh: Record<TranslationKey, string> = {
125125
'provider.providerType': '服务商类型',
126126
'provider.apiKey': 'API 密钥',
127127
'provider.baseUrl': '基础 URL',
128+
'provider.modelName': '模型名称',
128129
'provider.advancedOptions': '高级选项',
129130
'provider.extraEnvVars': '额外环境变量',
130131
'provider.notes': '备注',

0 commit comments

Comments
 (0)