chore: prepare GoodBuddy 0.8.2
Cross-platform packages / Validate source (push) Waiting to run
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, macos, macos-15) (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, windows, windows-2025) (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, linux, ubuntu-24.04) (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, macos, macos-15-intel) (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (x64, windows, windows-2025) (push) Blocked by required conditions
Cross-platform packages / Publish GitHub Release (push) Blocked by required conditions
Cross-platform packages / ${{ matrix.platform }} ${{ matrix.arch }} (arm64, linux, ubuntu-24.04-arm) (push) Blocked by required conditions

This commit is contained in:
lofyer
2026-08-06 22:47:13 +08:00
parent 8d00e6371d
commit b8fc7bc86e
114 changed files with 22916 additions and 1560 deletions
@@ -0,0 +1,532 @@
import { randomUUID } from 'node:crypto'
import type {
EmbeddingDiagnosticResult,
EmbeddingIndexJob,
EmbeddingIndexStatus
} from '../../shared/embedding-contracts'
import {
classifyEmbeddingError,
EmbeddingOperationError
} from './embedding-errors'
const DEFAULT_BATCH_SIZE = 32
const MAX_BATCH_SIZE = 256
const MAX_VECTOR_DIMENSIONS = 8_192
export interface EmbeddingIndexProvider {
readonly provider: string
readonly model: string
readonly fingerprint?: string
embed(input: readonly string[], signal?: AbortSignal): Promise<number[][]>
}
export interface EmbeddingIndexItem {
id: string
content: string
contentChecksum?: string
}
export interface EmbeddingIndexRecord {
itemId: string
contentChecksum?: string
vector: readonly number[]
}
export interface EmbeddingIndexDocument {
id: string
items: readonly EmbeddingIndexItem[]
}
export interface EmbeddingIndexRepository {
getLastJob?(): Promise<EmbeddingIndexJob | null>
saveStatus?(status: EmbeddingIndexStatus): Promise<void>
listIndexDocumentIds(
signal: AbortSignal
): Promise<readonly string[]>
getIndexDocument(
documentId: string,
signal: AbortSignal
): Promise<EmbeddingIndexDocument | undefined>
beginDocumentReplacement(
documentId: string,
provider: string,
model: string,
signal: AbortSignal
): Promise<string>
appendDocumentReplacement(
replacementId: string,
documentId: string,
provider: string,
model: string,
records: readonly EmbeddingIndexRecord[],
signal: AbortSignal
): Promise<void>
finishDocumentReplacement(
replacementId: string,
documentId: string,
provider: string,
model: string,
signal: AbortSignal
): Promise<void>
discardDocumentReplacement(replacementId: string): Promise<void>
recordDocumentError(
documentId: string,
provider: string,
model: string,
error: string
): Promise<void>
}
export interface EmbeddingIndexCoordinatorOptions {
batchSize?: number
now?: () => number
createId?: () => string
}
export interface EmbeddingDiagnosticOptions {
signal?: AbortSignal
probeText?: string
}
export interface EmbeddingRebuildOptions {
signal?: AbortSignal
}
type JobListener = (status: EmbeddingIndexStatus) => void
function validatedLabel(value: string, name: string): string {
const normalized = value.trim()
if (normalized.length < 1 || normalized.length > 256) {
throw new RangeError(`${name} must contain between 1 and 256 characters`)
}
return normalized
}
function validateVector(
vector: readonly number[],
expectedDimensions?: number
): number {
if (
!Array.isArray(vector) ||
vector.length < 1 ||
vector.length > MAX_VECTOR_DIMENSIONS
) {
throw new EmbeddingOperationError({
code: 'invalid_response',
message: '向量服务返回了无效结果。',
retryable: false,
remedy: '请确认服务返回维度一致的有效向量。'
})
}
let magnitudeSquared = 0
for (const component of vector) {
if (typeof component !== 'number' || !Number.isFinite(component)) {
throw new EmbeddingOperationError({
code: 'invalid_response',
message: '向量服务返回了无效结果。',
retryable: false,
remedy: '请确认服务返回维度一致的有效向量。'
})
}
magnitudeSquared += component * component
}
if (
!Number.isFinite(magnitudeSquared) ||
magnitudeSquared <= 0 ||
(expectedDimensions !== undefined &&
vector.length !== expectedDimensions)
) {
throw new EmbeddingOperationError({
code: 'invalid_response',
message: '向量服务返回了无效结果。',
retryable: false,
remedy: '请确认服务返回维度一致的有效向量。'
})
}
return vector.length
}
function percent(completed: number, total: number): number {
return total === 0 ? 0 : (completed / total) * 100
}
export class EmbeddingIndexCoordinator {
private readonly repository: EmbeddingIndexRepository
private readonly batchSize: number
private readonly now: () => number
private readonly createId: () => string
private readonly listeners = new Set<JobListener>()
private job: EmbeddingIndexJob | null = null
private controller: AbortController | null = null
private completion: Promise<EmbeddingIndexJob> | null = null
private persistenceTail: Promise<void> = Promise.resolve()
constructor(
repository: EmbeddingIndexRepository,
options: EmbeddingIndexCoordinatorOptions = {}
) {
this.repository = repository
const batchSize = options.batchSize ?? DEFAULT_BATCH_SIZE
if (
!Number.isSafeInteger(batchSize) ||
batchSize < 1 ||
batchSize > MAX_BATCH_SIZE
) {
throw new RangeError(
`batchSize must be an integer between 1 and ${MAX_BATCH_SIZE}`
)
}
this.batchSize = batchSize
this.now = options.now ?? Date.now
this.createId = options.createId ?? randomUUID
}
async initialize(): Promise<EmbeddingIndexStatus> {
this.job = (await this.repository.getLastJob?.()) ?? null
if (
this.job?.status === 'queued' ||
this.job?.status === 'running'
) {
this.job = {
...this.job,
status: 'cancelled',
completedAt: this.now()
}
await this.persistStatus()
}
return this.status()
}
status(): EmbeddingIndexStatus {
return {
job: this.job
}
}
subscribe(listener: JobListener): () => void {
this.listeners.add(listener)
listener(this.status())
return () => {
this.listeners.delete(listener)
}
}
async diagnose(
provider: EmbeddingIndexProvider,
options: EmbeddingDiagnosticOptions = {}
): Promise<EmbeddingDiagnosticResult> {
const providerName = validatedLabel(provider.provider, 'provider')
const model = validatedLabel(provider.model, 'model')
const startedAt = this.now()
try {
const vectors = await provider.embed(
[options.probeText ?? 'GoodBuddy 向量模型连接测试'],
options.signal
)
if (vectors.length !== 1 || !vectors[0]) {
throw new EmbeddingOperationError({
code: 'invalid_response',
message: '向量服务返回了无效结果。',
retryable: false,
remedy: '请确认服务为每个输入返回一个有效向量。'
})
}
const dimensions = validateVector(vectors[0])
const checkedAt = this.now()
return {
status: 'available',
provider: providerName,
model,
checkedAt,
latencyMs: Math.max(0, checkedAt - startedAt),
dimensions
}
} catch (error) {
const checkedAt = this.now()
return {
status: 'unavailable',
provider: providerName,
model,
checkedAt,
latencyMs: Math.max(0, checkedAt - startedAt),
error:
error instanceof EmbeddingOperationError
? error.toSafeError()
: classifyEmbeddingError(error, {
cancelled: options.signal?.aborted
})
}
}
}
startRebuild(
provider: EmbeddingIndexProvider,
options: EmbeddingRebuildOptions = {}
): EmbeddingIndexJob {
if (
this.job?.status === 'queued' ||
this.job?.status === 'running'
) {
throw new Error('An embedding index rebuild is already active')
}
const providerName = validatedLabel(provider.provider, 'provider')
const model = validatedLabel(provider.model, 'model')
const controller = new AbortController()
const createdAt = this.now()
this.job = {
id: this.createId(),
status: 'queued',
provider: providerName,
model,
progress: { completed: 0, total: 0, percent: 0 },
createdAt
}
this.controller = controller
this.emit()
const externalSignal = options.signal
const forwardAbort = (): void => {
controller.abort(externalSignal?.reason)
}
if (externalSignal?.aborted) {
forwardAbort()
} else {
externalSignal?.addEventListener('abort', forwardAbort, {
once: true
})
}
this.completion = Promise.resolve()
.then(() => this.runRebuild(provider, controller.signal))
.finally(() => {
externalSignal?.removeEventListener('abort', forwardAbort)
if (this.controller === controller) {
this.controller = null
}
})
return this.job
}
async waitForCompletion(): Promise<EmbeddingIndexJob | null> {
return this.completion
}
cancel(jobId?: string): boolean {
if (
!this.controller ||
!this.job ||
(jobId !== undefined && this.job.id !== jobId) ||
!['queued', 'running'].includes(this.job.status)
) {
return false
}
this.controller.abort(new Error('Embedding index rebuild cancelled'))
return true
}
private async runRebuild(
provider: EmbeddingIndexProvider,
signal: AbortSignal
): Promise<EmbeddingIndexJob> {
try {
signal.throwIfAborted()
const documentIds =
await this.repository.listIndexDocumentIds(signal)
signal.throwIfAborted()
this.updateJob({
status: 'running',
startedAt: this.now(),
progress: {
completed: 0,
total: documentIds.length,
percent: 0
}
})
let completed = 0
let dimensions: number | undefined
for (const documentId of documentIds) {
signal.throwIfAborted()
const document = await this.repository.getIndexDocument(
documentId,
signal
)
if (!document) {
completed += 1
this.updateJob({
progress: {
completed,
total: documentIds.length,
percent: percent(completed, documentIds.length)
}
})
continue
}
const replacementId =
await this.repository.beginDocumentReplacement(
document.id,
provider.provider,
provider.model,
signal
)
try {
for (
let offset = 0;
offset < document.items.length;
offset += this.batchSize
) {
signal.throwIfAborted()
const batch = document.items.slice(
offset,
offset + this.batchSize
)
const vectors = await provider.embed(
batch.map((item) => item.content),
signal
)
if (vectors.length !== batch.length) {
throw new EmbeddingOperationError({
code: 'invalid_response',
message: '向量服务返回了无效结果。',
retryable: false,
remedy: '请确认服务为每个输入返回一个有效向量。'
})
}
const records = batch.map((item, index) => {
const vector = vectors[index]
if (!vector) {
throw new EmbeddingOperationError({
code: 'invalid_response',
message: '向量服务返回了无效结果。',
retryable: false
})
}
dimensions = validateVector(vector, dimensions)
return {
itemId: item.id,
...(item.contentChecksum
? { contentChecksum: item.contentChecksum }
: {}),
vector
}
})
await this.repository.appendDocumentReplacement(
replacementId,
document.id,
provider.provider,
provider.model,
records,
signal
)
}
signal.throwIfAborted()
await this.repository.finishDocumentReplacement(
replacementId,
document.id,
provider.provider,
provider.model,
signal
)
} catch (error) {
await this.repository
.discardDocumentReplacement(replacementId)
.catch(() => undefined)
if (signal.aborted) {
throw error
}
const safeError =
error instanceof EmbeddingOperationError
? error.toSafeError()
: classifyEmbeddingError(error)
await this.repository.recordDocumentError(
document.id,
provider.provider,
provider.model,
safeError.message
)
throw error
}
completed += 1
this.updateJob({
progress: {
completed,
total: documentIds.length,
percent: percent(completed, documentIds.length)
}
})
}
signal.throwIfAborted()
this.updateJob({
status: 'completed',
completedAt: this.now(),
progress: {
completed: documentIds.length,
total: documentIds.length,
percent: 100
}
})
} catch (error) {
const cancelled =
signal.aborted ||
classifyEmbeddingError(error).code === 'cancelled'
this.updateJob(
cancelled
? {
status: 'cancelled',
completedAt: this.now()
}
: {
status: 'failed',
completedAt: this.now(),
error:
error instanceof EmbeddingOperationError
? error.toSafeError()
: classifyEmbeddingError(error)
}
)
}
await this.persistenceTail
if (!this.job) {
throw new Error('Embedding index job state was lost')
}
return this.job
}
private updateJob(update: Partial<EmbeddingIndexJob>): void {
if (!this.job) {
throw new Error('No embedding index job is active')
}
this.job = {
...this.job,
...update
}
this.emit()
}
private emit(): void {
const status = this.status()
this.persistenceTail = this.persistenceTail.then(async () => {
try {
await this.repository.saveStatus?.(status)
} catch {
// Persistence failure must not interrupt an active provider operation.
}
})
for (const listener of this.listeners) {
listener(status)
}
}
private async persistStatus(): Promise<void> {
const status = this.status()
this.persistenceTail = this.persistenceTail.then(async () => {
try {
await this.repository.saveStatus?.(status)
} catch {
// Persistence failure must not interrupt initialization.
}
})
await this.persistenceTail
}
}