refactor(suggestion): drive suggestion decisions from one core step and split SuggestionService collaborators
This commit is contained in:
parent
9aa7302944
commit
190b6db284
13 changed files with 1994 additions and 496 deletions
|
|
@ -84,6 +84,11 @@ export interface TypingContext {
|
|||
available: boolean
|
||||
appName: string | null
|
||||
windowTitle: string | null
|
||||
/**
|
||||
* 입력창이 속한 최상위 창 핸들 (HWND). 알 수 없으면 null.
|
||||
* 제안 수락 직전 "아직 같은 창인가" 를 확인하는 기준이다.
|
||||
*/
|
||||
windowHandle?: number | null
|
||||
idleMs: number
|
||||
capturedAt: number
|
||||
/** 포커스가 바뀐 뒤 이 필드에서 실제로 편집이 있었는가 (마우스로 들어오기만 한 경우 false) */
|
||||
|
|
@ -105,6 +110,13 @@ interface ForegroundInfo {
|
|||
}
|
||||
|
||||
const MAX_SAMPLE_CHARS = 500
|
||||
/**
|
||||
* 프로그램 삽입(제안 수락 등) 예고의 유효 시간.
|
||||
*
|
||||
* 수락은 수정자 키가 떼어지길 최대 1.5초 기다린 뒤 붙여넣고, 스냅샷은 디바운스(700ms)·
|
||||
* settle·UIA 조회(최대 1.5초)를 거쳐 온다 — 그 합을 넉넉히 덮는다.
|
||||
*/
|
||||
const PROGRAMMATIC_INSERT_TTL_MS = 10_000
|
||||
const RETENTION_PRUNE_INTERVAL_MS = 6 * 60 * 60 * 1000
|
||||
const MAX_READABILITY_APPS = 64
|
||||
|
||||
|
|
@ -187,6 +199,9 @@ class InputTelemetryService extends EventEmitter {
|
|||
private _lastSnapshotSummary: InputSnapshotSummary | null = null
|
||||
/** 원문 없이 앱별 UIA 읽기 가능성만 누적한다. */
|
||||
private _appReadability = new Map<string, AppReadabilityEvidence>()
|
||||
/** 곧(또는 방금) 프로그램이 삽입한 텍스트 — 다음 diff 에서 "타이핑" 으로 학습하지 않는다. */
|
||||
private _expectedInserts: Array<{ id: number; text: string; expiresAt: number }> = []
|
||||
private _expectedInsertSeq = 0
|
||||
|
||||
private _prunedAt = 0
|
||||
|
||||
|
|
@ -584,6 +599,7 @@ class InputTelemetryService extends EventEmitter {
|
|||
available: snapshot.available,
|
||||
appName: this._foreground.appName,
|
||||
windowTitle: snapshot.windowTitle ?? this._foreground.windowTitle,
|
||||
windowHandle: this._foreground.hwnd,
|
||||
idleMs,
|
||||
capturedAt: snapshot.capturedAt,
|
||||
editedSinceFocus: false,
|
||||
|
|
@ -705,8 +721,9 @@ class InputTelemetryService extends EventEmitter {
|
|||
bucket.words += delta.insertedWords
|
||||
bucket.sentences += delta.insertedSentences
|
||||
|
||||
if (delta.insertedText) {
|
||||
this._learnText(delta.insertedText, {
|
||||
const typedText = delta.insertedText ? this._withoutExpectedInsert(delta.insertedText) : ''
|
||||
if (typedText) {
|
||||
this._learnText(typedText, {
|
||||
appName: this._foreground.appName,
|
||||
windowTitle: snapshot.windowTitle ?? this._foreground.windowTitle,
|
||||
source: 'typed'
|
||||
|
|
@ -714,6 +731,66 @@ class InputTelemetryService extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 곧 프로그램이 이 텍스트를 삽입한다 (제안 수락 · 받아쓰기 삽입).
|
||||
*
|
||||
* 삽입된 텍스트는 다음 스냅샷 diff 에 그대로 나타난다. 붙여넣기 판정 임계(200자)보다
|
||||
* 짧으면 "타이핑" 으로 다시 학습돼, 삽입 경로가 이미 출처(source)를 붙여 기록한 문장이
|
||||
* 두 번 쌓이고 제안이 자기 출력을 강화했다. 예고된 텍스트는 diff 학습에서 뺀다.
|
||||
*
|
||||
* 삽입이 실패했으면 돌려준 함수로 예고를 취소한다.
|
||||
*/
|
||||
expectProgrammaticInsert(text: string): () => void {
|
||||
const trimmed = text.trim()
|
||||
if (!trimmed) return () => undefined
|
||||
const now = Date.now()
|
||||
this._pruneExpectedInserts(now)
|
||||
this._expectedInsertSeq += 1
|
||||
const id = this._expectedInsertSeq
|
||||
this._expectedInserts.push({ id, text: trimmed, expiresAt: now + PROGRAMMATIC_INSERT_TTL_MS })
|
||||
return () => {
|
||||
this._expectedInserts = this._expectedInserts.filter((entry) => entry.id !== id)
|
||||
}
|
||||
}
|
||||
|
||||
private _pruneExpectedInserts(now: number): void {
|
||||
if (this._expectedInserts.length === 0) return
|
||||
this._expectedInserts = this._expectedInserts.filter((entry) => entry.expiresAt > now)
|
||||
}
|
||||
|
||||
/**
|
||||
* diff 로 얻은 삽입 텍스트에서 예고된 프로그램 삽입을 뺀 "실제 타이핑" 부분.
|
||||
*
|
||||
* - 삽입 구간이 예고 텍스트를 품으면 그 부분만 빼고 나머지(앞뒤로 직접 친 글)는 남긴다.
|
||||
* - diff 경계가 반복 글자에서 한 칸 밀려 회전된 형태로 잡힌 경우도 같은 텍스트로 본다.
|
||||
* - 삽입 구간이 예고 텍스트의 앞부분이면(키보드 방식으로 치는 도중 잡힌 스냅샷) 전부
|
||||
* 프로그램 삽입이고, 남은 뒷부분을 계속 기다린다.
|
||||
*/
|
||||
private _withoutExpectedInsert(inserted: string): string {
|
||||
this._pruneExpectedInserts(Date.now())
|
||||
const trimmed = inserted.trim()
|
||||
if (!trimmed) return inserted
|
||||
for (let i = 0; i < this._expectedInserts.length; i += 1) {
|
||||
const expected = this._expectedInserts[i]
|
||||
const index = inserted.indexOf(expected.text)
|
||||
if (index >= 0) {
|
||||
this._expectedInserts.splice(i, 1)
|
||||
return `${inserted.slice(0, index)} ${inserted.slice(index + expected.text.length)}`.trim()
|
||||
}
|
||||
if (trimmed.length === expected.text.length && `${trimmed}${trimmed}`.includes(expected.text)) {
|
||||
this._expectedInserts.splice(i, 1)
|
||||
return ''
|
||||
}
|
||||
if (expected.text.startsWith(trimmed)) {
|
||||
const remainder = expected.text.slice(trimmed.length).trim()
|
||||
if (remainder) expected.text = remainder
|
||||
else this._expectedInserts.splice(i, 1)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
return inserted
|
||||
}
|
||||
|
||||
/**
|
||||
* 외부(음성 전사 등)에서 들어온 텍스트를 코퍼스에 넣는다.
|
||||
*
|
||||
|
|
@ -1239,11 +1316,26 @@ class InputTelemetryService extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 학습 문구 삭제.
|
||||
*
|
||||
* 그래프 엣지(phrase_edges)는 문장 텍스트를 외래 키 없이 들고 있다 — 문구 행만 지우면
|
||||
* follows 엣지를 따라 지운 문장이 제안 프롬프트와 그래프 통계에 계속 나온다.
|
||||
* 문구와 그 문장을 가리키는 엣지를 한 트랜잭션에서 함께 지운다.
|
||||
*/
|
||||
deletePhrase(id: string): boolean {
|
||||
try {
|
||||
const db = getDatabase()
|
||||
const result = db.delete(personalPhrases).where(eq(personalPhrases.id, id)).run()
|
||||
return Number(result.changes ?? 0) > 0
|
||||
return db.transaction(() => {
|
||||
const row = db
|
||||
.select({ phrase: personalPhrases.phrase })
|
||||
.from(personalPhrases)
|
||||
.where(eq(personalPhrases.id, id))
|
||||
.get()
|
||||
const result = db.delete(personalPhrases).where(eq(personalPhrases.id, id)).run()
|
||||
if (row) getPersonalGraphService().removePhrase(row.phrase)
|
||||
return Number(result.changes ?? 0) > 0
|
||||
})
|
||||
} catch (error) {
|
||||
logger.warn(`개인 문구 제 실패: ${error instanceof Error ? error.message : String(error)}`)
|
||||
return false
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue