fix: portal stability, session list DB fallback, and UX polish
- Free stale Memind listeners on 8081 before startup and exit cleanly on EADDRINUSE - Backfill owned sessions missing from Goose via h5_conversation_messages summaries - Strip Memind task orchestration prefixes from user-facing chat text - Repair missing public docx links before release MindSpace link checks Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -231,6 +231,14 @@ async function copyMindspacePublicLinkTools() {
|
||||
path.join(root, 'scripts', 'check-mindspace-public-links.mjs'),
|
||||
path.join(runtimeRoot, 'scripts', 'check-mindspace-public-links.mjs'),
|
||||
);
|
||||
await fs.copyFile(
|
||||
path.join(root, 'scripts', 'repair-mindspace-public-downloads.mjs'),
|
||||
path.join(runtimeRoot, 'scripts', 'repair-mindspace-public-downloads.mjs'),
|
||||
);
|
||||
await fs.copyFile(
|
||||
path.join(root, 'mindspace-public-finish-sync.mjs'),
|
||||
path.join(runtimeRoot, 'mindspace-public-finish-sync.mjs'),
|
||||
);
|
||||
}
|
||||
|
||||
async function writeMetadata() {
|
||||
@@ -260,41 +268,14 @@ async function writeMetadata() {
|
||||
throw new Error(`缺少隧道脚本: ${tunnelScript}`);
|
||||
}
|
||||
|
||||
await writeFile(
|
||||
path.join(runtimeScriptsDir, 'run-memind-portal-prod.sh'),
|
||||
[
|
||||
'#!/usr/bin/env bash',
|
||||
'set -euo pipefail',
|
||||
'',
|
||||
'ROOT="$(cd "$(dirname "$0")/.." && pwd)"',
|
||||
'cd "$ROOT"',
|
||||
'',
|
||||
'if [[ -f "${ROOT}/.env" ]]; then',
|
||||
' set -a',
|
||||
' # shellcheck disable=SC1091',
|
||||
' source "${ROOT}/.env"',
|
||||
' set +a',
|
||||
'fi',
|
||||
'',
|
||||
'export NODE_ENV=production',
|
||||
'export H5_PORT="${H5_PORT:-8081}"',
|
||||
'export H5_PUBLIC_BASE_URL="${H5_PUBLIC_BASE_URL:-https://m.tkmind.cn}"',
|
||||
'export TKMIND_API_TARGETS="${TKMIND_API_TARGETS:-https://127.0.0.1:18006,https://127.0.0.1:18007,https://127.0.0.1:18008,https://127.0.0.1:18009}"',
|
||||
'export TKMIND_API_TARGET="${TKMIND_API_TARGET:-https://127.0.0.1:18006}"',
|
||||
'export TKMIND_API_TARGET_1="${TKMIND_API_TARGET_1:-https://127.0.0.1:18007}"',
|
||||
'',
|
||||
'NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@24/bin/node}"',
|
||||
'if [[ ! -x "${NODE_BIN}" ]]; then',
|
||||
' NODE_BIN="$(command -v node)"',
|
||||
'fi',
|
||||
'',
|
||||
'export GOOSED_MCP_NODE_PATH="${GOOSED_MCP_NODE_PATH:-${NODE_BIN}}"',
|
||||
'export GOOSED_MCP_SERVER_PATH="${GOOSED_MCP_SERVER_PATH:-${ROOT}/mindspace-sandbox-mcp.mjs}"',
|
||||
'',
|
||||
'exec "${NODE_BIN}" "${ROOT}/server.mjs"',
|
||||
'',
|
||||
].join('\n'),
|
||||
);
|
||||
const prodStartScript = path.join(root, 'scripts', 'run-memind-portal-prod.sh');
|
||||
if (await exists(prodStartScript)) {
|
||||
await fs.copyFile(prodStartScript, path.join(runtimeScriptsDir, 'run-memind-portal-prod.sh'));
|
||||
await fs.chmod(path.join(runtimeScriptsDir, 'run-memind-portal-prod.sh'), 0o755);
|
||||
} else {
|
||||
throw new Error(`缺少 Portal 启动脚本: ${prodStartScript}`);
|
||||
}
|
||||
|
||||
await fs.copyFile(
|
||||
path.join(root, 'scripts', 'load-env.mjs'),
|
||||
path.join(runtimeRoot, 'scripts', 'load-env.mjs'),
|
||||
|
||||
@@ -555,11 +555,15 @@ if [[ "${portal_code}" != "200" ]]; then
|
||||
fi
|
||||
|
||||
if [[ "${ALLOW_MINDSPACE_PUBLIC_LINK_ISSUES:-0}" != "1" && -d "${APP_DIR}/MindSpace" && -f "${APP_DIR}/scripts/check-mindspace-public-links.mjs" ]]; then
|
||||
say "检查 MindSpace 公开页相对链接"
|
||||
say "修复 MindSpace 公开页缺失 docx 下载"
|
||||
node_bin="/opt/homebrew/opt/node@24/bin/node"
|
||||
if [[ ! -x "${node_bin}" ]]; then
|
||||
node_bin="$(command -v node)"
|
||||
fi
|
||||
if [[ -f "${APP_DIR}/scripts/repair-mindspace-public-downloads.mjs" ]]; then
|
||||
"${node_bin}" "${APP_DIR}/scripts/repair-mindspace-public-downloads.mjs" --root "${APP_DIR}/MindSpace" || true
|
||||
fi
|
||||
say "检查 MindSpace 公开页相对链接"
|
||||
if ! "${node_bin}" "${APP_DIR}/scripts/check-mindspace-public-links.mjs" --root "${APP_DIR}/MindSpace" --downloads-only; then
|
||||
echo "MindSpace public link check failed: broken relative download/asset links under public/*.html" >&2
|
||||
echo "Set ALLOW_MINDSPACE_PUBLIC_LINK_ISSUES=1 only if you accept shipping with known broken links." >&2
|
||||
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Repair missing public/*.docx download targets before release link checks.
|
||||
* 1) sync from oa/ when a safe source exists
|
||||
* 2) remove broken docx anchors from HTML when no source is available
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { syncPublicDocxDownloads } from '../mindspace-public-finish-sync.mjs';
|
||||
|
||||
const DOCX_HREF_RE = /<a\b[^>]*\bhref=(["'])([^"']+\.docx)\1[^>]*>[\s\S]*?<\/a>/gi;
|
||||
const DOCX_HREF_CAPTURE_RE = /<a\b[^>]*\bhref=(["'])([^"']+\.docx)\1/gi;
|
||||
|
||||
function listMissingDocxTargets(html, publicDir) {
|
||||
const missing = new Set();
|
||||
let match = DOCX_HREF_CAPTURE_RE.exec(html);
|
||||
while (match) {
|
||||
const docxPath = path.resolve(publicDir, path.basename(String(match[2] ?? '')));
|
||||
if (!fs.existsSync(docxPath)) {
|
||||
missing.add(path.basename(docxPath).toLowerCase());
|
||||
}
|
||||
match = DOCX_HREF_CAPTURE_RE.exec(html);
|
||||
}
|
||||
DOCX_HREF_CAPTURE_RE.lastIndex = 0;
|
||||
return [...missing];
|
||||
}
|
||||
|
||||
function stripBrokenDocxAnchors(html, missingBasenames) {
|
||||
const missing = new Set(missingBasenames.map((item) => String(item ?? '').toLowerCase()));
|
||||
if (!missing.size) return { html, changed: false };
|
||||
let changed = false;
|
||||
const next = html.replace(DOCX_HREF_RE, (match, _quote, hrefValue) => {
|
||||
const base = path.basename(String(hrefValue ?? '')).toLowerCase();
|
||||
if (!missing.has(base)) return match;
|
||||
changed = true;
|
||||
return '';
|
||||
});
|
||||
return { html: next, changed };
|
||||
}
|
||||
|
||||
export function repairMindspacePublicDownloads({ publishDir } = {}) {
|
||||
const root = path.resolve(String(publishDir ?? ''));
|
||||
if (!root || !fs.existsSync(root)) {
|
||||
return { synced: [], missing: [], stripped: [] };
|
||||
}
|
||||
|
||||
const syncResult = syncPublicDocxDownloads({ publishDir: root, minCompleteSize: 1024 });
|
||||
const stripped = [];
|
||||
|
||||
for (const userDir of fs.readdirSync(root, { withFileTypes: true })) {
|
||||
if (!userDir.isDirectory()) continue;
|
||||
const publicDir = path.join(root, userDir.name, 'public');
|
||||
if (!fs.existsSync(publicDir)) continue;
|
||||
for (const entry of fs.readdirSync(publicDir, { withFileTypes: true })) {
|
||||
if (!entry.isFile() || !entry.name.endsWith('.html')) continue;
|
||||
const htmlPath = path.join(publicDir, entry.name);
|
||||
const html = fs.readFileSync(htmlPath, 'utf8');
|
||||
const missingBasenames = listMissingDocxTargets(html, publicDir);
|
||||
const { html: nextHtml, changed } = stripBrokenDocxAnchors(html, missingBasenames);
|
||||
if (!changed) continue;
|
||||
fs.writeFileSync(htmlPath, nextHtml, 'utf8');
|
||||
stripped.push(path.relative(root, htmlPath));
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
synced: syncResult.synced,
|
||||
missing: syncResult.missing,
|
||||
stripped,
|
||||
};
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = process.argv.slice(2);
|
||||
let publishDir = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'MindSpace');
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
if (args[i] === '--root' && args[i + 1]) {
|
||||
publishDir = args[i + 1];
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
const result = repairMindspacePublicDownloads({ publishDir });
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
}
|
||||
|
||||
const isMain = process.argv[1]
|
||||
&& fileURLToPath(import.meta.url) === path.resolve(process.argv[1]);
|
||||
if (isMain) {
|
||||
main();
|
||||
}
|
||||
@@ -26,4 +26,31 @@ fi
|
||||
export GOOSED_MCP_NODE_PATH="${GOOSED_MCP_NODE_PATH:-${NODE_BIN}}"
|
||||
export GOOSED_MCP_SERVER_PATH="${GOOSED_MCP_SERVER_PATH:-${ROOT}/mindspace-sandbox-mcp.mjs}"
|
||||
|
||||
free_port_if_stale_memind_listener() {
|
||||
local port="${H5_PORT:-8081}"
|
||||
local pids
|
||||
pids="$(lsof -tiTCP:"${port}" -sTCP:LISTEN 2>/dev/null || true)"
|
||||
[[ -z "${pids}" ]] && return 0
|
||||
|
||||
while read -r pid; do
|
||||
[[ -z "${pid}" ]] && continue
|
||||
local args
|
||||
args="$(ps -p "${pid}" -o args= 2>/dev/null || true)"
|
||||
if [[ "${args}" == *"${ROOT}/server.mjs"* ]]; then
|
||||
echo "[portal-start] stopping stale Memind listener pid=${pid}" >&2
|
||||
kill "${pid}" 2>/dev/null || true
|
||||
sleep 1
|
||||
if kill -0 "${pid}" 2>/dev/null; then
|
||||
kill -9 "${pid}" 2>/dev/null || true
|
||||
sleep 1
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
echo "[portal-start] port ${port} held by foreign pid=${pid}; refusing to start" >&2
|
||||
exit 1
|
||||
done <<< "${pids}"
|
||||
}
|
||||
|
||||
free_port_if_stale_memind_listener
|
||||
|
||||
exec "${NODE_BIN}" "${ROOT}/server.mjs"
|
||||
|
||||
Reference in New Issue
Block a user