Merge remote-tracking branch 'origin/master' into hermes/wechat
# Conflicts: # .hermes/shared-memory/pitfalls.md # .hermes/todos/【后端架构】api-server能力模块化与图片资产Adapter收口计划-2026-05-14.md
This commit is contained in:
130
packages/shared/src/contracts/barkBattle.test.ts
Normal file
130
packages/shared/src/contracts/barkBattle.test.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import {
|
||||
BARK_BATTLE_DIFFICULTY_PRESETS,
|
||||
type BarkBattleDraftConfig,
|
||||
type BarkBattleFinishResponse,
|
||||
type BarkBattlePersonalBestSummary,
|
||||
type BarkBattleWorkStats,
|
||||
} from './barkBattle';
|
||||
|
||||
describe('Bark Battle shared contracts', () => {
|
||||
test('default draft config fixture uses normal difficulty and camelCase fields', () => {
|
||||
const draft: BarkBattleDraftConfig = {
|
||||
draftId: 'draft-bark-1',
|
||||
title: '汪汪声浪挑战',
|
||||
description: '轻配置草稿',
|
||||
themePreset: 'city-park',
|
||||
playerDogSkinPreset: 'corgi',
|
||||
opponentDogSkinPreset: 'husky',
|
||||
difficultyPreset: 'normal',
|
||||
leaderboardEnabled: true,
|
||||
updatedAt: '2026-05-13T03:00:00.000Z',
|
||||
};
|
||||
|
||||
expect(BARK_BATTLE_DIFFICULTY_PRESETS).toEqual(['easy', 'normal', 'hard']);
|
||||
expect(draft.difficultyPreset).toBe('normal');
|
||||
expect(Object.keys(draft)).toEqual([
|
||||
'draftId',
|
||||
'title',
|
||||
'description',
|
||||
'themePreset',
|
||||
'playerDogSkinPreset',
|
||||
'opponentDogSkinPreset',
|
||||
'difficultyPreset',
|
||||
'leaderboardEnabled',
|
||||
'updatedAt',
|
||||
]);
|
||||
});
|
||||
|
||||
test('finish accepted player_win fixture exposes backend adjudication result', () => {
|
||||
const response: BarkBattleFinishResponse = {
|
||||
status: 'accepted',
|
||||
runId: 'run-bark-1',
|
||||
workId: 'work-bark-1',
|
||||
configVersion: 3,
|
||||
rulesetVersion: 'bark-battle-ruleset-v1',
|
||||
difficultyPreset: 'hard',
|
||||
serverResult: 'player_win',
|
||||
scoreSummary: {
|
||||
finalEnergy: 87,
|
||||
triggerCount: 42,
|
||||
maxVolume: 0.96,
|
||||
averageVolume: 0.61,
|
||||
comboMax: 9,
|
||||
durationMs: 30000,
|
||||
},
|
||||
leaderboardScore: 870429630,
|
||||
antiCheatFlags: [],
|
||||
updatedAt: '2026-05-13T03:00:30.000Z',
|
||||
};
|
||||
|
||||
expect(response.status).toBe('accepted');
|
||||
expect(response.serverResult).toBe('player_win');
|
||||
expect(response.scoreSummary.finalEnergy).toBe(87);
|
||||
expect(response.antiCheatFlags).toEqual([]);
|
||||
});
|
||||
|
||||
test('work stats fixture tracks starts, finishes, result counts, flags and energy summary', () => {
|
||||
const stats: BarkBattleWorkStats = {
|
||||
workId: 'work-bark-1',
|
||||
configVersion: 3,
|
||||
rulesetVersion: 'bark-battle-ruleset-v1',
|
||||
difficultyPreset: 'normal',
|
||||
playStartCount: 18,
|
||||
finishCount: 15,
|
||||
winCount: 8,
|
||||
drawCount: 2,
|
||||
lossCount: 5,
|
||||
flaggedCount: 1,
|
||||
leaderboardEntryCount: 7,
|
||||
bestLeaderboardScore: 930389410,
|
||||
bestFinalEnergy: 93,
|
||||
averageFinalEnergy: 41.25,
|
||||
updatedAt: '2026-05-13T04:00:00.000Z',
|
||||
};
|
||||
|
||||
expect(stats.playStartCount).toBe(18);
|
||||
expect(stats.finishCount).toBe(15);
|
||||
expect(stats.winCount + stats.drawCount + stats.lossCount).toBe(15);
|
||||
expect(stats.flaggedCount).toBe(1);
|
||||
expect(stats.bestFinalEnergy).toBeGreaterThan(stats.averageFinalEnergy);
|
||||
});
|
||||
|
||||
test('optional score fields may be omitted instead of serialized as null', () => {
|
||||
const finishWithoutLeaderboard: BarkBattleFinishResponse = {
|
||||
status: 'accepted',
|
||||
runId: 'run-bark-no-rank',
|
||||
workId: 'work-bark-1',
|
||||
configVersion: 3,
|
||||
rulesetVersion: 'bark-battle-ruleset-v1',
|
||||
difficultyPreset: 'normal',
|
||||
serverResult: 'draw',
|
||||
scoreSummary: {
|
||||
finalEnergy: 50,
|
||||
triggerCount: 12,
|
||||
maxVolume: 0.7,
|
||||
averageVolume: 0.5,
|
||||
comboMax: 3,
|
||||
durationMs: 30000,
|
||||
},
|
||||
antiCheatFlags: [],
|
||||
updatedAt: '2026-05-13T03:00:30.000Z',
|
||||
};
|
||||
const personalBestWithoutWin: BarkBattlePersonalBestSummary = {
|
||||
workId: 'work-bark-1',
|
||||
rulesetVersion: 'bark-battle-ruleset-v1',
|
||||
difficultyPreset: 'normal',
|
||||
winCount: 0,
|
||||
drawCount: 1,
|
||||
lossCount: 2,
|
||||
finishCount: 3,
|
||||
updatedAt: '2026-05-13T04:00:00.000Z',
|
||||
};
|
||||
|
||||
expect('leaderboardScore' in finishWithoutLeaderboard).toBe(false);
|
||||
expect('bestLeaderboardScore' in personalBestWithoutWin).toBe(false);
|
||||
expect('bestFinalEnergy' in personalBestWithoutWin).toBe(false);
|
||||
});
|
||||
|
||||
});
|
||||
218
packages/shared/src/contracts/barkBattle.ts
Normal file
218
packages/shared/src/contracts/barkBattle.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
export const BARK_BATTLE_DIFFICULTY_PRESETS = [
|
||||
'easy',
|
||||
'normal',
|
||||
'hard',
|
||||
] as const;
|
||||
|
||||
export type BarkBattleDifficultyPreset =
|
||||
(typeof BARK_BATTLE_DIFFICULTY_PRESETS)[number];
|
||||
|
||||
export type BarkBattleServerResult = 'player_win' | 'opponent_win' | 'draw';
|
||||
|
||||
export type BarkBattleFinishStatus =
|
||||
| 'accepted'
|
||||
| 'accepted_with_flags'
|
||||
| 'rejected';
|
||||
|
||||
export type BarkBattlePlayTypeId = 'bark-battle';
|
||||
|
||||
export interface BarkBattleConfigEditorPayload {
|
||||
title: string;
|
||||
description?: string;
|
||||
themePreset: string;
|
||||
playerDogSkinPreset: string;
|
||||
opponentDogSkinPreset: string;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
leaderboardEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface BarkBattleDraftCreateRequest extends BarkBattleConfigEditorPayload {}
|
||||
|
||||
export interface BarkBattleWorkPublishRequest {
|
||||
draftId: string;
|
||||
workId?: string;
|
||||
publishedSnapshot?: BarkBattleConfigEditorPayload;
|
||||
}
|
||||
|
||||
export interface BarkBattleDraftConfig extends BarkBattleConfigEditorPayload {
|
||||
draftId: string;
|
||||
workId?: string;
|
||||
configVersion?: number;
|
||||
rulesetVersion?: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface BarkBattlePublishedConfig {
|
||||
workId: string;
|
||||
draftId?: string | null;
|
||||
configVersion: number;
|
||||
rulesetVersion: string;
|
||||
playTypeId: BarkBattlePlayTypeId;
|
||||
title: string;
|
||||
description?: string;
|
||||
themePreset: string;
|
||||
playerDogSkinPreset: string;
|
||||
opponentDogSkinPreset: string;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
leaderboardEnabled: boolean;
|
||||
updatedAt: string;
|
||||
publishedAt: string;
|
||||
}
|
||||
|
||||
export interface BarkBattleRuntimeConfig {
|
||||
workId: string;
|
||||
configVersion: number;
|
||||
rulesetVersion: string;
|
||||
playTypeId: BarkBattlePlayTypeId;
|
||||
durationMs: number;
|
||||
energyMin: number;
|
||||
energyMax: number;
|
||||
drawThreshold: number;
|
||||
minBarkGapMs: number;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
themePreset: string;
|
||||
playerDogSkinPreset: string;
|
||||
opponentDogSkinPreset: string;
|
||||
leaderboardEnabled: boolean;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface BarkBattleRunStartRequest {
|
||||
workId: string;
|
||||
configVersion?: number;
|
||||
sourceRoute?: string;
|
||||
clientRuntimeVersion?: string;
|
||||
}
|
||||
|
||||
export interface BarkBattleRunStartResponse {
|
||||
runId: string;
|
||||
runToken: string;
|
||||
workId: string;
|
||||
configVersion: number;
|
||||
rulesetVersion: string;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
runtimeConfig: BarkBattleRuntimeConfig;
|
||||
serverStartedAt: string;
|
||||
expiresAt: string;
|
||||
}
|
||||
|
||||
export interface BarkBattleDerivedMetrics {
|
||||
triggerCount: number;
|
||||
maxVolume: number;
|
||||
averageVolume: number;
|
||||
finalEnergy: number;
|
||||
comboMax: number;
|
||||
}
|
||||
|
||||
export interface BarkBattleRunFinishRequest {
|
||||
runId: string;
|
||||
runToken: string;
|
||||
workId: string;
|
||||
configVersion: number;
|
||||
rulesetVersion: string;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
clientStartedAt: string;
|
||||
clientFinishedAt: string;
|
||||
durationMs: number;
|
||||
derivedMetrics: BarkBattleDerivedMetrics;
|
||||
clientResult?: BarkBattleServerResult;
|
||||
sampleDigest?: string;
|
||||
clientRuntimeVersion?: string;
|
||||
}
|
||||
|
||||
export interface BarkBattleScoreSummary extends BarkBattleDerivedMetrics {
|
||||
durationMs: number;
|
||||
}
|
||||
|
||||
export interface BarkBattleFinishResponse {
|
||||
status: BarkBattleFinishStatus;
|
||||
runId: string;
|
||||
workId: string;
|
||||
configVersion: number;
|
||||
rulesetVersion: string;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
serverResult: BarkBattleServerResult;
|
||||
scoreSummary: BarkBattleScoreSummary;
|
||||
leaderboardScore?: number;
|
||||
antiCheatFlags: string[];
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface BarkBattleLeaderboardEntry {
|
||||
rank: number;
|
||||
runId: string;
|
||||
workId: string;
|
||||
configVersion: number;
|
||||
rulesetVersion: string;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
displayName: string;
|
||||
serverResult: BarkBattleServerResult;
|
||||
scoreSummary: BarkBattleScoreSummary;
|
||||
leaderboardScore: number;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface BarkBattleLeaderboardResponse {
|
||||
workId: string;
|
||||
configVersion?: number;
|
||||
rulesetVersion: string;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
entries: BarkBattleLeaderboardEntry[];
|
||||
viewerBest?: BarkBattleLeaderboardEntry | null;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface BarkBattlePersonalHistoryItem {
|
||||
runId: string;
|
||||
workId: string;
|
||||
configVersion: number;
|
||||
rulesetVersion: string;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
serverResult: BarkBattleServerResult;
|
||||
scoreSummary: BarkBattleScoreSummary;
|
||||
leaderboardScore?: number;
|
||||
antiCheatFlags: string[];
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface BarkBattlePersonalBestSummary {
|
||||
workId: string;
|
||||
configVersion?: number;
|
||||
rulesetVersion: string;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
bestLeaderboardScore?: number;
|
||||
bestFinalEnergy?: number;
|
||||
bestTriggerCount?: number;
|
||||
bestMaxVolume?: number;
|
||||
winCount: number;
|
||||
drawCount: number;
|
||||
lossCount: number;
|
||||
finishCount: number;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface BarkBattlePersonalHistoryResponse {
|
||||
workId?: string;
|
||||
difficultyPreset?: BarkBattleDifficultyPreset;
|
||||
items: BarkBattlePersonalHistoryItem[];
|
||||
bestSummary?: BarkBattlePersonalBestSummary | null;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface BarkBattleWorkStats {
|
||||
workId: string;
|
||||
configVersion?: number;
|
||||
rulesetVersion: string;
|
||||
difficultyPreset: BarkBattleDifficultyPreset;
|
||||
playStartCount: number;
|
||||
finishCount: number;
|
||||
winCount: number;
|
||||
drawCount: number;
|
||||
lossCount: number;
|
||||
flaggedCount: number;
|
||||
leaderboardEntryCount: number;
|
||||
bestLeaderboardScore?: number;
|
||||
bestFinalEnergy?: number;
|
||||
averageFinalEnergy?: number;
|
||||
updatedAt: string;
|
||||
}
|
||||
85
packages/shared/src/contracts/edutainmentBabyDrawing.ts
Normal file
85
packages/shared/src/contracts/edutainmentBabyDrawing.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
export const BABY_LOVE_DRAWING_TEMPLATE_ID = 'baby-love-drawing';
|
||||
export const BABY_LOVE_DRAWING_TEMPLATE_NAME = '宝贝爱画';
|
||||
export const BABY_LOVE_DRAWING_EDUTAINMENT_TAG = '寓教于乐';
|
||||
|
||||
export type BabyLoveDrawingTemplateId =
|
||||
typeof BABY_LOVE_DRAWING_TEMPLATE_ID;
|
||||
|
||||
export type BabyLoveDrawingTool = 'brush' | 'eraser';
|
||||
|
||||
export type BabyLoveDrawingSaveMode =
|
||||
| 'original-only'
|
||||
| 'original-and-magic';
|
||||
|
||||
export type BabyLoveDrawingGenerationProvider =
|
||||
| 'vector-engine-gpt-image-2'
|
||||
| 'local-demo';
|
||||
|
||||
export type BabyLoveDrawingPoint = {
|
||||
x: number;
|
||||
y: number;
|
||||
t: number;
|
||||
};
|
||||
|
||||
export type BabyLoveDrawingStroke = {
|
||||
strokeId: string;
|
||||
tool: BabyLoveDrawingTool;
|
||||
color: string;
|
||||
points: BabyLoveDrawingPoint[];
|
||||
};
|
||||
|
||||
export type BabyLoveDrawingRecord = {
|
||||
drawingId: string;
|
||||
templateId: BabyLoveDrawingTemplateId;
|
||||
templateName: typeof BABY_LOVE_DRAWING_TEMPLATE_NAME;
|
||||
originalImageSrc: string;
|
||||
magicImageSrc: string | null;
|
||||
strokeTrace: BabyLoveDrawingStroke[];
|
||||
saveMode: BabyLoveDrawingSaveMode;
|
||||
themeTags: string[];
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type CreateBabyLoveDrawingMagicRequest = {
|
||||
originalImageSrc: string;
|
||||
strokeTrace: BabyLoveDrawingStroke[];
|
||||
};
|
||||
|
||||
export type CreateBabyLoveDrawingMagicResponse = {
|
||||
magicImageSrc: string;
|
||||
generationProvider: BabyLoveDrawingGenerationProvider;
|
||||
prompt: string;
|
||||
};
|
||||
|
||||
export type SaveBabyLoveDrawingRequest = {
|
||||
originalImageSrc: string;
|
||||
magicImageSrc?: string | null;
|
||||
strokeTrace: BabyLoveDrawingStroke[];
|
||||
};
|
||||
|
||||
export type SaveBabyLoveDrawingResponse = {
|
||||
record: BabyLoveDrawingRecord;
|
||||
};
|
||||
|
||||
export const BABY_LOVE_DRAWING_RAINBOW_COLORS = [
|
||||
{ id: 'red', label: '红', value: '#ef4444' },
|
||||
{ id: 'orange', label: '橙', value: '#f97316' },
|
||||
{ id: 'yellow', label: '黄', value: '#facc15' },
|
||||
{ id: 'green', label: '绿', value: '#22c55e' },
|
||||
{ id: 'cyan', label: '青', value: '#06b6d4' },
|
||||
{ id: 'blue', label: '蓝', value: '#3b82f6' },
|
||||
{ id: 'purple', label: '紫', value: '#a855f7' },
|
||||
] as const;
|
||||
|
||||
export type BabyLoveDrawingRainbowColorId =
|
||||
(typeof BABY_LOVE_DRAWING_RAINBOW_COLORS)[number]['id'];
|
||||
|
||||
export function normalizeBabyLoveDrawingTags(tags: string[]) {
|
||||
return [
|
||||
...new Set([
|
||||
BABY_LOVE_DRAWING_EDUTAINMENT_TAG,
|
||||
...tags.map((tag) => tag.trim()).filter(Boolean),
|
||||
]),
|
||||
];
|
||||
}
|
||||
@@ -2,8 +2,7 @@ export const BABY_OBJECT_MATCH_TEMPLATE_ID = 'baby-object-match';
|
||||
export const BABY_OBJECT_MATCH_TEMPLATE_NAME = '宝贝识物';
|
||||
export const BABY_OBJECT_MATCH_EDUTAINMENT_TAG = '寓教于乐';
|
||||
|
||||
export type BabyObjectMatchTemplateId =
|
||||
typeof BABY_OBJECT_MATCH_TEMPLATE_ID;
|
||||
export type BabyObjectMatchTemplateId = typeof BABY_OBJECT_MATCH_TEMPLATE_ID;
|
||||
|
||||
export type BabyObjectMatchAssetProvider =
|
||||
| 'vector-engine-gpt-image-2'
|
||||
@@ -20,6 +19,27 @@ export type BabyObjectMatchItemAsset = {
|
||||
prompt: string;
|
||||
};
|
||||
|
||||
export type BabyObjectMatchVisualAssetKind =
|
||||
| 'background'
|
||||
| 'ui-frame'
|
||||
| 'gift-box'
|
||||
| 'basket'
|
||||
| 'smoke-puff';
|
||||
|
||||
export type BabyObjectMatchVisualAsset = {
|
||||
assetId: string;
|
||||
assetKind: BabyObjectMatchVisualAssetKind;
|
||||
imageSrc: string;
|
||||
assetObjectId: string | null;
|
||||
generationProvider: BabyObjectMatchAssetProvider;
|
||||
prompt: string;
|
||||
};
|
||||
|
||||
export type BabyObjectMatchVisualPackage = {
|
||||
themePrompt: string;
|
||||
assets: BabyObjectMatchVisualAsset[];
|
||||
};
|
||||
|
||||
export type BabyObjectMatchDraft = {
|
||||
draftId: string;
|
||||
profileId: string;
|
||||
@@ -29,6 +49,7 @@ export type BabyObjectMatchDraft = {
|
||||
workDescription: string;
|
||||
itemNames: [string, string];
|
||||
itemAssets: [BabyObjectMatchItemAsset, BabyObjectMatchItemAsset];
|
||||
visualPackage?: BabyObjectMatchVisualPackage | null;
|
||||
themeTags: string[];
|
||||
publicationStatus: BabyObjectMatchPublicationStatus;
|
||||
createdAt: string;
|
||||
@@ -41,6 +62,15 @@ export type CreateBabyObjectMatchDraftRequest = {
|
||||
itemBName: string;
|
||||
};
|
||||
|
||||
export type GenerateBabyObjectMatchAssetsRequest = {
|
||||
itemNames: [string, string];
|
||||
};
|
||||
|
||||
export type GenerateBabyObjectMatchAssetsResponse = {
|
||||
assets: [BabyObjectMatchItemAsset, BabyObjectMatchItemAsset];
|
||||
visualPackage?: BabyObjectMatchVisualPackage | null;
|
||||
};
|
||||
|
||||
export type BabyObjectMatchDraftResponse = {
|
||||
draft: BabyObjectMatchDraft;
|
||||
};
|
||||
|
||||
@@ -3,3 +3,4 @@ export type * from './creationAudio';
|
||||
export type * from './hyper3d';
|
||||
export type * from './puzzleCreativeTemplate';
|
||||
export type * from './visualNovel';
|
||||
export type * from './barkBattle';
|
||||
|
||||
@@ -73,7 +73,9 @@ export interface PersistMatch3DGeneratedModelResponse {
|
||||
|
||||
export interface GenerateMatch3DCoverImageRequest {
|
||||
prompt: string;
|
||||
uploadedImageSrc?: string | null;
|
||||
referenceImageSrc?: string | null;
|
||||
referenceImageSrcs?: string[];
|
||||
}
|
||||
|
||||
export interface GenerateMatch3DCoverImageResponse {
|
||||
@@ -95,8 +97,23 @@ export interface GenerateMatch3DBackgroundImageResponse {
|
||||
prompt: string;
|
||||
}
|
||||
|
||||
export interface GenerateMatch3DContainerImageRequest {
|
||||
prompt: string;
|
||||
}
|
||||
|
||||
export interface GenerateMatch3DContainerImageResponse {
|
||||
item: Match3DWorkProfile;
|
||||
containerImageSrc: string;
|
||||
containerImageObjectKey: string;
|
||||
generatedBackgroundAsset: Match3DGeneratedBackgroundAsset;
|
||||
prompt: string;
|
||||
}
|
||||
|
||||
export type GenerateMatch3DItemAssetsMode = 'append' | 'replace';
|
||||
|
||||
export interface GenerateMatch3DItemAssetsRequest {
|
||||
itemNames: string[];
|
||||
mode?: GenerateMatch3DItemAssetsMode;
|
||||
}
|
||||
|
||||
export interface GenerateMatch3DItemAssetsResponse {
|
||||
|
||||
@@ -58,6 +58,7 @@ export interface PuzzleRuntimeLevelSnapshot {
|
||||
themeTags: string[];
|
||||
coverImageSrc: string | null;
|
||||
uiBackgroundImageSrc?: string | null;
|
||||
uiBackgroundImageObjectKey?: string | null;
|
||||
backgroundMusic?: CreationAudioAsset | null;
|
||||
board: PuzzleBoardSnapshot;
|
||||
status: PuzzleRuntimeLevelStatus;
|
||||
|
||||
@@ -6,6 +6,7 @@ export type * from './contracts/creationAgentDocumentInput';
|
||||
export type * from './contracts/creationAudio';
|
||||
export type * from './contracts/creativeAgent';
|
||||
export type * from './contracts/customWorldAgent';
|
||||
export * from './contracts/edutainmentBabyDrawing';
|
||||
export * from './contracts/edutainmentBabyObject';
|
||||
export type * from './contracts/hyper3d';
|
||||
export * from './contracts/match3dAgent';
|
||||
|
||||
Reference in New Issue
Block a user