Files
tkmind_go/ui/h5/mindspace-chat-save.mjs
john 4e21ca937a
Deploy Documentation / deploy (push) Has been cancelled
Canary / Prepare Version (push) Has been cancelled
Canary / build-cli (push) Has been cancelled
Canary / Upload Install Script (push) Has been cancelled
Canary / bundle-desktop (push) Has been cancelled
Canary / bundle-desktop-intel (push) Has been cancelled
Canary / bundle-desktop-linux (push) Has been cancelled
Canary / bundle-desktop-windows (push) Has been cancelled
Canary / bundle-desktop-windows-cuda (push) Has been cancelled
Canary / Release (push) Has been cancelled
Unused Dependencies / machete (push) Has been cancelled
CI / changes (push) Has been cancelled
CI / Check Rust Code Format (push) Has been cancelled
CI / Build and Test Rust Project (push) Has been cancelled
CI / Build Rust Project on Windows (push) Has been cancelled
CI / Check MSRV (push) Has been cancelled
CI / Lint Rust Code (push) Has been cancelled
CI / Check Generated Schemas are Up-to-Date (push) Has been cancelled
CI / Test and Lint Electron Desktop App (push) Has been cancelled
CI / H5 Plaza Tests and Build (push) Has been cancelled
Live Provider Tests / check-fork (push) Has been cancelled
Live Provider Tests / changes (push) Has been cancelled
Live Provider Tests / Build Binary (push) Has been cancelled
Live Provider Tests / Smoke Tests (push) Has been cancelled
Live Provider Tests / Smoke Tests (Code Execution) (push) Has been cancelled
Live Provider Tests / Compaction Tests (push) Has been cancelled
Live Provider Tests / goose server HTTP integration tests (push) Has been cancelled
Publish Ask AI Bot Docker Image / docker (push) Has been cancelled
Publish Docker Image / docker (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Add TKMind platform extensions, H5/MindSpace stack, and deployment tooling.
Fork goose with custom MCP widgets, platform extensions (aider, git, web, search),
MindSpace H5 backend/frontend, Plaza/Ops UIs, and deploy scripts for tkmind.cn.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-14 21:30:20 +08:00

143 lines
4.3 KiB
JavaScript

import fs from 'node:fs/promises';
import path from 'node:path';
import { PUBLISH_ROOT_DIR } from './user-publish.mjs';
const URL_PATTERN =
/https?:\/\/[^\s<>"')\]]+\/(?:MindSpace|temp)\/([a-z0-9._-]+)\/([^\s<>"')\]]+\.html)/gi;
function decodePathSegment(segment) {
try {
return decodeURIComponent(segment);
} catch {
return segment;
}
}
export function extractStaticPageLinks(content, { username } = {}) {
const text = String(content ?? '');
const links = [];
const seen = new Set();
for (const match of text.matchAll(URL_PATTERN)) {
const owner = decodePathSegment(match[1]).toLowerCase();
const relativePath = decodePathSegment(match[2]);
if (username && owner !== String(username).trim().toLowerCase()) continue;
const key = `${owner}/${relativePath}`;
if (seen.has(key)) continue;
seen.add(key);
links.push({
publicUrl: match[0],
owner,
relativePath,
filename: path.basename(relativePath),
});
}
return links;
}
export function resolvePublishHtmlAbsolutePath(h5Root, username, relativePath) {
const slug = String(username ?? '').trim().toLowerCase();
const clean = String(relativePath ?? '')
.replace(/^\/+/, '')
.split('/')
.filter((part) => part && part !== '.' && part !== '..')
.join('/');
if (!slug || !clean || !clean.toLowerCase().endsWith('.html')) {
throw Object.assign(new Error('无效的页面路径'), { code: 'invalid_page_path' });
}
const publishRoot = path.resolve(h5Root, PUBLISH_ROOT_DIR, slug);
const absolute = path.resolve(publishRoot, clean);
if (absolute !== publishRoot && !absolute.startsWith(`${publishRoot}${path.sep}`)) {
throw Object.assign(new Error('页面路径越界'), { code: 'invalid_page_path' });
}
return absolute;
}
export async function readPublishHtml(h5Root, username, relativePath) {
const absolute = resolvePublishHtmlAbsolutePath(h5Root, username, relativePath);
const content = await fs.readFile(absolute, 'utf8');
if (!content.trim()) {
throw Object.assign(new Error('页面内容为空'), { code: 'empty_page_content' });
}
return { absolute, content, relativePath, filename: path.basename(relativePath) };
}
function titleFromHtml(html) {
const match = String(html).match(/<title[^>]*>([^<]+)<\/title>/i);
return match?.[1]?.trim() ?? '';
}
function summaryFromHtml(html) {
const stripped = String(html)
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
.replace(/<[^>]+>/g, ' ')
.replace(/\s+/g, ' ')
.trim();
return stripped.slice(0, 180);
}
export function analyzeChatMessageForSave({
content,
username,
h5Root,
selectedLinkIndex = 0,
}) {
const links = extractStaticPageLinks(content, { username });
const text = String(content ?? '').replace(/\s+/g, ' ').trim();
const suggestedTitleFromText = text
.replace(/^#{1,6}\s*/, '')
.replace(/[*_`~[\]]/g, '')
.trim()
.slice(0, 48);
if (links.length === 0) {
return {
contentMode: 'markdown',
links: [],
selectedLink: null,
suggestedTitle: suggestedTitleFromText || 'AI 创作页面',
suggestedSummary: text.slice(0, 160),
previewUrl: null,
relativePath: null,
filename: null,
};
}
const index = Math.min(Math.max(0, selectedLinkIndex), links.length - 1);
const selectedLink = links[index];
return {
contentMode: 'static_html',
links,
selectedLink,
suggestedTitle:
selectedLink.filename.replace(/\.html$/i, '').replace(/[-_]/g, ' ') ||
suggestedTitleFromText ||
'AI 生成页面',
suggestedSummary: text.slice(0, 160),
previewUrl: selectedLink.publicUrl,
relativePath: selectedLink.relativePath,
filename: selectedLink.filename,
h5Root,
username,
};
}
export async function resolveStaticHtmlContent(analysis) {
if (analysis.contentMode !== 'static_html' || !analysis.selectedLink) {
return null;
}
const loaded = await readPublishHtml(
analysis.h5Root,
analysis.username,
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,
};
}