Files
memind/scripts/news-item-wechat-context.mjs
john 028bb18dc0 feat(news): add single news-item page generation and early page-ready wait
Scheduled tasks can finish once the expected HTML is on disk and stable,
instead of waiting for Goose Finish. Adds news-item templates and scripts
for generating a single-event page and committing it as a WeChat draft.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-23 10:07:32 +08:00

221 lines
8.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 单条新闻 → 公众号草稿流程的共享上下文。
*
* 流程被拆成四个独立入口(生成 / 传图 / 生成配图 / 提交草稿),只有「生成」需要
* 完整的 Portal + Agent bootstrap;其余三个只要数据库连接和公众号凭证,因此这里
* 提供两级上下文,避免预览和推送也背上几十秒的 bootstrap 成本。
*/
import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
import { createDbPool, isDatabaseConfigured } from '../db.mjs';
import { createMindSpaceWechatMpConfigService, fetchWechatMpAccessToken } from '../mindspace-wechat-mp-config.mjs';
import { resolveMindSpaceAnalyticsConfig } from '../mindspace-analytics.mjs';
import {
resolveMindSpaceServerRuntimeOptions,
resolvePortalH5Root,
} from '../mindspace-runtime-config.mjs';
import { startWorkspaceAssetSyncWatcher } from '../mindspace-workspace-sync.mjs';
import { startWorkspaceThumbnailWatcher } from '../mindspace-workspace-thumbnails.mjs';
import { bootstrapPortalAgentServices } from '../server/portal-agent-services-bootstrap.mjs';
import { bootstrapPortalAuthServices } from '../server/portal-auth-services-bootstrap.mjs';
import { bootstrapPortalDomainServices } from '../server/portal-domain-services-bootstrap.mjs';
import { bootstrapPortalGatewayServices } from '../server/portal-gateway-services-bootstrap.mjs';
import { bootstrapPortalMemorySessionServices } from '../server/portal-memory-session-services-bootstrap.mjs';
import { resolveWechatFetch } from '../wechat-egress-fetch.mjs';
import { loadH5Environment } from './load-env.mjs';
import { loadMemindEnvFiles } from './memind-runtime-profile.mjs';
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
export const CODE_ROOT = path.join(scriptDir, '..');
export function resolvePortalRoot(env = process.env) {
return path.resolve(String(env.MEMIND_PORTAL_H5_ROOT ?? '/Users/john/Project/Memind').trim());
}
/** 读取 stdin 上的 JSON 载荷。四个脚本统一用这种调用约定。 */
export function readStdinPayload() {
const text = fs.readFileSync(0, 'utf8').trim();
if (!text) throw new Error('缺少 stdin JSON 输入');
return JSON.parse(text);
}
/**
* 统一的环境准备:news-engine 用 MEMIND_DATABASE_URL,Memind 侧用 DATABASE_URL。
*/
export function loadPushEnvironment(env = process.env) {
loadH5Environment(scriptDir);
loadMemindEnvFiles(resolvePortalRoot(env));
if (!String(env.DATABASE_URL ?? '').trim() && String(env.MEMIND_DATABASE_URL ?? '').trim()) {
env.DATABASE_URL = String(env.MEMIND_DATABASE_URL).trim();
}
env.MEMIND_PORTAL_H5_ROOT = resolvePortalRoot(env);
env.MEMIND_WORKSPACE_MAINTENANCE = env.MEMIND_WORKSPACE_MAINTENANCE ?? '0';
env.MEMIND_PUSH_SKIP_SCHEMA_INIT = env.MEMIND_PUSH_SKIP_SCHEMA_INIT ?? '1';
if (!isDatabaseConfigured()) {
throw new Error('DATABASE_URL / MEMIND_DATABASE_URL 未配置');
}
return env;
}
/**
* 轻量上下文:只要数据库和公众号凭证,用于传图 / 生成配图 / 提交草稿。
*/
export function createLightContext(env = process.env) {
const pool = createDbPool();
const h5Root = resolvePortalH5Root(CODE_ROOT, env);
const wechatFetch = resolveWechatFetch(env);
const wechatMpConfigService = createMindSpaceWechatMpConfigService(pool, { env, wechatFetch });
return {
pool,
h5Root,
wechatFetch,
wechatMpConfigService,
async resolveWechat(userId) {
const credentials = await wechatMpConfigService.getCredentials(userId);
const token = await fetchWechatMpAccessToken(credentials, { wechatFetch });
return { credentials, accessToken: token.accessToken };
},
async close() {
await pool.end().catch(() => {});
},
};
}
function resolveInitSchemaFn(env = process.env) {
const skip = String(env.MEMIND_PUSH_SKIP_SCHEMA_INIT ?? '1').trim().toLowerCase();
if (skip === '1' || skip === 'true' || skip === 'yes') return async () => {};
if (fs.existsSync(path.join(CODE_ROOT, 'schema.sql'))) return undefined;
console.warn('[news-item] schema.sql 不在 memind-lib,跳过 initSchema(假定生产库已由 Portal 初始化)');
return async () => {};
}
/**
* 完整 Agent 上下文:只有页面生成阶段需要。
*/
export async function bootstrapAgentContext(env = process.env) {
const h5Root = resolvePortalH5Root(CODE_ROOT, env);
const usersRoot = env.H5_USERS_ROOT?.trim() || path.join(h5Root, 'users');
const runtime = resolveMindSpaceServerRuntimeOptions(h5Root, env);
const apiTarget = String(env.TKMIND_API_TARGET ?? 'https://127.0.0.1:18006').trim();
const apiSecret = env.TKMIND_SERVER__SECRET_KEY ?? 'local-dev-secret';
const analyticsConfig = resolveMindSpaceAnalyticsConfig();
const pool = createDbPool();
let userAuth = null;
let sessionSnapshotService = null;
const domainServices = await bootstrapPortalDomainServices({
pool,
h5Root,
env,
runtime,
analyticsConfig,
getUserAuth: () => userAuth,
getSessionSnapshotService: () => sessionSnapshotService,
workspaceMaintenanceEnabled: env.MEMIND_WORKSPACE_MAINTENANCE !== '0',
logger: console,
initSchemaFn: resolveInitSchemaFn(env),
});
const authServices = await bootstrapPortalAuthServices({
pool,
h5Root,
usersRoot,
mindSearchConfigService: domainServices.mindSearchConfigService,
env,
logger: console,
});
userAuth = authServices.userAuth;
const agentServices = await bootstrapPortalAgentServices({
pool,
h5Root,
env,
runtime,
apiTarget,
apiTargets: [apiTarget],
apiSecret,
userAuth,
sessionAccess: authServices.sessionAccess,
subscriptionService: authServices.subscriptionService,
mindSpaceRuntimeAdapter: domainServices.mindSpaceRuntimeAdapter,
mindSpaceAssets: domainServices.mindSpaceAssets,
resolveUserIdByDirKey: domainServices.resolveUserIdByDirKey,
workspaceMaintenanceEnabled: env.MEMIND_WORKSPACE_MAINTENANCE !== '0',
startWorkspaceThumbnailWatcher,
startWorkspaceAssetSyncWatcher,
logger: console,
});
const memorySessionServices = await bootstrapPortalMemorySessionServices({
pool,
h5Root,
env,
llmProviderService: agentServices.llmProviderService,
userAuth,
sessionAccess: authServices.sessionAccess,
logger: console,
});
sessionSnapshotService = memorySessionServices.sessionSnapshotService;
const gatewayServices = bootstrapPortalGatewayServices({
pool,
h5Root,
env,
apiTarget,
apiTargets: [apiTarget],
apiSecret,
userAuth,
sessionAccess: authServices.sessionAccess,
sessionStreamStore: memorySessionServices.sessionStreamStore,
llmProviderService: agentServices.llmProviderService,
subscriptionService: authServices.subscriptionService,
billingConfigService: authServices.billingConfigService,
sessionSnapshotService,
conversationMemoryService: memorySessionServices.conversationMemoryService,
memoryV2: memorySessionServices.memoryV2,
systemDisclosurePolicyService: memorySessionServices.systemDisclosurePolicyService,
wechatCursorExecutorPolicyService: memorySessionServices.wechatCursorExecutorPolicyService,
mindSpaceAssets: domainServices.mindSpaceAssets,
directChatService: memorySessionServices.directChatService,
chatIntentRouter: memorySessionServices.chatIntentRouter,
syncUserGeneratedPages: domainServices.mindSpacePageSync
? (userId, options) => domainServices.mindSpacePageSync.syncUserGeneratedPages(userId, options)
: async () => {},
isSessionPageDeliveryActive: () => false,
experienceService: agentServices.experienceService,
});
return {
pool,
h5Root,
userAuth,
tkmindProxy: gatewayServices.tkmindProxy,
agentRunGateway: gatewayServices.agentRunGateway,
cursorExecutorPolicyService: memorySessionServices.wechatCursorExecutorPolicyService,
sessionSnapshotService,
wechatMpConfigService: createMindSpaceWechatMpConfigService(pool, { env }),
async close() {
await pool.end().catch(() => {});
},
};
}
/** 四个脚本统一的 stdout 协议:最后一行是 JSON。 */
export function runScript(main) {
main()
.then((result) => {
console.log(JSON.stringify({ ok: true, ...result }));
})
.catch((error) => {
console.log(JSON.stringify({
ok: false,
error: error instanceof Error ? error.message : String(error),
code: error?.code ?? null,
}));
process.exit(1);
});
}