Add image-designer skill, align billing to DeepSeek ×1, and enrich Plaza demo.

Introduce AI image generation with chat shortcut and agent API, improve MindSpace chat-to-page save resolution, and seed Plaza covers with production deploy scripts.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
John
2026-06-16 16:29:19 -07:00
parent 03690ee354
commit 6d99d762da
155 changed files with 2748 additions and 190 deletions
+15 -4
View File
@@ -49,12 +49,12 @@ H5_ADMIN_PASSWORD=change-me-admin
H5_ACCESS_PASSWORD=change-me
# 计费(Phase 2,金额单位均为人民币分)
# 默认:DeepSeek V4 Flash 公开价 × 3(随 H5_USD_CNY_RATE 换算);可用下方变量覆盖
# 默认:DeepSeek V4 Flash 公开价 × 1(随 H5_USD_CNY_RATE 换算);可用下方变量覆盖
# H5_USE_BACKEND_COST=0
# H5_USD_CNY_RATE=7.2
# 汇率 7.2 时默认约:输入 0.302 分/1k(¥0.00302/1k)、输出 0.605 分/1k(¥0.00605/1k)
# H5_BILL_INPUT_CENTS_PER_1K=0.302
# H5_BILL_OUTPUT_CENTS_PER_1K=0.605
# 汇率 7.2 时默认约:输入 0.101 分/1k(¥0.00101/1k)、输出 0.202 分/1k(¥0.00202/1k)
# H5_BILL_INPUT_CENTS_PER_1K=0.101
# H5_BILL_OUTPUT_CENTS_PER_1K=0.202
# H5_MIN_BILL_CENTS=1
# 仅调试:H5_USE_BACKEND_COST=1 + H5_USD_CNY_RATE=7.2(上游 USD × 汇率 → 人民币分)
@@ -112,6 +112,17 @@ H5_ACCESS_PASSWORD=change-me
# H5_ASR_MAX_BYTES=5242880
# H5_ASR_TIMEOUT_MS=45000
# AI 图片生成(image-designer 技能;OpenAI 兼容 /v1/images/generations)
# H5_IMAGE_GEN_ENABLED=1
# H5_IMAGE_GEN_API_URL=https://api.openai.com/v1/images/generations
# H5_IMAGE_GEN_API_KEY=sk-...
# H5_IMAGE_GEN_MODEL=dall-e-3
# H5_IMAGE_GEN_DEFAULT_SIZE=1024x1024
# H5_IMAGE_GEN_DEFAULT_STYLE=vivid
# H5_IMAGE_GEN_DEFAULT_QUALITY=standard
# H5_IMAGE_GEN_BILL_CENTS=30
# 未配置 H5_IMAGE_GEN_API_* 时,回退使用管理后台已选 LLM 的 API Key(需支持 images/generations)
# 前端构建时注入(Vite,需 VITE_ 前缀)
# 工作目录:新建会话时使用,必填
VITE_TKMIND_WORKING_DIR=/Users/john/PycharmProjects/tkmind
+1 -1
View File
@@ -1,7 +1,7 @@
// DeepSeek V4 Flash 公开价($/1M tokens,cache-miss 输入);见 api-docs.deepseek.com
export const DEEPSEEK_FLASH_INPUT_USD_PER_1M = 0.14;
export const DEEPSEEK_FLASH_OUTPUT_USD_PER_1M = 0.28;
export const DEEPSEEK_BILLING_MARKUP = 3;
export const DEEPSEEK_BILLING_MARKUP = 1;
function deepSeekFlashCentsPer1k(usdPer1M, usdCnyRate) {
return (usdPer1M / 1000) * usdCnyRate * 100 * DEEPSEEK_BILLING_MARKUP;
+6 -6
View File
@@ -14,7 +14,7 @@ const config = {
minBillCents: 1,
};
test('loadBillingConfig defaults to DeepSeek Flash × 3 at USD/CNY 7.2', () => {
test('loadBillingConfig defaults to DeepSeek Flash × 1 at USD/CNY 7.2', () => {
const savedInput = process.env.H5_BILL_INPUT_CENTS_PER_1K;
const savedOutput = process.env.H5_BILL_OUTPUT_CENTS_PER_1K;
const savedRate = process.env.H5_USD_CNY_RATE;
@@ -23,8 +23,8 @@ test('loadBillingConfig defaults to DeepSeek Flash × 3 at USD/CNY 7.2', () => {
process.env.H5_USD_CNY_RATE = '7.2';
try {
const billing = loadBillingConfig();
assert.ok(Math.abs(billing.inputCentsPer1k - 0.3024) < 1e-9);
assert.ok(Math.abs(billing.outputCentsPer1k - 0.6048) < 1e-9);
assert.ok(Math.abs(billing.inputCentsPer1k - 0.1008) < 1e-9);
assert.ok(Math.abs(billing.outputCentsPer1k - 0.2016) < 1e-9);
} finally {
if (savedInput == null) delete process.env.H5_BILL_INPUT_CENTS_PER_1K;
else process.env.H5_BILL_INPUT_CENTS_PER_1K = savedInput;
@@ -35,12 +35,12 @@ test('loadBillingConfig defaults to DeepSeek Flash × 3 at USD/CNY 7.2', () => {
}
});
test('computeDeltaCostCents bills DeepSeek Flash × 3 defaults with min charge', () => {
test('computeDeltaCostCents bills DeepSeek Flash × 1 defaults with min charge', () => {
const billing = {
useBackendCost: false,
usdCnyRate: 7.2,
inputCentsPer1k: 0.3024,
outputCentsPer1k: 0.6048,
inputCentsPer1k: 0.1008,
outputCentsPer1k: 0.2016,
minBillCents: 1,
};
const previous = { lastInputTokens: 0, lastOutputTokens: 0 };
+10
View File
@@ -60,6 +60,14 @@ export const CHAT_SKILL_DEFINITIONS = [
requiresPublish: true,
promptKey: 'generate-page',
},
{
id: 'generate-image',
label: '生成图片',
icon: 'image',
skillName: 'image-designer',
prefillOnly: true,
promptKey: 'generate-image',
},
];
export function buildChatSkillPrompt(promptKey, skillName) {
@@ -78,6 +86,8 @@ export function buildChatSkillPrompt(promptKey, skillName) {
return '请深入分析以下内容,给出结构化解读:背景、关键发现、风险或机会、以及建议下一步:';
case 'generate-page':
return `请使用 ${skillName ?? PUBLISH_SKILL_NAME} 技能:在我的专属 MindSpace 发布目录生成静态 HTML 页面,并给出可公网访问的完整链接。`;
case 'generate-image':
return `请使用 ${skillName ?? 'image-designer'} 技能:以高级视觉设计师身份,根据我的需求生成高质量图片,保存到工作区并给出可点击预览链接。我的需求是:`;
default:
return '';
}
+1
View File
@@ -31,6 +31,7 @@ test('filterChatSkills shows generate-page when publish is allowed', () => {
test('buildChatSkillPrompt includes skill name for platform skills', () => {
assert.match(buildChatSkillPrompt('web', 'web'), /请使用 web 技能/);
assert.match(buildChatSkillPrompt('generate-page'), /static-page-publish/);
assert.match(buildChatSkillPrompt('generate-image', 'image-designer'), /image-designer/);
});
test('prefillOnly is set for open-ended chat skills', () => {
+340
View File
@@ -0,0 +1,340 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { Agent, fetch as undiciFetch } from 'undici';
import {
decryptSecret,
normalizeApiUrl,
resolveChatCompletionsUrl,
} from './llm-providers.mjs';
import { buildPublicUrl, resolvePublishKey } from './user-publish.mjs';
import { isPathInsideUserWorkspace, resolveUserWorkspaceRoot } from './user-space.mjs';
const insecureDispatcher = new Agent({
connect: { rejectUnauthorized: false },
});
const ALLOWED_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.webp']);
const ALLOWED_SIZES = new Set(['1024x1024', '1024x1792', '1792x1024', '512x512', '768x768']);
const ALLOWED_STYLES = new Set(['vivid', 'natural']);
const ALLOWED_QUALITIES = new Set(['standard', 'hd']);
export function loadImageGenConfig(env = process.env) {
return {
enabled: env.H5_IMAGE_GEN_ENABLED !== '0',
apiUrl: String(env.H5_IMAGE_GEN_API_URL ?? '').trim(),
apiKey: String(env.H5_IMAGE_GEN_API_KEY ?? '').trim(),
model: String(env.H5_IMAGE_GEN_MODEL ?? 'dall-e-3').trim() || 'dall-e-3',
defaultSize: String(env.H5_IMAGE_GEN_DEFAULT_SIZE ?? '1024x1024').trim() || '1024x1024',
defaultStyle: String(env.H5_IMAGE_GEN_DEFAULT_STYLE ?? 'vivid').trim() || 'vivid',
defaultQuality: String(env.H5_IMAGE_GEN_DEFAULT_QUALITY ?? 'standard').trim() || 'standard',
billCents: Number(env.H5_IMAGE_GEN_BILL_CENTS ?? 30),
};
}
export function resolveImagesGenerationsUrl(apiUrl) {
const configured = normalizeApiUrl(apiUrl);
if (!configured) return '';
if (configured.endsWith('/images/generations')) return configured;
if (configured.endsWith('/chat/completions')) {
return configured.replace(/\/chat\/completions$/, '/images/generations');
}
if (configured.endsWith('/v1')) return `${configured}/images/generations`;
return `${configured}/v1/images/generations`;
}
function resolveEncryptionKey(explicitKey) {
return (
explicitKey ??
process.env.H5_SETTINGS_ENCRYPTION_KEY ??
process.env.TKMIND_SERVER__SECRET_KEY ??
'local-dev-secret'
);
}
function slugifyFilename(value) {
return String(value ?? '')
.normalize('NFKC')
.trim()
.toLowerCase()
.replace(/[^\p{L}\p{N}]+/gu, '-')
.replace(/^-+|-+$/g, '')
.slice(0, 48);
}
export function normalizeImageOutputPath(rawPath, prompt) {
const cleaned = String(rawPath ?? '')
.normalize('NFKC')
.trim()
.replace(/^\.?\/+/, '')
.replace(/\\/g, '/');
const fallback = `assets/${slugifyFilename(prompt) || 'generated-image'}.png`;
const relative = cleaned || fallback;
if (
!relative ||
relative.includes('..') ||
relative.startsWith('/') ||
relative.includes('\0')
) {
throw Object.assign(new Error('输出路径无效'), { code: 'invalid_output_path' });
}
const ext = path.extname(relative).toLowerCase();
if (!ALLOWED_EXTENSIONS.has(ext)) {
throw Object.assign(new Error('仅支持 png / jpg / webp 输出'), { code: 'invalid_output_path' });
}
return relative;
}
export function resolveWorkspaceOutputPath(workspaceRoot, relativePath) {
const absolute = path.resolve(workspaceRoot, relativePath);
if (!isPathInsideUserWorkspace(workspaceRoot, absolute)) {
throw Object.assign(new Error('输出路径必须位于当前用户工作区内'), { code: 'invalid_output_path' });
}
return absolute;
}
export function composeDesignerPrompt({ prompt, styleNotes, aspectRatio, brandColors, subject }) {
const parts = [
'Professional commercial visual design, high-end art direction, polished composition, clean lighting, production-ready quality.',
];
if (subject) parts.push(`Subject: ${subject}.`);
if (styleNotes) parts.push(`Style direction: ${styleNotes}.`);
if (aspectRatio) parts.push(`Aspect ratio intent: ${aspectRatio}.`);
if (brandColors) parts.push(`Brand palette: ${brandColors}.`);
parts.push(`Creative brief: ${String(prompt ?? '').trim()}`);
parts.push('Avoid text overlays, watermarks, logos, distorted anatomy, blurry details, or cluttered backgrounds unless explicitly requested.');
return parts.join(' ');
}
function parseImagePayload(data) {
const item = data?.data?.[0];
if (!item) return null;
if (typeof item.b64_json === 'string' && item.b64_json.trim()) {
return { kind: 'base64', value: item.b64_json.trim(), revisedPrompt: item.revised_prompt ?? null };
}
if (typeof item.url === 'string' && item.url.trim()) {
return { kind: 'url', value: item.url.trim(), revisedPrompt: item.revised_prompt ?? null };
}
return null;
}
async function loadProviderCredentials(pool, encryptionKey) {
const [rows] = await pool.query(
`SELECT api_url, api_key_ciphertext, api_key_iv, api_key_tag, relay_provider
FROM h5_llm_provider_keys
WHERE is_selected = 1 AND status = 'active'
LIMIT 1`,
);
const row = rows[0];
if (!row) {
throw Object.assign(new Error('请先在管理后台配置并启用 LLM'), { code: 'image_gen_not_configured' });
}
let apiKey;
try {
apiKey = decryptSecret(
{
ciphertext: row.api_key_ciphertext,
iv: row.api_key_iv,
tag: row.api_key_tag,
},
resolveEncryptionKey(encryptionKey),
);
} catch {
throw Object.assign(new Error('LLM 密钥解密失败,请重新配置'), { code: 'image_gen_not_configured' });
}
return {
apiUrl: normalizeApiUrl(row.api_url),
apiKey,
relayProvider: row.relay_provider ?? null,
};
}
async function resolveProvider(pool, encryptionKey) {
const config = loadImageGenConfig();
if (config.apiUrl && config.apiKey) {
return {
url: resolveImagesGenerationsUrl(config.apiUrl),
apiKey: config.apiKey,
model: config.model,
relayProvider: null,
};
}
const provider = await loadProviderCredentials(pool, encryptionKey);
const chatUrl = resolveChatCompletionsUrl(provider.apiUrl);
return {
url: resolveImagesGenerationsUrl(chatUrl || provider.apiUrl),
apiKey: provider.apiKey,
model: config.model,
relayProvider: provider.relayProvider,
};
}
async function downloadImageBuffer(payload) {
if (payload.kind === 'base64') {
return Buffer.from(payload.value, 'base64');
}
const response = await undiciFetch(payload.value, {
dispatcher: payload.value.startsWith('https://') ? insecureDispatcher : undefined,
});
if (!response.ok) {
throw Object.assign(new Error(`下载生成图片失败 (${response.status})`), { code: 'image_gen_failed' });
}
const arrayBuffer = await response.arrayBuffer();
return Buffer.from(arrayBuffer);
}
function mimeTypeForPath(relativePath) {
switch (path.extname(relativePath).toLowerCase()) {
case '.jpg':
case '.jpeg':
return 'image/jpeg';
case '.webp':
return 'image/webp';
default:
return 'image/png';
}
}
export function buildImageGenerateCurlExample({ h5ApiBase, sessionId = '<当前会话ID>' } = {}) {
const base = String(h5ApiBase ?? 'http://127.0.0.1:8081').replace(/\/$/, '');
return [
'```bash',
`curl -sS -X POST '${base}/api/agent/generate_image' \\`,
" -H 'Content-Type: application/json' \\",
` -d '{"session_id":"${sessionId}","prompt":"A premium travel poster for Malaysia, cinematic sunset, editorial photography","output_path":"assets/malaysia-hero.png","style_notes":"modern editorial, warm palette","aspect_ratio":"3:4 portrait"}'`,
'```',
].join('\n');
}
export function createImageGenerationService({
pool,
h5Root,
encryptionKey,
resolveUserIdForAgentSession,
publicBaseUrl,
}) {
async function generateForAgentSession(input = {}) {
const config = loadImageGenConfig();
if (!config.enabled) {
throw Object.assign(new Error('图片生成未启用'), { code: 'image_gen_unavailable' });
}
const sessionId = String(input.sessionId ?? input.session_id ?? '').trim();
const prompt = String(input.prompt ?? '').trim();
if (!sessionId) {
throw Object.assign(new Error('缺少 session_id'), { code: 'invalid_request' });
}
if (!prompt) {
throw Object.assign(new Error('缺少 prompt'), { code: 'invalid_request' });
}
if (prompt.length > 4000) {
throw Object.assign(new Error('prompt 过长'), { code: 'invalid_request' });
}
const userId = await resolveUserIdForAgentSession(sessionId);
if (!userId) {
throw Object.assign(new Error('无效的 Agent 会话'), { code: 'forbidden' });
}
const user = { id: userId };
const relativePath = normalizeImageOutputPath(input.outputPath ?? input.output_path, prompt);
const workspaceRoot = resolveUserWorkspaceRoot(h5Root, user);
const absolutePath = resolveWorkspaceOutputPath(workspaceRoot, relativePath);
const size = ALLOWED_SIZES.has(String(input.size ?? '').trim())
? String(input.size).trim()
: config.defaultSize;
const style = ALLOWED_STYLES.has(String(input.style ?? '').trim())
? String(input.style).trim()
: config.defaultStyle;
const quality = ALLOWED_QUALITIES.has(String(input.quality ?? '').trim())
? String(input.quality).trim()
: config.defaultQuality;
const designerPrompt = composeDesignerPrompt({
prompt,
styleNotes: input.styleNotes ?? input.style_notes,
aspectRatio: input.aspectRatio ?? input.aspect_ratio,
brandColors: input.brandColors ?? input.brand_colors,
subject: input.subject,
});
const provider = await resolveProvider(pool, encryptionKey);
if (!provider.url) {
throw Object.assign(new Error('图片生成 API 地址无效'), { code: 'image_gen_not_configured' });
}
let upstream;
try {
upstream = await undiciFetch(provider.url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${provider.apiKey}`,
},
body: JSON.stringify({
model: provider.model,
prompt: designerPrompt,
n: 1,
size,
style,
quality,
response_format: 'b64_json',
...(provider.relayProvider ? { provider: provider.relayProvider } : {}),
}),
dispatcher: provider.url.startsWith('https://') ? insecureDispatcher : undefined,
});
} catch (error) {
throw Object.assign(
new Error(`图片生成失败:${error instanceof Error ? error.message : '网络错误'}`),
{ code: 'image_gen_failed' },
);
}
const text = await upstream.text().catch(() => '');
if (!upstream.ok) {
throw Object.assign(new Error(`图片生成失败 (${upstream.status})`), {
code: 'image_gen_failed',
details: text.slice(0, 500),
});
}
let data;
try {
data = JSON.parse(text);
} catch {
throw Object.assign(new Error('图片生成响应不是 JSON'), { code: 'image_gen_failed' });
}
const payload = parseImagePayload(data);
if (!payload) {
throw Object.assign(new Error('图片生成 API 未返回图片'), { code: 'image_gen_failed' });
}
const buffer = await downloadImageBuffer(payload);
if (buffer.length === 0) {
throw Object.assign(new Error('图片内容为空'), { code: 'image_gen_failed' });
}
await fs.mkdir(path.dirname(absolutePath), { recursive: true });
await fs.writeFile(absolutePath, buffer);
const publishKey = resolvePublishKey(user);
const publicUrl = buildPublicUrl(publicBaseUrl, publishKey, relativePath);
return {
relativePath,
absolutePath,
publicUrl,
mimeType: mimeTypeForPath(relativePath),
size,
style,
quality,
revisedPrompt: payload.revisedPrompt,
prompt,
designerPrompt,
};
}
return { generateForAgentSession };
}
+68
View File
@@ -0,0 +1,68 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import {
buildImageGenerateCurlExample,
composeDesignerPrompt,
loadImageGenConfig,
normalizeImageOutputPath,
resolveImagesGenerationsUrl,
} from './image-generation.mjs';
test('resolveImagesGenerationsUrl derives from chat completions base', () => {
assert.equal(
resolveImagesGenerationsUrl('https://api.openai.com/v1/chat/completions'),
'https://api.openai.com/v1/images/generations',
);
assert.equal(
resolveImagesGenerationsUrl('https://api.openai.com/v1/images/generations'),
'https://api.openai.com/v1/images/generations',
);
});
test('normalizeImageOutputPath defaults to assets slug png', () => {
assert.equal(
normalizeImageOutputPath('', '马来西亚旅行海报'),
'assets/马来西亚旅行海报.png',
);
assert.equal(normalizeImageOutputPath('public/hero.webp', 'x'), 'public/hero.webp');
});
test('normalizeImageOutputPath rejects traversal and unsupported extensions', () => {
assert.throws(
() => normalizeImageOutputPath('../secret.png', 'x'),
(error) => error.code === 'invalid_output_path',
);
assert.throws(
() => normalizeImageOutputPath('assets/hero.svg', 'x'),
(error) => error.code === 'invalid_output_path',
);
});
test('composeDesignerPrompt merges creative brief and guardrails', () => {
const prompt = composeDesignerPrompt({
prompt: 'premium coffee brand poster',
styleNotes: 'minimal, warm beige palette',
aspectRatio: '3:4',
brandColors: '#c8a27d',
subject: 'latte art',
});
assert.match(prompt, /premium coffee brand poster/);
assert.match(prompt, /minimal, warm beige palette/);
assert.match(prompt, /Avoid text overlays/);
});
test('buildImageGenerateCurlExample includes agent endpoint', () => {
const example = buildImageGenerateCurlExample({
h5ApiBase: 'http://127.0.0.1:8081',
sessionId: 'sess-1',
});
assert.match(example, /\/api\/agent\/generate_image/);
assert.match(example, /sess-1/);
});
test('loadImageGenConfig defaults', () => {
const config = loadImageGenConfig({});
assert.equal(config.enabled, true);
assert.equal(config.model, 'dall-e-3');
assert.equal(config.defaultSize, '1024x1024');
});
+184 -31
View File
@@ -1,6 +1,11 @@
import fs from 'node:fs/promises';
import path from 'node:path';
import { PUBLISH_ROOT_DIR, PUBLISH_KEY_UUID } from './user-publish.mjs';
import {
migrateUserPublishDir,
PUBLISH_ROOT_DIR,
PUBLISH_KEY_UUID,
resolveLegacyPublishDir,
} from './user-publish.mjs';
import { workspaceThumbnailRelativePath } from './mindspace-workspace-thumbnails.mjs';
const URL_PATTERN =
@@ -23,11 +28,10 @@ export function extractStaticPageLinks(content, { userId, username } = {}) {
for (const match of text.matchAll(URL_PATTERN)) {
const owner = decodePathSegment(match[1]).toLowerCase();
const relativePath = decodePathSegment(match[2]);
if (normalizedUserId) {
if (owner !== normalizedUserId) continue;
} else if (normalizedUsername && owner !== normalizedUsername) {
continue;
}
const ownedByUser =
(normalizedUserId && owner === normalizedUserId) ||
(normalizedUsername && owner === normalizedUsername);
if ((normalizedUserId || normalizedUsername) && !ownedByUser) continue;
const key = `${owner}/${relativePath}`;
if (seen.has(key)) continue;
seen.add(key);
@@ -114,7 +118,7 @@ async function walkPublishHtmlByBasename(publishRoot, basename, maxDepth = 6, de
return null;
}
export async function findPublishHtml(h5Root, userId, relativePath) {
async function findPublishHtmlInRoot(publishRoot, relativePath, { readFile = fs.readFile } = {}) {
const normalized = String(relativePath ?? '').replace(/^\/+/, '');
const basename = path.basename(normalized);
const candidates = [
@@ -124,22 +128,59 @@ export async function findPublishHtml(h5Root, userId, relativePath) {
].filter((value, index, list) => value && list.indexOf(value) === index);
for (const candidate of candidates) {
const absolute = path.resolve(publishRoot, candidate);
if (absolute !== publishRoot && !absolute.startsWith(`${publishRoot}${path.sep}`)) continue;
if (!absolute.toLowerCase().endsWith('.html')) continue;
try {
return await readPublishHtml(h5Root, userId, candidate);
const content = await readFile(absolute, 'utf8');
if (!content.trim()) continue;
const resolvedRelativePath = path.relative(publishRoot, absolute).split(path.sep).join('/');
return {
absolute,
content,
relativePath: resolvedRelativePath,
filename: path.basename(resolvedRelativePath),
};
} catch {
// try next candidate
}
}
const publishRoot = path.resolve(
h5Root,
PUBLISH_ROOT_DIR,
String(userId ?? '').trim().toLowerCase(),
);
const absolute = await walkPublishHtmlByBasename(publishRoot, basename);
if (absolute) {
const resolvedRelativePath = path.relative(publishRoot, absolute).split(path.sep).join('/');
return readPublishHtml(h5Root, userId, resolvedRelativePath);
const content = await readFile(absolute, 'utf8');
if (!content.trim()) {
throw Object.assign(new Error('页面内容为空'), { code: 'empty_page_content' });
}
return {
absolute,
content,
relativePath: resolvedRelativePath,
filename: path.basename(resolvedRelativePath),
};
}
return null;
}
export async function findPublishHtml(h5Root, userId, relativePath, { username } = {}) {
const key = String(userId ?? '').trim().toLowerCase();
if (username && key) {
migrateUserPublishDir(h5Root, { id: key, username });
}
const publishRoot = path.resolve(h5Root, PUBLISH_ROOT_DIR, key);
const primary = await findPublishHtmlInRoot(publishRoot, relativePath);
if (primary) return primary;
const legacyRoot = username ? resolveLegacyPublishDir(h5Root, { username }) : null;
if (legacyRoot && path.resolve(legacyRoot) !== publishRoot) {
const legacy = await findPublishHtmlInRoot(legacyRoot, relativePath);
if (legacy) {
migrateUserPublishDir(h5Root, { id: key, username });
return readPublishHtml(h5Root, userId, legacy.relativePath);
}
}
throw Object.assign(new Error('无法读取链接页面内容'), { code: 'static_page_not_found' });
@@ -215,8 +256,9 @@ function dedupeDuplicatedFilename(filename) {
if (half >= 2 && base.slice(0, half) === base.slice(half)) {
base = base.slice(0, half);
}
base = base.replace(/(.+?)\1+/g, '$1');
base = base.replace(/(.{4,}?)\1+/g, '$1');
base = base.replace(/-v-v(\d+)/g, '-v$1');
base = base.replace(/-v(\d)\1+$/g, '-v$1');
return `${base}.html`;
}
@@ -288,23 +330,123 @@ export function analyzeChatMessageForSave({
};
}
export async function resolveStaticHtmlContent(analysis) {
function uniqueRelativePaths(relativePath) {
const basename = path.basename(String(relativePath ?? ''));
return [
String(relativePath ?? '').replace(/^\/+/, ''),
String(relativePath ?? '')
.replace(/^\/+/, '')
.replace(/^public\//, ''),
`public/${basename}`,
basename,
].filter((value, index, list) => value && list.indexOf(value) === index);
}
function buildPublishHtmlFetchUrls({ userId, link, publicBaseUrl }) {
const urls = new Set();
const base = String(publicBaseUrl ?? '').replace(/\/$/, '');
if (base && link) {
for (const rel of uniqueRelativePaths(link.relativePath)) {
const asset = buildWorkspaceAssetUrl(userId, rel);
if (asset) urls.add(`${base}${asset}`);
}
}
if (link?.publicUrl) urls.add(link.publicUrl);
return [...urls];
}
async function mirrorFetchedPublishHtml(h5Root, userId, relativePath, content, { username } = {}) {
try {
if (username) {
migrateUserPublishDir(h5Root, { id: userId, username });
}
const absolute = resolvePublishHtmlAbsolutePath(h5Root, userId, relativePath);
await fs.mkdir(path.dirname(absolute), { recursive: true });
await fs.writeFile(absolute, content, 'utf8');
return absolute;
} catch {
return null;
}
}
export async function fetchPublishHtmlFallback(
analysis,
{ publicBaseUrl, fetchImpl = fetch } = {},
) {
if (analysis.contentMode !== 'static_html' || !analysis.selectedLink) return null;
const candidatePaths = uniqueRelativePaths(analysis.selectedLink.relativePath);
const urls = buildPublishHtmlFetchUrls({
userId: analysis.userId,
link: analysis.selectedLink,
publicBaseUrl,
});
for (const url of urls) {
try {
const response = await fetchImpl(url, { redirect: 'follow' });
if (!response.ok) continue;
const contentType = response.headers.get('content-type') ?? '';
if (contentType.includes('json')) continue;
const content = await response.text();
if (!content.trim() || !/<html/i.test(content)) continue;
let relativePath = candidatePaths[0];
for (const rel of candidatePaths) {
if (url.includes(encodeURIComponent(rel)) || url.endsWith(`/${rel}`) || url.endsWith(rel)) {
relativePath = rel;
break;
}
}
const absolute = await mirrorFetchedPublishHtml(
analysis.h5Root,
analysis.userId,
relativePath,
content,
{ username: analysis.username },
);
const title = titleFromHtml(content);
const summary = summaryFromHtml(content);
return {
absolute:
absolute ??
resolvePublishHtmlAbsolutePath(analysis.h5Root, analysis.userId, relativePath),
content,
relativePath,
filename: path.basename(relativePath),
suggestedTitle: title || analysis.suggestedTitle,
suggestedSummary: summary || analysis.suggestedSummary,
publicUrl: analysis.selectedLink.publicUrl,
};
} catch {
// try next url
}
}
return null;
}
export async function resolveStaticHtmlContent(analysis, options = {}) {
if (analysis.contentMode !== 'static_html' || !analysis.selectedLink) {
return null;
}
const loaded = await findPublishHtml(
analysis.h5Root,
analysis.userId,
analysis.selectedLink.relativePath,
);
const title = titleFromHtml(loaded.content);
const summary = summaryFromHtml(loaded.content);
return {
...loaded,
suggestedTitle: title || analysis.suggestedTitle,
suggestedSummary: summary || analysis.suggestedSummary,
publicUrl: analysis.selectedLink.publicUrl,
};
try {
const loaded = await findPublishHtml(
analysis.h5Root,
analysis.userId,
analysis.selectedLink.relativePath,
{ username: analysis.username },
);
const title = titleFromHtml(loaded.content);
const summary = summaryFromHtml(loaded.content);
return {
...loaded,
suggestedTitle: title || analysis.suggestedTitle,
suggestedSummary: summary || analysis.suggestedSummary,
publicUrl: analysis.selectedLink.publicUrl,
};
} catch (error) {
if (error?.code !== 'static_page_not_found') throw error;
const fetched = await fetchPublishHtmlFallback(analysis, options);
if (fetched) return fetched;
throw error;
}
}
export async function resolveChatSaveAnalysis({
@@ -313,6 +455,8 @@ export async function resolveChatSaveAnalysis({
username,
h5Root,
selectedLinkIndex = 0,
publicBaseUrl,
fetchImpl,
}) {
const sanitized = sanitizeMessageContentForSave(content);
let analysis = analyzeChatMessageForSave({
@@ -323,9 +467,11 @@ export async function resolveChatSaveAnalysis({
selectedLinkIndex,
});
const resolveOptions = { publicBaseUrl, fetchImpl };
if (analysis.contentMode === 'static_html' && analysis.selectedLink) {
try {
const resolvedHtml = await resolveStaticHtmlContent(analysis);
const resolvedHtml = await resolveStaticHtmlContent(analysis, resolveOptions);
return { analysis, resolvedHtml };
} catch {
// fall through to filename hints
@@ -334,7 +480,7 @@ export async function resolveChatSaveAnalysis({
for (const hint of extractHtmlFilenameHints(sanitized)) {
try {
const loaded = await findPublishHtml(h5Root, userId, hint);
const loaded = await findPublishHtml(h5Root, userId, hint, { username });
const syntheticLink = {
publicUrl:
buildWorkspaceAssetUrl(userId, loaded.relativePath) ??
@@ -373,5 +519,12 @@ export async function resolveChatSaveAnalysis({
}
}
if (analysis.contentMode === 'static_html' && analysis.selectedLink) {
const fetched = await fetchPublishHtmlFallback(analysis, resolveOptions);
if (fetched) {
return { analysis, resolvedHtml: fetched };
}
}
return { analysis, resolvedHtml: null };
}
+75
View File
@@ -11,10 +11,12 @@ import {
buildWorkspaceBaseHref,
buildWorkspaceThumbnailUrl,
extractStaticPageLinks,
extractHtmlFilenameHints,
findPublishHtml,
injectHtmlBaseHref,
resolveChatSaveAnalysis,
sanitizeMessageContentForSave,
fetchPublishHtmlFallback,
} from './mindspace-chat-save.mjs';
const USER_ID = 'a6fb1e97-2b0f-447b-b138-4561d8e5c53e';
@@ -42,6 +44,13 @@ test('extractStaticPageLinks accepts legacy username urls when username provided
assert.equal(links.length, 1);
});
test('extractStaticPageLinks accepts legacy username urls when userId and username provided', () => {
const content = 'https://goo.tkmind.cn/MindSpace/john/report.html';
const links = extractStaticPageLinks(content, { userId: USER_ID, username: 'john' });
assert.equal(links.length, 1);
assert.equal(links[0].relativePath, 'report.html');
});
test('analyzeChatMessageForSave prefers static html mode', () => {
const analysis = analyzeChatMessageForSave({
content: `链接 https://g2.tkmind.cn/MindSpace/${USER_ID}/report.html`,
@@ -96,6 +105,42 @@ test('buildChatSavePreviewUrls use local api routes', () => {
);
});
test('findPublishHtml resolves html stored only in legacy username directory', async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'memind-chat-save-legacy-'));
const legacyDir = path.join(root, 'MindSpace', 'john', 'public');
await fs.mkdir(legacyDir, { recursive: true });
await fs.writeFile(
path.join(legacyDir, 'legacy-only.html'),
'<!DOCTYPE html><html><head><title>Legacy</title></head><body>legacy</body></html>',
'utf8',
);
const loaded = await findPublishHtml(root, USER_ID, 'public/legacy-only.html', { username: 'john' });
assert.equal(loaded.relativePath, 'public/legacy-only.html');
assert.match(loaded.content, /Legacy/);
const migrated = path.join(root, 'MindSpace', USER_ID, 'public', 'legacy-only.html');
assert.ok(await fs.stat(migrated).then(() => true).catch(() => false));
});
test('resolveChatSaveAnalysis resolves uuid link when html exists only in legacy username dir', async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'memind-chat-save-uuid-link-'));
const legacyDir = path.join(root, 'MindSpace', 'john', 'public');
await fs.mkdir(legacyDir, { recursive: true });
await fs.writeFile(
path.join(legacyDir, 'report.html'),
'<!DOCTYPE html><html><head><title>Report</title></head><body>ok</body></html>',
'utf8',
);
const content = `链接 https://g2.tkmind.cn/MindSpace/${USER_ID}/public/report.html`;
const result = await resolveChatSaveAnalysis({
content,
userId: USER_ID,
username: 'john',
h5Root: root,
});
assert.equal(result.analysis.contentMode, 'static_html');
assert.equal(result.resolvedHtml?.relativePath, 'public/report.html');
});
test('findPublishHtml resolves nested public html by basename', async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'memind-chat-save-'));
const publishDir = path.join(root, 'MindSpace', USER_ID, 'public');
@@ -116,6 +161,11 @@ test('injectHtmlBaseHref adds base tag for relative assets', () => {
assert.match(next, /<base href="\/MindSpace\//);
});
test('extractHtmlFilenameHints preserves normal hyphenated filenames', () => {
const hints = extractHtmlFilenameHints('已生成 training-registration.html');
assert.deepEqual(hints, ['training-registration.html']);
});
test('sanitizeMessageContentForSave repairs duplicated streaming urls', () => {
const raw =
'httpshttps://://gg22.t.tkmkmindind.cn.cn/M/MindSpace/a6fb1e97/public/public/w/welcomeelcome-v-v22.html.html';
@@ -144,3 +194,28 @@ test('resolveChatSaveAnalysis falls back to local html filename hints', async ()
assert.equal(result.analysis.contentMode, 'static_html');
assert.equal(result.resolvedHtml?.relativePath, 'public/welcome-v2.html');
});
test('fetchPublishHtmlFallback mirrors remote html into local MindSpace when disk is missing', async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'memind-chat-save-fetch-'));
const html =
'<!DOCTYPE html><html><head><title>Remote</title></head><body>from remote</body></html>';
const analysis = analyzeChatMessageForSave({
content: `https://g2.tkmind.cn/MindSpace/${USER_ID}/public/remote-page.html`,
userId: USER_ID,
username: 'john',
h5Root: root,
});
const fetched = await fetchPublishHtmlFallback(analysis, {
publicBaseUrl: 'http://127.0.0.1:8081',
fetchImpl: async () => ({
ok: true,
headers: { get: () => 'text/html' },
text: async () => html,
}),
});
assert.ok(fetched);
assert.equal(fetched.relativePath, 'public/remote-page.html');
assert.match(fetched.content, /from remote/);
const local = path.join(root, 'MindSpace', USER_ID, 'public', 'remote-page.html');
assert.match(await fs.readFile(local, 'utf8'), /from remote/);
});
+1 -1
View File
@@ -440,7 +440,7 @@ export function createPageService(pool, options = {}) {
? await resolveWorkspacePublishDir(userId)
: null;
if (workspacePublishDir && workspaceHtmlRelativePath) {
await ensureWorkspaceHtmlThumbnail(
void ensureWorkspaceHtmlThumbnail(
workspacePublishDir,
workspaceHtmlRelativePath,
normalized.content,
+235 -39
View File
@@ -8,6 +8,7 @@
"name": "tkmind-h5",
"version": "0.1.0",
"dependencies": {
"argon2": "^0.44.0",
"debug": "^4.4.3",
"express": "^4.21.2",
"http-proxy-middleware": "^3.0.3",
@@ -17,6 +18,7 @@
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.13.1",
"redis": "^4.7.1",
"undici": "^6.26.0"
},
"devDependencies": {
@@ -310,6 +312,12 @@
"node": ">=6.9.0"
}
},
"node_modules/@epic-web/invariant": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz",
"integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
@@ -802,6 +810,80 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@phc/format": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz",
"integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/@redis/bloom": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz",
"integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==",
"license": "MIT",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/client": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/@redis/client/-/client-1.6.1.tgz",
"integrity": "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw==",
"license": "MIT",
"dependencies": {
"cluster-key-slot": "1.1.2",
"generic-pool": "3.9.0",
"yallist": "4.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@redis/client/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"license": "ISC"
},
"node_modules/@redis/graph": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.1.tgz",
"integrity": "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw==",
"license": "MIT",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/json": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.7.tgz",
"integrity": "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ==",
"license": "MIT",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/search": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@redis/search/-/search-1.2.0.tgz",
"integrity": "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw==",
"license": "MIT",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@redis/time-series": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.1.0.tgz",
"integrity": "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g==",
"license": "MIT",
"peerDependencies": {
"@redis/client": "^1.0.0"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.27",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
@@ -901,9 +983,6 @@
"arm"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -918,9 +997,6 @@
"arm"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -935,9 +1011,6 @@
"arm64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -952,9 +1025,6 @@
"arm64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -969,9 +1039,6 @@
"loong64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -986,9 +1053,6 @@
"loong64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1003,9 +1067,6 @@
"ppc64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1020,9 +1081,6 @@
"ppc64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1037,9 +1095,6 @@
"riscv64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1054,9 +1109,6 @@
"riscv64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1071,9 +1123,6 @@
"s390x"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1088,9 +1137,6 @@
"x64"
],
"dev": true,
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1105,9 +1151,6 @@
"x64"
],
"dev": true,
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
@@ -1363,6 +1406,22 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/argon2": {
"version": "0.44.0",
"resolved": "https://registry.npmjs.org/argon2/-/argon2-0.44.0.tgz",
"integrity": "sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@phc/format": "^1.0.0",
"cross-env": "^10.0.0",
"node-addon-api": "^8.5.0",
"node-gyp-build": "^4.8.4"
},
"engines": {
"node": ">=16.17.0"
}
},
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@@ -1568,6 +1627,15 @@
"wrap-ansi": "^6.2.0"
}
},
"node_modules/cluster-key-slot": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz",
"integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -1629,6 +1697,37 @@
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"license": "MIT"
},
"node_modules/cross-env": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz",
"integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==",
"license": "MIT",
"dependencies": {
"@epic-web/invariant": "^1.0.0",
"cross-spawn": "^7.0.6"
},
"bin": {
"cross-env": "dist/bin/cross-env.js",
"cross-env-shell": "dist/bin/cross-env-shell.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -2044,6 +2143,15 @@
"is-property": "^1.0.2"
}
},
"node_modules/generic-pool": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz",
"integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==",
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -2287,6 +2395,12 @@
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
"license": "MIT"
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -2538,6 +2652,26 @@
"node": ">= 0.6"
}
},
"node_modules/node-addon-api": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.8.0.tgz",
"integrity": "sha512-c5Ko1fZJIJmzhFIkhRN76WTq+fC6tWnGy9CXA0fA+XygsWZmEwG8vmbkNqxMyoaa0Tin4djul49NzdVcJJcjeA==",
"license": "MIT",
"engines": {
"node": "^18 || ^20 || >= 21"
}
},
"node_modules/node-gyp-build": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"license": "MIT",
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/node-releases": {
"version": "2.0.47",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz",
@@ -2626,6 +2760,15 @@
"node": ">=8"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-to-regexp": {
"version": "0.1.13",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
@@ -2840,6 +2983,23 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/redis": {
"version": "4.7.1",
"resolved": "https://registry.npmjs.org/redis/-/redis-4.7.1.tgz",
"integrity": "sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ==",
"license": "MIT",
"workspaces": [
"./packages/*"
],
"dependencies": {
"@redis/bloom": "1.2.0",
"@redis/client": "1.6.1",
"@redis/graph": "1.1.1",
"@redis/json": "1.0.7",
"@redis/search": "1.2.0",
"@redis/time-series": "1.1.0"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -3020,6 +3180,27 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/side-channel": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
@@ -3437,6 +3618,21 @@
"node": ">=12"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",
+3 -1
View File
@@ -15,6 +15,7 @@
"check:plaza": "node scripts/check-plaza-local.mjs",
"setup:plaza-local": "node scripts/setup-plaza-local.mjs",
"setup:plaza-tunnel": "bash scripts/install-plaza-tunnel.sh",
"setup:prod-services": "bash scripts/install-prod-services.sh",
"setup:plaza-tls": "node scripts/plaza-local-tls.mjs",
"dev:vite": "vite",
"dev:server": "node server.mjs",
@@ -25,7 +26,7 @@
"seed:plaza-covers": "node scripts/seed-plaza-covers.mjs",
"enrich:plaza": "node scripts/enrich-plaza-posts.mjs",
"build": "vite build",
"test": "node --test auth.test.mjs asr-proxy.test.mjs billing.test.mjs billing-recharge.test.mjs wechat-pay.test.mjs wechat-oauth.test.mjs capabilities.test.mjs policies.test.mjs chat-skills.test.mjs user-publish.test.mjs user-memory-profile.test.mjs skills-registry.test.mjs mindspace.test.mjs mindspace-scan.test.mjs mindspace-assets.test.mjs mindspace-pages.test.mjs mindspace-publications.test.mjs mindspace-chat-save.test.mjs mindspace-chat-context.test.mjs mindspace-content-scan.test.mjs mindspace-html-localize.test.mjs mindspace-visual-editor.test.mjs mindspace-cleanup.test.mjs mindspace-thumbnails.test.mjs mindspace-workspace-thumbnails.test.mjs mindspace-workspace-sync.test.mjs mindspace-asset-preview.test.mjs mindspace-agent-jobs.test.mjs mindspace-agent-runner.test.mjs message-stream.test.mjs plaza-posts.test.mjs plaza-interactions.test.mjs plaza-algorithm.test.mjs plaza-seo.test.mjs plaza-ops.test.mjs user-auth.test.mjs llm-providers.test.mjs",
"test": "node --test auth.test.mjs asr-proxy.test.mjs billing.test.mjs billing-recharge.test.mjs wechat-pay.test.mjs wechat-oauth.test.mjs capabilities.test.mjs policies.test.mjs chat-skills.test.mjs user-publish.test.mjs user-memory-profile.test.mjs skills-registry.test.mjs mindspace.test.mjs mindspace-scan.test.mjs mindspace-assets.test.mjs mindspace-pages.test.mjs mindspace-publications.test.mjs mindspace-chat-save.test.mjs mindspace-chat-context.test.mjs mindspace-content-scan.test.mjs mindspace-html-localize.test.mjs mindspace-visual-editor.test.mjs mindspace-cleanup.test.mjs mindspace-thumbnails.test.mjs mindspace-workspace-thumbnails.test.mjs mindspace-workspace-sync.test.mjs mindspace-asset-preview.test.mjs mindspace-agent-jobs.test.mjs mindspace-agent-runner.test.mjs message-stream.test.mjs plaza-posts.test.mjs plaza-interactions.test.mjs plaza-algorithm.test.mjs plaza-seo.test.mjs plaza-ops.test.mjs user-auth.test.mjs llm-providers.test.mjs image-generation.test.mjs",
"test:mindspace-e2e": "node scripts/mindspace-e2e.mjs",
"test:mindspace-pages-e2e": "node scripts/mindspace-pages-e2e.mjs",
"test:mindspace-publications-e2e": "node scripts/mindspace-publications-e2e.mjs",
@@ -41,6 +42,7 @@
"deploy:plaza-105": "bash scripts/deploy-plaza-105.sh"
},
"dependencies": {
"argon2": "^0.44.0",
"debug": "^4.4.3",
"express": "^4.21.2",
"http-proxy-middleware": "^3.0.3",
+91
View File
@@ -8,6 +8,9 @@ importers:
.:
dependencies:
argon2:
specifier: ^0.44.0
version: 0.44.0
debug:
specifier: ^4.4.3
version: 4.4.3
@@ -146,6 +149,9 @@ packages:
resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==}
engines: {node: '>=6.9.0'}
'@epic-web/invariant@1.0.0':
resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==}
'@esbuild/aix-ppc64@0.25.12':
resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
engines: {node: '>=18'}
@@ -318,6 +324,10 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@phc/format@1.0.0':
resolution: {integrity: sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==}
engines: {node: '>=10'}
'@redis/bloom@1.2.0':
resolution: {integrity: sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==}
peerDependencies:
@@ -541,6 +551,10 @@ packages:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
engines: {node: '>=8'}
argon2@0.44.0:
resolution: {integrity: sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig==}
engines: {node: '>=16.17.0'}
array-flatten@1.1.1:
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
@@ -625,6 +639,15 @@ packages:
resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==}
engines: {node: '>=18'}
cross-env@10.1.0:
resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==}
engines: {node: '>=20'}
hasBin: true
cross-spawn@7.0.6:
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
engines: {node: '>= 8'}
csstype@3.2.3:
resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
@@ -855,6 +878,9 @@ packages:
is-property@1.0.2:
resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==}
isexe@2.0.0:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -943,6 +969,14 @@ packages:
resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==}
engines: {node: '>= 0.6'}
node-addon-api@8.8.0:
resolution: {integrity: sha512-c5Ko1fZJIJmzhFIkhRN76WTq+fC6tWnGy9CXA0fA+XygsWZmEwG8vmbkNqxMyoaa0Tin4djul49NzdVcJJcjeA==}
engines: {node: ^18 || ^20 || >= 21}
node-gyp-build@4.8.4:
resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
hasBin: true
node-releases@2.0.47:
resolution: {integrity: sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==}
engines: {node: '>=18'}
@@ -975,6 +1009,10 @@ packages:
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
engines: {node: '>=8'}
path-key@3.1.1:
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
engines: {node: '>=8'}
path-to-regexp@0.1.13:
resolution: {integrity: sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==}
@@ -1096,6 +1134,14 @@ packages:
setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
shebang-command@2.0.0:
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
engines: {node: '>=8'}
shebang-regex@3.0.0:
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
engines: {node: '>=8'}
side-channel-list@1.0.1:
resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==}
engines: {node: '>= 0.4'}
@@ -1225,6 +1271,11 @@ packages:
which-module@2.0.1:
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
which@2.0.2:
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
engines: {node: '>= 8'}
hasBin: true
wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
@@ -1372,6 +1423,8 @@ snapshots:
'@babel/helper-string-parser': 7.29.7
'@babel/helper-validator-identifier': 7.29.7
'@epic-web/invariant@1.0.0': {}
'@esbuild/aix-ppc64@0.25.12':
optional: true
@@ -1469,6 +1522,8 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@phc/format@1.0.0': {}
'@redis/bloom@1.2.0(@redis/client@1.6.1)':
dependencies:
'@redis/client': 1.6.1
@@ -1640,6 +1695,13 @@ snapshots:
dependencies:
color-convert: 2.0.1
argon2@0.44.0:
dependencies:
'@phc/format': 1.0.0
cross-env: 10.1.0
node-addon-api: 8.8.0
node-gyp-build: 4.8.4
array-flatten@1.1.1: {}
aws-ssl-profiles@1.1.2: {}
@@ -1723,6 +1785,17 @@ snapshots:
cookie@1.1.1: {}
cross-env@10.1.0:
dependencies:
'@epic-web/invariant': 1.0.0
cross-spawn: 7.0.6
cross-spawn@7.0.6:
dependencies:
path-key: 3.1.1
shebang-command: 2.0.0
which: 2.0.2
csstype@3.2.3: {}
debug@2.6.9:
@@ -1980,6 +2053,8 @@ snapshots:
is-property@1.0.2: {}
isexe@2.0.0: {}
js-tokens@4.0.0: {}
jsesc@3.1.0: {}
@@ -2045,6 +2120,10 @@ snapshots:
negotiator@0.6.3: {}
node-addon-api@8.8.0: {}
node-gyp-build@4.8.4: {}
node-releases@2.0.47: {}
object-inspect@1.13.4: {}
@@ -2067,6 +2146,8 @@ snapshots:
path-exists@4.0.0: {}
path-key@3.1.1: {}
path-to-regexp@0.1.13: {}
picocolors@1.1.1: {}
@@ -2217,6 +2298,12 @@ snapshots:
setprototypeof@1.2.0: {}
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
shebang-regex@3.0.0: {}
side-channel-list@1.0.1:
dependencies:
es-errors: 1.3.0
@@ -2311,6 +2398,10 @@ snapshots:
which-module@2.0.1: {}
which@2.0.2:
dependencies:
isexe: 2.0.0
wrap-ansi@6.2.0:
dependencies:
ansi-styles: 4.3.0
+2
View File
@@ -0,0 +1,2 @@
allowBuilds:
argon2: set this to true or false
Binary file not shown.

After

Width:  |  Height:  |  Size: 407 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 569 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 555 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 569 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 407 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 672 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 294 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 210 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 583 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 KiB

Some files were not shown because too many files have changed in this diff Show More