diff --git a/.env.example b/.env.example index e13ffea..042710b 100644 --- a/.env.example +++ b/.env.example @@ -212,6 +212,15 @@ H5_PUBLIC_BASE_URL=http://127.0.0.1:5173 # H5_SCHEDULED_TASK_SCAN_INTERVAL_MS=30000 # H5_SCHEDULED_TASK_MAX_ATTEMPTS=3 # H5_SCHEDULED_TASK_EXECUTION_TIMEOUT_MS=900000 +# 新闻早报 · 微信草稿箱自动 worker(默认跟随 H5_REMINDER_WORKER_ENABLED) +# H5_WECHAT_NEWS_MORNING_DRAFT_ENABLED=1 +# H5_WECHAT_NEWS_MORNING_DRAFT_AUTO=1 +# H5_WECHAT_NEWS_MORNING_DRAFT_WORKER_ENABLED=1 +# H5_WECHAT_NEWS_MORNING_DRAFT_USER_ID= +# H5_WECHAT_NEWS_MORNING_DRAFT_HOUR=6 +# H5_WECHAT_NEWS_MORNING_DRAFT_MINUTE=0 +# H5_WECHAT_NEWS_MORNING_DRAFT_AUTO_GENERATE=1 +# H5_WECHAT_NEWS_MORNING_DRAFT_GENERATE_LEAD_MINUTES=60 # Intent Transaction Layer(Phase A:Draft → Confirm → Commit;默认关闭) # H5_INTENT_TRANSACTION_ENABLED=1 diff --git a/package.json b/package.json index 480c651..6bc24b0 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "verify:experience-agent-run-local": "node scripts/verify-experience-agent-run-local.mjs", "verify:experience-reflect-local": "node scripts/verify-experience-reflect-local.mjs", "test:scenario:john4-diet": "node scripts/run-scenario-test.mjs --scenario john4-children-hobby-diet-update", - "test": "node --test api-core-retry.test.mjs 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-voice-reco.test.mjs wechat-mp.test.mjs wechat-media.test.mjs wechat/image-generation-policy.test.mjs wechat/verify/generated-thumbnail.test.mjs schedule-intent.test.mjs schedule-reminder-worker.test.mjs scheduled-task-intent.test.mjs scheduled-task-service.test.mjs scheduled-task-executor.test.mjs scheduled-task-worker.test.mjs scheduled-task-worker-config.test.mjs wechat/handlers/scheduled-task.test.mjs capabilities.test.mjs policies.test.mjs server/portal-api-auth-middleware.test.mjs server/portal-config-routes.test.mjs server/portal-plaza-discovery-routes.test.mjs server/portal-runtime-routes.test.mjs server/portal-gateway-services-bootstrap.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 skill-runtime-policy.test.mjs excel-analyst.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 services/orchestrator/admin-config.test.mjs services/orchestrator/contracts.test.mjs services/orchestrator/checkpoint.test.mjs services/orchestrator/runtime.test.mjs services/orchestrator/app.test.mjs services/orchestrator/server.test.mjs services/orchestrator/shadow-dispatcher.test.mjs services/orchestrator/shadow-observer.test.mjs services/orchestrator/observability.test.mjs services/orchestrator/executor-gateway.test.mjs services/orchestrator/executor-job-store.test.mjs image-make-admin-config.test.mjs asset-gateway.test.mjs image-make-client.test.mjs mindspace-image-generation.test.mjs mindspace-image-generation-routes.test.mjs mindspace-image-review.test.mjs mindspace-run-public-html-scope.test.mjs direct-chat-service.test.mjs tool-gateway.test.mjs mindspace.test.mjs health-channel-state.test.mjs health-intent-rules.test.mjs health-extraction.test.mjs health-observation-validate.test.mjs health-baseline-maturity.test.mjs health-publish-guard.test.mjs health-p0-experiment.test.mjs health-wechat-channel.test.mjs server/portal-health-routes.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-long-image.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-save-service.test.mjs mindspace-chat-docx-package.test.mjs mindspace-public-finish-sync.test.mjs mindspace-wechat-html-delivery.test.mjs mindspace-wechat-mp-config.test.mjs mindspace-wechat-page-draft.test.mjs server/portal-mindspace-wechat-routes.test.mjs mindspace-chat-context.test.mjs mindspace-canonical-url.test.mjs mindspace-conversation-package.test.mjs mindspace-conversation-package-artifact-service.test.mjs mindspace-conversation-package-audit.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-workspace-publication-delivery-service.test.mjs mindspace-workspace-tool-service.test.mjs mindspace-mcp-scoped-token.test.mjs mindspace-asset-preview.test.mjs mindspace-agent-jobs.test.mjs mindspace-agent-runner.test.mjs mindspace-sandbox-mcp.test.mjs mindspace-userdata-postgres.test.mjs postgres-user-data-space-service.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": "node --test api-core-retry.test.mjs 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-voice-reco.test.mjs wechat-mp.test.mjs wechat-media.test.mjs wechat/image-generation-policy.test.mjs wechat/verify/generated-thumbnail.test.mjs schedule-intent.test.mjs schedule-reminder-worker.test.mjs scheduled-task-intent.test.mjs scheduled-task-service.test.mjs scheduled-task-executor.test.mjs scheduled-task-worker.test.mjs scheduled-task-worker-config.test.mjs wechat-news-morning-draft.test.mjs wechat-news-morning-draft-worker.test.mjs wechat-news-morning-draft-worker-config.test.mjs wechat/handlers/scheduled-task.test.mjs capabilities.test.mjs policies.test.mjs server/portal-api-auth-middleware.test.mjs server/portal-config-routes.test.mjs server/portal-plaza-discovery-routes.test.mjs server/portal-runtime-routes.test.mjs server/portal-gateway-services-bootstrap.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 skill-runtime-policy.test.mjs excel-analyst.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 services/orchestrator/admin-config.test.mjs services/orchestrator/contracts.test.mjs services/orchestrator/checkpoint.test.mjs services/orchestrator/runtime.test.mjs services/orchestrator/app.test.mjs services/orchestrator/server.test.mjs services/orchestrator/shadow-dispatcher.test.mjs services/orchestrator/shadow-observer.test.mjs services/orchestrator/observability.test.mjs services/orchestrator/executor-gateway.test.mjs services/orchestrator/executor-job-store.test.mjs image-make-admin-config.test.mjs asset-gateway.test.mjs image-make-client.test.mjs mindspace-image-generation.test.mjs mindspace-image-generation-routes.test.mjs mindspace-image-review.test.mjs mindspace-run-public-html-scope.test.mjs direct-chat-service.test.mjs tool-gateway.test.mjs mindspace.test.mjs health-channel-state.test.mjs health-intent-rules.test.mjs health-extraction.test.mjs health-observation-validate.test.mjs health-baseline-maturity.test.mjs health-publish-guard.test.mjs health-p0-experiment.test.mjs health-wechat-channel.test.mjs server/portal-health-routes.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-long-image.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-save-service.test.mjs mindspace-chat-docx-package.test.mjs mindspace-public-finish-sync.test.mjs mindspace-wechat-html-delivery.test.mjs mindspace-wechat-mp-config.test.mjs mindspace-wechat-page-draft.test.mjs server/portal-mindspace-wechat-routes.test.mjs mindspace-chat-context.test.mjs mindspace-canonical-url.test.mjs mindspace-conversation-package.test.mjs mindspace-conversation-package-artifact-service.test.mjs mindspace-conversation-package-audit.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-workspace-publication-delivery-service.test.mjs mindspace-workspace-tool-service.test.mjs mindspace-mcp-scoped-token.test.mjs mindspace-asset-preview.test.mjs mindspace-agent-jobs.test.mjs mindspace-agent-runner.test.mjs mindspace-sandbox-mcp.test.mjs mindspace-userdata-postgres.test.mjs postgres-user-data-space-service.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:episodic-memory": "node --test episodic-memory.test.mjs direct-chat-service.test.mjs chat-intent-router.test.mjs", "test:deep-search": "node --test deep-search.test.mjs mindsearch.test.mjs", "test:image-review": "node --test mindspace-image-review.test.mjs mindspace-image-generation.test.mjs", diff --git a/server/portal-integration-services-bootstrap.mjs b/server/portal-integration-services-bootstrap.mjs index 7c57da2..c388285 100644 --- a/server/portal-integration-services-bootstrap.mjs +++ b/server/portal-integration-services-bootstrap.mjs @@ -10,6 +10,8 @@ import { startScheduleReminderWorker } from '../schedule-reminder-worker.mjs'; import { startScheduledTaskWorker } from '../scheduled-task-worker.mjs'; import { isScheduledTaskWorkerEnabled } from '../scheduled-task-worker-config.mjs'; import { startHealthBaselineWorker } from '../health-baseline-worker.mjs'; +import { startWechatNewsMorningDraftWorker } from '../wechat-news-morning-draft-worker.mjs'; +import { createWechatNewsMorningDraftService } from '../wechat-news-morning-draft.mjs'; import { createHealthEventNotificationService } from '../health-event-notification-service.mjs'; import { isPassiveCanaryRuntime } from './portal-runtime-role.mjs'; import { loadWechatMpModule } from '../wechat-mp-loader.mjs'; @@ -72,6 +74,10 @@ export async function bootstrapPortalIntegrationServices({ startScheduledTaskWorker, startHealthBaselineWorkerFn = startHealthBaselineWorker, + startWechatNewsMorningDraftWorkerFn = + startWechatNewsMorningDraftWorker, + createWechatNewsMorningDraftServiceFn = + createWechatNewsMorningDraftService, createPageEditSessionServiceFn = createPageEditSessionService, createToolGatewayFn = createToolGateway, @@ -337,6 +343,38 @@ export async function bootstrapPortalIntegrationServices({ ); } + const wechatNewsMorningDraftService = createWechatNewsMorningDraftServiceFn(pool, { + mpConfig: wechatMpConfig, + h5Root, + memindLibRoot: codeRoot || h5Root, + env, + }); + + let wechatNewsMorningDraftWorker = null; + if ( + !isPassiveCanaryRuntime(env) + && wechatMpConfig?.enabled + && userAuth + && tkmindProxy + ) { + wechatNewsMorningDraftWorker = startWechatNewsMorningDraftWorkerFn({ + wechatNewsMorningDraftService, + mpConfig: wechatMpConfig, + userAuth, + tkmindProxy, + agentRunGateway, + cursorExecutorPolicyService: wechatCursorExecutorPolicyService, + sessionSnapshotService, + pool, + h5Root, + env, + logger, + }); + if (wechatNewsMorningDraftWorker?.runOnce) { + logger.log?.('WeChat news morning draft worker enabled'); + } + } + let healthBaselineWorker = null; if (!isPassiveCanaryRuntime(env) && healthDataRuntime) { const healthEventNotificationService = createHealthEventNotificationService({ @@ -414,6 +452,8 @@ export async function bootstrapPortalIntegrationServices({ notificationDispatcher, scheduleReminderWorker, scheduledTaskWorker, + wechatNewsMorningDraftService, + wechatNewsMorningDraftWorker, healthBaselineWorker, subscriptionExpiryTimer, mindSpacePageEditSession, diff --git a/server/portal-integration-services-bootstrap.test.mjs b/server/portal-integration-services-bootstrap.test.mjs index 6774a60..a998ca2 100644 --- a/server/portal-integration-services-bootstrap.test.mjs +++ b/server/portal-integration-services-bootstrap.test.mjs @@ -173,6 +173,14 @@ function createSetup(overrides = {}) { reminderOptions = receivedOptions; return { id: 'reminder-worker' }; }, + createWechatNewsMorningDraftServiceFn(pool, options) { + calls.push(['news-morning-draft-service', pool, options]); + return { id: 'news-morning-draft-service' }; + }, + startWechatNewsMorningDraftWorkerFn(receivedOptions) { + calls.push(['news-morning-draft-worker']); + return { id: 'news-morning-draft-worker', runOnce: async () => {} }; + }, createPageEditSessionServiceFn(receivedOptions) { calls.push(['page-edit']); pageEditOptions = receivedOptions; @@ -449,6 +457,9 @@ test('preserves notification, recharge, reminder, and page-edit wiring', async ( setup.notificationDispatcher, }); assert.equal(result.scheduleReminderWorker.id, 'reminder-worker'); + assert.equal(result.wechatNewsMorningDraftWorker.id, 'news-morning-draft-worker'); + assert.ok(setup.calls.some(([name]) => name === 'news-morning-draft-service')); + assert.ok(setup.calls.some(([name]) => name === 'news-morning-draft-worker')); assert.deepEqual(captured.pageEditOptions, { apiTarget: 'http://api', apiSecret: 'secret', @@ -498,6 +509,7 @@ test('preserves subscription timer work, logs, and unref', async () => { test('keeps optional integrations disabled and contains timer failures', async () => { const setup = createSetup({ env: {}, + wechatMpConfig: { enabled: false }, mindSpacePublicFinish: null, scheduleService: null, subscriptionService: { @@ -546,6 +558,7 @@ test('keeps optional integrations disabled and contains timer failures', async ( null, ); assert.equal(result.scheduleReminderWorker, null); + assert.equal(result.wechatNewsMorningDraftWorker, null); assert.equal( captured.notificationOptions.sendWechatTextToUser, null, @@ -575,6 +588,7 @@ test('passive candidate runtime disables singleton reminder and subscription tim const captured = setup.getCaptured(); assert.equal(result.scheduleReminderWorker, null); + assert.equal(result.wechatNewsMorningDraftWorker, null); assert.equal(result.subscriptionExpiryTimer, null); assert.equal(captured.reminderOptions, undefined); assert.equal(captured.timerCallback, undefined); diff --git a/wechat-news-morning-draft-worker-config.mjs b/wechat-news-morning-draft-worker-config.mjs new file mode 100644 index 0000000..e6f9e3a --- /dev/null +++ b/wechat-news-morning-draft-worker-config.mjs @@ -0,0 +1,26 @@ +/** + * News morning WeChat draft auto worker enablement. + * Default follows H5_REMINDER_WORKER_ENABLED when unset. + */ +export function isWechatNewsMorningDraftWorkerEnabled(env = process.env) { + const explicit = String(env.H5_WECHAT_NEWS_MORNING_DRAFT_WORKER_ENABLED ?? '').trim(); + if (explicit === '1' || explicit === 'true') return true; + if (explicit === '0' || explicit === 'false') return false; + return env.H5_REMINDER_WORKER_ENABLED === '1'; +} + +export function wechatNewsMorningDraftWorkerIntervalMs(env = process.env) { + const raw = Number(env.H5_WECHAT_NEWS_MORNING_DRAFT_WORKER_INTERVAL_MS ?? 30_000); + if (!Number.isFinite(raw) || raw < 5_000) return 30_000; + return raw; +} + +export function wechatNewsMorningDraftExecutionTimeoutMs(env = process.env) { + const raw = Number( + env.H5_WECHAT_NEWS_MORNING_DRAFT_GENERATION_TIMEOUT_MS + ?? env.H5_SCHEDULED_TASK_EXECUTION_TIMEOUT_MS + ?? 15 * 60 * 1000, + ); + if (!Number.isFinite(raw) || raw < 60_000) return 15 * 60 * 1000; + return raw; +} diff --git a/wechat-news-morning-draft-worker-config.test.mjs b/wechat-news-morning-draft-worker-config.test.mjs new file mode 100644 index 0000000..1c7feff --- /dev/null +++ b/wechat-news-morning-draft-worker-config.test.mjs @@ -0,0 +1,34 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + isWechatNewsMorningDraftWorkerEnabled, + wechatNewsMorningDraftWorkerIntervalMs, +} from './wechat-news-morning-draft-worker-config.mjs'; + +test('news morning draft worker follows reminder worker by default', () => { + assert.equal( + isWechatNewsMorningDraftWorkerEnabled({ H5_REMINDER_WORKER_ENABLED: '1' }), + true, + ); + assert.equal( + isWechatNewsMorningDraftWorkerEnabled({ H5_REMINDER_WORKER_ENABLED: '0' }), + false, + ); + assert.equal( + isWechatNewsMorningDraftWorkerEnabled({ + H5_WECHAT_NEWS_MORNING_DRAFT_WORKER_ENABLED: '1', + H5_REMINDER_WORKER_ENABLED: '0', + }), + true, + ); +}); + +test('news morning draft worker interval has sane floor', () => { + assert.equal(wechatNewsMorningDraftWorkerIntervalMs({}), 30_000); + assert.equal( + wechatNewsMorningDraftWorkerIntervalMs({ + H5_WECHAT_NEWS_MORNING_DRAFT_WORKER_INTERVAL_MS: '1000', + }), + 30_000, + ); +}); diff --git a/wechat-news-morning-draft-worker.mjs b/wechat-news-morning-draft-worker.mjs new file mode 100644 index 0000000..d0d8616 --- /dev/null +++ b/wechat-news-morning-draft-worker.mjs @@ -0,0 +1,190 @@ +import { executeScheduledTask } from './scheduled-task-executor.mjs'; +import { localDateKey } from './schedule-time.mjs'; +import { + buildNewsMorningAutoGenerationTask, + findLatestNewsMorningPage, + isLocalScheduleDue, + isNewsMorningPageForToday, + isWithinNewsMorningGenerateLeadWindow, +} from './wechat-news-morning-draft.mjs'; +import { + isWechatNewsMorningDraftWorkerEnabled, + wechatNewsMorningDraftExecutionTimeoutMs, + wechatNewsMorningDraftWorkerIntervalMs, +} from './wechat-news-morning-draft-worker-config.mjs'; +import { isPassiveCanaryRuntime } from './server/portal-runtime-role.mjs'; + +function resolveTodayPage(config, h5Root, now = Date.now()) { + const page = findLatestNewsMorningPage({ + h5Root, + userId: config.sourceUserId, + slugPattern: config.pageSlugPattern, + date: new Date(now), + timezone: config.timezone, + }); + if (!page || !isNewsMorningPageForToday(page, { date: new Date(now), timezone: config.timezone })) { + return null; + } + return page; +} + +export function startWechatNewsMorningDraftWorker({ + wechatNewsMorningDraftService = null, + mpConfig = null, + userAuth = null, + tkmindProxy = null, + agentRunGateway = null, + cursorExecutorPolicyService = null, + sessionSnapshotService = null, + pool = null, + h5Root = null, + env = process.env, + executeTask = executeScheduledTask, + logger = console, + intervalMs = wechatNewsMorningDraftWorkerIntervalMs(env), + executionTimeoutMs = wechatNewsMorningDraftExecutionTimeoutMs(env), + runOnStart = true, + setIntervalFn = setInterval, +} = {}) { + if ( + isPassiveCanaryRuntime(env) + || !isWechatNewsMorningDraftWorkerEnabled(env) + || !wechatNewsMorningDraftService + || !mpConfig?.enabled + || !userAuth + || !tkmindProxy + || !h5Root + ) { + return { stop() {} }; + } + + let stopped = false; + let running = false; + let generationInFlightDateKey = null; + + const ensureTodayPage = async (config, dateKey) => { + if (generationInFlightDateKey === dateKey) return null; + generationInFlightDateKey = dateKey; + try { + const task = buildNewsMorningAutoGenerationTask(config); + await executeTask(task, { + userAuth, + tkmindProxy, + agentRunGateway, + cursorExecutorPolicyService, + sessionSnapshotService, + pool, + h5Root, + timeoutMs: executionTimeoutMs, + logger, + }); + const page = resolveTodayPage(config, h5Root); + if (!page) { + throw new Error('新闻早报页面生成未完成'); + } + await wechatNewsMorningDraftService.recordAutoGenerationRun({ + status: 'success', + pageSlug: page.slug, + }); + logger.log?.('[NewsMorningDraft] auto-generated today page', { + slug: page.slug, + userId: config.sourceUserId, + }); + return page; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + await wechatNewsMorningDraftService.recordAutoGenerationRun({ + status: 'failed', + errorMessage: message, + }).catch(() => {}); + logger.warn?.('[NewsMorningDraft] auto-generate failed:', message); + throw error; + } finally { + if (generationInFlightDateKey === dateKey) { + generationInFlightDateKey = null; + } + } + }; + + const runOnce = async () => { + if (running || stopped) return; + running = true; + try { + const config = await wechatNewsMorningDraftService.getConfig(); + if (!config.enabled || !config.autoPushEnabled || !config.sourceUserId) return; + + const now = Date.now(); + const timezone = config.timezone; + const dateKey = localDateKey(now, timezone); + const toleranceMinutes = Math.max(1, Math.ceil(Number(intervalMs) / 60_000)); + let todayPage = resolveTodayPage(config, h5Root, now); + + const shouldGenerate = + config.autoGenerateEnabled !== false + && !todayPage + && ( + isWithinNewsMorningGenerateLeadWindow(config, now) + || isLocalScheduleDue( + { hour: config.pushHour, minute: config.pushMinute, timezone }, + now, + toleranceMinutes + 120, + ) + ); + + if (shouldGenerate && generationInFlightDateKey !== dateKey) { + try { + todayPage = await ensureTodayPage(config, dateKey); + } catch { + // keep trying during generate window on later scans + } + } + + if ( + !isLocalScheduleDue( + { hour: config.pushHour, minute: config.pushMinute, timezone }, + now, + toleranceMinutes, + ) + ) { + return; + } + + if (await wechatNewsMorningDraftService.hasSuccessfulAutoPushToday({ now })) { + return; + } + + todayPage = todayPage ?? resolveTodayPage(config, h5Root, now); + if (!todayPage) { + logger.warn?.('[NewsMorningDraft] skip auto push: today page missing', { + userId: config.sourceUserId, + dateKey, + }); + return; + } + + const result = await wechatNewsMorningDraftService.pushDraft({ + triggeredBy: 'auto-push', + }); + logger.log?.('[NewsMorningDraft] auto push succeeded', { + draftMediaId: result.draftMediaId, + pageSlug: result.preview?.page?.slug ?? todayPage.slug, + }); + } catch (error) { + logger.warn?.('[NewsMorningDraft] auto worker failed:', error); + } finally { + running = false; + } + }; + + const timer = setIntervalFn(runOnce, Math.max(5_000, Number(intervalMs) || 30_000)); + timer.unref?.(); + if (runOnStart) void runOnce(); + + return { + runOnce, + stop() { + stopped = true; + clearInterval(timer); + }, + }; +} diff --git a/wechat-news-morning-draft-worker.test.mjs b/wechat-news-morning-draft-worker.test.mjs new file mode 100644 index 0000000..9372315 --- /dev/null +++ b/wechat-news-morning-draft-worker.test.mjs @@ -0,0 +1,141 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; +import { PUBLIC_ZONE_DIR, PUBLISH_ROOT_DIR } from './user-publish.mjs'; +import { startWechatNewsMorningDraftWorker } from './wechat-news-morning-draft-worker.mjs'; +import { + createWechatNewsMorningDraftService, + wechatNewsMorningDraftInternals, +} from './wechat-news-morning-draft.mjs'; + +const SAMPLE_HTML = `