feat(mindspace): add opt-in local analytics
This commit is contained in:
@@ -12,6 +12,14 @@ H5_PORT=8081
|
|||||||
# pnpm dev / dev-core 在未显式覆盖时会把 H5_PUBLIC_BASE_URL 设为 8081。
|
# pnpm dev / dev-core 在未显式覆盖时会把 H5_PUBLIC_BASE_URL 设为 8081。
|
||||||
H5_PUBLIC_BASE_URL=http://127.0.0.1:5173
|
H5_PUBLIC_BASE_URL=http://127.0.0.1:5173
|
||||||
|
|
||||||
|
# Local Umami analytics (disabled by default; points only to the local
|
||||||
|
# memind-analytics service when enabled).
|
||||||
|
# MEMIND_ANALYTICS_ENABLED=true
|
||||||
|
# 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_DOMAINS=127.0.0.1,localhost
|
||||||
|
|
||||||
# 生产 H5 public base 当前临时切到 https://mm.tkmind.cn。
|
# 生产 H5 public base 当前临时切到 https://mm.tkmind.cn。
|
||||||
# 后续公网 H5 不再依赖 105 转发链路;m.tkmind.cn 仅作为 legacy/rollback 记录处理。
|
# 后续公网 H5 不再依赖 105 转发链路;m.tkmind.cn 仅作为 legacy/rollback 记录处理。
|
||||||
# H5_PUBLIC_BASE_URL=https://mm.tkmind.cn
|
# H5_PUBLIC_BASE_URL=https://mm.tkmind.cn
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Local Memind + Umami analytics
|
||||||
|
|
||||||
|
The integration is local-only by default. Memind proxies `/analytics/*` to the
|
||||||
|
local Umami service at `http://127.0.0.1:3100`; it does not contact 103/105.
|
||||||
|
|
||||||
|
1. Start `/Users/john/Project/memind-analytics` and verify:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl --fail http://127.0.0.1:3100/api/heartbeat
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Create one Umami Website for the local generated-page host. Do not create a
|
||||||
|
Website per page or per user.
|
||||||
|
|
||||||
|
3. Put the Website ID and a local-only pseudonymization secret in Memind's
|
||||||
|
`.env`:
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
MEMIND_ANALYTICS_ENABLED=true
|
||||||
|
MEMIND_ANALYTICS_URL=http://127.0.0.1:3100
|
||||||
|
MEMIND_ANALYTICS_WEBSITE_ID=<website-id>
|
||||||
|
MEMIND_ANALYTICS_ID_SECRET=<random-local-secret>
|
||||||
|
MEMIND_ANALYTICS_DOMAINS=127.0.0.1,localhost
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Restart the local Memind server. Full generated HTML pages will receive a
|
||||||
|
same-origin `/analytics/script.js` tracker and a `page_view` event with
|
||||||
|
pseudonymous `owner_id`, `page_id`, and `channel` dimensions.
|
||||||
|
|
||||||
|
The integration is fail-open: missing configuration, disabled analytics, or a
|
||||||
|
down Umami service leaves page generation and page delivery unchanged. User
|
||||||
|
facing analytics must be queried through a future Memind API that filters by
|
||||||
|
the authenticated owner; do not expose the Umami dashboard directly to users.
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import crypto from 'node:crypto';
|
||||||
|
|
||||||
|
const ANALYTICS_MARKER = 'data-memind-analytics="1"';
|
||||||
|
|
||||||
|
export function resolveMindSpaceAnalyticsConfig(env = process.env) {
|
||||||
|
const enabled = String(env.MEMIND_ANALYTICS_ENABLED ?? '').toLowerCase() === 'true';
|
||||||
|
const websiteId = String(env.MEMIND_ANALYTICS_WEBSITE_ID ?? '').trim();
|
||||||
|
const secret = String(env.MEMIND_ANALYTICS_ID_SECRET ?? '').trim();
|
||||||
|
return {
|
||||||
|
enabled: enabled && Boolean(websiteId) && Boolean(secret),
|
||||||
|
websiteId,
|
||||||
|
idSecret: secret,
|
||||||
|
analyticsUrl: String(env.MEMIND_ANALYTICS_URL ?? 'http://127.0.0.1:3100').trim() || 'http://127.0.0.1:3100',
|
||||||
|
scriptPath: String(env.MEMIND_ANALYTICS_SCRIPT_PATH ?? '/analytics/script.js').trim() || '/analytics/script.js',
|
||||||
|
hostPath: String(env.MEMIND_ANALYTICS_HOST_PATH ?? '/analytics').trim() || '/analytics',
|
||||||
|
domains: String(env.MEMIND_ANALYTICS_DOMAINS ?? '').trim(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pseudonymizeAnalyticsId(value, secret) {
|
||||||
|
const normalized = String(value ?? '').trim();
|
||||||
|
const key = String(secret ?? '').trim();
|
||||||
|
if (!normalized || !key) return '';
|
||||||
|
return crypto.createHmac('sha256', key).update(normalized).digest('hex').slice(0, 32);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveAnalyticsOwnerSegment(user = {}) {
|
||||||
|
if (user?.role === 'admin') return 'admin';
|
||||||
|
const plan = String(user?.planType ?? user?.plan_type ?? 'free').trim().toLowerCase();
|
||||||
|
return `plan:${plan || 'free'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveAnalyticsOwnerLabel(user = {}) {
|
||||||
|
const label = String(user?.displayName ?? user?.display_name ?? user?.username ?? '').trim();
|
||||||
|
return label.replace(/[\r\n\t]+/g, ' ').slice(0, 80) || '未命名用户';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sendMindSpaceAnalyticsEvent({
|
||||||
|
config,
|
||||||
|
eventName,
|
||||||
|
ownerId,
|
||||||
|
pageId = '',
|
||||||
|
publicationId = '',
|
||||||
|
agentRunId = '',
|
||||||
|
channel = 'h5',
|
||||||
|
ownerSegment = 'unknown',
|
||||||
|
ownerLabel = '未命名用户',
|
||||||
|
url = '',
|
||||||
|
} = {}) {
|
||||||
|
if (!config?.enabled || !config.websiteId || !config.idSecret || !eventName) return Promise.resolve(false);
|
||||||
|
const owner = pseudonymizeAnalyticsId(ownerId, config.idSecret);
|
||||||
|
if (!owner) return Promise.resolve(false);
|
||||||
|
const endpoint = `${String(config.analyticsUrl || 'http://127.0.0.1:3100').replace(/\/$/, '')}/api/send`;
|
||||||
|
const payload = {
|
||||||
|
website: config.websiteId,
|
||||||
|
hostname: '127.0.0.1',
|
||||||
|
url: url || '/',
|
||||||
|
name: String(eventName),
|
||||||
|
data: {
|
||||||
|
owner_id: owner,
|
||||||
|
page_id: String(pageId || ''),
|
||||||
|
publication_id: String(publicationId || ''),
|
||||||
|
agent_run_id: String(agentRunId || ''),
|
||||||
|
channel,
|
||||||
|
owner_segment: String(ownerSegment || 'unknown'),
|
||||||
|
owner_label: resolveAnalyticsOwnerLabel({ displayName: ownerLabel }),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return fetch(endpoint, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'content-type': 'application/json', 'user-agent': 'Memind/local-analytics' },
|
||||||
|
body: JSON.stringify({ type: 'event', payload }),
|
||||||
|
signal: AbortSignal.timeout(1500),
|
||||||
|
}).then((response) => response.ok).catch(() => false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonForInlineScript(value) {
|
||||||
|
return JSON.stringify(value)
|
||||||
|
.replaceAll('<', '\\u003c')
|
||||||
|
.replaceAll('>', '\\u003e')
|
||||||
|
.replaceAll('&', '\\u0026');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function injectMindSpaceAnalytics(html, {
|
||||||
|
ownerId,
|
||||||
|
pageId = '',
|
||||||
|
publicationId = '',
|
||||||
|
ownerSegment = 'unknown',
|
||||||
|
ownerLabel = '未命名用户',
|
||||||
|
channel = 'h5',
|
||||||
|
config = resolveMindSpaceAnalyticsConfig(),
|
||||||
|
} = {}) {
|
||||||
|
const source = String(html ?? '');
|
||||||
|
if (!config?.enabled || !config.websiteId || !/^\s*(<!doctype html|<html\b)/i.test(source)) return source;
|
||||||
|
if (source.includes(ANALYTICS_MARKER)) return source;
|
||||||
|
const owner = pseudonymizeAnalyticsId(ownerId, config.idSecret);
|
||||||
|
if (!owner) return source;
|
||||||
|
const metadata = { owner_id: owner, owner_segment: String(ownerSegment || 'unknown'), owner_label: resolveAnalyticsOwnerLabel({ displayName: ownerLabel }), page_id: String(pageId || ''), publication_id: String(publicationId || ''), channel };
|
||||||
|
const attrs = [
|
||||||
|
ANALYTICS_MARKER,
|
||||||
|
`data-website-id="${config.websiteId.replaceAll('"', '"')}"`,
|
||||||
|
'data-auto-track="false"',
|
||||||
|
`data-host-url="${config.hostPath}"`,
|
||||||
|
];
|
||||||
|
if (config.domains) attrs.push(`data-domains="${config.domains.replaceAll('"', '"')}"`);
|
||||||
|
const block = `<script ${attrs.join(' ')} defer src="${config.scriptPath}"></script><script ${ANALYTICS_MARKER}>(function(){var d=${jsonForInlineScript(metadata)},seen={};function t(n,x){if(!window.umami||typeof window.umami.track!=='function')return;var p=Object.assign({},d,{page_url:location.href,page_title:document.title},x||{});window.umami.track(n,p);}function once(n,x){if(seen[n])return;seen[n]=1;t(n,x);}function ready(){t('page_view');document.addEventListener('click',function(e){var el=e.target&&e.target.closest?e.target.closest('a,button,[role="button"],[data-umami-event]'):null;if(!el)return;var custom=el.getAttribute('data-umami-event');var href=el.tagName==='A'?el.getAttribute('href')||'':'';t(custom||'page_click',{element:el.tagName.toLowerCase(),element_id:el.id||'',event_label:(custom||el.getAttribute('aria-label')||'').slice(0,100),target_url:href.slice(0,500)});},{passive:true});document.addEventListener('submit',function(e){var form=e.target;t('page_form_submit',{form_id:form&&form.id||'',form_action:form&&form.getAttribute('action')||''});},{passive:true});var marks=[25,50,75,90];function scroll(){var h=document.documentElement.scrollHeight-window.innerHeight;if(h<=0){once('page_scroll_100');return;}var pct=Math.round(window.scrollY/h*100);marks.forEach(function(m){if(pct>=m)once('page_scroll_'+m);});}window.addEventListener('scroll',scroll,{passive:true});setTimeout(function(){once('page_engaged_10s');},10000);setTimeout(function(){once('page_engaged_30s');},30000);scroll();}if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',ready,{once:true});}else{ready();}})();</script>`;
|
||||||
|
if (/<\/head>/i.test(source)) return source.replace(/<\/head>/i, `${block}</head>`);
|
||||||
|
return source.replace(/<body\b/i, `${block}<body`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
|
import {
|
||||||
|
injectMindSpaceAnalytics,
|
||||||
|
pseudonymizeAnalyticsId,
|
||||||
|
resolveAnalyticsOwnerSegment,
|
||||||
|
resolveAnalyticsOwnerLabel,
|
||||||
|
resolveMindSpaceAnalyticsConfig,
|
||||||
|
sendMindSpaceAnalyticsEvent,
|
||||||
|
} from './mindspace-analytics.mjs';
|
||||||
|
|
||||||
|
test('analytics config is disabled unless explicitly enabled and configured', () => {
|
||||||
|
assert.equal(resolveMindSpaceAnalyticsConfig({ MEMIND_ANALYTICS_ENABLED: 'true' }).enabled, false);
|
||||||
|
assert.equal(resolveMindSpaceAnalyticsConfig({
|
||||||
|
MEMIND_ANALYTICS_ENABLED: 'true',
|
||||||
|
MEMIND_ANALYTICS_WEBSITE_ID: 'local-website',
|
||||||
|
}).enabled, false);
|
||||||
|
const config = resolveMindSpaceAnalyticsConfig({
|
||||||
|
MEMIND_ANALYTICS_ENABLED: 'true',
|
||||||
|
MEMIND_ANALYTICS_URL: 'http://127.0.0.1:3200',
|
||||||
|
MEMIND_ANALYTICS_WEBSITE_ID: 'local-website',
|
||||||
|
MEMIND_ANALYTICS_ID_SECRET: 'local-secret',
|
||||||
|
});
|
||||||
|
assert.equal(config.enabled, true);
|
||||||
|
assert.equal(config.analyticsUrl, 'http://127.0.0.1:3200');
|
||||||
|
assert.equal(config.hostPath, '/analytics');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('owner ids are stable pseudonyms and never expose the source id', () => {
|
||||||
|
const first = pseudonymizeAnalyticsId('user-123', 'secret');
|
||||||
|
assert.equal(first, pseudonymizeAnalyticsId('user-123', 'secret'));
|
||||||
|
assert.notEqual(first, 'user-123');
|
||||||
|
assert.notEqual(first, pseudonymizeAnalyticsId('user-456', 'secret'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('owner segments come from server-side Memind user profile data', () => {
|
||||||
|
assert.equal(resolveAnalyticsOwnerSegment({ role: 'admin' }), 'admin');
|
||||||
|
assert.equal(resolveAnalyticsOwnerSegment({ role: 'user', planType: 'pro' }), 'plan:pro');
|
||||||
|
assert.equal(resolveAnalyticsOwnerSegment({ role: 'user' }), 'plan:free');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('owner labels are readable but bounded and stripped of control characters', () => {
|
||||||
|
assert.equal(resolveAnalyticsOwnerLabel({ displayName: '张三\n管理员' }), '张三 管理员');
|
||||||
|
assert.equal(resolveAnalyticsOwnerLabel({}), '未命名用户');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('injects one local same-origin tracker with page dimensions', () => {
|
||||||
|
const html = '<!doctype html><html><head><title>Demo</title></head><body><h1>Demo</h1></body></html>';
|
||||||
|
const out = injectMindSpaceAnalytics(html, {
|
||||||
|
ownerId: 'user-123',
|
||||||
|
pageId: 'page-1',
|
||||||
|
publicationId: 'pub-1',
|
||||||
|
config: {
|
||||||
|
enabled: true,
|
||||||
|
websiteId: 'local-website',
|
||||||
|
idSecret: 'secret',
|
||||||
|
scriptPath: '/analytics/script.js',
|
||||||
|
hostPath: '/analytics',
|
||||||
|
domains: '127.0.0.1,localhost',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.match(out, /src="\/analytics\/script\.js"/);
|
||||||
|
assert.match(out, /data-host-url="\/analytics"/);
|
||||||
|
assert.match(out, /data-auto-track="false"/);
|
||||||
|
assert.match(out, /page_id/);
|
||||||
|
assert.match(out, /owner_segment/);
|
||||||
|
assert.match(out, /owner_label/);
|
||||||
|
assert.match(out, /page_click/);
|
||||||
|
assert.match(out, /page_form_submit/);
|
||||||
|
assert.match(out, /page_scroll_/);
|
||||||
|
assert.match(out, /page_engaged_10s/);
|
||||||
|
assert.doesNotMatch(out, /user-123/);
|
||||||
|
assert.equal(injectMindSpaceAnalytics(out, { ownerId: 'user-123', config: { enabled: true, websiteId: 'local-website', idSecret: 'secret' } }), out);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('does not alter non-full-html or disabled pages', () => {
|
||||||
|
const fragment = '<div>hello</div>';
|
||||||
|
assert.equal(injectMindSpaceAnalytics(fragment, { ownerId: 'u', config: { enabled: true, websiteId: 'w', idSecret: 's' } }), fragment);
|
||||||
|
const html = '<!doctype html><html><head></head><body></body></html>';
|
||||||
|
assert.equal(injectMindSpaceAnalytics(html, { ownerId: 'u', config: { enabled: false, websiteId: 'w', idSecret: 's' } }), html);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('analytics event sender is fail-open when analytics is disabled', async () => {
|
||||||
|
assert.equal(await sendMindSpaceAnalyticsEvent({ eventName: 'page_generated', ownerId: 'u', config: { enabled: false } }), false);
|
||||||
|
});
|
||||||
+59
-1
@@ -1,4 +1,33 @@
|
|||||||
|
import crypto from 'node:crypto';
|
||||||
|
|
||||||
const PUBLIC_PAGE_LIMIT_KEY = 'public_page_limit';
|
const PUBLIC_PAGE_LIMIT_KEY = 'public_page_limit';
|
||||||
|
const ANALYTICS_ENABLED_KEY = 'analytics_enabled';
|
||||||
|
const ANALYTICS_WEBSITE_ID_KEY = 'analytics_website_id';
|
||||||
|
const ANALYTICS_URL_KEY = 'analytics_url';
|
||||||
|
const ANALYTICS_DOMAINS_KEY = 'analytics_domains';
|
||||||
|
const ANALYTICS_ID_SECRET_KEY = 'analytics_id_secret';
|
||||||
|
|
||||||
|
function secretKey(env = process.env) {
|
||||||
|
return crypto.createHash('sha256').update(String(env.TKMIND_SERVER__SECRET_KEY ?? 'local-dev-secret')).digest();
|
||||||
|
}
|
||||||
|
|
||||||
|
function encryptSecret(value, env = process.env) {
|
||||||
|
const iv = crypto.randomBytes(12);
|
||||||
|
const cipher = crypto.createCipheriv('aes-256-gcm', secretKey(env), iv);
|
||||||
|
const ciphertext = Buffer.concat([cipher.update(String(value), 'utf8'), cipher.final()]);
|
||||||
|
return JSON.stringify({ v: 1, iv: iv.toString('base64'), tag: cipher.getAuthTag().toString('base64'), data: ciphertext.toString('base64') });
|
||||||
|
}
|
||||||
|
|
||||||
|
function decryptSecret(value, env = process.env) {
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(String(value));
|
||||||
|
const decipher = crypto.createDecipheriv('aes-256-gcm', secretKey(env), Buffer.from(payload.iv, 'base64'));
|
||||||
|
decipher.setAuthTag(Buffer.from(payload.tag, 'base64'));
|
||||||
|
return Buffer.concat([decipher.update(Buffer.from(payload.data, 'base64')), decipher.final()]).toString('utf8');
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function asPositiveInteger(value, fallback) {
|
function asPositiveInteger(value, fallback) {
|
||||||
const parsed = Number(value);
|
const parsed = Number(value);
|
||||||
@@ -20,6 +49,13 @@ async function ensureConfigTable(pool) {
|
|||||||
export function defaultMindSpaceConfig(env = process.env) {
|
export function defaultMindSpaceConfig(env = process.env) {
|
||||||
return {
|
return {
|
||||||
publicPageLimit: asPositiveInteger(env.MINDSPACE_FREE_PUBLIC_PAGE_LIMIT ?? 5, 5),
|
publicPageLimit: asPositiveInteger(env.MINDSPACE_FREE_PUBLIC_PAGE_LIMIT ?? 5, 5),
|
||||||
|
analytics: {
|
||||||
|
enabled: String(env.MEMIND_ANALYTICS_ENABLED ?? '').toLowerCase() === 'true',
|
||||||
|
websiteId: String(env.MEMIND_ANALYTICS_WEBSITE_ID ?? '').trim(),
|
||||||
|
analyticsUrl: String(env.MEMIND_ANALYTICS_URL ?? 'http://127.0.0.1:3100').trim(),
|
||||||
|
domains: String(env.MEMIND_ANALYTICS_DOMAINS ?? '127.0.0.1,localhost').trim(),
|
||||||
|
idSecretConfigured: Boolean(String(env.MEMIND_ANALYTICS_ID_SECRET ?? '').trim()),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +81,7 @@ export async function ensureMindSpaceConfig(pool, { env = process.env, seedDefau
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadMindSpaceConfig(pool, { env = process.env } = {}) {
|
export async function loadMindSpaceConfig(pool, { env = process.env, includeAnalyticsSecret = false } = {}) {
|
||||||
const config = defaultMindSpaceConfig(env);
|
const config = defaultMindSpaceConfig(env);
|
||||||
try {
|
try {
|
||||||
const rows = await readConfigRows(pool);
|
const rows = await readConfigRows(pool);
|
||||||
@@ -53,6 +89,15 @@ export async function loadMindSpaceConfig(pool, { env = process.env } = {}) {
|
|||||||
if (row.key === PUBLIC_PAGE_LIMIT_KEY) {
|
if (row.key === PUBLIC_PAGE_LIMIT_KEY) {
|
||||||
config.publicPageLimit = asPositiveInteger(row.value, config.publicPageLimit);
|
config.publicPageLimit = asPositiveInteger(row.value, config.publicPageLimit);
|
||||||
}
|
}
|
||||||
|
if (row.key === ANALYTICS_ENABLED_KEY) config.analytics.enabled = row.value === 'true';
|
||||||
|
if (row.key === ANALYTICS_WEBSITE_ID_KEY) config.analytics.websiteId = String(row.value ?? '');
|
||||||
|
if (row.key === ANALYTICS_URL_KEY) config.analytics.analyticsUrl = String(row.value ?? '');
|
||||||
|
if (row.key === ANALYTICS_DOMAINS_KEY) config.analytics.domains = String(row.value ?? '');
|
||||||
|
if (row.key === ANALYTICS_ID_SECRET_KEY) {
|
||||||
|
const secret = decryptSecret(row.value, env);
|
||||||
|
config.analytics.idSecretConfigured = Boolean(secret);
|
||||||
|
if (includeAnalyticsSecret) config.analytics.idSecret = secret;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error?.code === 'ER_NO_SUCH_TABLE') return config;
|
if (error?.code === 'ER_NO_SUCH_TABLE') return config;
|
||||||
@@ -73,6 +118,14 @@ export async function updateMindSpaceConfig(pool, patch, { env = process.env } =
|
|||||||
}
|
}
|
||||||
updates.push([PUBLIC_PAGE_LIMIT_KEY, String(publicPageLimit), '公开页面数量上限']);
|
updates.push([PUBLIC_PAGE_LIMIT_KEY, String(publicPageLimit), '公开页面数量上限']);
|
||||||
}
|
}
|
||||||
|
if (patch?.analytics) {
|
||||||
|
const analytics = patch.analytics;
|
||||||
|
if (analytics.enabled !== undefined) updates.push([ANALYTICS_ENABLED_KEY, String(Boolean(analytics.enabled)), '本地 Umami 分析开关']);
|
||||||
|
if (analytics.websiteId !== undefined) updates.push([ANALYTICS_WEBSITE_ID_KEY, String(analytics.websiteId ?? '').trim(), '本地 Umami Website ID']);
|
||||||
|
if (analytics.analyticsUrl !== undefined) updates.push([ANALYTICS_URL_KEY, String(analytics.analyticsUrl || 'http://127.0.0.1:3100').trim(), '本地 Umami 地址']);
|
||||||
|
if (analytics.domains !== undefined) updates.push([ANALYTICS_DOMAINS_KEY, String(analytics.domains ?? '').trim(), '本地统计域名']);
|
||||||
|
if (analytics.idSecret !== undefined && String(analytics.idSecret).trim()) updates.push([ANALYTICS_ID_SECRET_KEY, encryptSecret(String(analytics.idSecret).trim(), env), '本地分析匿名化密钥']);
|
||||||
|
}
|
||||||
|
|
||||||
if (updates.length === 0) return loadMindSpaceConfig(pool, { env });
|
if (updates.length === 0) return loadMindSpaceConfig(pool, { env });
|
||||||
|
|
||||||
@@ -93,4 +146,9 @@ export async function updateMindSpaceConfig(pool, patch, { env = process.env } =
|
|||||||
|
|
||||||
export const mindspaceConfigInternals = {
|
export const mindspaceConfigInternals = {
|
||||||
PUBLIC_PAGE_LIMIT_KEY,
|
PUBLIC_PAGE_LIMIT_KEY,
|
||||||
|
ANALYTICS_ENABLED_KEY,
|
||||||
|
ANALYTICS_WEBSITE_ID_KEY,
|
||||||
|
ANALYTICS_URL_KEY,
|
||||||
|
ANALYTICS_DOMAINS_KEY,
|
||||||
|
ANALYTICS_ID_SECRET_KEY,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -64,3 +64,35 @@ test('updateMindSpaceConfig persists a positive integer limit', async () => {
|
|||||||
assert.equal(config.publicPageLimit, 15);
|
assert.equal(config.publicPageLimit, 15);
|
||||||
assert.equal(calls.some(({ sql }) => sql.includes('ON DUPLICATE KEY UPDATE')), true);
|
assert.equal(calls.some(({ sql }) => sql.includes('ON DUPLICATE KEY UPDATE')), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('analytics settings encrypt the id secret and only reveal it on internal loads', async () => {
|
||||||
|
const rows = new Map();
|
||||||
|
const pool = {
|
||||||
|
async query(sql, params = []) {
|
||||||
|
if (sql.includes('FROM mindspace_config')) {
|
||||||
|
return [[...rows].map(([key, value]) => ({ key, value }))];
|
||||||
|
}
|
||||||
|
if (sql.includes('ON DUPLICATE KEY UPDATE')) rows.set(params[0], params[1]);
|
||||||
|
return [[]];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const env = { TKMIND_SERVER__SECRET_KEY: 'test-server-secret' };
|
||||||
|
|
||||||
|
const publicConfig = await updateMindSpaceConfig(pool, {
|
||||||
|
analytics: {
|
||||||
|
enabled: true,
|
||||||
|
websiteId: 'website-1',
|
||||||
|
analyticsUrl: 'http://127.0.0.1:3200',
|
||||||
|
domains: 'localhost',
|
||||||
|
idSecret: 'analytics-secret',
|
||||||
|
},
|
||||||
|
}, { env });
|
||||||
|
|
||||||
|
assert.equal(publicConfig.analytics.enabled, true);
|
||||||
|
assert.equal(publicConfig.analytics.idSecretConfigured, true);
|
||||||
|
assert.equal('idSecret' in publicConfig.analytics, false);
|
||||||
|
assert.doesNotMatch(rows.get('analytics_id_secret'), /analytics-secret/);
|
||||||
|
|
||||||
|
const internalConfig = await loadMindSpaceConfig(pool, { env, includeAnalyticsSecret: true });
|
||||||
|
assert.equal(internalConfig.analytics.idSecret, 'analytics-secret');
|
||||||
|
});
|
||||||
|
|||||||
+1
-1
@@ -58,7 +58,7 @@
|
|||||||
"test:scenario": "node scripts/run-scenario-test.mjs",
|
"test:scenario": "node scripts/run-scenario-test.mjs",
|
||||||
"verify:children-hobby-diet-survey": "node scripts/verify-children-hobby-diet-survey.mjs",
|
"verify:children-hobby-diet-survey": "node scripts/verify-children-hobby-diet-survey.mjs",
|
||||||
"test:scenario:john4-diet": "node scripts/run-scenario-test.mjs --scenario john4-children-hobby-diet-update",
|
"test:scenario:john4-diet": "node scripts/run-scenario-test.mjs --scenario john4-children-hobby-diet-update",
|
||||||
"test": "node --test auth.test.mjs asr-proxy.test.mjs billing.test.mjs billing-token-state.test.mjs billing-recharge.test.mjs wechat-pay.test.mjs wechat-oauth.test.mjs wechat-mp.test.mjs schedule-intent.test.mjs schedule-reminder-worker.test.mjs capabilities.test.mjs policies.test.mjs chat-skills.test.mjs chat-intent-router.test.mjs chat-finish-sync.test.mjs chat-agent-run-gate.test.mjs conversation-display.test.mjs user-publish.test.mjs user-memory-profile.test.mjs skills-registry.test.mjs agent-run-gateway.test.mjs agent-run-routes.test.mjs session-broker.test.mjs sse-event-taxonomy.test.mjs goosed-proxy-boundary.test.mjs agent-run-stream.test.mjs mindspace-h5-html-finish-guard.test.mjs admin-routes.test.mjs direct-chat-service.test.mjs tool-gateway.test.mjs mindspace.test.mjs mindspace-scan.test.mjs mindspace-assets.test.mjs mindspace-local-runtime-services.test.mjs mindspace-local-server-adapter.test.mjs mindspace-public-asset-token.test.mjs mindspace-remote-server-adapter.test.mjs mindspace-server-adapter.test.mjs mindspace-pages.test.mjs mindspace-page-sync-service.test.mjs public-site-bases.test.mjs mindspace-html-download-links.test.mjs mindspace-page-purge.test.mjs mindspace-public-delivery.test.mjs mindspace-public-page-context.test.mjs mindspace-published-page-csp.test.mjs mindspace-published-script-localize.test.mjs agent-run-deliverable-check.test.mjs mindspace-public-route.test.mjs mindspace-publications.test.mjs mindspace-public-links.test.mjs mindspace-chat-save.test.mjs mindspace-chat-docx-package.test.mjs mindspace-public-finish-sync.test.mjs mindspace-chat-context.test.mjs mindspace-canonical-url.test.mjs mindspace-conversation-package.test.mjs mindspace-conversation-package-backfill.test.mjs mindspace-conversation-package-public-html.test.mjs mindspace-conversation-package-verify.test.mjs mindspace-conversation-package-registry.test.mjs mindspace-conversation-package-routes.test.mjs mindspace-conversation-package-store.test.mjs mindspace-conversation-schema.test.mjs mindspace-runtime-config.test.mjs mindspace-service.test.mjs mindspace-storage-adapter.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 mindspace-sandbox-mcp.test.mjs user-data-space-service.test.mjs page-data-routes.test.mjs page-access-policy.test.mjs page-access-visitor.test.mjs page-data-public-service.test.mjs page-data-integration.test.mjs page-data-log-store.test.mjs page-data-ops.test.mjs page-data-session-store.test.mjs page-data-browser-client.test.mjs page-data-policy-index.test.mjs message-stream.test.mjs mindspace-service/mindspace-rpc-server.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 admin-guard.test.mjs user-feedback.test.mjs memory-v2.test.mjs memory-v2-admin-config.test.mjs memory-v2-adapter-scaffold.test.mjs memory-v2-backend-contract.test.mjs memory-v2-health.test.mjs memory-v2-runtime.test.mjs memory-v2-plugin-backends.test.mjs memory-v2-pgvector.test.mjs memory-v2-pgvector-schema.test.mjs memory-v2-pgvector-backfill.test.mjs memory-v2-pgvector-smoke.test.mjs memory-v2-qdrant.test.mjs memory-v2-weaviate.test.mjs memory-v2-mem0.test.mjs memory-v2-letta.test.mjs memory-v2-external-adapters.test.mjs scripts/embed-memory-v2-local-hash.test.mjs scripts/check-memory-v2-app-canary.test.mjs scripts/check-memory-v2-config-gaps.test.mjs scripts/check-memory-v2-contracts.test.mjs scripts/check-memory-v2-health.test.mjs scripts/check-memory-v2-session-flow.test.mjs scripts/check-memory-v2-stack.test.mjs scripts/setup-memory-v2-pgvector-schema.test.mjs scripts/backfill-memory-v2-pgvector.test.mjs scripts/scaffold-memory-v2-backend.test.mjs scripts/smoke-memory-v2-pgvector.test.mjs scripts/smoke-memory-v2-qdrant.test.mjs scripts/smoke-memory-v2-external.test.mjs scripts/mock-memory-v2-services.test.mjs",
|
"test": "node --test auth.test.mjs asr-proxy.test.mjs billing.test.mjs billing-token-state.test.mjs billing-recharge.test.mjs wechat-pay.test.mjs wechat-oauth.test.mjs wechat-mp.test.mjs schedule-intent.test.mjs schedule-reminder-worker.test.mjs capabilities.test.mjs policies.test.mjs chat-skills.test.mjs chat-intent-router.test.mjs chat-finish-sync.test.mjs chat-agent-run-gate.test.mjs conversation-display.test.mjs user-publish.test.mjs user-memory-profile.test.mjs skills-registry.test.mjs agent-run-gateway.test.mjs agent-run-routes.test.mjs session-broker.test.mjs sse-event-taxonomy.test.mjs goosed-proxy-boundary.test.mjs agent-run-stream.test.mjs mindspace-h5-html-finish-guard.test.mjs admin-routes.test.mjs direct-chat-service.test.mjs tool-gateway.test.mjs mindspace.test.mjs mindspace-scan.test.mjs mindspace-assets.test.mjs mindspace-local-runtime-services.test.mjs mindspace-local-server-adapter.test.mjs mindspace-public-asset-token.test.mjs mindspace-remote-server-adapter.test.mjs mindspace-server-adapter.test.mjs mindspace-pages.test.mjs mindspace-page-sync-service.test.mjs public-site-bases.test.mjs mindspace-html-download-links.test.mjs mindspace-page-purge.test.mjs mindspace-public-delivery.test.mjs mindspace-public-page-context.test.mjs mindspace-published-page-csp.test.mjs mindspace-published-script-localize.test.mjs agent-run-deliverable-check.test.mjs mindspace-public-route.test.mjs mindspace-publications.test.mjs mindspace-public-links.test.mjs mindspace-chat-save.test.mjs mindspace-chat-docx-package.test.mjs mindspace-public-finish-sync.test.mjs mindspace-chat-context.test.mjs mindspace-canonical-url.test.mjs mindspace-conversation-package.test.mjs mindspace-conversation-package-backfill.test.mjs mindspace-conversation-package-public-html.test.mjs mindspace-conversation-package-verify.test.mjs mindspace-conversation-package-registry.test.mjs mindspace-conversation-package-routes.test.mjs mindspace-conversation-package-store.test.mjs mindspace-conversation-schema.test.mjs mindspace-runtime-config.test.mjs mindspace-config.test.mjs mindspace-analytics.test.mjs mindspace-service.test.mjs mindspace-storage-adapter.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 mindspace-sandbox-mcp.test.mjs user-data-space-service.test.mjs page-data-routes.test.mjs page-access-policy.test.mjs page-access-visitor.test.mjs page-data-public-service.test.mjs page-data-integration.test.mjs page-data-log-store.test.mjs page-data-ops.test.mjs page-data-session-store.test.mjs page-data-browser-client.test.mjs page-data-policy-index.test.mjs message-stream.test.mjs mindspace-service/mindspace-rpc-server.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 admin-guard.test.mjs user-feedback.test.mjs memory-v2.test.mjs memory-v2-admin-config.test.mjs memory-v2-lifecycle.test.mjs memory-v2-adapter-scaffold.test.mjs memory-v2-backend-contract.test.mjs memory-v2-health.test.mjs memory-v2-runtime.test.mjs memory-v2-plugin-backends.test.mjs memory-v2-pgvector.test.mjs memory-v2-pgvector-schema.test.mjs memory-v2-pgvector-backfill.test.mjs memory-v2-pgvector-smoke.test.mjs memory-v2-qdrant.test.mjs memory-v2-weaviate.test.mjs memory-v2-mem0.test.mjs memory-v2-letta.test.mjs memory-v2-external-adapters.test.mjs scripts/embed-memory-v2-local-hash.test.mjs scripts/check-memory-v2-app-canary.test.mjs scripts/check-memory-v2-config-gaps.test.mjs scripts/check-memory-v2-contracts.test.mjs scripts/check-memory-v2-health.test.mjs scripts/check-memory-v2-session-flow.test.mjs scripts/check-memory-v2-stack.test.mjs scripts/setup-memory-v2-pgvector-schema.test.mjs scripts/backfill-memory-v2-pgvector.test.mjs scripts/scaffold-memory-v2-backend.test.mjs scripts/smoke-memory-v2-pgvector.test.mjs scripts/smoke-memory-v2-qdrant.test.mjs scripts/smoke-memory-v2-external.test.mjs scripts/mock-memory-v2-services.test.mjs",
|
||||||
"test:mindspace-service": "node --test mindspace-service/mindspace-rpc-server.test.mjs",
|
"test:mindspace-service": "node --test mindspace-service/mindspace-rpc-server.test.mjs",
|
||||||
"verify:chat-finish-sync": "node scripts/verify-chat-finish-sync.mjs",
|
"verify:chat-finish-sync": "node scripts/verify-chat-finish-sync.mjs",
|
||||||
"verify:public-finish-sync-runtime": "node scripts/verify-public-finish-sync-runtime.mjs",
|
"verify:public-finish-sync-runtime": "node scripts/verify-public-finish-sync-runtime.mjs",
|
||||||
|
|||||||
+71
-4
@@ -38,13 +38,14 @@ import { createWikiAuth } from './wiki-auth.mjs';
|
|||||||
import { isLocalDevHostname } from './scripts/local-test-config.mjs';
|
import { isLocalDevHostname } from './scripts/local-test-config.mjs';
|
||||||
import { PUBLISH_ROOT_DIR, PUBLISH_KEY_UUID, PUBLIC_ZONE_DIR } from './user-publish.mjs';
|
import { PUBLISH_ROOT_DIR, PUBLISH_KEY_UUID, PUBLIC_ZONE_DIR } from './user-publish.mjs';
|
||||||
import { ensureWorkspaceHtmlThumbnail, startWorkspaceThumbnailWatcher, workspaceThumbnailRelativePath } from './mindspace-workspace-thumbnails.mjs';
|
import { ensureWorkspaceHtmlThumbnail, startWorkspaceThumbnailWatcher, workspaceThumbnailRelativePath } from './mindspace-workspace-thumbnails.mjs';
|
||||||
|
import { injectMindSpaceAnalytics, resolveAnalyticsOwnerLabel, resolveAnalyticsOwnerSegment, resolveMindSpaceAnalyticsConfig, sendMindSpaceAnalyticsEvent } from './mindspace-analytics.mjs';
|
||||||
import { startWorkspaceAssetSyncWatcher } from './mindspace-workspace-sync.mjs';
|
import { startWorkspaceAssetSyncWatcher } from './mindspace-workspace-sync.mjs';
|
||||||
import { attachRequestId, sendData, sendError } from './api-response.mjs';
|
import { attachRequestId, sendData, sendError } from './api-response.mjs';
|
||||||
import { createNotificationDispatcher } from './notification-dispatcher.mjs';
|
import { createNotificationDispatcher } from './notification-dispatcher.mjs';
|
||||||
import { createMindSpaceAuditWriter } from './mindspace-audit.mjs';
|
import { createMindSpaceAuditWriter } from './mindspace-audit.mjs';
|
||||||
import { assertMindSpaceRoute, mindspaceFlags } from './mindspace-flags.mjs';
|
import { assertMindSpaceRoute, mindspaceFlags } from './mindspace-flags.mjs';
|
||||||
import { createMindSpaceService } from './mindspace.mjs';
|
import { createMindSpaceService } from './mindspace.mjs';
|
||||||
import { ensureMindSpaceConfig } from './mindspace-config.mjs';
|
import { ensureMindSpaceConfig, loadMindSpaceConfig } from './mindspace-config.mjs';
|
||||||
import { createMindSearchConfigService } from './mindsearch-config.mjs';
|
import { createMindSearchConfigService } from './mindsearch-config.mjs';
|
||||||
import { pageInternals, inlinePrivateAssetsInHtml, normalizeWorkspaceRelativePath } from './mindspace-pages.mjs';
|
import { pageInternals, inlinePrivateAssetsInHtml, normalizeWorkspaceRelativePath } from './mindspace-pages.mjs';
|
||||||
import {
|
import {
|
||||||
@@ -404,6 +405,7 @@ let wordFilterService = null;
|
|||||||
let authPool = null;
|
let authPool = null;
|
||||||
let pageDataService = null;
|
let pageDataService = null;
|
||||||
let pageDataPublicService = null;
|
let pageDataPublicService = null;
|
||||||
|
let mindSpaceAnalyticsConfig = resolveMindSpaceAnalyticsConfig();
|
||||||
|
|
||||||
async function bootstrapUserAuth() {
|
async function bootstrapUserAuth() {
|
||||||
try {
|
try {
|
||||||
@@ -416,6 +418,19 @@ async function bootstrapUserAuth() {
|
|||||||
await ensureMindSpaceConfig(pool, {
|
await ensureMindSpaceConfig(pool, {
|
||||||
env: process.env,
|
env: process.env,
|
||||||
});
|
});
|
||||||
|
const storedMindSpaceConfig = await loadMindSpaceConfig(pool, {
|
||||||
|
env: process.env,
|
||||||
|
includeAnalyticsSecret: true,
|
||||||
|
});
|
||||||
|
if (storedMindSpaceConfig?.analytics) {
|
||||||
|
mindSpaceAnalyticsConfig = {
|
||||||
|
...mindSpaceAnalyticsConfig,
|
||||||
|
...storedMindSpaceConfig.analytics,
|
||||||
|
enabled: Boolean(storedMindSpaceConfig.analytics.enabled && storedMindSpaceConfig.analytics.websiteId && storedMindSpaceConfig.analytics.idSecret),
|
||||||
|
hostPath: '/analytics',
|
||||||
|
scriptPath: '/analytics/script.js',
|
||||||
|
};
|
||||||
|
}
|
||||||
scheduleService = createScheduleService(pool, {
|
scheduleService = createScheduleService(pool, {
|
||||||
defaultTimezone: process.env.H5_DEFAULT_TIMEZONE || 'Asia/Shanghai',
|
defaultTimezone: process.env.H5_DEFAULT_TIMEZONE || 'Asia/Shanghai',
|
||||||
});
|
});
|
||||||
@@ -808,6 +823,22 @@ async function bootstrapUserAuth() {
|
|||||||
scheduleService: process.env.H5_SCHEDULE_ENABLED === '1' ? scheduleService : null,
|
scheduleService: process.env.H5_SCHEDULE_ENABLED === '1' ? scheduleService : null,
|
||||||
wechatScheduleLlmConfigService,
|
wechatScheduleLlmConfigService,
|
||||||
llmProviderService,
|
llmProviderService,
|
||||||
|
onPageGenerated: async ({ userId, sessionId, artifacts = [] }) => {
|
||||||
|
for (const artifact of artifacts) {
|
||||||
|
void sendMindSpaceAnalyticsEvent({
|
||||||
|
config: mindSpaceAnalyticsConfig,
|
||||||
|
eventName: 'page_generated',
|
||||||
|
ownerId: userId,
|
||||||
|
ownerSegment: resolveAnalyticsOwnerSegment(await userAuth?.getUserById(userId).catch(() => null) ?? {}),
|
||||||
|
ownerLabel: resolveAnalyticsOwnerLabel(await userAuth?.getUserById(userId).catch(() => null) ?? {}),
|
||||||
|
pageId: artifact.relativePath,
|
||||||
|
publicationId: sessionId,
|
||||||
|
agentRunId: sessionId,
|
||||||
|
channel: 'wechat_mp',
|
||||||
|
url: artifact.url || artifact.relativePath || '/',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
applySessionLlmProvider: (sessionId) => tkmindProxy.applySessionLlmProvider(sessionId),
|
applySessionLlmProvider: (sessionId) => tkmindProxy.applySessionLlmProvider(sessionId),
|
||||||
refreshSessionSnapshot:
|
refreshSessionSnapshot:
|
||||||
sessionSnapshotService?.isEnabled()
|
sessionSnapshotService?.isEnabled()
|
||||||
@@ -5155,6 +5186,7 @@ api.get('/sessions/:sessionId/events', async (req, res, next) => {
|
|||||||
// Retain every in-flight contract write and await it before marking files
|
// Retain every in-flight contract write and await it before marking files
|
||||||
// deliverable after Finish.
|
// deliverable after Finish.
|
||||||
const deliveryContractWrites = new Map();
|
const deliveryContractWrites = new Map();
|
||||||
|
const generationAnalyticsEvents = new Set();
|
||||||
const syncPublicHtmlDuringStream = (event) => {
|
const syncPublicHtmlDuringStream = (event) => {
|
||||||
const paths = collectPublicHtmlWritePathsFromSessionEvent(event, { publishDir });
|
const paths = collectPublicHtmlWritePathsFromSessionEvent(event, { publishDir });
|
||||||
const eventMessages = event?.type === 'Message' && event.message
|
const eventMessages = event?.type === 'Message' && event.message
|
||||||
@@ -5180,6 +5212,23 @@ api.get('/sessions/:sessionId/events', async (req, res, next) => {
|
|||||||
deliveryContractWrites.set(relativePath, write);
|
deliveryContractWrites.set(relativePath, write);
|
||||||
}
|
}
|
||||||
materializePublicHtmlWritesFromSessionEvent(event, { publishDir });
|
materializePublicHtmlWritesFromSessionEvent(event, { publishDir });
|
||||||
|
for (const relativePath of paths) {
|
||||||
|
const absolutePath = path.resolve(publishDir, relativePath);
|
||||||
|
if (generationAnalyticsEvents.has(relativePath) || !fs.existsSync(absolutePath)) continue;
|
||||||
|
generationAnalyticsEvents.add(relativePath);
|
||||||
|
void sendMindSpaceAnalyticsEvent({
|
||||||
|
config: mindSpaceAnalyticsConfig,
|
||||||
|
eventName: 'page_generated',
|
||||||
|
ownerId: req.currentUser.id,
|
||||||
|
ownerSegment: resolveAnalyticsOwnerSegment(req.currentUser),
|
||||||
|
ownerLabel: resolveAnalyticsOwnerLabel(req.currentUser),
|
||||||
|
pageId: relativePath,
|
||||||
|
publicationId: sid,
|
||||||
|
agentRunId: sid,
|
||||||
|
channel: 'h5',
|
||||||
|
url: `/${PUBLISH_ROOT_DIR}/${req.currentUser.id}/${relativePath}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
// After Finish, refresh the snapshot and persist any newly generated public
|
// After Finish, refresh the snapshot and persist any newly generated public
|
||||||
// workspace HTML into the asset store before a later restart rebuilds the
|
// workspace HTML into the asset store before a later restart rebuilds the
|
||||||
@@ -5391,6 +5440,13 @@ api.use(
|
|||||||
);
|
);
|
||||||
|
|
||||||
app.use('/api', api);
|
app.use('/api', api);
|
||||||
|
app.use('/analytics', createProxyMiddleware({
|
||||||
|
target: 'http://127.0.0.1:3100',
|
||||||
|
router: () => mindSpaceAnalyticsConfig.analyticsUrl || process.env.MEMIND_ANALYTICS_URL || 'http://127.0.0.1:3100',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
pathRewrite: { [`^${mindSpaceAnalyticsConfig.hostPath}`]: '' },
|
||||||
|
}));
|
||||||
// Express routing is case-insensitive by default, so the lowercase /mindspace API
|
// Express routing is case-insensitive by default, so the lowercase /mindspace API
|
||||||
// mount would otherwise capture public /MindSpace/... page URLs.
|
// mount would otherwise capture public /MindSpace/... page URLs.
|
||||||
app.use(`/${PUBLISH_ROOT_DIR}`, async (req, res, next) => {
|
app.use(`/${PUBLISH_ROOT_DIR}`, async (req, res, next) => {
|
||||||
@@ -6226,12 +6282,15 @@ async function sendPublishFile(req, res, filePath, { isOwner = true } = {}) {
|
|||||||
filePath,
|
filePath,
|
||||||
thumbnailPngPathForSvg,
|
thumbnailPngPathForSvg,
|
||||||
});
|
});
|
||||||
|
const parsedPublishPath = parseMindSpacePublishFilePath(filePath, __dirname);
|
||||||
|
const pageOwner = parsedPublishPath?.userId && userAuth
|
||||||
|
? await userAuth.getUserById(parsedPublishPath.userId).catch(() => null)
|
||||||
|
: null;
|
||||||
let pageDataContext = null;
|
let pageDataContext = null;
|
||||||
if (mindSpacePages) {
|
if (mindSpacePages) {
|
||||||
const parsed = parseMindSpacePublishFilePath(filePath, __dirname);
|
if (parsedPublishPath?.userId && parsedPublishPath.relativePath) {
|
||||||
if (parsed?.userId && parsed.relativePath) {
|
|
||||||
const page = await mindSpacePages
|
const page = await mindSpacePages
|
||||||
.findPageByRelativePath(parsed.userId, parsed.relativePath)
|
.findPageByRelativePath(parsedPublishPath.userId, parsedPublishPath.relativePath)
|
||||||
.catch(() => null);
|
.catch(() => null);
|
||||||
if (page?.id) {
|
if (page?.id) {
|
||||||
pageDataContext = {
|
pageDataContext = {
|
||||||
@@ -6241,6 +6300,14 @@ async function sendPublishFile(req, res, filePath, { isOwner = true } = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
html = injectMindSpaceAnalytics(html, {
|
||||||
|
ownerId: parsedPublishPath?.userId ?? '',
|
||||||
|
ownerSegment: resolveAnalyticsOwnerSegment(pageOwner ?? {}),
|
||||||
|
ownerLabel: resolveAnalyticsOwnerLabel(pageOwner ?? {}),
|
||||||
|
pageId: pageDataContext?.pageId ?? '',
|
||||||
|
publicationId: pageDataContext?.publicationId ?? pageDataContext?.publication_id ?? '',
|
||||||
|
config: mindSpaceAnalyticsConfig,
|
||||||
|
});
|
||||||
const decorated = decorateMindSpacePublishedHtml({
|
const decorated = decorateMindSpacePublishedHtml({
|
||||||
html,
|
html,
|
||||||
embed,
|
embed,
|
||||||
|
|||||||
@@ -888,6 +888,9 @@ function resolveHtmlPublishArtifacts({
|
|||||||
workingDir,
|
workingDir,
|
||||||
publicBaseUrl,
|
publicBaseUrl,
|
||||||
requestStartedAt,
|
requestStartedAt,
|
||||||
|
userId = '',
|
||||||
|
sessionId = '',
|
||||||
|
onPageGenerated = null,
|
||||||
}) {
|
}) {
|
||||||
materializeMissingPublicHtmlWrites({
|
materializeMissingPublicHtmlWrites({
|
||||||
messages: reply?.messages ?? [],
|
messages: reply?.messages ?? [],
|
||||||
@@ -918,6 +921,9 @@ function resolveHtmlPublishArtifacts({
|
|||||||
recentArtifacts,
|
recentArtifacts,
|
||||||
replyText: reply?.text,
|
replyText: reply?.text,
|
||||||
});
|
});
|
||||||
|
if (typeof onPageGenerated === 'function' && confirmedArtifacts.length > 0) {
|
||||||
|
void onPageGenerated({ userId, sessionId, artifacts: confirmedArtifacts });
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
publishedArtifacts,
|
publishedArtifacts,
|
||||||
expectedArtifacts,
|
expectedArtifacts,
|
||||||
@@ -1406,6 +1412,7 @@ export function createWechatMpService({
|
|||||||
scheduleService = null,
|
scheduleService = null,
|
||||||
wechatScheduleLlmConfigService = null,
|
wechatScheduleLlmConfigService = null,
|
||||||
llmProviderService = null,
|
llmProviderService = null,
|
||||||
|
onPageGenerated = null,
|
||||||
applySessionLlmProvider = null,
|
applySessionLlmProvider = null,
|
||||||
refreshSessionSnapshot = null,
|
refreshSessionSnapshot = null,
|
||||||
pageDataFinishGuard = null,
|
pageDataFinishGuard = null,
|
||||||
@@ -1979,6 +1986,9 @@ export function createWechatMpService({
|
|||||||
workingDir,
|
workingDir,
|
||||||
publicBaseUrl: config.publicBaseUrl,
|
publicBaseUrl: config.publicBaseUrl,
|
||||||
requestStartedAt,
|
requestStartedAt,
|
||||||
|
userId: user.userId,
|
||||||
|
sessionId,
|
||||||
|
onPageGenerated,
|
||||||
});
|
});
|
||||||
const hasValidLinkInReply = await hasAnyValidPublishedHtmlLink(reply?.text, linkExistsForRequest, {
|
const hasValidLinkInReply = await hasAnyValidPublishedHtmlLink(reply?.text, linkExistsForRequest, {
|
||||||
confirmedArtifacts,
|
confirmedArtifacts,
|
||||||
@@ -2135,6 +2145,9 @@ export function createWechatMpService({
|
|||||||
workingDir,
|
workingDir,
|
||||||
publicBaseUrl: config.publicBaseUrl,
|
publicBaseUrl: config.publicBaseUrl,
|
||||||
requestStartedAt: retryStartedAt,
|
requestStartedAt: retryStartedAt,
|
||||||
|
userId: user.userId,
|
||||||
|
sessionId,
|
||||||
|
onPageGenerated,
|
||||||
});
|
});
|
||||||
const hasValidLinkInReply = await hasAnyValidPublishedHtmlLink(reply?.text, linkExistsForRequest, {
|
const hasValidLinkInReply = await hasAnyValidPublishedHtmlLink(reply?.text, linkExistsForRequest, {
|
||||||
confirmedArtifacts,
|
confirmedArtifacts,
|
||||||
|
|||||||
Reference in New Issue
Block a user