Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cloudflare-worker/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export default {
for (let round = 0; round < 10; round++) {
const result = await env.VECTORIZE.query(zeroVector, {
topK: 100,
returnMetadata: false,
returnMetadata: 'none',
});
const staleIds = result.matches
.filter((m) => !keepSet.has(m.id))
Expand Down
2 changes: 1 addition & 1 deletion cloudflare-worker/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export default {
for (let round = 0; round < 10; round++) {
const result = await env.VECTORIZE.query(zeroVector, {
topK: 100,
returnMetadata: false,
returnMetadata: 'none',
});
const staleIds = result.matches
.filter((m) => !keepSet.has(m.id))
Expand Down
1 change: 1 addition & 0 deletions server/src/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,5 @@ export function initializeSchema(db: Database.Database): void {
addColumnIfMissing(db, 'asset_filters', 'sort_order', 'INTEGER DEFAULT 0');
addColumnIfMissing(db, 'vector_search_configs', 'index_mode', "TEXT NOT NULL DEFAULT 'readme'");
addColumnIfMissing(db, 'vector_search_configs', 'readme_max_chars', 'INTEGER NOT NULL DEFAULT 6000');
addColumnIfMissing(db, 'repositories', 'vector_indexed_at', 'TEXT');
}
26 changes: 22 additions & 4 deletions server/src/routes/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ function transformRepo(row: Record<string, unknown>) {
category_locked: !!row.category_locked,
last_edited: row.last_edited,
subscribed_to_releases: !!row.subscribed_to_releases,
vector_indexed_at: row.vector_indexed_at ?? undefined,
};
}

Expand Down Expand Up @@ -126,8 +127,8 @@ router.put('/api/repositories', (req, res) => {
owner_login, owner_avatar_url, topics,
ai_summary, ai_tags, ai_platforms, analyzed_at, analysis_failed,
custom_description, custom_tags, custom_category, category_locked, last_edited,
subscribed_to_releases
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
subscribed_to_releases, vector_indexed_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
name = excluded.name,
full_name = excluded.full_name,
Expand All @@ -152,7 +153,8 @@ router.put('/api/repositories', (req, res) => {
custom_category = excluded.custom_category,
category_locked = excluded.category_locked,
last_edited = CASE WHEN excluded.last_edited IS NOT NULL AND excluded.last_edited != '' THEN excluded.last_edited ELSE repositories.last_edited END,
subscribed_to_releases = excluded.subscribed_to_releases
subscribed_to_releases = excluded.subscribed_to_releases,
vector_indexed_at = excluded.vector_indexed_at
`);

const deleteAllReleases = db.prepare('DELETE FROM releases');
Expand Down Expand Up @@ -198,7 +200,8 @@ router.put('/api/repositories', (req, res) => {
repo.custom_description ?? null,
JSON.stringify(Array.isArray(repo.custom_tags) ? repo.custom_tags : []),
repo.custom_category ?? null, (repo.category_locked === true || repo.category_locked === 1) ? 1 : 0, repo.last_edited ?? null,
(repo.subscribed_to_releases === true || repo.subscribed_to_releases === 1) ? 1 : 0
(repo.subscribed_to_releases === true || repo.subscribed_to_releases === 1) ? 1 : 0,
repo.vector_indexed_at ?? null
Comment thread
coderabbitai[bot] marked this conversation as resolved.
);
count++;
}
Expand Down Expand Up @@ -232,10 +235,25 @@ router.patch('/api/repositories/:id', (req, res) => {
category_locked: (v) => (v === true || v === 1) ? 1 : 0,
last_edited: (v) => v,
subscribed_to_releases: (v) => (v === true || v === 1) ? 1 : 0,
// 规范化:null/undefined/空字符串 → null;仅接受字符串(ISO 时间戳)
vector_indexed_at: (v) =>
(v === null || v === undefined || v === '') ? null : v,
description: (v) => v,
name: (v) => v,
};

// 校验 vector_indexed_at 类型:只允许 null 或 ISO 字符串,拒绝数字/布尔/对象
if ('vector_indexed_at' in updates) {
const v = updates.vector_indexed_at;
if (v !== null && v !== undefined && v !== '' && typeof v !== 'string') {
res.status(400).json({
error: 'vector_indexed_at must be an ISO string or null',
code: 'INVALID_VECTOR_INDEXED_AT',
});
return;
}
}

const setClauses: string[] = [];
const values: unknown[] = [];

Expand Down
7 changes: 4 additions & 3 deletions server/src/routes/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,8 @@ router.post('/api/sync/import', (req, res) => {
owner_login, owner_avatar_url, topics,
ai_summary, ai_tags, ai_platforms, analyzed_at, analysis_failed,
custom_description, custom_tags, custom_category, category_locked, last_edited,
subscribed_to_releases
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
subscribed_to_releases, vector_indexed_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
for (const r of repos) {
// 验证必需的字段
Expand All @@ -130,7 +130,8 @@ router.post('/api/sync/import', (req, res) => {
r.custom_description ?? null,
typeof r.custom_tags === 'string' ? r.custom_tags : JSON.stringify(r.custom_tags ?? []),
r.custom_category ?? null, (r.category_locked === true || r.category_locked === 1) ? 1 : 0, r.last_edited ?? null,
r.subscribed_to_releases ? 1 : 0
r.subscribed_to_releases ? 1 : 0,
r.vector_indexed_at ?? null
);
}
counts.repositories = repos.length;
Expand Down
21 changes: 19 additions & 2 deletions src/components/SearchBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ vi.mock('../store/useAppStore', () => ({
getAllCategories: vi.fn(() => []),
}));

vi.mock('../hooks/useDialog', () => ({
useDialog: () => ({
toast: vi.fn(),
confirm: vi.fn(),
}),
}));

const localStorageMock = (() => {
let store: Record<string, string> = {};

Expand Down Expand Up @@ -62,25 +69,34 @@ const baseStoreState = () => ({
customCategories: [],
hiddenDefaultCategoryIds: [],
defaultCategoryOverrides: {},
vectorSearchConfig: { enabled: false, workerUrl: '', authToken: '', embeddingConfigId: '', indexMode: 'readme' as const, readmeMaxChars: 6000 },
vectorSearchStatus: { connected: false, vectorCount: 0, dimensions: 0 },
embeddingConfigs: [],
});

const mockUseAppStore = vi.mocked(useAppStore);
// Track the current mock state so getState() returns the same overrides as the hook.
let currentState = baseStoreState();
(mockUseAppStore as unknown as { getState: () => ReturnType<typeof baseStoreState> }).getState =
() => currentState;

describe('SearchBar', () => {
beforeEach(() => {
vi.clearAllMocks();
localStorage.clear();
currentState = baseStoreState();
});

it('clears the committed query when the search input is manually emptied', () => {
const setSearchFilters = vi.fn();
mockUseAppStore.mockReturnValue(createStoreState({
currentState = createStoreState({
searchFilters: {
...defaultSearchFilters,
query: 'react',
},
setSearchFilters,
}) as ReturnType<typeof useAppStore>);
});
mockUseAppStore.mockReturnValue(currentState as ReturnType<typeof useAppStore>);

render(<SearchBar />);

Expand All @@ -106,6 +122,7 @@ describe('SearchBar', () => {
});
storeState.setSearchFilters = setSearchFilters;

currentState = storeState;
mockUseAppStore.mockReturnValue(storeState as ReturnType<typeof useAppStore>);

const { rerender } = render(<SearchBar />);
Expand Down
21 changes: 0 additions & 21 deletions src/components/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -880,27 +880,6 @@ export const SearchBar: React.FC = () => {
toast(t('同步完成!所有仓库都是最新的。', 'Sync completed! All repositories are up to date.'), 'info');
}

// 向量搜索开启时,后台自动索引新仓库
const vsCfg = useAppStore.getState().vectorSearchConfig;
const embCfgs = useAppStore.getState().embeddingConfigs;
const activeEmb = embCfgs.find(c => c.id === vsCfg?.embeddingConfigId);
if (vsCfg?.enabled && vsCfg?.workerUrl && activeEmb && newRepoCount > 0) {
const { VectorSearchService, EmbeddingClient, indexAllRepos } = await import('../services/vectorSearchService');
const embClient = new EmbeddingClient(activeEmb);
const vecService = new VectorSearchService(vsCfg);
const readmeFetcher = githubToken
? (owner: string, repo: string, signal?: AbortSignal) => new GitHubApiService(githubToken).getRepositoryReadme(owner, repo, signal)
: undefined;
// 只索引新增仓库,不重复索引已有仓库
const newRepos = mergedRepositories.filter(repo => !existingRepoIds.has(repo.id));
if (newRepos.length > 0) {
indexAllRepos(newRepos, embClient, vecService, {
readmeFetcher,
indexMode: vsCfg.indexMode,
readmeMaxChars: vsCfg.readmeMaxChars,
}).catch(() => {});
}
}
} catch (error) {
console.error('Sync failed:', error);
if (error instanceof Error && error.message.includes('token')) {
Expand Down
Loading
Loading