Makelore 2.0 initial clean snapshot

This commit is contained in:
inman
2026-07-29 17:22:35 +08:00
commit b8ca3f8eea
694 changed files with 139782 additions and 0 deletions

View File

@@ -0,0 +1,166 @@
import type { RawMessage } from '@/types/chat';
import { extractText } from '@/pages/Chat/message-utils';
export const GAME_ASSET_SELECTION_MARKER = '<!-- niancode:game-asset-selection -->';
export const GAME_ASSET_REVIEW_MARKER_PREFIX = '<!-- niancode:game-asset-review ';
export const GAME_ASSET_REVIEW_ACTION_MARKER_PREFIX = '<!-- niancode:game-asset-review-action ';
export const GAME_ASSET_REVIEW_ACTIONS_MARKER_PREFIX = '<!-- niancode:game-asset-review-actions ';
export const GAME_ASSET_CONFIRMATION_PREFIX = '我确认选择以下游戏素材:';
export type GameAssetReviewInvocation = {
invocationId: string;
candidateIds: string[];
};
export type GameAssetReviewAction = 'approve' | 'discard' | 'replace';
function parseJsonMarker<T>(text: string, prefix: string): T | null {
const escapedPrefix = prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const match = text.match(new RegExp(`${escapedPrefix}(\\{[\\s\\S]*?\\})\\s*-->`));
if (!match?.[1]) return null;
try {
return JSON.parse(match[1]) as T;
} catch {
return null;
}
}
function normalizeIds(value: unknown): string[] {
if (!Array.isArray(value)) return [];
return [...new Set(value.filter((item): item is string => typeof item === 'string').map((item) => item.trim()).filter(Boolean))];
}
export function parseGameAssetReviewInvocation(message: RawMessage): GameAssetReviewInvocation | null {
if (message.role !== 'assistant') return null;
const text = extractText(message);
const parsed = parseJsonMarker<{ invocationId?: unknown; candidateIds?: unknown }>(text, GAME_ASSET_REVIEW_MARKER_PREFIX);
if (!parsed || typeof parsed.invocationId !== 'string' || !parsed.invocationId.trim()) return null;
return {
invocationId: parsed.invocationId.trim(),
candidateIds: normalizeIds(parsed.candidateIds),
};
}
export function buildGameAssetReviewMarker(invocationId: string, candidateIds: string[]): string {
return `${GAME_ASSET_REVIEW_MARKER_PREFIX}${JSON.stringify({ invocationId, candidateIds })} -->`;
}
export function hasGameAssetSelectionMarker(message: RawMessage): boolean {
if (message.role !== 'assistant') return false;
const text = extractText(message);
return text.includes(GAME_ASSET_SELECTION_MARKER) || text.includes(GAME_ASSET_REVIEW_MARKER_PREFIX);
}
/**
* Compatibility signal for an Agent response that contains a concrete preview
* list but forgot to emit the machine-readable review marker. Keep this
* intentionally narrow: a preview-file/table label plus stable candidate ids
* is required, so ordinary asset discussion does not create an approval desk.
*/
export function hasGameAssetReviewContent(message: RawMessage): boolean {
if (message.role !== 'assistant') return false;
const text = extractText(message);
if (text.includes(GAME_ASSET_SELECTION_MARKER) || text.includes(GAME_ASSET_REVIEW_MARKER_PREFIX)) return true;
const hasPreviewEvidence = /预览文件|预览图|preview(?:path|file)|assets\/(?:review-previews|previews)/i.test(text);
const hasStableCandidateIds = /\bC-\d{3,}\b/i.test(text);
return hasPreviewEvidence && hasStableCandidateIds;
}
export function stripGameAssetSelectionMarker(text: string): string {
return text
.replaceAll(GAME_ASSET_SELECTION_MARKER, '')
.replace(/<!-- niancode:game-asset-review \{[\s\S]*?\} -->/g, '')
.trim();
}
export function buildGameAssetConfirmationMessage(assets: Array<{ id: string; name: string }>): string {
const selection = assets.map((asset) => `「${asset.name}」(${asset.id})`).join('、');
return `${GAME_ASSET_CONFIRMATION_PREFIX}${selection}。请按这条用户确认更新 ASSET_PLAN.md 的选定记录。`;
}
export function buildGameAssetReviewActionMessage(input: {
invocationId: string;
asset: { id: string; name: string };
action: GameAssetReviewAction;
}): string {
const actionText = input.action === 'approve' ? '纳入开发' : input.action === 'replace' ? '舍弃并重新寻找同用途素材' : '舍弃,不再推荐';
return [
`游戏素材审核:${actionText}`,
`素材:${input.asset.name}(${input.asset.id})`,
`审核批次:${input.invocationId}`,
input.action === 'approve'
? '请把它记录为已确认素材,并在 ASSET_PLAN.md 中保留来源、授权和接入说明。'
: input.action === 'replace'
? '请把当前素材记录为舍弃,禁止后续再次提交,并重新搜索满足同一用途的新素材。'
: '请把当前素材记录为舍弃,禁止后续再次提交。',
`${GAME_ASSET_REVIEW_ACTION_MARKER_PREFIX}${JSON.stringify({ invocationId: input.invocationId, assetId: input.asset.id, action: input.action })} -->`,
].join('\n');
}
export function buildGameAssetReviewBatchMessage(input: {
invocationId: string;
decisions: Array<{ asset: { id: string; name: string }; action: GameAssetReviewAction }>;
}): string {
const grouped = {
approve: input.decisions.filter((item) => item.action === 'approve'),
discard: input.decisions.filter((item) => item.action === 'discard'),
replace: input.decisions.filter((item) => item.action === 'replace'),
};
const format = (items: Array<{ asset: { id: string; name: string } }>): string => (
items.length > 0 ? items.map((item) => `「${item.asset.name}」(${item.asset.id})`).join('、') : '无'
);
const markerDecisions = input.decisions.map((item) => [item.asset.id, item.action]);
return [
'游戏素材审核结果(本轮一次性提交)',
`审核批次:${input.invocationId}`,
`纳入开发:${format(grouped.approve)}`,
`舍弃:${format(grouped.discard)}`,
`不合适,重新寻找:${format(grouped.replace)}`,
'请按以上用户决定一次性更新 ASSET_PLAN.md:纳入开发的记录为已确认素材;舍弃的素材禁止再次提交;需要重新寻找的素材保留原用途并搜索替代品。不要把这些决定拆成多轮询问。',
`${GAME_ASSET_REVIEW_ACTIONS_MARKER_PREFIX}${JSON.stringify({ invocationId: input.invocationId, decisions: markerDecisions })} -->`,
].join('\n');
}
export function parseGameAssetReviewAction(message: RawMessage): {
invocationId: string;
assetId: string;
action: GameAssetReviewAction;
} | null {
if (message.role !== 'user') return null;
const parsed = parseJsonMarker<{ invocationId?: unknown; assetId?: unknown; action?: unknown }>(extractText(message), GAME_ASSET_REVIEW_ACTION_MARKER_PREFIX);
if (!parsed || typeof parsed.invocationId !== 'string' || typeof parsed.assetId !== 'string') return null;
if (parsed.action !== 'approve' && parsed.action !== 'discard' && parsed.action !== 'replace') return null;
return { invocationId: parsed.invocationId, assetId: parsed.assetId, action: parsed.action };
}
export function parseGameAssetReviewActions(message: RawMessage): {
invocationId: string;
decisions: Array<{ assetId: string; action: GameAssetReviewAction }>;
} | null {
if (message.role !== 'user') return null;
const parsed = parseJsonMarker<{ invocationId?: unknown; decisions?: unknown }>(extractText(message), GAME_ASSET_REVIEW_ACTIONS_MARKER_PREFIX);
if (!parsed || typeof parsed.invocationId !== 'string' || !Array.isArray(parsed.decisions)) return null;
const decisions = parsed.decisions
.filter((item): item is [unknown, unknown] => Array.isArray(item) && item.length === 2)
.map(([assetId, action]) => ({ assetId, action }))
.filter((item): item is { assetId: string; action: GameAssetReviewAction } => (
typeof item.assetId === 'string'
&& (item.action === 'approve' || item.action === 'discard' || item.action === 'replace')
));
if (decisions.length === 0) return null;
return { invocationId: parsed.invocationId, decisions };
}
export function findGameAssetRoundConfirmation(
messages: RawMessage[],
roundMessageIndex: number,
): RawMessage | null {
for (let index = roundMessageIndex + 1; index < messages.length; index += 1) {
const message = messages[index];
if (hasGameAssetSelectionMarker(message)) return null;
if (message?.role === 'user' && extractText(message).trim().startsWith(GAME_ASSET_CONFIRMATION_PREFIX)) {
return message;
}
}
return null;
}