Skip to content

Commit 97ff29f

Browse files
committed
feat(ai): add parallel processing and configurable settings
- Add configurable request interval (default 1s) and concurrency (default 3) - Implement parallel processing for batch classification - Prompt now requires classification (no empty results allowed) - Fix sidebar scrolling when too many Lists - Fix repository names overflow in batch classify dialog - Add concurrency and interval settings in AI config page Bump version to 1.2.3
1 parent 36927d1 commit 97ff29f

9 files changed

Lines changed: 179 additions & 52 deletions

File tree

CHANGELOG.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
---
1111

12+
## [1.2.3] - 2025-01-16
13+
14+
### Added / 新增
15+
16+
- Configurable request interval and concurrency for batch classification
17+
- 可配置批量分类的请求间隔和并发数
18+
- Parallel processing support (default 3 concurrent requests)
19+
- 并行处理支持(默认 3 并发)
20+
21+
### Improved / 改进
22+
23+
- Prompt now requires classification (no empty results)
24+
- Prompt 现在强制要求给出分类(不允许空结果)
25+
- Fixed sidebar scrolling issue when too many Lists
26+
- 修复 Lists 过多时侧边栏无法滚动的问题
27+
- Fixed repository names overflow in batch classify dialog
28+
- 修复批量分类对话框中仓库名称溢出问题
29+
- Reduced default request interval from 2.5s to 1s
30+
- 默认请求间隔从 2.5 秒降至 1 秒
31+
32+
---
33+
1234
## [1.2.2] - 2025-01-15
1335

1436
### Added / 新增
@@ -107,6 +129,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
107129

108130
---
109131

132+
[1.2.3]: https://github.com/GEMILUXVII/starflow/compare/v1.2.2...v1.2.3
110133
[1.2.2]: https://github.com/GEMILUXVII/starflow/compare/v1.2.1...v1.2.2
111134
[1.2.1]: https://github.com/GEMILUXVII/starflow/compare/v1.2.0...v1.2.1
112135
[1.2.0]: https://github.com/GEMILUXVII/starflow/compare/v1.1.1...v1.2.0

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "starflow",
3-
"version": "1.2.2",
3+
"version": "1.2.3",
44
"private": true,
55
"scripts": {
66
"dev": "next dev",

prisma/schema.prisma

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -149,15 +149,17 @@ model Note {
149149
}
150150

151151
model AIConfig {
152-
id String @id @default(cuid())
153-
userId String @unique
154-
provider String @default("openai") // openai, ollama
155-
apiKey String?
156-
baseUrl String? // 自定义端点(代理/Ollama)
157-
model String @default("gpt-3.5-turbo")
158-
enabled Boolean @default(false)
159-
createdAt DateTime @default(now())
160-
updatedAt DateTime @updatedAt
152+
id String @id @default(cuid())
153+
userId String @unique
154+
provider String @default("openai") // openai, ollama
155+
apiKey String?
156+
baseUrl String? // 自定义端点(代理/Ollama)
157+
model String @default("gpt-3.5-turbo")
158+
enabled Boolean @default(false)
159+
requestInterval Int @default(1000) // 请求间隔(毫秒)
160+
concurrency Int @default(3) // 并发数
161+
createdAt DateTime @default(now())
162+
updatedAt DateTime @updatedAt
161163
162164
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
163165
}

src/app/api/ai/config/route.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ export async function GET() {
1616
baseUrl: true,
1717
model: true,
1818
enabled: true,
19-
apiKey: true, // 需要查询 apiKey 来判断是否已配置
19+
apiKey: true,
20+
requestInterval: true,
21+
concurrency: true,
2022
},
2123
});
2224

@@ -26,6 +28,8 @@ export async function GET() {
2628
model: config?.model,
2729
enabled: config?.enabled,
2830
hasApiKey: !!config?.apiKey,
31+
requestInterval: config?.requestInterval ?? 1000,
32+
concurrency: config?.concurrency ?? 3,
2933
});
3034
}
3135

@@ -37,7 +41,7 @@ export async function POST(request: Request) {
3741
}
3842

3943
const body = await request.json();
40-
const { provider, apiKey, baseUrl, model, enabled } = body;
44+
const { provider, apiKey, baseUrl, model, enabled, requestInterval, concurrency } = body;
4145

4246
const config = await prisma.aIConfig.upsert({
4347
where: { userId: session.user.id },
@@ -47,6 +51,8 @@ export async function POST(request: Request) {
4751
baseUrl,
4852
model,
4953
enabled,
54+
requestInterval: requestInterval ?? 1000,
55+
concurrency: concurrency ?? 3,
5056
},
5157
create: {
5258
userId: session.user.id,
@@ -55,6 +61,8 @@ export async function POST(request: Request) {
5561
baseUrl,
5662
model,
5763
enabled,
64+
requestInterval: requestInterval ?? 1000,
65+
concurrency: concurrency ?? 3,
5866
},
5967
});
6068

@@ -64,5 +72,7 @@ export async function POST(request: Request) {
6472
model: config.model,
6573
enabled: config.enabled,
6674
hasApiKey: !!config.apiKey,
75+
requestInterval: config.requestInterval,
76+
concurrency: config.concurrency,
6777
});
6878
}

src/app/settings/client.tsx

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ export function SettingsClient({ user }: { user: User }) {
8484
model: "gpt-3.5-turbo",
8585
enabled: false,
8686
hasApiKey: false,
87+
requestInterval: 1000,
88+
concurrency: 3,
8789
});
8890
const [aiLoading, setAiLoading] = useState(false);
8991
const [aiTesting, setAiTesting] = useState(false);
@@ -105,6 +107,8 @@ export function SettingsClient({ user }: { user: User }) {
105107
model: data.model || "gpt-3.5-turbo",
106108
enabled: data.enabled || false,
107109
hasApiKey: data.hasApiKey || false,
110+
requestInterval: data.requestInterval || 1000,
111+
concurrency: data.concurrency || 3,
108112
}));
109113
}
110114
} catch (error) {
@@ -124,6 +128,8 @@ export function SettingsClient({ user }: { user: User }) {
124128
baseUrl: aiConfig.baseUrl || undefined,
125129
model: aiConfig.model,
126130
enabled: aiConfig.enabled,
131+
requestInterval: aiConfig.requestInterval,
132+
concurrency: aiConfig.concurrency,
127133
}),
128134
});
129135

@@ -730,6 +736,42 @@ export function SettingsClient({ user }: { user: User }) {
730736
</p>
731737
</div>
732738

739+
<div className="space-y-2">
740+
<Label>请求间隔 (毫秒)</Label>
741+
<Input
742+
type="number"
743+
placeholder="1000"
744+
min={100}
745+
max={10000}
746+
step={100}
747+
value={aiConfig.requestInterval}
748+
onChange={(e) =>
749+
setAiConfig((prev) => ({ ...prev, requestInterval: parseInt(e.target.value) || 1000 }))
750+
}
751+
/>
752+
<p className="text-xs text-muted-foreground">
753+
每个请求的间隔,建议 500-2000ms
754+
</p>
755+
</div>
756+
757+
<div className="space-y-2">
758+
<Label>并发数</Label>
759+
<Input
760+
type="number"
761+
placeholder="3"
762+
min={1}
763+
max={10}
764+
step={1}
765+
value={aiConfig.concurrency}
766+
onChange={(e) =>
767+
setAiConfig((prev) => ({ ...prev, concurrency: parseInt(e.target.value) || 3 }))
768+
}
769+
/>
770+
<p className="text-xs text-muted-foreground">
771+
同时处理的请求数,建议 2-5,过高可能触发限流
772+
</p>
773+
</div>
774+
733775
<div className="flex gap-2 pt-2">
734776
<Button
735777
onClick={handleSaveAiConfig}
@@ -807,7 +849,7 @@ export function SettingsClient({ user }: { user: User }) {
807849
<div className="space-y-2 text-sm">
808850
<div className="flex justify-between">
809851
<span className="text-muted-foreground">版本</span>
810-
<span>1.2.2</span>
852+
<span>1.2.3</span>
811853
</div>
812854
<div className="flex justify-between">
813855
<span className="text-muted-foreground">开源协议</span>

src/app/stars/client.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ export function StarsClient({ user }: { user: User }) {
127127
// 批量分类状态
128128
const [showBatchClassify, setShowBatchClassify] = useState(false);
129129
const [uncategorizedRepos, setUncategorizedRepos] = useState<{ id: string; fullName: string }[]>([]);
130+
const [aiRequestInterval, setAiRequestInterval] = useState(1000);
131+
const [aiConcurrency, setAiConcurrency] = useState(3);
130132

131133
// Load preferences on mount
132134
useEffect(() => {
@@ -141,6 +143,8 @@ export function StarsClient({ user }: { user: User }) {
141143
.then((res) => res.json())
142144
.then((data) => {
143145
setAiEnabled(data.enabled && data.hasApiKey);
146+
setAiRequestInterval(data.requestInterval || 1000);
147+
setAiConcurrency(data.concurrency || 3);
144148
})
145149
.catch(() => setAiEnabled(false));
146150
}, []);
@@ -741,6 +745,8 @@ export function StarsClient({ user }: { user: User }) {
741745
uncategorizedRepos={uncategorizedRepos}
742746
existingLists={stats?.lists.map((l) => ({ id: l.id, name: l.name })) || []}
743747
onComplete={handleBatchClassifyComplete}
748+
requestInterval={aiRequestInterval}
749+
concurrency={aiConcurrency}
744750
/>
745751
</div>
746752
);

src/components/batch-classify-dialog.tsx

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ interface BatchClassifyDialogProps {
4646
uncategorizedRepos: Repository[];
4747
existingLists: { id: string; name: string }[];
4848
onComplete: () => void;
49+
requestInterval?: number;
50+
concurrency?: number;
4951
}
5052

5153
export function BatchClassifyDialog({
@@ -54,6 +56,8 @@ export function BatchClassifyDialog({
5456
uncategorizedRepos,
5557
existingLists,
5658
onComplete,
59+
requestInterval = 1000,
60+
concurrency = 3,
5761
}: BatchClassifyDialogProps) {
5862
const [phase, setPhase] = useState<"confirm" | "processing" | "review" | "done">("confirm");
5963
const [progress, setProgress] = useState(0);
@@ -124,32 +128,46 @@ export function BatchClassifyDialog({
124128
}
125129
};
126130

127-
// 批量处理(串行,带间隔)
131+
// 批量处理(并行,带间隔)
128132
const startProcessing = async () => {
129133
setPhase("processing");
130134
setProgress(0);
131135
setResults([]);
132136
abortRef.current = false;
133137

134138
const allResults: ClassifyResult[] = [];
135-
const requestInterval = 2500; // 每个请求间隔 2.5 秒
139+
const queue = [...uncategorizedRepos];
140+
let activeCount = 0;
141+
let completedCount = 0;
136142

137-
for (let i = 0; i < uncategorizedRepos.length; i++) {
138-
if (abortRef.current) break;
143+
const processNext = async (): Promise<void> => {
144+
if (abortRef.current || queue.length === 0) return;
139145

140-
const repo = uncategorizedRepos[i];
141-
setCurrentRepo(repo.fullName);
146+
const repo = queue.shift()!;
147+
activeCount++;
148+
setCurrentRepo(`${repo.fullName} (+${activeCount - 1} 并行)`);
142149

143150
const result = await classifyRepo(repo);
144151
allResults.push(result);
152+
completedCount++;
153+
activeCount--;
154+
145155
setResults([...allResults]);
146-
setProgress(Math.round((allResults.length / totalRepos) * 100));
156+
setProgress(Math.round((completedCount / totalRepos) * 100));
147157

148-
// 请求间隔(最后一个不需要等待)
149-
if (i < uncategorizedRepos.length - 1 && !abortRef.current) {
158+
// 间隔后处理下一个
159+
if (queue.length > 0 && !abortRef.current) {
150160
await delay(requestInterval);
161+
await processNext();
151162
}
152-
}
163+
};
164+
165+
// 启动并行任务
166+
const workers = Array(Math.min(concurrency, queue.length))
167+
.fill(null)
168+
.map(() => processNext());
169+
170+
await Promise.all(workers);
153171

154172
// 处理完成,分析结果
155173
processResults(allResults);
@@ -308,7 +326,7 @@ export function BatchClassifyDialog({
308326
<li>• 仅处理未分类的仓库</li>
309327
<li>• 匹配现有 List 的会自动归类</li>
310328
<li>• 建议新 List 的会让您确认后创建</li>
311-
<li>• 预计耗时:约 {Math.ceil(totalRepos * 3 / 60)} 分钟(避免 API 限流</li>
329+
<li>• 预计耗时:约 {Math.ceil(totalRepos / concurrency * (requestInterval / 1000 + 2) / 60)} 分钟({concurrency} 并发</li>
312330
</ul>
313331
</div>
314332
<div className="flex justify-end gap-2">
@@ -358,23 +376,25 @@ export function BatchClassifyDialog({
358376
<Checkbox
359377
checked={suggestion.selected}
360378
onCheckedChange={() => toggleListSelection(index)}
379+
className="mt-1 shrink-0"
361380
/>
362-
<div className="flex-1 min-w-0">
363-
<div className="flex items-center gap-2">
364-
<FolderPlus className="h-4 w-4 text-primary" />
381+
<div className="flex-1 min-w-0 overflow-hidden">
382+
<div className="flex items-center gap-2 flex-wrap">
383+
<FolderPlus className="h-4 w-4 text-primary shrink-0" />
365384
<span className="font-medium">{suggestion.name}</span>
366385
<span className="text-xs text-muted-foreground">
367386
({suggestion.repos.length} 个仓库)
368387
</span>
369388
</div>
370-
<p className="text-xs text-muted-foreground mt-1 truncate">
371-
{suggestion.repos.map((r) => r.name).join(", ")}
389+
<p className="text-xs text-muted-foreground mt-1 line-clamp-2">
390+
{suggestion.repos.slice(0, 5).map((r) => r.name).join(", ")}
391+
{suggestion.repos.length > 5 && ` 等 ${suggestion.repos.length} 个`}
372392
</p>
373393
</div>
374394
</div>
375395
))}
376396
</ScrollArea>
377-
<div className="flex justify-end gap-2">
397+
<div className="flex justify-end gap-2 pt-2">
378398
<Button variant="outline" onClick={() => setPhase("done")}>
379399
跳过
380400
</Button>

src/components/sidebar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ export function Sidebar({
5151
const [languagesExpanded, setLanguagesExpanded] = useState(true);
5252

5353
return (
54-
<aside className="w-64 border-r bg-muted/10 flex flex-col">
55-
<ScrollArea className="flex-1">
54+
<aside className="w-64 border-r bg-muted/10 flex flex-col h-full overflow-hidden">
55+
<ScrollArea className="flex-1 h-full">
5656
<div className="p-4 space-y-4">
5757
{/* 全部 Stars */}
5858
<div className="space-y-1">

0 commit comments

Comments
 (0)