Compare commits
133 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c7718c683b | |||
| 48c61a3279 | |||
| 5ae166fc44 | |||
| d1be5d9e2a | |||
| 17c59fde71 | |||
| 7002007128 | |||
| 77be18d340 | |||
| 6878827224 | |||
| 40dcdaf49f | |||
| 70264a4c4c | |||
| e94052ff24 | |||
| dfab78c75a | |||
| c88623855f | |||
| 6b479c16f9 | |||
| 291178d2c3 | |||
| 56ff170a60 | |||
| 32da0c0583 | |||
| 0708d56d61 | |||
| 5ca6ed9094 | |||
| cb38225704 | |||
| 6e25008df2 | |||
| a6a9bb4eab | |||
| 9e2aa4f71d | |||
| 41bf775c4c | |||
| 67bf2c2bbb | |||
| fb6865638a | |||
| 046704816f | |||
| 02f2dc6b17 | |||
| 473ad80882 | |||
| 286069449b | |||
| 058d646b32 | |||
| 441703de32 | |||
| bb6b70a8c4 | |||
| e885310f39 | |||
| f1f9a1cf8f | |||
| ea07f6f250 | |||
| b1577a16e9 | |||
| d3141a3e91 | |||
| 8ccf2db05c | |||
| 6b6f9ed01c | |||
| 793e64bfbc | |||
| 721db19a92 | |||
| 417aa9c78b | |||
| e02495a6c0 | |||
| da818bbbbc | |||
| d2cfd54d43 | |||
| 104fb4e370 | |||
| 0122d89db3 | |||
| be9c25f1d0 | |||
| 1c82b34ad9 | |||
| d479e89fc7 | |||
| 258e4a8a07 | |||
| 4c1890f344 | |||
| 299bee82ba | |||
| 69dabd5186 | |||
| 62fd5188ab | |||
| 5d08d56f91 | |||
| 6df82818c5 | |||
| 08a48e4849 | |||
| db593c44fd | |||
| 36eca57506 | |||
| 764981c086 | |||
| cf36a9c84a | |||
| e3d7b9ee76 | |||
| f6f2cd0933 | |||
| 396bb78200 | |||
| bbb43f97a3 | |||
| d21c4849a9 | |||
| 19706eb3a0 | |||
| c9ad841543 | |||
| 646178944d | |||
| 85e888b340 | |||
| ff9c68e57f | |||
| 3e57f85d3a | |||
| 24336d0178 | |||
| 46ea22b342 | |||
| c5f84f54b9 | |||
| d87bfde501 | |||
| 785731d98d | |||
| 1d291fd528 | |||
| 550969bed2 | |||
| 412e5f149b | |||
| 03f03cf7c5 | |||
| 54637beffb | |||
| 3fd1db8c2f | |||
| 6573c6bee0 | |||
| 22a45f34f5 | |||
| 25c09f1e8f | |||
| 53ed38c250 | |||
| 46041c3456 | |||
| 784a10a592 | |||
| be1e4c18c0 | |||
| 4e36b98c9e | |||
| 48e9f68211 | |||
| 205b5fd8be | |||
| dab6140f99 | |||
| 239c41f935 | |||
| 13f5c740a1 | |||
| 898f6fff44 | |||
| ba6cc10f08 | |||
| 7e20ec14f1 | |||
| 890264ba10 | |||
| d9eb11f5f5 | |||
| 6ee8542da2 | |||
| baa383548c | |||
| a69e2766ed | |||
| d81e798b28 | |||
| db225b784e | |||
| aea3c1e83e | |||
| 5242cbf08b | |||
| 881f70f4bf | |||
| 93d7bc0cd5 | |||
| 092ed16d82 | |||
| cb62b36cd5 | |||
| 57d2c14fca | |||
| d52aab8ab0 | |||
| c10788dfe1 | |||
| 1f9e147dea | |||
| 7b3acb6813 | |||
| aafda0cf64 | |||
| c6ae7cd21c | |||
| 1765cac65a | |||
| efbbe69e2c | |||
| c2e2209344 | |||
| 6f4a3b53d0 | |||
| a4522854d8 | |||
| 175095cbf3 | |||
| 91026882cb | |||
| 1df2489831 | |||
| 8438eec08c | |||
| a0043e95c1 | |||
| 32e697b698 | |||
| 4959535b5e |
@@ -18,7 +18,18 @@ H5_PUBLIC_BASE_URL=http://127.0.0.1:5173
|
||||
# MEMIND_ANALYTICS_URL=http://127.0.0.1:3100
|
||||
# MEMIND_ANALYTICS_WEBSITE_ID=<local Umami Website ID>
|
||||
# MEMIND_ANALYTICS_ID_SECRET=<local-only pseudonymization secret>
|
||||
# MEMIND_ANALYTICS_IDENTITY_MODE=pseudonymous # use raw only in an approved first-party analytics environment
|
||||
# MEMIND_ANALYTICS_DOMAINS=127.0.0.1,localhost
|
||||
# Memind 产品壳层使用独立 Website,避免与用户生成页面的浏览口径混在一起
|
||||
# MEMIND_PRODUCT_ANALYTICS_ENABLED=true
|
||||
# MEMIND_PRODUCT_ANALYTICS_WEBSITE_ID=<local Umami product Website ID>
|
||||
|
||||
# Local Rybbit analytics (same-origin /rybbit proxy to rybbit.tkmind.cn).
|
||||
# Create a Rybbit Site for localhost / 127.0.0.1, then set its numeric site_id.
|
||||
# MEMIND_RYBBIT_ENABLED=true
|
||||
# MEMIND_RYBBIT_URL=https://rybbit.tkmind.cn
|
||||
# MEMIND_RYBBIT_SITE_ID=<numeric site id>
|
||||
# MEMIND_RYBBIT_ID_SECRET=<local-only pseudonymization secret>
|
||||
|
||||
# 生产 H5 public base 当前临时切到 https://mm.tkmind.cn。
|
||||
# 后续公网 H5 不再依赖 105 转发链路;m.tkmind.cn 仅作为 legacy/rollback 记录处理。
|
||||
@@ -32,9 +43,12 @@ H5_PUBLIC_BASE_URL=http://127.0.0.1:5173
|
||||
# Code-tool agent run 灰度开关(默认关闭)。
|
||||
# 后端必须先开启 MEMIND_AGENT_CODE_RUNS_ENABLED,H5 构建开关才会生效。
|
||||
# 开启后,页面编辑子聊天会传 tool_mode=code;普通聊天仍需同时开启 AUTODETECT 才会按文本自动识别代码任务。
|
||||
# Phase 1.5:memindadm 运行时策略优先于 VITE_;设为 env 可强制只读 env(紧急 override)。
|
||||
# MEMIND_CODE_RUN_POLICY_SOURCE=env
|
||||
# MEMIND_AGENT_CODE_RUNS_ENABLED=0
|
||||
# VITE_AGENT_CODE_RUNS_ENABLED=0
|
||||
# VITE_AGENT_CODE_RUNS_AUTODETECT=0
|
||||
# VITE_AGENT_PAGE_DATA_DEV_AUTODETECT=0
|
||||
|
||||
# Tool Gateway Queue v0(/agent/runs 后台任务队列,默认保守限流)。
|
||||
# MEMIND_AGENT_RUN_QUEUE_CONCURRENCY=1
|
||||
@@ -46,8 +60,36 @@ H5_PUBLIC_BASE_URL=http://127.0.0.1:5173
|
||||
# MEMIND_AGENT_RUN_WORKER_BATCH_SIZE=1
|
||||
# MEMIND_AGENT_RUN_HEARTBEAT_MS=30000
|
||||
# MEMIND_AGENT_RUN_HEARTBEAT_STALE_MS=90000
|
||||
# MEMIND_AGENT_RUN_RECOVERY_INTERVAL_MS=30000
|
||||
# MEMIND_RUNTIME_BUILD_ID=<release-or-build-id>
|
||||
# MEMIND_AGENT_RUN_WORKER_ID=<optional-instance-id>
|
||||
# MEMIND_AGENT_RUN_WORKER_EXPECT_RUNNING=0
|
||||
|
||||
# LangGraph Orchestrator Shadow(默认完全不接入 Agent Run 请求路径)。
|
||||
# 仅在独立 Orchestrator 已健康、service token 已配置后,先显式打开观察 wiring,
|
||||
# 再通过 memindadm 选择 shadow。关闭 wiring 时不查询编排配置、不排队、不传用户数据。
|
||||
# MEMIND_ORCHESTRATOR_SHADOW_OBSERVATION_ENABLED=0
|
||||
# MEMIND_ORCHESTRATOR_URL=http://127.0.0.1:8093
|
||||
# MEMIND_ORCHESTRATOR_SERVICE_TOKEN=<local-or-deployment-service-token>
|
||||
# MEMIND_ORCHESTRATOR_SHADOW_MAX_CONCURRENCY=2
|
||||
# MEMIND_ORCHESTRATOR_SHADOW_MAX_QUEUE=100
|
||||
# 非 loopback URL 必须精确列入;逗号分隔,只接受 origin。
|
||||
# MEMIND_ORCHESTRATOR_ALLOWED_ORIGINS=https://orchestrator.internal
|
||||
# 真实执行交接是另一道独立闸门,默认保持关闭。
|
||||
# MEMIND_ORCHESTRATOR_EXECUTION_HANDOFF_ENABLED=0
|
||||
# MEMIND_ORCHESTRATOR_KILL_SWITCH=0
|
||||
# Page Data 审查强制闸门(fail-closed)。默认关闭=仅观察/写建议,不改变 Native 成功语义。
|
||||
# 生效前提:memindadm mode=shadow 且 MEMIND_ORCHESTRATOR_SHADOW_OBSERVATION_ENABLED=1。
|
||||
# 本地/开发:保持 0。灰度强制审查再设 1。生产启用需 Ops 明确批准。
|
||||
# 失败 UX:H5 会看到 codes 对应的中文纠正建议(page-data-validation-suggestions.mjs)。
|
||||
# MEMIND_ORCHESTRATOR_PAGE_DATA_VALIDATION_GATE_ENABLED=0
|
||||
# 可修复 codes 失败时,把纠正建议再发一轮给 goosed(每 run 最多 1 次;默认开启)。
|
||||
# MEMIND_PAGE_DATA_SUGGESTION_REPAIR_ENABLED=1
|
||||
# Orchestrator 终态数据保留清理默认关闭;在独立服务环境配置,建议真实 Canary 前明确周期。
|
||||
# MEMIND_ORCHESTRATOR_RETENTION_DAYS=0
|
||||
# MEMIND_ORCHESTRATOR_RETENTION_SWEEP_INTERVAL_MS=86400000
|
||||
# MEMIND_ORCHESTRATOR_RETENTION_SWEEP_LIMIT=100
|
||||
|
||||
# Runtime SLO 日报(默认每日 23:55 写 reports/runtime-slo)。
|
||||
# MEMIND_RUNTIME_REPORT_DIR=/Users/john/Project/Memind/reports/runtime-slo
|
||||
# MEMIND_RUNTIME_SLO_HOUR=23
|
||||
@@ -148,6 +190,16 @@ H5_ADMIN_PASSWORD=change-me-admin
|
||||
# H5_LOCAL_LLM_API_KEY=ollama
|
||||
# H5_LOCAL_LLM_NAME=Local Ollama 7B
|
||||
H5_ACCESS_PASSWORD=change-me
|
||||
# Portal API 访问策略第一阶段仅支持 off / shadow;shadow 只记录差异,不改变放行结果。
|
||||
# MEMIND_PORTAL_ACCESS_POLICY_MODE=shadow
|
||||
# 同一访问策略差异在窗口内只记录一次,后续日志携带累计与抑制次数。
|
||||
# MEMIND_PORTAL_ACCESS_SHADOW_LOG_INTERVAL_MS=60000
|
||||
# Phase C 灰度执行必须同时开启总开关并精确列出策略组;默认全部关闭,不支持 all。
|
||||
# 可选组:legacy-page-data,page-data-public,plaza-optional-user
|
||||
# MEMIND_PORTAL_ACCESS_POLICY_ENFORCEMENT_ENABLED=0
|
||||
# MEMIND_PORTAL_ACCESS_POLICY_ENFORCE_GROUPS=
|
||||
# 紧急熔断优先级最高;设为 1 后所有策略组立即回到旧鉴权路径(重启 Portal 生效)。
|
||||
# MEMIND_PORTAL_ACCESS_POLICY_KILL_SWITCH=0
|
||||
|
||||
# 计费(Phase 2,金额单位均为人民币分)
|
||||
# 默认按 Token 单价扣费;生产推荐开启成本模式(见 docs/h5-metering-gateway.md)
|
||||
@@ -265,9 +317,29 @@ VITE_TKMIND_WORKING_DIR=/Users/john/PycharmProjects/tkmind
|
||||
# IMAGE_MAKE_SEMANTIC_REVIEW_ENABLED=1
|
||||
# IMAGE_MAKE_SEMANTIC_REVIEW_MIN_SCORE=70
|
||||
# IMAGE_MAKE_SEMANTIC_REVIEW_MAX_ATTEMPTS=3
|
||||
# H5_WECHAT_MP_FRESH_PAGE_THUMBNAILS=1
|
||||
# H5_WECHAT_MP_FRESH_THUMBNAIL_REPAIR=0
|
||||
# Sandbox MCP 调用 Portal 内部生图入口;容器内通常使用 host.docker.internal。
|
||||
# MINDSPACE_AGENT_API_BASE_URL=http://127.0.0.1:8081/api
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MindSearch / standalone Deep Search
|
||||
# ---------------------------------------------------------------------------
|
||||
# Production SearXNG on 103 is bound to the host at 127.0.0.1:20080.
|
||||
# Goosed MCP endpoints are automatically rewritten to host.docker.internal.
|
||||
# TKMIND_SEARCH_SEARXNG_URL=http://127.0.0.1:20080/search
|
||||
# TKMIND_SEARCH_MCP_HOST_GATEWAY=host.docker.internal
|
||||
#
|
||||
# Deep Search runs as an independently released LaunchAgent on 127.0.0.1:20100.
|
||||
# Its production environment is stored separately at:
|
||||
# /Users/john/Project/deep-search-runtime/shared/.env
|
||||
# TKMIND_DEEP_SEARCH_SECRET=
|
||||
# TKMIND_SEARCH_GITHUB_GATEWAY_URL=http://127.0.0.1:20100
|
||||
# TKMIND_SEARCH_GITHUB_GATEWAY_SECRET=${TKMIND_DEEP_SEARCH_SECRET}
|
||||
#
|
||||
# GITHUB_TOKEN must only be set in the standalone Deep Search environment.
|
||||
# Do not pass it to goosed MCP extension envs or commit it to Git.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Memind runtime profile(本地 vs 生产 必须区分)
|
||||
# 由 scripts/memind-runtime-profile.mjs 在 pnpm dev / server.mjs 启动时应用。
|
||||
@@ -281,6 +353,10 @@ VITE_TKMIND_WORKING_DIR=/Users/john/PycharmProjects/tkmind
|
||||
|
||||
# 【本地开发 — 复制到 .env 的默认值】
|
||||
MEMIND_RUNTIME_PROFILE=local
|
||||
# Portal 代码目录与持久化数据目录分离(例如从 git worktree 启动)时显式设置。
|
||||
# MindSpace/<userId>、users/ 与默认 data/mindspace 都以此目录为根;
|
||||
# 前端 dist/public 等代码静态资源仍从当前 runtime 目录读取。
|
||||
# MEMIND_PORTAL_H5_ROOT=/Users/john/Project/Memind
|
||||
# MINDSPACE_STORAGE_ROOT 留空即可,自动使用 <repo>/data/mindspace
|
||||
|
||||
# 【本机 split-service 联调 — 需要独立 MindSpace LaunchAgent 已启动】
|
||||
|
||||
@@ -42,9 +42,13 @@ jobs:
|
||||
npm ci --include=optional
|
||||
SHARP_ARM64_VERSION="$(node -p "require('./package-lock.json').packages['node_modules/@img/sharp-linux-arm64'].version")"
|
||||
LIBVIPS_ARM64_VERSION="$(node -p "require('./package-lock.json').packages['node_modules/@img/sharp-libvips-linux-arm64'].version")"
|
||||
npm install --no-save --package-lock=false \
|
||||
"@img/sharp-linux-arm64@${SHARP_ARM64_VERSION}" \
|
||||
"@img/sharp-libvips-linux-arm64@${LIBVIPS_ARM64_VERSION}"
|
||||
if [[ ! -d node_modules/@img/sharp-linux-arm64 || ! -d node_modules/@img/sharp-libvips-linux-arm64 ]]; then
|
||||
npm install --no-save --package-lock=false \
|
||||
"@img/sharp-linux-arm64@${SHARP_ARM64_VERSION}" \
|
||||
"@img/sharp-libvips-linux-arm64@${LIBVIPS_ARM64_VERSION}"
|
||||
else
|
||||
echo "Locked Sharp ARM64 optional packages are already installed"
|
||||
fi
|
||||
node -e "import('sharp').then((sharp) => sharp.default({ create: { width: 1, height: 1, channels: 4, background: '#000' } }).png().toBuffer())"
|
||||
|
||||
- name: Check patch formatting
|
||||
@@ -53,6 +57,9 @@ jobs:
|
||||
- name: Run full test suite
|
||||
run: npm test
|
||||
|
||||
- name: Verify release gate and canary routing
|
||||
run: npm run test:release-gate:unit
|
||||
|
||||
- name: Build production frontend
|
||||
run: npm run build
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ ops/node_modules/
|
||||
dist/
|
||||
ops/dist/
|
||||
.runtime/
|
||||
.deep-search/
|
||||
|
||||
.env
|
||||
.env.local
|
||||
@@ -12,6 +13,7 @@ scripts/g2-aliyun-cert.env
|
||||
*.log
|
||||
*.pid
|
||||
.local/
|
||||
.release-gate/
|
||||
|
||||
MindSpace/
|
||||
data/
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
Portal runtime artifact
|
||||
|
||||
This directory is meant to run on production without the source tree.
|
||||
Required persisted items to inherit on the host:
|
||||
.env
|
||||
MindSpace/
|
||||
data/
|
||||
users/
|
||||
.tailscale/
|
||||
public/plaza-covers/
|
||||
logs/
|
||||
|
||||
Bundled alongside server.mjs (required for sandbox-fs MCP):
|
||||
mindspace-sandbox-mcp.mjs (esbuild bundle; includes schedule-service deps)
|
||||
|
||||
Post-deploy validation (scripts/check-mindspace-public-links.mjs):
|
||||
Scans MindSpace/*/public/*.html for missing relative href/src/cover targets.
|
||||
|
||||
Runtime install mode: bundle-node-modules
|
||||
Bundle target: node24
|
||||
|
||||
Platform agent skills (synced into user workspaces):
|
||||
skills/
|
||||
Key runtime differences must stay in .env, not in the artifact:
|
||||
DATABASE_URL / MYSQL_*
|
||||
H5_PUBLIC_BASE_URL
|
||||
TKMIND_API_TARGETS / TKMIND_API_TARGET / TKMIND_API_TARGET_1
|
||||
H5_USERS_ROOT / MINDSPACE_STORAGE_ROOT / MEMIND_SHARED_PUBLISH_ROOT
|
||||
|
||||
Deployment and operations transport:
|
||||
H5 public domain: mm.tkmind.cn
|
||||
Current public path: mm.tkmind.cn -> local nginx -> Portal :8081
|
||||
Legacy rollback-only path: m.tkmind.cn -> 105 nginx -> reverse SSH tunnel -> Portal :8081
|
||||
Future H5 traffic must not depend on 105 forwarding unless explicitly rolling back.
|
||||
|
||||
Streaming runtime operations:
|
||||
node scripts/check-stream-runtime.mjs
|
||||
node scripts/runtime-worker-drain.mjs status
|
||||
node scripts/runtime-worker-drain.mjs reconcile
|
||||
node scripts/runtime-worker-drain.mjs reconcile --apply
|
||||
node scripts/runtime-worker-metrics.mjs status
|
||||
node scripts/runtime-worker-metrics.mjs sample
|
||||
bash scripts/install-runtime-metrics-agent.sh
|
||||
node scripts/runtime-worker-heartbeat.mjs once
|
||||
node scripts/runtime-worker-heartbeat.mjs serve
|
||||
bash scripts/install-runtime-heartbeat-agent.sh
|
||||
node scripts/runtime-slo-report.mjs
|
||||
node scripts/runtime-slo-report.mjs --write-report
|
||||
node scripts/runtime-slo-report.mjs --write-report --prune --retention-days 30
|
||||
bash scripts/install-runtime-slo-report-agent.sh
|
||||
bash scripts/install-runtime-slo-soak-agent.sh
|
||||
node scripts/runtime-worker-drain.mjs drain goosed-3
|
||||
node scripts/runtime-worker-drain.mjs undrain goosed-3
|
||||
node scripts/check-tool-runtime.mjs
|
||||
node scripts/check-agent-code-run-entry.mjs
|
||||
curl -sk https://mm.tkmind.cn/api/runtime/status # includes toolRuntime.queue
|
||||
node scripts/agent-run-worker.mjs --status
|
||||
node scripts/agent-run-worker.mjs --once
|
||||
bash scripts/install-agent-run-worker-agent.sh # installs disabled by default
|
||||
node scripts/check-agent-run-worker.mjs # read-only LaunchAgent/queue check
|
||||
node scripts/agent-run-guard.mjs # dry-run auto-pause guard check
|
||||
node scripts/agent-run-guard.mjs --apply # stop worker and disable code-run gate when thresholds trip
|
||||
bash scripts/install-agent-run-guard-agent.sh # installs guard LaunchAgent
|
||||
@@ -1,151 +0,0 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const SKIP_REFERENCE = /^(?:https?:|mailto:|javascript:|data:|tel:|#|\/\/)/i;
|
||||
const DOWNLOAD_EXTENSIONS = /\.(?:docx|doc|pdf|xlsx|xls|pptx|ppt|zip|rar|7z|csv)$/i;
|
||||
|
||||
export function isFilesystemPublicReference(raw) {
|
||||
const value = String(raw ?? '').trim();
|
||||
if (!value) return false;
|
||||
if (SKIP_REFERENCE.test(value)) return false;
|
||||
// /api/... and /MindSpace/... are server routes, not on-disk siblings of public HTML.
|
||||
if (value.startsWith('/')) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @deprecated use isFilesystemPublicReference */
|
||||
export function isRelativePublicReference(raw) {
|
||||
return isFilesystemPublicReference(raw);
|
||||
}
|
||||
|
||||
export function isDownloadLikeReference(raw, source = '') {
|
||||
if (!isFilesystemPublicReference(raw)) return false;
|
||||
const ref = normalizeReferencePath(raw);
|
||||
if (DOWNLOAD_EXTENSIONS.test(ref)) return true;
|
||||
return /\bdownload\b/i.test(String(source ?? ''));
|
||||
}
|
||||
|
||||
export function normalizeReferencePath(raw) {
|
||||
const trimmed = String(raw ?? '')
|
||||
.trim()
|
||||
.split('#')[0]
|
||||
.split('?')[0]
|
||||
.trim();
|
||||
if (!trimmed) return '';
|
||||
try {
|
||||
return decodeURIComponent(trimmed);
|
||||
} catch {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveReferencePath(htmlDir, raw) {
|
||||
const ref = normalizeReferencePath(raw);
|
||||
if (!ref) return null;
|
||||
return path.resolve(htmlDir, ref);
|
||||
}
|
||||
|
||||
function parseMindspaceCoverPaths(contentAttr) {
|
||||
const paths = [];
|
||||
if (!contentAttr) return paths;
|
||||
let parsed = null;
|
||||
try {
|
||||
parsed = JSON.parse(contentAttr);
|
||||
} catch {
|
||||
for (const key of ['cover', 'image']) {
|
||||
const match = contentAttr.match(new RegExp(`"${key}"\\s*:\\s*"([^"]+)"`));
|
||||
if (match?.[1]) paths.push(match[1]);
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
for (const key of ['cover', 'image']) {
|
||||
if (parsed?.[key]) paths.push(String(parsed[key]));
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
export function collectPublicHtmlReferences(html, htmlDir, { downloadsOnly = false } = {}) {
|
||||
const refs = [];
|
||||
const seen = new Set();
|
||||
|
||||
const add = (raw, source) => {
|
||||
if (downloadsOnly && !isDownloadLikeReference(raw, source)) return;
|
||||
if (!downloadsOnly && !isFilesystemPublicReference(raw)) return;
|
||||
const ref = normalizeReferencePath(raw);
|
||||
if (!ref) return;
|
||||
const key = `${source}\0${ref}`;
|
||||
if (seen.has(key)) return;
|
||||
seen.add(key);
|
||||
refs.push({
|
||||
ref,
|
||||
source,
|
||||
resolvedPath: resolveReferencePath(htmlDir, ref),
|
||||
});
|
||||
};
|
||||
|
||||
for (const match of String(html ?? '').matchAll(/\b(?:href|src)\s*=\s*["']([^"']+)["']/gi)) {
|
||||
add(match[1], match[0]);
|
||||
}
|
||||
|
||||
if (!downloadsOnly) {
|
||||
for (const match of String(html ?? '').matchAll(
|
||||
/<meta\b[^>]*\bname\s*=\s*["']mindspace-cover["'][^>]*>/gi,
|
||||
)) {
|
||||
const tag = match[0];
|
||||
const contentMatch =
|
||||
tag.match(/\bcontent\s*=\s*"([^"]*)"/i) ?? tag.match(/\bcontent\s*=\s*'([^']*)'/i);
|
||||
for (const coverPath of parseMindspaceCoverPaths(contentMatch?.[1])) {
|
||||
add(coverPath, 'mindspace-cover');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return refs;
|
||||
}
|
||||
|
||||
export function findMissingPublicHtmlReferences(htmlPath, html = null, options = {}) {
|
||||
const resolvedHtmlPath = path.resolve(htmlPath);
|
||||
const content = html ?? fs.readFileSync(resolvedHtmlPath, 'utf8');
|
||||
const htmlDir = path.dirname(resolvedHtmlPath);
|
||||
const missing = [];
|
||||
const seen = new Set();
|
||||
|
||||
for (const item of collectPublicHtmlReferences(content, htmlDir, options)) {
|
||||
const key = item.ref;
|
||||
if (seen.has(key)) continue;
|
||||
seen.add(key);
|
||||
if (!item.resolvedPath || !fs.existsSync(item.resolvedPath)) {
|
||||
missing.push({ htmlPath: resolvedHtmlPath, ...item });
|
||||
}
|
||||
}
|
||||
|
||||
return missing;
|
||||
}
|
||||
|
||||
export function listPublicHtmlFiles(publishRoot, { userId = null } = {}) {
|
||||
const root = path.resolve(publishRoot);
|
||||
if (!fs.existsSync(root)) return [];
|
||||
|
||||
const userDirs = userId
|
||||
? [path.join(root, userId)]
|
||||
: fs.readdirSync(root, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => path.join(root, d.name));
|
||||
|
||||
const files = [];
|
||||
for (const userDir of userDirs) {
|
||||
const publicDir = path.join(userDir, 'public');
|
||||
if (!fs.existsSync(publicDir) || !fs.statSync(publicDir).isDirectory()) continue;
|
||||
for (const name of fs.readdirSync(publicDir)) {
|
||||
if (!name.toLowerCase().endsWith('.html')) continue;
|
||||
files.push(path.join(publicDir, name));
|
||||
}
|
||||
}
|
||||
return files.sort();
|
||||
}
|
||||
|
||||
export function scanPublicHtmlLinks(publishRoot, options = {}) {
|
||||
const issues = [];
|
||||
for (const htmlPath of listPublicHtmlFiles(publishRoot, options)) {
|
||||
issues.push(...findMissingPublicHtmlReferences(htmlPath, null, options));
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"name": "tkmind-h5-portal-runtime",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"dependencies": {
|
||||
"@node-rs/argon2": "^2.0.2",
|
||||
"@resvg/resvg-js": "^2.6.2",
|
||||
"debug": "^4.4.3",
|
||||
"express": "^4.21.2",
|
||||
"http-proxy-middleware": "^3.0.3",
|
||||
"jsonrepair": "^3.14.0",
|
||||
"mysql2": "^3.22.5",
|
||||
"playwright": "^1.61.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"redis": "^4.7.1",
|
||||
"sharp": "^0.35.2",
|
||||
"undici": "^6.26.0"
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
nXjhx0ErC6MhQ0SZ
|
||||
|
Before Width: | Height: | Size: 61 KiB |
@@ -1,50 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>微信分享预览 · 连云港三天两夜攻略 🌊</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; min-height: 100vh; padding: 24px; background: #ececec; font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, sans-serif; color: #111; }
|
||||
.wrap { max-width: 420px; margin: 0 auto; }
|
||||
h1 { margin: 0 0 8px; font-size: 18px; }
|
||||
.hint, .note { margin: 0 0 16px; color: #666; font-size: 13px; line-height: 1.6; }
|
||||
.card { display: grid; grid-template-columns: 1fr 72px; gap: 12px; padding: 12px; border-radius: 8px; background: #fff; box-shadow: 0 1px 0 rgba(0,0,0,.06); }
|
||||
.card-main { min-width: 0; }
|
||||
.card-title { margin: 0; font-size: 16px; font-weight: 600; line-height: 1.35; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.card-desc { margin: 6px 0 0; color: #888; font-size: 13px; line-height: 1.45; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
|
||||
.card-foot { display: flex; align-items: center; gap: 6px; margin-top: 10px; color: #999; font-size: 12px; }
|
||||
.card-foot img { width: 16px; height: 16px; border-radius: 999px; object-fit: cover; }
|
||||
.card-thumb { width: 72px; height: 72px; border-radius: 4px; object-fit: cover; background: #f3f3f3; }
|
||||
.card-thumb-empty { display: grid; place-items: center; color: #bbb; font-size: 12px; }
|
||||
.meta { margin-top: 18px; padding: 14px; border-radius: 10px; background: rgba(255,255,255,.72); font-size: 12px; line-height: 1.7; word-break: break-all; }
|
||||
code { background: rgba(0,0,0,.06); padding: 1px 4px; border-radius: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>微信链接卡片预览</h1>
|
||||
<p class="hint">本地模拟的是「粘贴链接后看到的卡片」,不是 JS-SDK 内部分享弹层。源链接:<code>https://m.tkmind.cn/MindSpace/john/public/lianyungang-guide.html</code></p>
|
||||
<p class="note">修复后:标题 + 描述 + 缩略图 + 底部「TKMind 智趣」小图标应同时出现。</p>
|
||||
<article class="card" aria-label="微信分享卡片预览">
|
||||
<div class="card-main">
|
||||
<h2 class="card-title">连云港三天两夜攻略 🌊</h2>
|
||||
<p class="card-desc">三天两夜·沙滩酒店·必吃美食</p>
|
||||
<div class="card-foot">
|
||||
<img src="http://127.0.0.1:8081/brand/tkmind-icon.png" alt="" onerror="this.style.display='none'">
|
||||
<span>TKMind 智趣</span>
|
||||
</div>
|
||||
</div>
|
||||
<img class="card-thumb" src="https://images.unsplash.com/photo-1507525428034-b723cf961d3e?auto=format&fit=crop&w=200&q=80" alt="">
|
||||
</article>
|
||||
<div class="meta">
|
||||
<div><strong>og:title</strong> 连云港三天两夜攻略 🌊</div>
|
||||
<div><strong>og:description</strong> 三天两夜·沙滩酒店·必吃美食</div>
|
||||
<div><strong>og:site_name</strong> TKMind 智趣</div>
|
||||
<div><strong>og:image</strong> https://images.unsplash.com/photo-1507525428034-b723cf961d3e?auto=format&fit=crop&w=200&q=80</div>
|
||||
<div><strong>icon</strong> /brand/tkmind-icon.png</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,41 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>微信分享预览工具</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; min-height: 100vh; padding: 24px; background: #f3f3f3; font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, sans-serif; color: #111; }
|
||||
.panel { max-width: 560px; margin: 0 auto; padding: 20px; border-radius: 16px; background: #fff; box-shadow: 0 8px 30px rgba(0,0,0,.06); }
|
||||
h1 { margin: 0 0 8px; font-size: 22px; }
|
||||
p { margin: 0 0 16px; color: #666; line-height: 1.6; font-size: 14px; }
|
||||
label { display: block; margin-bottom: 8px; font-size: 13px; font-weight: 600; }
|
||||
input { width: 100%; padding: 12px 14px; border: 1px solid #ddd; border-radius: 10px; font: inherit; }
|
||||
button { margin-top: 14px; width: 100%; padding: 12px 14px; border: 0; border-radius: 10px; background: #2f6f57; color: #fff; font: inherit; font-weight: 700; cursor: pointer; }
|
||||
code { background: rgba(0,0,0,.06); padding: 1px 4px; border-radius: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="panel">
|
||||
<h1>微信分享预览</h1>
|
||||
<p>输入已发布页面路径,本地模拟微信「粘贴链接后」看到的卡片。开发环境走 Portal API:<code>/dev/wechat-share-preview</code></p>
|
||||
<form id="preview-form">
|
||||
<label for="url">页面 URL 或路径</label>
|
||||
<input id="url" name="url" placeholder="/MindSpace/john/public/demo.html" required>
|
||||
<button type="submit">生成预览</button>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('preview-form').addEventListener('submit', (event) => {
|
||||
event.preventDefault();
|
||||
const value = document.getElementById('url').value.trim();
|
||||
if (!value) return;
|
||||
const target = value.startsWith('http')
|
||||
? `/dev/wechat-share-preview?url=${encodeURIComponent(value)}`
|
||||
: `/dev/wechat-share-preview?url=${encodeURIComponent(value.startsWith('/') ? value : `/${value}`)}`;
|
||||
window.location.href = target;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,159 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="John 的专属欢迎页面 - TKMind 为您呈现">
|
||||
<meta name="mindspace-cover" content='{"tag":"报告","emoji":"👋","accent":"#f093fb","accent2":"#24243e","subtitle":"Hello John","cover":"https://images.unsplash.com/photo-1579547945413-497e1b99dac0?w=1200&q=80"}'>
|
||||
<title>Hello, John!</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 动态粒子背景 */
|
||||
.bg-particles {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bg-particles span {
|
||||
position: absolute;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 50%;
|
||||
animation: float 20s infinite;
|
||||
}
|
||||
|
||||
.bg-particles span:nth-child(1) { top: 10%; left: 10%; animation-duration: 18s; animation-delay: 0s; }
|
||||
.bg-particles span:nth-child(2) { top: 30%; left: 80%; width: 10px; height: 10px; animation-duration: 22s; animation-delay: 2s; }
|
||||
.bg-particles span:nth-child(3) { top: 70%; left: 20%; animation-duration: 16s; animation-delay: 4s; }
|
||||
.bg-particles span:nth-child(4) { top: 85%; left: 85%; width: 8px; height: 8px; animation-duration: 24s; animation-delay: 1s; }
|
||||
.bg-particles span:nth-child(5) { top: 50%; left: 50%; width: 4px; height: 4px; animation-duration: 20s; animation-delay: 3s; }
|
||||
.bg-particles span:nth-child(6) { top: 15%; left: 60%; animation-duration: 19s; animation-delay: 5s; }
|
||||
.bg-particles span:nth-child(7) { top: 60%; left: 5%; width: 7px; height: 7px; animation-duration: 21s; animation-delay: 0.5s; }
|
||||
.bg-particles span:nth-child(8) { top: 40%; left: 70%; width: 5px; height: 5px; animation-duration: 17s; animation-delay: 2.5s; }
|
||||
|
||||
@keyframes float {
|
||||
0% { transform: translateY(0) rotate(0deg); opacity: 0; }
|
||||
10% { opacity: 1; }
|
||||
90% { opacity: 1; }
|
||||
100% { transform: translateY(-100vh) rotate(720deg); opacity: 0; }
|
||||
}
|
||||
|
||||
.card {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
text-align: center;
|
||||
padding: 60px 80px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 24px;
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow:
|
||||
0 25px 50px rgba(0, 0, 0, 0.3),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
animation: fadeIn 1.2s ease-out;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 72px;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #f093fb, #f5576c, #4facfe);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
margin-bottom: 16px;
|
||||
animation: pulse 3s ease-in-out infinite;
|
||||
letter-spacing: 4px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 24px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.emoji {
|
||||
font-size: 48px;
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
animation: wave 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 36px;
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { transform: scale(1); }
|
||||
50% { transform: scale(1.03); }
|
||||
}
|
||||
|
||||
@keyframes wave {
|
||||
0%, 100% { transform: rotate(0deg); }
|
||||
25% { transform: rotate(15deg); }
|
||||
75% { transform: rotate(-15deg); }
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.card {
|
||||
padding: 40px 32px;
|
||||
margin: 20px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 48px;
|
||||
}
|
||||
p {
|
||||
font-size: 18px;
|
||||
}
|
||||
.emoji {
|
||||
font-size: 36px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="bg-particles">
|
||||
<span></span><span></span><span></span><span></span>
|
||||
<span></span><span></span><span></span><span></span>
|
||||
</div>
|
||||
<div class="card">
|
||||
<span class="emoji">👋</span>
|
||||
<h1>Hello!</h1>
|
||||
<p>Welcome, John</p>
|
||||
<div class="footer">powered by TKMind</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
Before Width: | Height: | Size: 407 KiB |
|
Before Width: | Height: | Size: 569 KiB |
|
Before Width: | Height: | Size: 328 KiB |
|
Before Width: | Height: | Size: 212 KiB |
|
Before Width: | Height: | Size: 407 KiB |
|
Before Width: | Height: | Size: 292 KiB |
|
Before Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 292 KiB |
|
Before Width: | Height: | Size: 555 KiB |
|
Before Width: | Height: | Size: 292 KiB |
|
Before Width: | Height: | Size: 407 KiB |
|
Before Width: | Height: | Size: 555 KiB |
|
Before Width: | Height: | Size: 569 KiB |
|
Before Width: | Height: | Size: 292 KiB |
|
Before Width: | Height: | Size: 328 KiB |
|
Before Width: | Height: | Size: 407 KiB |
|
Before Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 672 KiB |
|
Before Width: | Height: | Size: 723 KiB |
|
Before Width: | Height: | Size: 672 KiB |
|
Before Width: | Height: | Size: 458 KiB |
|
Before Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 160 KiB |
|
Before Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 723 KiB |
|
Before Width: | Height: | Size: 723 KiB |
|
Before Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 672 KiB |
|
Before Width: | Height: | Size: 458 KiB |
|
Before Width: | Height: | Size: 152 KiB |
|
Before Width: | Height: | Size: 672 KiB |
|
Before Width: | Height: | Size: 723 KiB |
|
Before Width: | Height: | Size: 309 KiB |
|
Before Width: | Height: | Size: 338 KiB |
|
Before Width: | Height: | Size: 338 KiB |
|
Before Width: | Height: | Size: 294 KiB |
|
Before Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 294 KiB |
|
Before Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 309 KiB |
|
Before Width: | Height: | Size: 338 KiB |
|
Before Width: | Height: | Size: 309 KiB |
|
Before Width: | Height: | Size: 294 KiB |
|
Before Width: | Height: | Size: 309 KiB |
|
Before Width: | Height: | Size: 338 KiB |
|
Before Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 294 KiB |
|
Before Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 334 KiB |
|
Before Width: | Height: | Size: 376 KiB |
|
Before Width: | Height: | Size: 334 KiB |
|
Before Width: | Height: | Size: 334 KiB |
|
Before Width: | Height: | Size: 310 KiB |
|
Before Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 310 KiB |
|
Before Width: | Height: | Size: 310 KiB |
|
Before Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 376 KiB |
|
Before Width: | Height: | Size: 310 KiB |
|
Before Width: | Height: | Size: 334 KiB |
|
Before Width: | Height: | Size: 310 KiB |
|
Before Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 310 KiB |
|
Before Width: | Height: | Size: 342 KiB |
|
Before Width: | Height: | Size: 342 KiB |
|
Before Width: | Height: | Size: 353 KiB |
|
Before Width: | Height: | Size: 613 KiB |
|
Before Width: | Height: | Size: 342 KiB |
|
Before Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 353 KiB |
|
Before Width: | Height: | Size: 353 KiB |
|
Before Width: | Height: | Size: 306 KiB |
|
Before Width: | Height: | Size: 274 KiB |
|
Before Width: | Height: | Size: 353 KiB |
|
Before Width: | Height: | Size: 342 KiB |
|
Before Width: | Height: | Size: 342 KiB |
|
Before Width: | Height: | Size: 342 KiB |
|
Before Width: | Height: | Size: 274 KiB |
|
Before Width: | Height: | Size: 613 KiB |
|
Before Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 227 KiB |
|
Before Width: | Height: | Size: 274 KiB |
|
Before Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 166 KiB |