Skip to content

Commit c9c49ed

Browse files
committed
色々を修正
1 parent cbf24b8 commit c9c49ed

6 files changed

Lines changed: 73 additions & 84 deletions

File tree

packages/backend/src/core/EmojiSuggestionService.ts

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { Inject, Injectable } from '@nestjs/common';
88
import { DI } from '@/di-symbols.js';
99
import type { Config } from '@/config.js';
1010
import type { MiNote } from '@/models/Note.js';
11-
import type { MiMeta } from '@/models/Meta.js';
1211
import { HttpRequestService } from '@/core/HttpRequestService.js';
1312
import type Logger from '@/logger.js';
1413
import { LoggerService } from '@/core/LoggerService.js';
@@ -21,6 +20,7 @@ const NORMALIZATION_SCHEMA_VERSION = 'emoji-suggest-normalization-v1';
2120
const WORKER_OWNED_VERSION_PLACEHOLDER = 'worker-owned';
2221
const MAX_NORMALIZED_TEXT_LENGTH = 1000;
2322
const MIN_SUGGESTION_SCORE = 0.4;
23+
const MAX_RENOTE_NORMALIZATION_DEPTH = 4;
2424

2525
export type EmojiSuggestionCandidate = {
2626
name: string;
@@ -31,8 +31,14 @@ export type EmojiSuggestionCandidate = {
3131

3232
export type EmojiSuggestionResponse = {
3333
items: EmojiSuggestionCandidate[];
34-
source: 'cache' | 'live' | 'fallback';
3534
reason: string | null;
35+
};
36+
37+
type EmojiSuggestionSource = 'cache' | 'live' | 'fallback';
38+
39+
type ParsedWorkerResponse = {
40+
response: EmojiSuggestionResponse;
41+
source: EmojiSuggestionSource;
3642
modelVersion: string;
3743
emojiIndexVersion: string;
3844
};
@@ -51,7 +57,7 @@ type EmojiSuggestionLogEvent = {
5157
schemaVersion: 'emoji-suggestion-observability-v1';
5258
requestId: string;
5359
outcome: 'success' | 'fallback';
54-
source: EmojiSuggestionResponse['source'];
60+
source: EmojiSuggestionSource;
5561
fallbackReason: string | null;
5662
workerStatus: number | null;
5763
workerAuthStatus: 'not_attempted' | 'accepted' | 'rejected';
@@ -107,7 +113,7 @@ export class EmojiSuggestionService {
107113

108114
try {
109115
const meta = await this.metaService.fetch();
110-
const fallback = (reason: string): EmojiSuggestionResponse => this.createFallback(meta, reason);
116+
const fallback = (reason: string): EmojiSuggestionResponse => this.createFallback(reason);
111117

112118
if (!meta.emojiSuggestionEnabled) return response = fallback('disabled');
113119
if (!meta.emojiSuggestionEndpoint || !meta.emojiSuggestionApiKey) return response = fallback('unconfigured');
@@ -162,7 +168,11 @@ export class EmojiSuggestionService {
162168

163169
event.workerAuthStatus = 'accepted';
164170

165-
return response = parseWorkerResponse(await res.json(), maxResults);
171+
const workerResponse = parseWorkerResponse(await res.json(), maxResults);
172+
event.source = workerResponse.source;
173+
event.modelVersion = workerResponse.modelVersion;
174+
event.emojiIndexVersion = workerResponse.emojiIndexVersion;
175+
return response = workerResponse.response;
166176
} catch {
167177
return response = fallback('timeout');
168178
}
@@ -177,25 +187,19 @@ export class EmojiSuggestionService {
177187
return note.visibility === 'public' || note.visibility === 'home';
178188
}
179189

180-
private createFallback(meta: MiMeta, reason: string): EmojiSuggestionResponse {
190+
private createFallback(reason: string): EmojiSuggestionResponse {
181191
return {
182192
items: [],
183-
source: 'fallback',
184193
reason,
185-
modelVersion: WORKER_OWNED_VERSION_PLACEHOLDER,
186-
emojiIndexVersion: WORKER_OWNED_VERSION_PLACEHOLDER,
187194
};
188195
}
189196

190197
private logCompletion(event: EmojiSuggestionLogEvent, response: EmojiSuggestionResponse, startedAt: number): void {
191198
const durationMs = Math.max(0, Date.now() - startedAt);
192-
event.outcome = response.source === 'fallback' ? 'fallback' : 'success';
193-
event.source = response.source;
199+
event.outcome = event.source === 'fallback' ? 'fallback' : 'success';
194200
event.fallbackReason = response.reason;
195-
event.cacheStatus = response.source === 'cache' ? 'hit' : response.source === 'live' ? 'miss' : event.cacheStatus;
201+
event.cacheStatus = event.source === 'cache' ? 'hit' : event.source === 'live' ? 'miss' : event.cacheStatus;
196202
event.resultCount = response.items.length;
197-
event.modelVersion = response.modelVersion;
198-
event.emojiIndexVersion = response.emojiIndexVersion;
199203
event.durationMs = durationMs;
200204
event.latencyBucket = bucketDurationMs(durationMs);
201205
this.logger.info('emoji_suggestion_request', event);
@@ -207,10 +211,17 @@ export function clampMaxSuggestions(value: number): number {
207211
return Math.min(16, Math.max(1, Math.trunc(value)));
208212
}
209213

210-
export function normalizeEmojiSuggestionNoteText(note: Pick<MiNote, 'text' | 'cw' | 'tags'>): string {
211-
const source = note.cw != null
212-
? [note.cw, ...(note.tags ?? []).map(tag => `#${tag}`)].join(' ')
213-
: note.text ?? '';
214+
type EmojiSuggestionTextSource = Pick<MiNote, 'text' | 'cw' | 'tags' | 'renote'>;
215+
216+
export function normalizeEmojiSuggestionNoteText(note: EmojiSuggestionTextSource, depth = 0): string {
217+
let source = '';
218+
if (note.cw != null) {
219+
source = [note.cw, ...(note.tags ?? []).map(tag => `#${tag}`)].join(' ');
220+
} else if (note.text != null) {
221+
source = note.text;
222+
} else if (note.renote != null && depth < MAX_RENOTE_NORMALIZATION_DEPTH) {
223+
source = normalizeEmojiSuggestionNoteText(note.renote, depth + 1);
224+
}
214225

215226
return source
216227
.normalize('NFKC')
@@ -224,7 +235,7 @@ export function normalizeEmojiSuggestionNoteText(note: Pick<MiNote, 'text' | 'cw
224235
.slice(0, MAX_NORMALIZED_TEXT_LENGTH);
225236
}
226237

227-
function parseWorkerResponse(value: unknown, maxResults: number): EmojiSuggestionResponse {
238+
function parseWorkerResponse(value: unknown, maxResults: number): ParsedWorkerResponse {
228239
if (!isWorkerSuggestResponse(value)) return createMalformedFallback();
229240
if (!Array.isArray(value.items)) return createMalformedFallback();
230241
if (value.source !== 'cache' && value.source !== 'live' && value.source !== 'fallback') return createMalformedFallback();
@@ -245,9 +256,11 @@ function parseWorkerResponse(value: unknown, maxResults: number): EmojiSuggestio
245256
}
246257

247258
return {
248-
items,
259+
response: {
260+
items,
261+
reason: value.reason,
262+
},
249263
source: value.source,
250-
reason: value.reason,
251264
modelVersion: value.modelVersion,
252265
emojiIndexVersion: value.emojiIndexVersion,
253266
};
@@ -268,11 +281,13 @@ function isWorkerSuggestItem(value: unknown): value is EmojiSuggestionCandidate
268281
&& (typeof item.category === 'string' || item.category === null);
269282
}
270283

271-
function createMalformedFallback(): EmojiSuggestionResponse {
284+
function createMalformedFallback(): ParsedWorkerResponse {
272285
return {
273-
items: [],
286+
response: {
287+
items: [],
288+
reason: 'malformedResponse',
289+
},
274290
source: 'fallback',
275-
reason: 'malformedResponse',
276291
modelVersion: WORKER_OWNED_VERSION_PLACEHOLDER,
277292
emojiIndexVersion: WORKER_OWNED_VERSION_PLACEHOLDER,
278293
};

packages/backend/src/server/api/endpoints/notes/reactions/suggestions.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,7 @@ export const meta = {
3838
},
3939
},
4040
},
41-
source: { type: 'string', enum: ['cache', 'live', 'fallback'], optional: false, nullable: false },
4241
reason: { type: 'string', optional: false, nullable: true },
43-
modelVersion: { type: 'string', optional: false, nullable: false },
44-
emojiIndexVersion: { type: 'string', optional: false, nullable: false },
4542
},
4643
},
4744

@@ -58,8 +55,6 @@ export const paramDef = {
5855
type: 'object',
5956
properties: {
6057
noteId: { type: 'string', format: 'misskey:id' },
61-
locale: { type: 'string', default: 'ja-JP' },
62-
language: { type: 'string', default: 'ja' },
6358
},
6459
required: ['noteId'],
6560
} as const;
@@ -71,12 +66,12 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
7166
private emojiSuggestionService: EmojiSuggestionService,
7267
) {
7368
super(meta, paramDef, async (ps) => {
74-
const note = await this.getterService.getNote(ps.noteId).catch(err => {
69+
const note = await this.getterService.getNoteWithRelations(ps.noteId).catch(err => {
7570
if (err.id === '9725d0ce-ba28-4dde-95a7-2cbb2c15de24') throw new ApiError(meta.errors.noSuchNote);
7671
throw err;
7772
});
7873

79-
return await this.emojiSuggestionService.suggestForNote(note, ps.locale, ps.language);
74+
return await this.emojiSuggestionService.suggestForNote(note);
8075
});
8176
}
8277
}

packages/backend/test/unit/EmojiSuggestionService.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -86,10 +86,13 @@ describe('EmojiSuggestionService', () => {
8686
const send = vi.fn(async () => jsonResponse(workerResponse()));
8787
const { service, logInfo } = createService(createMeta(), send);
8888

89-
await expect(service.suggestForNote(createNote())).resolves.toMatchObject({
90-
source: 'live',
89+
const publicResult = await service.suggestForNote(createNote());
90+
expect(publicResult).toMatchObject({
9191
items: [{ name: 'ablobgoodnightreverse' }],
9292
});
93+
expect(publicResult).not.toHaveProperty('source');
94+
expect(publicResult).not.toHaveProperty('modelVersion');
95+
expect(publicResult).not.toHaveProperty('emojiIndexVersion');
9396
expect(send).toHaveBeenCalledTimes(1);
9497
expect(readLastLogEvent(logInfo)).toMatchObject({
9598
event: 'emoji_suggestion_request',
@@ -118,15 +121,13 @@ describe('EmojiSuggestionService', () => {
118121
expect(body.normalizedText).toContain('@user');
119122

120123
await expect(service.suggestForNote(createNote({ localOnly: true }))).resolves.toMatchObject({
121-
source: 'live',
122124
items: [{ name: 'ablobgoodnightreverse' }],
123125
});
124126
expect(send).toHaveBeenCalledTimes(2);
125127
const localOnlyBody = JSON.parse(send.mock.calls[1][1].body as string) as { eligibility: { visibility: string; localOnly: boolean } };
126128
expect(localOnlyBody.eligibility).toEqual({ visibility: 'public', localOnly: true });
127129

128130
await expect(service.suggestForNote(createNote({ visibility: 'home' }))).resolves.toMatchObject({
129-
source: 'live',
130131
items: [{ name: 'ablobgoodnightreverse' }],
131132
});
132133
expect(send).toHaveBeenCalledTimes(3);
@@ -139,7 +140,6 @@ describe('EmojiSuggestionService', () => {
139140
]) {
140141
await expect(service.suggestForNote(note)).resolves.toMatchObject({
141142
items: [],
142-
source: 'fallback',
143143
reason: 'ineligible',
144144
});
145145
}
@@ -158,7 +158,7 @@ describe('EmojiSuggestionService', () => {
158158

159159
const cacheSend = vi.fn(async () => jsonResponse(workerResponse({ source: 'cache' })));
160160
const cacheHarness = createService(createMeta(), cacheSend);
161-
await expect(cacheHarness.service.suggestForNote(createNote())).resolves.toMatchObject({ source: 'cache' });
161+
await expect(cacheHarness.service.suggestForNote(createNote())).resolves.toMatchObject({ items: [{ name: 'ablobgoodnightreverse' }] });
162162
expect(readLastLogEvent(cacheHarness.logInfo)).toMatchObject({
163163
outcome: 'success',
164164
source: 'cache',
@@ -170,7 +170,7 @@ describe('EmojiSuggestionService', () => {
170170

171171
const authSend = vi.fn(async () => jsonResponse({ ok: false }, 401));
172172
const authHarness = createService(createMeta(), authSend);
173-
await expect(authHarness.service.suggestForNote(createNote())).resolves.toMatchObject({ source: 'fallback', reason: 'workerUnauthorized' });
173+
await expect(authHarness.service.suggestForNote(createNote())).resolves.toMatchObject({ reason: 'workerUnauthorized' });
174174
expect(readLastLogEvent(authHarness.logInfo)).toMatchObject({
175175
outcome: 'fallback',
176176
fallbackReason: 'workerUnauthorized',
@@ -181,7 +181,7 @@ describe('EmojiSuggestionService', () => {
181181

182182
const timeoutSend = vi.fn(async () => { throw new Error('aborted'); });
183183
const timeoutHarness = createService(createMeta(), timeoutSend);
184-
await expect(timeoutHarness.service.suggestForNote(createNote())).resolves.toMatchObject({ source: 'fallback', reason: 'timeout' });
184+
await expect(timeoutHarness.service.suggestForNote(createNote())).resolves.toMatchObject({ reason: 'timeout' });
185185
expect(readLastLogEvent(timeoutHarness.logInfo)).toMatchObject({
186186
outcome: 'fallback',
187187
fallbackReason: 'timeout',
@@ -202,7 +202,6 @@ describe('EmojiSuggestionService', () => {
202202

203203
await expect(service.suggestForNote(createNote())).resolves.toMatchObject({
204204
items: [],
205-
source: 'fallback',
206205
});
207206
expect(send).not.toHaveBeenCalled();
208207
}
@@ -219,7 +218,6 @@ describe('EmojiSuggestionService', () => {
219218

220219
await expect(service.suggestForNote(createNote())).resolves.toMatchObject({
221220
items: [],
222-
source: 'fallback',
223221
});
224222
expect(send).toHaveBeenCalledTimes(1);
225223
}
@@ -235,7 +233,6 @@ describe('EmojiSuggestionService', () => {
235233
const { service, logInfo } = createService(createMeta(), send);
236234

237235
await expect(service.suggestForNote(createNote({ visibility: 'home' }))).resolves.toMatchObject({
238-
source: 'live',
239236
items: [{ name: 'threshold-ok', score: 0.4 }],
240237
});
241238
expect(send).toHaveBeenCalledTimes(1);
@@ -254,6 +251,19 @@ describe('EmojiSuggestionService', () => {
254251
expect(normalized).not.toContain('hidden body');
255252
expect(normalized).not.toContain('https://example.invalid');
256253
});
254+
255+
test('uses renote source text when the reaction target has no body text', async () => {
256+
const send = vi.fn(async () => jsonResponse(workerResponse()));
257+
const { service } = createService(createMeta(), send);
258+
259+
await service.suggestForNote(createNote({
260+
text: null,
261+
renote: createNote({ text: 'renote source text' }),
262+
}));
263+
264+
const body = JSON.parse(send.mock.calls[0][1].body as string) as { normalizedText: string };
265+
expect(body.normalizedText).toBe('renote source text');
266+
});
257267
});
258268

259269
function readLastLogEvent(logInfo: ReturnType<typeof vi.fn>): Record<string, unknown> {

packages/frontend/src/components/MkEmojiPicker.stories.impl.ts

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,6 @@ type EmojiSuggestionStoryMeta = typeof instance & {
2525

2626
type SuggestionRequestBody = {
2727
noteId: string;
28-
locale?: string;
29-
language?: string;
3028
i?: string | null;
3129
};
3230

@@ -97,8 +95,6 @@ function renderDisabledReactionPickerStory(args: Record<string, unknown>) {
9795
function isSuggestionRequestBody(value: unknown): value is SuggestionRequestBody {
9896
return typeof value === 'object' && value !== null &&
9997
'noteId' in value && typeof value.noteId === 'string' &&
100-
(!('locale' in value) || typeof value.locale === 'string') &&
101-
(!('language' in value) || typeof value.language === 'string') &&
10298
(!('i' in value) || value.i == null || typeof value.i === 'string');
10399
}
104100

@@ -108,6 +104,8 @@ function assertSuggestionRequestBody(value: unknown, noteId: string): asserts va
108104
throw new Error('invalid suggestion request body');
109105
}
110106
expect(value.noteId).toBe(noteId);
107+
expect(value).not.toHaveProperty('locale');
108+
expect(value).not.toHaveProperty('language');
111109
expect(value).not.toHaveProperty('text');
112110
expect(value).not.toHaveProperty(['c', 'w'].join(''));
113111
}
@@ -221,10 +219,7 @@ export const SuggestedReaction = {
221219
aliases: suggestedEmoji.aliases,
222220
category: suggestedEmoji.category,
223221
}],
224-
source: 'live',
225222
reason: 'story',
226-
modelVersion: 'story-model',
227-
emojiIndexVersion: 'story-index',
228223
});
229224
}),
230225
],
@@ -260,7 +255,7 @@ export const SuggestionsDisabledNoCall = {
260255
http.post('/api/notes/reactions/suggestions', async ({ request }) => {
261256
const body: unknown = await request.json();
262257
noCallRequestBodies.push(body);
263-
return HttpResponse.json({ items: [], source: 'fallback' });
258+
return HttpResponse.json({ items: [], reason: 'fallback' });
264259
}),
265260
],
266261
},
@@ -299,7 +294,7 @@ export const SuggestionsIneligibleNoCall = {
299294
http.post('/api/notes/reactions/suggestions', async ({ request }) => {
300295
const body: unknown = await request.json();
301296
noCallRequestBodies.push(body);
302-
return HttpResponse.json({ items: [], source: 'fallback' });
297+
return HttpResponse.json({ items: [], reason: 'fallback' });
303298
}),
304299
],
305300
},

0 commit comments

Comments
 (0)