target-supabase-sdk 0.1.9 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +184 -1
- package/dist/browser.d.ts +60 -0
- package/dist/browser.d.ts.map +1 -0
- package/dist/browser.js +6 -0
- package/dist/browser.js.map +1 -0
- package/dist/chunks/create-logger-DyH3mZ8p.js +1028 -0
- package/dist/chunks/create-logger-DyH3mZ8p.js.map +1 -0
- package/dist/chunks/enable-log-persist-C_bSw6Kx.js +1362 -0
- package/dist/chunks/enable-log-persist-C_bSw6Kx.js.map +1 -0
- package/dist/chunks/word.interface-DLgRobKV.js +1973 -0
- package/dist/chunks/word.interface-DLgRobKV.js.map +1 -0
- package/dist/command/command.api.d.ts +1 -1
- package/dist/command/command.api.d.ts.map +1 -1
- package/dist/command/command.interface.d.ts +1 -1
- package/dist/command/command.interface.d.ts.map +1 -1
- package/dist/core.api.d.ts +10 -6
- package/dist/core.api.d.ts.map +1 -1
- package/dist/core.interface.d.ts +3 -2
- package/dist/core.interface.d.ts.map +1 -1
- package/dist/core.utils.d.ts +1 -1
- package/dist/core.utils.d.ts.map +1 -1
- package/dist/idea/idea.interface.d.ts +1 -1
- package/dist/idea/idea.interface.d.ts.map +1 -1
- package/dist/index.d.ts +2 -48
- package/dist/index.d.ts.map +1 -1
- package/dist/link/link.api.d.ts +1 -1
- package/dist/link/link.api.d.ts.map +1 -1
- package/dist/link/link.interface.d.ts +3 -3
- package/dist/link/link.interface.d.ts.map +1 -1
- package/dist/list/list.api.d.ts +3 -1
- package/dist/list/list.api.d.ts.map +1 -1
- package/dist/list/list.interface.d.ts +2 -2
- package/dist/list/list.interface.d.ts.map +1 -1
- package/dist/node/env/env-parsers.d.ts +19 -0
- package/dist/node/env/env-parsers.d.ts.map +1 -0
- package/dist/node/env/index.d.ts +11 -0
- package/dist/node/env/index.d.ts.map +1 -0
- package/dist/node/env/init-supabase-from-env.d.ts +17 -0
- package/dist/node/env/init-supabase-from-env.d.ts.map +1 -0
- package/dist/node/env/load-env.d.ts +20 -0
- package/dist/node/env/load-env.d.ts.map +1 -0
- package/dist/node/env/project-root.d.ts +11 -0
- package/dist/node/env/project-root.d.ts.map +1 -0
- package/dist/node/env/public-base-url.d.ts +10 -0
- package/dist/node/env/public-base-url.d.ts.map +1 -0
- package/dist/node/env/require-env.d.ts +3 -0
- package/dist/node/env/require-env.d.ts.map +1 -0
- package/dist/node/fs/json-state-store.d.ts +19 -0
- package/dist/node/fs/json-state-store.d.ts.map +1 -0
- package/dist/node/fs/read-source-file.d.ts +19 -0
- package/dist/node/fs/read-source-file.d.ts.map +1 -0
- package/dist/node/node-liveness.d.ts +30 -0
- package/dist/node/node-liveness.d.ts.map +1 -0
- package/dist/node/node-runtime.base.d.ts +60 -0
- package/dist/node/node-runtime.base.d.ts.map +1 -0
- package/dist/node/node.api.d.ts +4 -4
- package/dist/node/node.api.d.ts.map +1 -1
- package/dist/node/node.interface.d.ts +1 -6
- package/dist/node/node.interface.d.ts.map +1 -1
- package/dist/node/preload/run-service-preload.d.ts +14 -0
- package/dist/node/preload/run-service-preload.d.ts.map +1 -0
- package/dist/node/preload/service-preload.interface.d.ts +15 -0
- package/dist/node/preload/service-preload.interface.d.ts.map +1 -0
- package/dist/node/process/index.d.ts +5 -0
- package/dist/node/process/index.d.ts.map +1 -0
- package/dist/node/process/managed-child-processes.d.ts +37 -0
- package/dist/node/process/managed-child-processes.d.ts.map +1 -0
- package/dist/node/process/process-spawn.d.ts +27 -0
- package/dist/node/process/process-spawn.d.ts.map +1 -0
- package/dist/node/readiness/index.d.ts +9 -0
- package/dist/node/readiness/index.d.ts.map +1 -0
- package/dist/node/readiness/poll-until.d.ts +11 -0
- package/dist/node/readiness/poll-until.d.ts.map +1 -0
- package/dist/node/readiness/readiness-checks.d.ts +22 -0
- package/dist/node/readiness/readiness-checks.d.ts.map +1 -0
- package/dist/node/readiness/readiness.types.d.ts +12 -0
- package/dist/node/readiness/readiness.types.d.ts.map +1 -0
- package/dist/node/readiness/run-readiness-checks.d.ts +4 -0
- package/dist/node/readiness/run-readiness-checks.d.ts.map +1 -0
- package/dist/node/readiness/service-ready-gate.d.ts +26 -0
- package/dist/node/readiness/service-ready-gate.d.ts.map +1 -0
- package/dist/node.d.ts +36 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +2483 -0
- package/dist/node.js.map +1 -0
- package/dist/parcel/chunk-fetch-registry.d.ts +14 -0
- package/dist/parcel/chunk-fetch-registry.d.ts.map +1 -0
- package/dist/parcel/chunk-url.utils.d.ts +9 -0
- package/dist/parcel/chunk-url.utils.d.ts.map +1 -0
- package/dist/parcel/parcel-manager.d.ts +44 -20
- package/dist/parcel/parcel-manager.d.ts.map +1 -1
- package/dist/parcel/parcel.api.d.ts +306 -0
- package/dist/parcel/parcel.api.d.ts.map +1 -0
- package/dist/parcel/parcel.interface.d.ts +26 -5
- package/dist/parcel/parcel.interface.d.ts.map +1 -1
- package/dist/parcel/parcel.service.d.ts +38 -0
- package/dist/parcel/parcel.service.d.ts.map +1 -0
- package/dist/parcel/storage-provider-registry.d.ts +17 -0
- package/dist/parcel/storage-provider-registry.d.ts.map +1 -0
- package/dist/parcel/storage-provider.types.d.ts +31 -0
- package/dist/parcel/storage-provider.types.d.ts.map +1 -0
- package/dist/preload.d.ts +7 -0
- package/dist/preload.d.ts.map +1 -0
- package/dist/preload.js +47 -0
- package/dist/preload.js.map +1 -0
- package/dist/repo/repo-context.utils.d.ts.map +1 -1
- package/dist/repo/repo-manager.d.ts +14 -3
- package/dist/repo/repo-manager.d.ts.map +1 -1
- package/dist/repo/repo.api.d.ts +1 -1
- package/dist/repo/repo.api.d.ts.map +1 -1
- package/dist/repo/repo.interface.d.ts +1 -1
- package/dist/repo/repo.interface.d.ts.map +1 -1
- package/dist/repo/repo.script-loader.d.ts +5 -5
- package/dist/repo/repo.script-loader.d.ts.map +1 -1
- package/dist/service/base.interface.d.ts +54 -57
- package/dist/service/base.interface.d.ts.map +1 -1
- package/dist/service/index.d.ts +21 -0
- package/dist/service/index.d.ts.map +1 -0
- package/dist/service/service.api.d.ts +503 -0
- package/dist/service/service.api.d.ts.map +1 -0
- package/dist/service/service.interface.d.ts +36 -9
- package/dist/service/service.interface.d.ts.map +1 -1
- package/dist/shared/http/fetch-retry.d.ts +21 -0
- package/dist/shared/http/fetch-retry.d.ts.map +1 -0
- package/dist/shared/log/create-logger.d.ts +27 -0
- package/dist/shared/log/create-logger.d.ts.map +1 -0
- package/dist/shared/log/enable-log-persist.d.ts +28 -0
- package/dist/shared/log/enable-log-persist.d.ts.map +1 -0
- package/dist/shared/log/index.d.ts +10 -0
- package/dist/shared/log/index.d.ts.map +1 -0
- package/dist/shared/log/log-manager.d.ts +37 -60
- package/dist/shared/log/log-manager.d.ts.map +1 -1
- package/dist/shared/log/log-persist-batch-id.d.ts +13 -0
- package/dist/shared/log/log-persist-batch-id.d.ts.map +1 -0
- package/dist/shared/log/log-persist-flush.d.ts +8 -0
- package/dist/shared/log/log-persist-flush.d.ts.map +1 -0
- package/dist/shared/log/log-persist-hook.d.ts +6 -0
- package/dist/shared/log/log-persist-hook.d.ts.map +1 -0
- package/dist/shared/log/log-persist-logger.d.ts +15 -0
- package/dist/shared/log/log-persist-logger.d.ts.map +1 -0
- package/dist/shared/log/log-persist-registry.d.ts +17 -0
- package/dist/shared/log/log-persist-registry.d.ts.map +1 -0
- package/dist/shared/log/log-persist.config.d.ts +10 -0
- package/dist/shared/log/log-persist.config.d.ts.map +1 -0
- package/dist/shared/log/log-persist.d.ts +55 -0
- package/dist/shared/log/log-persist.d.ts.map +1 -0
- package/dist/shared/log/log-persist.interface.d.ts +116 -0
- package/dist/shared/log/log-persist.interface.d.ts.map +1 -0
- package/dist/shared/log/log-scope.d.ts +56 -0
- package/dist/shared/log/log-scope.d.ts.map +1 -0
- package/dist/shared/utils/fetch-url.d.ts +3 -0
- package/dist/shared/utils/fetch-url.d.ts.map +1 -0
- package/dist/shared/utils/network-error.d.ts +14 -0
- package/dist/shared/utils/network-error.d.ts.map +1 -0
- package/dist/shared/utils/safe-file-name.d.ts +14 -0
- package/dist/shared/utils/safe-file-name.d.ts.map +1 -0
- package/dist/shared/utils/sha256.d.ts +3 -0
- package/dist/shared/utils/sha256.d.ts.map +1 -0
- package/dist/supabase.d.ts +7 -2
- package/dist/supabase.d.ts.map +1 -1
- package/dist/task/index.d.ts +16 -0
- package/dist/task/index.d.ts.map +1 -0
- package/dist/task/local-task-registry.d.ts +11 -1
- package/dist/task/local-task-registry.d.ts.map +1 -1
- package/dist/task/task-manager.d.ts +21 -11
- package/dist/task/task-manager.d.ts.map +1 -1
- package/dist/task/task-node.d.ts +19 -0
- package/dist/task/task-node.d.ts.map +1 -0
- package/dist/task/task-post-validated.api.d.ts +19 -0
- package/dist/task/task-post-validated.api.d.ts.map +1 -0
- package/dist/task/task-post.api.d.ts +54 -0
- package/dist/task/task-post.api.d.ts.map +1 -0
- package/dist/task/task-queue.d.ts +7 -0
- package/dist/task/task-queue.d.ts.map +1 -0
- package/dist/task/task-repo-context.d.ts +2 -0
- package/dist/task/task-repo-context.d.ts.map +1 -1
- package/dist/task/task-repo-validation.d.ts +46 -0
- package/dist/task/task-repo-validation.d.ts.map +1 -0
- package/dist/task/task.api.d.ts +5 -47
- package/dist/task/task.api.d.ts.map +1 -1
- package/dist/task/task.constant.d.ts +4 -0
- package/dist/task/task.constant.d.ts.map +1 -0
- package/dist/task/task.interface.d.ts +3 -1
- package/dist/task/task.interface.d.ts.map +1 -1
- package/dist/trigger/trigger-manager.d.ts +25 -0
- package/dist/trigger/trigger-manager.d.ts.map +1 -0
- package/dist/trigger/trigger-node.d.ts +23 -0
- package/dist/trigger/trigger-node.d.ts.map +1 -0
- package/dist/trigger/trigger.api.d.ts +140 -0
- package/dist/trigger/trigger.api.d.ts.map +1 -0
- package/dist/trigger/trigger.constant.d.ts +8 -0
- package/dist/trigger/trigger.constant.d.ts.map +1 -0
- package/dist/trigger/trigger.interface.d.ts +87 -0
- package/dist/trigger/trigger.interface.d.ts.map +1 -0
- package/dist/trigger/trigger.utils.d.ts +6 -0
- package/dist/trigger/trigger.utils.d.ts.map +1 -0
- package/dist/word/word.api.d.ts +1 -1
- package/dist/word/word.api.d.ts.map +1 -1
- package/dist/word/word.interface.d.ts +2 -2
- package/dist/word/word.interface.d.ts.map +1 -1
- package/package.json +91 -61
- package/src/browser.ts +165 -0
- package/src/command/command.api.ts +20 -18
- package/src/command/command.interface.ts +2 -3
- package/src/core.api.ts +466 -477
- package/src/core.interface.ts +63 -62
- package/src/core.utils.ts +64 -64
- package/src/idea/idea.interface.ts +3 -3
- package/src/index.ts +2 -79
- package/src/link/link.api.ts +28 -30
- package/src/link/link.interface.ts +4 -5
- package/src/list/list.api.ts +50 -29
- package/src/list/list.interface.ts +3 -4
- package/src/node/env/env-parsers.ts +77 -0
- package/src/node/env/index.ts +10 -0
- package/src/node/env/init-supabase-from-env.ts +38 -0
- package/src/node/env/load-env.ts +67 -0
- package/src/node/env/project-root.ts +34 -0
- package/src/node/env/public-base-url.ts +18 -0
- package/src/node/env/require-env.ts +15 -0
- package/src/node/fs/json-state-store.ts +97 -0
- package/src/node/fs/read-source-file.ts +75 -0
- package/src/node/node-liveness.ts +80 -0
- package/src/node/node-runtime.base.ts +481 -0
- package/src/node/node.api.ts +12 -11
- package/src/node/node.interface.ts +1 -8
- package/src/node/node.utils.ts +1 -1
- package/src/node/preload/run-service-preload.ts +48 -0
- package/src/node/preload/service-preload.interface.ts +14 -0
- package/src/node/process/index.ts +8 -0
- package/src/node/process/managed-child-processes.ts +121 -0
- package/src/node/process/process-spawn.ts +61 -0
- package/src/node/readiness/index.ts +20 -0
- package/src/node/readiness/poll-until.ts +27 -0
- package/src/node/readiness/readiness-checks.ts +79 -0
- package/src/node/readiness/readiness.types.ts +13 -0
- package/src/node/readiness/run-readiness-checks.ts +20 -0
- package/src/node/readiness/service-ready-gate.ts +65 -0
- package/src/node.ts +153 -0
- package/src/parcel/chunk-fetch-registry.ts +96 -0
- package/src/parcel/chunk-url.utils.ts +30 -0
- package/src/parcel/parcel-manager.ts +431 -173
- package/src/parcel/parcel.api.ts +103 -0
- package/src/parcel/parcel.interface.ts +44 -23
- package/src/parcel/parcel.service.ts +79 -0
- package/src/parcel/storage-provider-registry.ts +49 -0
- package/src/parcel/storage-provider.types.ts +45 -0
- package/src/preload.ts +7 -0
- package/src/repo/repo-context.utils.ts +3 -4
- package/src/repo/repo-manager.ts +54 -39
- package/src/repo/repo.api.ts +5 -5
- package/src/repo/repo.interface.ts +1 -2
- package/src/repo/repo.script-loader.ts +16 -20
- package/src/service/base.interface.ts +81 -90
- package/src/service/index.ts +173 -0
- package/src/service/service.api.ts +294 -0
- package/src/service/service.interface.ts +49 -17
- package/src/shared/http/fetch-retry.ts +145 -0
- package/src/shared/log/create-logger.ts +51 -0
- package/src/shared/log/enable-log-persist.ts +225 -0
- package/src/shared/log/index.ts +31 -0
- package/src/shared/log/log-manager.ts +112 -121
- package/src/shared/log/log-persist-batch-id.ts +43 -0
- package/src/shared/log/log-persist-flush.ts +128 -0
- package/src/shared/log/log-persist-hook.ts +13 -0
- package/src/shared/log/log-persist-logger.ts +33 -0
- package/src/shared/log/log-persist-registry.ts +109 -0
- package/src/shared/log/log-persist.config.ts +97 -0
- package/src/shared/log/log-persist.interface.ts +132 -0
- package/src/shared/log/log-persist.ts +704 -0
- package/src/shared/log/log-scope.ts +123 -0
- package/src/shared/utils/config-path.utils.ts +1 -1
- package/src/shared/utils/fetch-url.ts +13 -0
- package/src/shared/utils/network-error.ts +152 -0
- package/src/shared/utils/safe-file-name.ts +37 -0
- package/src/shared/utils/sha256.ts +15 -0
- package/src/supabase.ts +125 -119
- package/src/task/index.ts +45 -0
- package/src/task/local-task-registry.ts +115 -36
- package/src/task/task-manager.ts +101 -113
- package/src/task/task-node.ts +302 -0
- package/src/task/task-post-validated.api.ts +47 -0
- package/src/task/task-post.api.ts +99 -0
- package/src/task/task-queue.ts +18 -0
- package/src/task/task-repo-context.ts +4 -1
- package/src/task/task-repo-validation.ts +169 -0
- package/src/task/task.api.ts +68 -92
- package/src/task/task.constant.ts +4 -0
- package/src/task/task.interface.ts +6 -7
- package/src/trigger/trigger-manager.ts +287 -0
- package/src/trigger/trigger-node.ts +80 -0
- package/src/trigger/trigger.api.ts +166 -0
- package/src/trigger/trigger.constant.ts +10 -0
- package/src/trigger/trigger.interface.ts +100 -0
- package/src/trigger/trigger.utils.ts +56 -0
- package/src/word/word.api.ts +28 -30
- package/src/word/word.interface.ts +3 -4
- package/dist/auth/auth.api.d.ts +0 -28
- package/dist/auth/auth.api.d.ts.map +0 -1
- package/dist/auth/auth.api.js +0 -51
- package/dist/auth/auth.api.js.map +0 -1
- package/dist/command/command.api.js +0 -58
- package/dist/command/command.api.js.map +0 -1
- package/dist/command/command.interface.js +0 -9
- package/dist/command/command.interface.js.map +0 -1
- package/dist/core.api.js +0 -412
- package/dist/core.api.js.map +0 -1
- package/dist/core.interface.js +0 -46
- package/dist/core.interface.js.map +0 -1
- package/dist/core.utils.js +0 -81
- package/dist/core.utils.js.map +0 -1
- package/dist/feed/feed.interface.d.ts +0 -27
- package/dist/feed/feed.interface.d.ts.map +0 -1
- package/dist/feed/feed.interface.js +0 -17
- package/dist/feed/feed.interface.js.map +0 -1
- package/dist/file/file-manager.d.ts +0 -24
- package/dist/file/file-manager.d.ts.map +0 -1
- package/dist/file/file-manager.js +0 -109
- package/dist/file/file-manager.js.map +0 -1
- package/dist/file/file.api.d.ts +0 -15
- package/dist/file/file.api.d.ts.map +0 -1
- package/dist/file/file.api.js +0 -30
- package/dist/file/file.api.js.map +0 -1
- package/dist/file/file.interface.d.ts +0 -25
- package/dist/file/file.interface.d.ts.map +0 -1
- package/dist/file/file.interface.js +0 -9
- package/dist/file/file.interface.js.map +0 -1
- package/dist/file-list/file-list.api.d.ts +0 -15
- package/dist/file-list/file-list.api.d.ts.map +0 -1
- package/dist/file-list/file-list.api.js +0 -54
- package/dist/file-list/file-list.api.js.map +0 -1
- package/dist/file-list/file-list.interface.d.ts +0 -22
- package/dist/file-list/file-list.interface.d.ts.map +0 -1
- package/dist/file-list/file-list.interface.js +0 -5
- package/dist/file-list/file-list.interface.js.map +0 -1
- package/dist/idea/idea.interface.js +0 -15
- package/dist/idea/idea.interface.js.map +0 -1
- package/dist/index.js +0 -42
- package/dist/index.js.map +0 -1
- package/dist/link/link.api.js +0 -30
- package/dist/link/link.api.js.map +0 -1
- package/dist/link/link.interface.js +0 -7
- package/dist/link/link.interface.js.map +0 -1
- package/dist/list/list.api.js +0 -30
- package/dist/list/list.api.js.map +0 -1
- package/dist/list/list.interface.js +0 -6
- package/dist/list/list.interface.js.map +0 -1
- package/dist/node/node-manager.d.ts +0 -48
- package/dist/node/node-manager.d.ts.map +0 -1
- package/dist/node/node-manager.js +0 -619
- package/dist/node/node-manager.js.map +0 -1
- package/dist/node/node-task.preset.d.ts +0 -1
- package/dist/node/node-task.preset.d.ts.map +0 -1
- package/dist/node/node-task.preset.js +0 -14
- package/dist/node/node-task.preset.js.map +0 -1
- package/dist/node/node.api.js +0 -100
- package/dist/node/node.api.js.map +0 -1
- package/dist/node/node.interface.js +0 -19
- package/dist/node/node.interface.js.map +0 -1
- package/dist/node/node.utils.js +0 -36
- package/dist/node/node.utils.js.map +0 -1
- package/dist/parcel/parcel-manager.js +0 -156
- package/dist/parcel/parcel-manager.js.map +0 -1
- package/dist/parcel/parcel.interface.js +0 -11
- package/dist/parcel/parcel.interface.js.map +0 -1
- package/dist/question-list/question-list.api.d.ts +0 -13
- package/dist/question-list/question-list.api.d.ts.map +0 -1
- package/dist/question-list/question-list.api.js +0 -35
- package/dist/question-list/question-list.api.js.map +0 -1
- package/dist/question-list/question-list.interface.d.ts +0 -18
- package/dist/question-list/question-list.interface.d.ts.map +0 -1
- package/dist/question-list/question-list.interface.js +0 -5
- package/dist/question-list/question-list.interface.js.map +0 -1
- package/dist/repo/repo-context.utils.js +0 -81
- package/dist/repo/repo-context.utils.js.map +0 -1
- package/dist/repo/repo-manager.js +0 -156
- package/dist/repo/repo-manager.js.map +0 -1
- package/dist/repo/repo.api.js +0 -40
- package/dist/repo/repo.api.js.map +0 -1
- package/dist/repo/repo.interface.js +0 -8
- package/dist/repo/repo.interface.js.map +0 -1
- package/dist/repo/repo.script-loader.js +0 -108
- package/dist/repo/repo.script-loader.js.map +0 -1
- package/dist/review/review.api.d.ts +0 -14
- package/dist/review/review.api.d.ts.map +0 -1
- package/dist/review/review.api.js +0 -133
- package/dist/review/review.api.js.map +0 -1
- package/dist/review/review.interface.d.ts +0 -24
- package/dist/review/review.interface.d.ts.map +0 -1
- package/dist/review/review.interface.js +0 -10
- package/dist/review/review.interface.js.map +0 -1
- package/dist/review-v2/apis/post-review-setting.api.d.ts +0 -12
- package/dist/review-v2/apis/post-review-setting.api.d.ts.map +0 -1
- package/dist/review-v2/apis/post-review-setting.api.js +0 -27
- package/dist/review-v2/apis/post-review-setting.api.js.map +0 -1
- package/dist/review-v2/apis/post-review.api.d.ts +0 -13
- package/dist/review-v2/apis/post-review.api.d.ts.map +0 -1
- package/dist/review-v2/apis/post-review.api.js +0 -46
- package/dist/review-v2/apis/post-review.api.js.map +0 -1
- package/dist/review-v2/review-setting.interface.d.ts +0 -23
- package/dist/review-v2/review-setting.interface.d.ts.map +0 -1
- package/dist/review-v2/review-setting.interface.js +0 -5
- package/dist/review-v2/review-setting.interface.js.map +0 -1
- package/dist/review-v2/review.interface.d.ts +0 -20
- package/dist/review-v2/review.interface.d.ts.map +0 -1
- package/dist/review-v2/review.interface.js +0 -5
- package/dist/review-v2/review.interface.js.map +0 -1
- package/dist/service/api.interface.d.ts +0 -35
- package/dist/service/api.interface.d.ts.map +0 -1
- package/dist/service/api.interface.js +0 -12
- package/dist/service/api.interface.js.map +0 -1
- package/dist/service/base.interface.js +0 -7
- package/dist/service/base.interface.js.map +0 -1
- package/dist/service/service.interface.js +0 -11
- package/dist/service/service.interface.js.map +0 -1
- package/dist/shared/log/create-api-logger.d.ts +0 -17
- package/dist/shared/log/create-api-logger.d.ts.map +0 -1
- package/dist/shared/log/create-api-logger.js +0 -15
- package/dist/shared/log/create-api-logger.js.map +0 -1
- package/dist/shared/log/log-manager.js +0 -218
- package/dist/shared/log/log-manager.js.map +0 -1
- package/dist/shared/utils/config-path.utils.js +0 -85
- package/dist/shared/utils/config-path.utils.js.map +0 -1
- package/dist/shared/utils/error.utils.js +0 -15
- package/dist/shared/utils/error.utils.js.map +0 -1
- package/dist/shared/utils/id.utils.js +0 -5
- package/dist/shared/utils/id.utils.js.map +0 -1
- package/dist/supabase.js +0 -100
- package/dist/supabase.js.map +0 -1
- package/dist/task/local-task-registry.js +0 -153
- package/dist/task/local-task-registry.js.map +0 -1
- package/dist/task/task-manager.js +0 -217
- package/dist/task/task-manager.js.map +0 -1
- package/dist/task/task-repo-context.js +0 -12
- package/dist/task/task-repo-context.js.map +0 -1
- package/dist/task/task.api.js +0 -351
- package/dist/task/task.api.js.map +0 -1
- package/dist/task/task.interface.js +0 -36
- package/dist/task/task.interface.js.map +0 -1
- package/dist/task/task.utils.js +0 -2
- package/dist/task/task.utils.js.map +0 -1
- package/dist/word/word.api.js +0 -30
- package/dist/word/word.api.js.map +0 -1
- package/dist/word/word.interface.js +0 -6
- package/dist/word/word.interface.js.map +0 -1
- package/src/auth/auth.api.ts +0 -87
- package/src/feed/feed.interface.ts +0 -33
- package/src/file/file-manager.ts +0 -145
- package/src/file/file.api.ts +0 -43
- package/src/file/file.interface.ts +0 -30
- package/src/file-list/file-list.api.ts +0 -64
- package/src/file-list/file-list.interface.ts +0 -24
- package/src/node/node-manager.ts +0 -673
- package/src/node/node-task.preset.ts +0 -13
- package/src/question-list/question-list.api.ts +0 -42
- package/src/question-list/question-list.interface.ts +0 -21
- package/src/review/review.api.ts +0 -145
- package/src/review/review.interface.ts +0 -26
- package/src/review-v2/apis/post-review-setting.api.ts +0 -35
- package/src/review-v2/apis/post-review.api.ts +0 -59
- package/src/review-v2/review-setting.interface.ts +0 -26
- package/src/review-v2/review.interface.ts +0 -23
- package/src/service/api.interface.ts +0 -47
- package/src/shared/log/create-api-logger.ts +0 -25
package/dist/node.js
ADDED
|
@@ -0,0 +1,2483 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { M as MAX_POLL_TARGET_LIST_SIZE, E as validateWithSchema, d as createLogger, u as pollTargetList, g as generateResponse, e as createTarget, z as supabase, s as logManager, G as createScope, L as LogLevel, H as patchScope, y as scanTargetList, h as getPossibleTarget, p as isOptimisticLockResponse, I as withModule, A as updateTargetDetails } from './chunks/create-logger-DyH3mZ8p.js';
|
|
3
|
+
export { B as BaseValidator, C as CREATE_TARGET_ALREADY_EXISTS_MESSAGE, a as CategoryList, b as MAX_TARGET_LIST_PAGE_SIZE, O as OPTIMISTIC_LOCK_ERROR_CODE, c as OPTIMISTIC_LOCK_FAILED_MESSAGE, P as PostListValidator, S as StatusCode, f as deleteTarget, i as getTarget, j as getTargetList, k as getTargetTotalCount, l as handleSupabaseError, m as isCreateTargetAlreadyExistsError, n as isDevEnvironment, o as isOptimisticLockError, q as logDev, r as logDevGroup, t as patchTarget, v as postListCreate, w as postLogListCreate, x as postTarget, D as validateWith } from './chunks/create-logger-DyH3mZ8p.js';
|
|
4
|
+
import { l as loadEnvFiles, b as readEnv, c as requireEnv } from './chunks/enable-log-persist-C_bSw6Kx.js';
|
|
5
|
+
export { d as createJsonFileStateStore, e as disableLogPersist, f as enableLogPersist, g as ensureLogPersistFromEnv, h as envBool, i as envInt, j as envMs, k as envNumber, m as envPort, n as getLogPersistStats, a as logPersistEnabledFromEnv, p as parseEnvFile, o as parseEnvLine, q as pollUntil, s as resolveLogPersistRegistryPath, r as resolveProjectRootByPackageName, t as resolveProjectRootFromModule, u as snapshotLogPersistReady, v as validateLogPersistPreloadEnv, w as waitForLogPersistReady } from './chunks/enable-log-persist-C_bSw6Kx.js';
|
|
6
|
+
import { access, stat, readFile, mkdir, writeFile, readdir } from 'node:fs/promises';
|
|
7
|
+
import { resolve, join, isAbsolute, dirname } from 'node:path';
|
|
8
|
+
import { aq as sha256Hex, as as getErrorMessage, a9 as postRegisterNode, U as patchChangeNodeStatus, N as NodeStatus, a0 as patchStopNode, _ as patchNodeHeartBeat, d as CategoryRepo, R as ResultCode, at as LOG_TOPIC_TASK, T as TASK_REPO_USAGE, D as getScanRemoteRepoValues, Y as patchClaimTask, au as CRITICAL_UNKNOWN_TASK_TRACE_ID, W as patchChangeTaskStatus, j as TaskStatusAction, av as toError, ae as postTaskSchema, aw as createTaskRow, i as TaskStatus } from './chunks/word.interface-DLgRobKV.js';
|
|
9
|
+
export { A as ApiMethod, C as CategoryIdea, a as CategoryLink, b as CategoryNode, c as CategoryParcel, e as CategoryService, f as CategoryTask, g as CategoryWord, M as Market, P as ParcelManager, h as PostLinkValidator, S as ServiceLifecycleStatus, k as apiDetailsSchema, l as classifyNetworkError, m as countTasksByType, n as createStorageProviderRegistry, o as createUploadTracker, p as deleteParcel, q as deleteParcelSchema, r as discoverService, s as evaluateBusyNodeLiveness, t as fetchBinaryWithRetry, u as fetchWithRetry, v as fieldDefinitionSchema, w as formatNetworkError, x as getApi, y as getApiSchema, z as getParcel, B as getParcelSchema, E as getScanRemoteRepoValuesSchema, F as getService, G as getServiceSchema, H as installChunkFetchRegistry, I as isHttpUrl, J as isLocalFilesystemPath, K as isOpaqueChunkUrl, L as isRetryableHttpStatus, O as parcelDetailsSchema, Q as parseProviderPrefixedUrl, V as patchChangeNodeStatusSchema, X as patchChangeTaskStatusSchema, Z as patchClaimTaskSchema, $ as patchNodeHeartBeatSchema, a1 as patchStopNodeSchema, a2 as patchTaskProgress, a3 as patchTaskProgressSchema, a4 as postApi, a5 as postApiSchema, a6 as postLinkCreate, a7 as postParcel, a8 as postParcelSchema, aa as postRegisterNodeSchema, ab as postService, ac as postServiceSchema, ad as postTask, af as publishParcel, ag as registerProviderChunkResolver, ah as requestHasBody, ai as resolveFetchUrl, aj as restoreParcel, ak as restoreParcelById, al as sanitizeFileName, am as sanitizeParcelFileName, an as schemaDefinitionSchema, ao as serviceDetailsSchema, ap as serviceLifecycleSchema, ar as summarizeTaskQueue } from './chunks/word.interface-DLgRobKV.js';
|
|
10
|
+
import { spawn } from 'node:child_process';
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { pathToFileURL } from 'node:url';
|
|
14
|
+
import '@supabase/supabase-js';
|
|
15
|
+
import 'lodash-es';
|
|
16
|
+
import 'node:fs';
|
|
17
|
+
|
|
18
|
+
var CategoryCommand;
|
|
19
|
+
(function (CategoryCommand) {
|
|
20
|
+
CategoryCommand["COMMAND"] = "command";
|
|
21
|
+
})(CategoryCommand || (CategoryCommand = {}));
|
|
22
|
+
var CommandType;
|
|
23
|
+
(function (CommandType) {
|
|
24
|
+
CommandType["STOP_NODE"] = "stop-node";
|
|
25
|
+
})(CommandType || (CommandType = {}));
|
|
26
|
+
|
|
27
|
+
const nodeIdSchema = z.string().trim().min(1);
|
|
28
|
+
const traceIdSchema$1 = z.string().trim().min(1).optional();
|
|
29
|
+
// ─── Zod schemas ─────────────────────────────────────────────────────────────
|
|
30
|
+
const postCommandSchema = z.object({
|
|
31
|
+
nodeId: nodeIdSchema,
|
|
32
|
+
command: z.nativeEnum(CommandType),
|
|
33
|
+
traceId: traceIdSchema$1,
|
|
34
|
+
});
|
|
35
|
+
const getPollCommandListSchema = z.object({
|
|
36
|
+
nodeId: nodeIdSchema,
|
|
37
|
+
size: z.number().int().min(1).max(MAX_POLL_TARGET_LIST_SIZE).optional(),
|
|
38
|
+
traceId: traceIdSchema$1,
|
|
39
|
+
});
|
|
40
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
41
|
+
function commandNodeFilter(nodeId) {
|
|
42
|
+
return [{ field: "value", operator: "eq", value: nodeId }];
|
|
43
|
+
}
|
|
44
|
+
// ─── postCommand ─────────────────────────────────────────────────────────────
|
|
45
|
+
/** Enqueue a control command for `nodeId` (scheduler / admin). */
|
|
46
|
+
const postCommand = validateWithSchema(postCommandSchema, "postCommandSchema")(async ({ nodeId, command, traceId }) => {
|
|
47
|
+
const logger = createLogger({ module: "postCommand", traceId, labels: { nodeId } });
|
|
48
|
+
const result = await createTarget({
|
|
49
|
+
payload: { nodeId, command, traceId },
|
|
50
|
+
createFn: () => ({
|
|
51
|
+
name: command,
|
|
52
|
+
value: nodeId,
|
|
53
|
+
category: CategoryCommand.COMMAND,
|
|
54
|
+
tagList: [],
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
57
|
+
logger.info("Command已入隊", {
|
|
58
|
+
topic: "command",
|
|
59
|
+
data: { nodeId, command },
|
|
60
|
+
});
|
|
61
|
+
return result;
|
|
62
|
+
});
|
|
63
|
+
// ─── getPollCommandList ──────────────────────────────────────────────────────
|
|
64
|
+
/** Dequeue control commands for `nodeId` (oldest first). */
|
|
65
|
+
const getPollCommandList = validateWithSchema(getPollCommandListSchema, "getPollCommandListSchema")(async ({ nodeId, size, traceId }) => {
|
|
66
|
+
const logger = createLogger({ module: "getPollCommandList", traceId, labels: { nodeId } });
|
|
67
|
+
const { data: dequeuedList = [] } = await pollTargetList({
|
|
68
|
+
category: CategoryCommand.COMMAND,
|
|
69
|
+
size: size ?? 50,
|
|
70
|
+
filterList: commandNodeFilter(nodeId),
|
|
71
|
+
ascending: true,
|
|
72
|
+
});
|
|
73
|
+
if (dequeuedList.length === 0) {
|
|
74
|
+
logger.debug("Command", {
|
|
75
|
+
topic: "command",
|
|
76
|
+
data: { nodeId, dequeued: dequeuedList.length },
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
logger.info("Command", {
|
|
81
|
+
topic: "command",
|
|
82
|
+
data: { nodeId, dequeued: dequeuedList.length },
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
return generateResponse.success(dequeuedList);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Load standard Supabase env vars and initialize the process-wide Supabase holder.
|
|
90
|
+
*
|
|
91
|
+
* Required: `SUPABASE_URL`, `SUPABASE_ANON_KEY`
|
|
92
|
+
* Optional: `SUPABASE_NEED_AUTH_URL`, `SUPABASE_NEED_AUTH_ANON_KEY`
|
|
93
|
+
*/
|
|
94
|
+
async function initSupabaseFromStandardEnv(options = {}) {
|
|
95
|
+
const env = options.env ?? process.env;
|
|
96
|
+
const loadEnv = options.loadEnv ?? true;
|
|
97
|
+
if (loadEnv) {
|
|
98
|
+
if (options.root == null || options.root === "") {
|
|
99
|
+
throw new Error("initSupabaseFromStandardEnv: root is required when loadEnv is true");
|
|
100
|
+
}
|
|
101
|
+
loadEnvFiles(options.root, { env, afterLoad: options.afterLoadEnv });
|
|
102
|
+
}
|
|
103
|
+
await supabase.initialize({
|
|
104
|
+
supabaseUrl: requireEnv("SUPABASE_URL", env),
|
|
105
|
+
supabaseAnonKey: requireEnv("SUPABASE_ANON_KEY", env),
|
|
106
|
+
supabaseNeedAuthUrl: readEnv("SUPABASE_NEED_AUTH_URL", env),
|
|
107
|
+
supabaseNeedAuthAnonKey: readEnv("SUPABASE_NEED_AUTH_ANON_KEY", env),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Public base URL: `envKey` if set (trailing slash stripped), else `http://localhost:{port}`.
|
|
113
|
+
*/
|
|
114
|
+
function publicBaseUrlFromEnv(port, options) {
|
|
115
|
+
const configured = readEnv(options.envKey, options.env ?? process.env);
|
|
116
|
+
if (configured != null) {
|
|
117
|
+
return configured.replace(/\/$/, "");
|
|
118
|
+
}
|
|
119
|
+
return `http://localhost:${port}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Copy a Node.js Buffer view to a standalone ArrayBuffer. */
|
|
123
|
+
function nodeBufferToArrayBuffer(nodeBuffer) {
|
|
124
|
+
return nodeBuffer.buffer.slice(nodeBuffer.byteOffset, nodeBuffer.byteOffset + nodeBuffer.byteLength);
|
|
125
|
+
}
|
|
126
|
+
/** Read a local file with basic validation and SHA-256 digest. */
|
|
127
|
+
async function readAndVerifySourceFile(filePath, options = {}) {
|
|
128
|
+
const absolutePath = resolve(filePath);
|
|
129
|
+
const allowEmpty = options.allowEmpty === true;
|
|
130
|
+
try {
|
|
131
|
+
await access(absolutePath);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
throw new Error(`Source file not found: ${absolutePath}`);
|
|
135
|
+
}
|
|
136
|
+
const fileStat = await stat(absolutePath);
|
|
137
|
+
if (!fileStat.isFile()) {
|
|
138
|
+
throw new Error(`Source path is not a file: ${absolutePath}`);
|
|
139
|
+
}
|
|
140
|
+
if (!allowEmpty && fileStat.size <= 0) {
|
|
141
|
+
throw new Error(`Source file is empty: ${absolutePath}`);
|
|
142
|
+
}
|
|
143
|
+
if (options.maxBytes != null && fileStat.size > options.maxBytes) {
|
|
144
|
+
throw new Error(`Source file exceeds maxBytes (${fileStat.size} > ${options.maxBytes}): ${absolutePath}`);
|
|
145
|
+
}
|
|
146
|
+
const nodeBuffer = await readFile(absolutePath);
|
|
147
|
+
const buffer = nodeBufferToArrayBuffer(nodeBuffer);
|
|
148
|
+
if (buffer.byteLength !== fileStat.size) {
|
|
149
|
+
throw new Error(`Source file size mismatch after read (${buffer.byteLength} !== ${fileStat.size}): ${absolutePath}`);
|
|
150
|
+
}
|
|
151
|
+
const digest = await sha256Hex(buffer);
|
|
152
|
+
if (options.expectedSha256 != null && digest.toLowerCase() !== options.expectedSha256.toLowerCase()) {
|
|
153
|
+
throw new Error(`Source file SHA-256 mismatch (expected ${options.expectedSha256}, got ${digest}): ${absolutePath}`);
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
absolutePath,
|
|
157
|
+
buffer,
|
|
158
|
+
size: buffer.byteLength,
|
|
159
|
+
sha256: digest,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const formatTimestamp = (timestamp) => {
|
|
164
|
+
return new Intl.DateTimeFormat("zh-CN", {
|
|
165
|
+
year: "numeric",
|
|
166
|
+
month: "2-digit",
|
|
167
|
+
day: "2-digit",
|
|
168
|
+
hour: "2-digit",
|
|
169
|
+
minute: "2-digit",
|
|
170
|
+
second: "2-digit",
|
|
171
|
+
hour12: false,
|
|
172
|
+
fractionalSecondDigits: 3,
|
|
173
|
+
}).format(timestamp);
|
|
174
|
+
};
|
|
175
|
+
const formatRelativeTime = (timestamp) => {
|
|
176
|
+
const now = Date.now();
|
|
177
|
+
const diff = now - timestamp;
|
|
178
|
+
if (diff < 1000)
|
|
179
|
+
return "刚刚";
|
|
180
|
+
if (diff < 60000)
|
|
181
|
+
return `${Math.floor(diff / 1000)}秒前`;
|
|
182
|
+
if (diff < 3600000)
|
|
183
|
+
return `${Math.floor(diff / 60000)}分钟前`;
|
|
184
|
+
if (diff < 86400000)
|
|
185
|
+
return `${Math.floor(diff / 3600000)}小时前`;
|
|
186
|
+
return formatTimestamp(timestamp);
|
|
187
|
+
};
|
|
188
|
+
const getRandomInterval = () => {
|
|
189
|
+
const min = 15 * 1000;
|
|
190
|
+
const max = 1 * 60 * 1000;
|
|
191
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
192
|
+
};
|
|
193
|
+
const formatHeartbeat = (timestamp) => {
|
|
194
|
+
const absolute = formatTimestamp(timestamp);
|
|
195
|
+
const relative = formatRelativeTime(timestamp);
|
|
196
|
+
return `${absolute} (${relative})`;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const LOG_TOPIC_NODE = "node";
|
|
200
|
+
const LOG_TOPIC_COMMAND = "command";
|
|
201
|
+
const LOG_TOPIC_PROCESS = "process";
|
|
202
|
+
/**
|
|
203
|
+
* Shared node process runtime: bootstrap, main-loop frame, heartbeat, commands, shutdown.
|
|
204
|
+
* Subclasses implement {@link onBeforeRegisterNode} and {@link runLoopSteps}.
|
|
205
|
+
*/
|
|
206
|
+
class BaseNodeRuntime {
|
|
207
|
+
static HEARTBEAT_FAILURE_THRESHOLD = 3;
|
|
208
|
+
/** Log `nodeId` label while startup has not yet received an assigned id from Supabase. */
|
|
209
|
+
static NODE_ID_PENDING_ASSIGNMENT = "pending-node-id";
|
|
210
|
+
runtimeModule;
|
|
211
|
+
_localNodeId = null;
|
|
212
|
+
_isNodeIdLocked = false;
|
|
213
|
+
isRunning;
|
|
214
|
+
loopCount;
|
|
215
|
+
consecutiveHeartbeatFailures = 0;
|
|
216
|
+
startupTraceId;
|
|
217
|
+
startupScope;
|
|
218
|
+
shutdownPromise = null;
|
|
219
|
+
startPromise = null;
|
|
220
|
+
get localNodeId() {
|
|
221
|
+
return this._localNodeId;
|
|
222
|
+
}
|
|
223
|
+
set localNodeId(value) {
|
|
224
|
+
if (this._isNodeIdLocked) {
|
|
225
|
+
throw new Error("Node ID cannot be modified after initial assignment");
|
|
226
|
+
}
|
|
227
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
228
|
+
throw new Error("Invalid node ID");
|
|
229
|
+
}
|
|
230
|
+
this._localNodeId = value;
|
|
231
|
+
this._isNodeIdLocked = true;
|
|
232
|
+
}
|
|
233
|
+
constructor(runtimeModule) {
|
|
234
|
+
this.runtimeModule = runtimeModule;
|
|
235
|
+
this.isRunning = false;
|
|
236
|
+
this.loopCount = 0;
|
|
237
|
+
this.startupTraceId = logManager.generateTraceId();
|
|
238
|
+
this.startupScope = createScope({ module: runtimeModule, traceId: this.startupTraceId });
|
|
239
|
+
}
|
|
240
|
+
/** Hook: minimum log level for main-loop scoped loggers (heartbeat, batchCommand, iteration). */
|
|
241
|
+
loopLoggerMinLevel() {
|
|
242
|
+
return LogLevel.INFO;
|
|
243
|
+
}
|
|
244
|
+
/** Hook: ms to wait between loop iterations. Default: random 15–60s. */
|
|
245
|
+
getLoopIntervalMs() {
|
|
246
|
+
return getRandomInterval();
|
|
247
|
+
}
|
|
248
|
+
getLogNodeId() {
|
|
249
|
+
return this.localNodeId ?? BaseNodeRuntime.NODE_ID_PENDING_ASSIGNMENT;
|
|
250
|
+
}
|
|
251
|
+
isShuttingDown() {
|
|
252
|
+
return this.shutdownPromise != null;
|
|
253
|
+
}
|
|
254
|
+
requestShutdown(trigger) {
|
|
255
|
+
void this.shutdown({ trigger });
|
|
256
|
+
}
|
|
257
|
+
registerProcessLifecycle(logger) {
|
|
258
|
+
process.on("SIGTERM", () => {
|
|
259
|
+
logger.warn("收到 SIGTERM 信號,準備關閉節點", { topic: LOG_TOPIC_PROCESS });
|
|
260
|
+
this.requestShutdown("SIGTERM");
|
|
261
|
+
});
|
|
262
|
+
process.on("SIGINT", () => {
|
|
263
|
+
logger.warn("收到 SIGINT 信號,準備關閉節點", { topic: LOG_TOPIC_PROCESS });
|
|
264
|
+
this.requestShutdown("SIGINT");
|
|
265
|
+
});
|
|
266
|
+
process.on("uncaughtException", (error) => {
|
|
267
|
+
if (this.shutdownPromise != null) {
|
|
268
|
+
logger.critical("shutdown 進行中發生未捕獲異常,立即關閉節點", {
|
|
269
|
+
topic: LOG_TOPIC_PROCESS,
|
|
270
|
+
data: {
|
|
271
|
+
name: error.name,
|
|
272
|
+
message: error.message,
|
|
273
|
+
stack: error.stack,
|
|
274
|
+
},
|
|
275
|
+
});
|
|
276
|
+
process.exit(1);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
logger.critical("未捕獲的異常,準備關閉節點", {
|
|
280
|
+
topic: LOG_TOPIC_PROCESS,
|
|
281
|
+
data: {
|
|
282
|
+
name: error.name,
|
|
283
|
+
message: error.message,
|
|
284
|
+
stack: error.stack,
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
this.requestShutdown("uncaughtException");
|
|
288
|
+
});
|
|
289
|
+
process.on("unhandledRejection", (reason) => {
|
|
290
|
+
if (this.shutdownPromise != null) {
|
|
291
|
+
logger.error("shutdown 進行中發生未處理的 Promise 拒絕", {
|
|
292
|
+
topic: LOG_TOPIC_PROCESS,
|
|
293
|
+
data: {
|
|
294
|
+
reason: getErrorMessage(reason),
|
|
295
|
+
stack: reason instanceof Error ? reason.stack : undefined,
|
|
296
|
+
},
|
|
297
|
+
});
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
logger.error("未處理的 Promise 拒絕,準備關閉節點", {
|
|
301
|
+
topic: LOG_TOPIC_PROCESS,
|
|
302
|
+
data: {
|
|
303
|
+
reason: getErrorMessage(reason),
|
|
304
|
+
stack: reason instanceof Error ? reason.stack : undefined,
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
this.requestShutdown("unhandledRejection");
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
async start() {
|
|
311
|
+
if (this.startPromise != null) {
|
|
312
|
+
return this.startPromise;
|
|
313
|
+
}
|
|
314
|
+
this.startPromise = this.runStart();
|
|
315
|
+
return this.startPromise;
|
|
316
|
+
}
|
|
317
|
+
async runStart() {
|
|
318
|
+
const logger = createLogger({
|
|
319
|
+
scope: patchScope({ scope: this.startupScope, patch: { labels: { nodeId: this.getLogNodeId() } } }),
|
|
320
|
+
});
|
|
321
|
+
logger.debug("節點進程啟動中", {
|
|
322
|
+
topic: LOG_TOPIC_NODE,
|
|
323
|
+
data: {
|
|
324
|
+
pid: process.pid,
|
|
325
|
+
env: process.env.NODE_ENV ?? "development",
|
|
326
|
+
runtime: this.runtimeModule,
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
this.registerProcessLifecycle(logger);
|
|
330
|
+
try {
|
|
331
|
+
await this.onBeforeRegisterNode(logger);
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
logger.error("節點啟動前置步驟失敗", {
|
|
335
|
+
topic: LOG_TOPIC_NODE,
|
|
336
|
+
data: { error: getErrorMessage(error) },
|
|
337
|
+
});
|
|
338
|
+
this.requestShutdown("bootstrap:pre-register");
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
logger.debug("開始向 Supabase 註冊節點", { topic: LOG_TOPIC_NODE });
|
|
342
|
+
try {
|
|
343
|
+
const { data, error } = await postRegisterNode({ traceId: this.startupTraceId });
|
|
344
|
+
if (data == null || error) {
|
|
345
|
+
throw error ?? new Error("postRegisterNode 失敗");
|
|
346
|
+
}
|
|
347
|
+
this.localNodeId = data.id;
|
|
348
|
+
logger.resetScope({ labels: { nodeId: data.id } });
|
|
349
|
+
logger.success("節點註冊成功", {
|
|
350
|
+
topic: LOG_TOPIC_NODE,
|
|
351
|
+
data: {
|
|
352
|
+
nodeId: data.id,
|
|
353
|
+
details: data.details,
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
catch (error) {
|
|
358
|
+
logger.error("節點註冊失敗", {
|
|
359
|
+
topic: LOG_TOPIC_NODE,
|
|
360
|
+
data: { error: getErrorMessage(error) },
|
|
361
|
+
});
|
|
362
|
+
this.requestShutdown("bootstrap:register-node");
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const nodeId = this.localNodeId;
|
|
366
|
+
logger.debug("節點準備進入主循環", { topic: LOG_TOPIC_NODE });
|
|
367
|
+
const { error: enterBusyError } = await patchChangeNodeStatus({
|
|
368
|
+
nodeId,
|
|
369
|
+
status: NodeStatus.BUSY,
|
|
370
|
+
fromStatus: NodeStatus.READY,
|
|
371
|
+
traceId: this.startupTraceId,
|
|
372
|
+
});
|
|
373
|
+
if (enterBusyError) {
|
|
374
|
+
logger.error("節點進入主循環失敗,無法更新節點狀態為 BUSY", {
|
|
375
|
+
topic: LOG_TOPIC_NODE,
|
|
376
|
+
data: { error: enterBusyError.message },
|
|
377
|
+
});
|
|
378
|
+
this.requestShutdown("bootstrap:enter-busy");
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
logger.debug("節點狀態已設為 BUSY 進入主循環", { topic: LOG_TOPIC_NODE, data: { nodeId } });
|
|
382
|
+
await this.runLoop(nodeId);
|
|
383
|
+
}
|
|
384
|
+
async runLoop(nodeId) {
|
|
385
|
+
this.isRunning = true;
|
|
386
|
+
while (this.isRunning) {
|
|
387
|
+
this.loopCount++;
|
|
388
|
+
const loopTraceId = logManager.generateTraceId();
|
|
389
|
+
const loopScope = createScope({
|
|
390
|
+
module: `${this.runtimeModule}-loop-iteration`,
|
|
391
|
+
traceId: loopTraceId,
|
|
392
|
+
labels: { nodeId },
|
|
393
|
+
});
|
|
394
|
+
const loopCtx = { loopTraceId, nodeId };
|
|
395
|
+
const iterLogger = createLogger({
|
|
396
|
+
scope: loopScope,
|
|
397
|
+
minLevel: this.loopLoggerMinLevel(),
|
|
398
|
+
});
|
|
399
|
+
iterLogger.debug(`主循環第 ${this.loopCount} 輪開始`, {
|
|
400
|
+
topic: LOG_TOPIC_NODE,
|
|
401
|
+
data: { nodeId, loopCount: this.loopCount },
|
|
402
|
+
});
|
|
403
|
+
try {
|
|
404
|
+
await this.batchCommand(loopCtx);
|
|
405
|
+
if (this.isShuttingDown()) {
|
|
406
|
+
iterLogger.debug("節點關閉中,跳過本輪剩餘步驟", { topic: LOG_TOPIC_NODE });
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
const heartbeatOk = await this.heartbeat(loopCtx);
|
|
410
|
+
if (this.isShuttingDown()) {
|
|
411
|
+
iterLogger.debug("節點關閉中,跳過本輪剩餘步驟", { topic: LOG_TOPIC_NODE });
|
|
412
|
+
break;
|
|
413
|
+
}
|
|
414
|
+
await this.runLoopSteps(loopCtx, heartbeatOk);
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
iterLogger.critical(`主循環第 ${this.loopCount} 輪遇到未知錯誤`, {
|
|
418
|
+
topic: LOG_TOPIC_NODE,
|
|
419
|
+
data: { error: getErrorMessage(error) },
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
if (this.isShuttingDown()) {
|
|
423
|
+
iterLogger.debug("節點關閉中,跳過等待間隔", { topic: LOG_TOPIC_NODE });
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
const interval = this.getLoopIntervalMs();
|
|
427
|
+
iterLogger.debug(`主循環第 ${this.loopCount} 輪完成,等待下一輪`, {
|
|
428
|
+
topic: LOG_TOPIC_NODE,
|
|
429
|
+
data: { intervalMs: interval },
|
|
430
|
+
});
|
|
431
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
async shutdown(options) {
|
|
435
|
+
if (this.shutdownPromise != null) {
|
|
436
|
+
const logger = createLogger({
|
|
437
|
+
scope: patchScope({ scope: this.startupScope, patch: { labels: { nodeId: this.getLogNodeId() } } }),
|
|
438
|
+
});
|
|
439
|
+
logger.debug("shutdown 已在進行,跳過重複調用", {
|
|
440
|
+
topic: LOG_TOPIC_NODE,
|
|
441
|
+
data: { trigger: options?.trigger ?? "unknown" },
|
|
442
|
+
});
|
|
443
|
+
return this.shutdownPromise;
|
|
444
|
+
}
|
|
445
|
+
this.isRunning = false;
|
|
446
|
+
this.shutdownPromise = this.runShutdown(options?.trigger).catch((error) => {
|
|
447
|
+
const logger = createLogger({
|
|
448
|
+
scope: patchScope({ scope: this.startupScope, patch: { labels: { nodeId: this.getLogNodeId() } } }),
|
|
449
|
+
});
|
|
450
|
+
logger.error("shutdown 流程異常", {
|
|
451
|
+
topic: LOG_TOPIC_NODE,
|
|
452
|
+
data: {
|
|
453
|
+
trigger: options?.trigger ?? "explicit",
|
|
454
|
+
error: getErrorMessage(error),
|
|
455
|
+
},
|
|
456
|
+
});
|
|
457
|
+
process.exit(1);
|
|
458
|
+
});
|
|
459
|
+
return this.shutdownPromise;
|
|
460
|
+
}
|
|
461
|
+
async runShutdown(trigger) {
|
|
462
|
+
const logger = createLogger({
|
|
463
|
+
scope: patchScope({ scope: this.startupScope, patch: { labels: { nodeId: this.getLogNodeId() } } }),
|
|
464
|
+
});
|
|
465
|
+
logger.debug("正在關閉節點進程", {
|
|
466
|
+
topic: LOG_TOPIC_NODE,
|
|
467
|
+
data: { trigger: trigger ?? "explicit" },
|
|
468
|
+
});
|
|
469
|
+
if (this.localNodeId == null) {
|
|
470
|
+
logger.warn("本地節點未註冊,進程退出", { topic: LOG_TOPIC_NODE });
|
|
471
|
+
process.exit(0);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
logger.debug("正在登出節點", { topic: LOG_TOPIC_NODE, data: { nodeId: this.localNodeId } });
|
|
475
|
+
try {
|
|
476
|
+
const { error } = await patchStopNode({
|
|
477
|
+
nodeId: this.localNodeId,
|
|
478
|
+
traceId: this.startupTraceId,
|
|
479
|
+
});
|
|
480
|
+
if (error) {
|
|
481
|
+
throw new Error(`patchStopNode 失敗: ${error.message}`);
|
|
482
|
+
}
|
|
483
|
+
logger.success("節點正常關閉,進程退出", { topic: LOG_TOPIC_NODE });
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
logger.critical("登出節點失敗,進程退出", {
|
|
487
|
+
topic: LOG_TOPIC_NODE,
|
|
488
|
+
data: { error: getErrorMessage(error) },
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
finally {
|
|
492
|
+
process.exit(0);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
async heartbeat(ctx) {
|
|
496
|
+
const { loopTraceId, nodeId } = ctx;
|
|
497
|
+
const logger = createLogger({
|
|
498
|
+
scope: createScope({
|
|
499
|
+
module: "heartbeat",
|
|
500
|
+
traceId: loopTraceId,
|
|
501
|
+
labels: { nodeId },
|
|
502
|
+
}),
|
|
503
|
+
minLevel: this.loopLoggerMinLevel(),
|
|
504
|
+
});
|
|
505
|
+
logger.debug("開始發送心跳", { topic: LOG_TOPIC_NODE });
|
|
506
|
+
let error;
|
|
507
|
+
let lastHeartBeat;
|
|
508
|
+
try {
|
|
509
|
+
const response = await patchNodeHeartBeat({ nodeId, traceId: loopTraceId });
|
|
510
|
+
error = response.error;
|
|
511
|
+
lastHeartBeat = response.data;
|
|
512
|
+
}
|
|
513
|
+
catch (caught) {
|
|
514
|
+
error = { message: getErrorMessage(caught) };
|
|
515
|
+
lastHeartBeat = null;
|
|
516
|
+
}
|
|
517
|
+
if (error || lastHeartBeat == null) {
|
|
518
|
+
this.consecutiveHeartbeatFailures++;
|
|
519
|
+
const failureCount = this.consecutiveHeartbeatFailures;
|
|
520
|
+
const logData = {
|
|
521
|
+
error: getErrorMessage(error),
|
|
522
|
+
consecutiveFailures: failureCount,
|
|
523
|
+
threshold: BaseNodeRuntime.HEARTBEAT_FAILURE_THRESHOLD,
|
|
524
|
+
};
|
|
525
|
+
if (failureCount >= BaseNodeRuntime.HEARTBEAT_FAILURE_THRESHOLD) {
|
|
526
|
+
logger.critical("連續心跳失敗達上限,準備關閉節點", {
|
|
527
|
+
topic: LOG_TOPIC_NODE,
|
|
528
|
+
data: logData,
|
|
529
|
+
});
|
|
530
|
+
this.requestShutdown("heartbeat:consecutive-failures");
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
logger.warn("心跳更新失敗", { topic: LOG_TOPIC_NODE, data: logData });
|
|
534
|
+
}
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
this.consecutiveHeartbeatFailures = 0;
|
|
538
|
+
logger.success("心跳更新成功", {
|
|
539
|
+
topic: LOG_TOPIC_NODE,
|
|
540
|
+
data: { lastHeartBeat: formatHeartbeat(lastHeartBeat) },
|
|
541
|
+
});
|
|
542
|
+
return true;
|
|
543
|
+
}
|
|
544
|
+
async batchCommand(ctx) {
|
|
545
|
+
const { loopTraceId, nodeId } = ctx;
|
|
546
|
+
const cmdLogger = createLogger({
|
|
547
|
+
scope: createScope({
|
|
548
|
+
module: "batchCommand",
|
|
549
|
+
traceId: loopTraceId,
|
|
550
|
+
labels: { nodeId },
|
|
551
|
+
}),
|
|
552
|
+
minLevel: this.loopLoggerMinLevel(),
|
|
553
|
+
});
|
|
554
|
+
cmdLogger.debug("開始檢查控制指令", { topic: LOG_TOPIC_COMMAND });
|
|
555
|
+
const { data: commandList = [], error: pollError } = await getPollCommandList({
|
|
556
|
+
nodeId,
|
|
557
|
+
traceId: loopTraceId,
|
|
558
|
+
size: MAX_POLL_TARGET_LIST_SIZE,
|
|
559
|
+
});
|
|
560
|
+
if (pollError) {
|
|
561
|
+
cmdLogger.warn("獲取指令失敗", {
|
|
562
|
+
topic: LOG_TOPIC_COMMAND,
|
|
563
|
+
data: { error: pollError.message },
|
|
564
|
+
});
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
if (commandList.length === 0) {
|
|
568
|
+
cmdLogger.debug("獲取指令數量為0 無需執行", { topic: LOG_TOPIC_COMMAND });
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
cmdLogger.debug(`獲取到控制指令數量為${commandList.length}`, {
|
|
572
|
+
topic: LOG_TOPIC_COMMAND,
|
|
573
|
+
data: { count: commandList.length, commandList: commandList.map((command) => command.name) },
|
|
574
|
+
});
|
|
575
|
+
for (const command of commandList) {
|
|
576
|
+
const cmd = command.name;
|
|
577
|
+
cmdLogger.info("執行控制指令", {
|
|
578
|
+
topic: LOG_TOPIC_COMMAND,
|
|
579
|
+
data: { commandId: command.id, cmd },
|
|
580
|
+
});
|
|
581
|
+
try {
|
|
582
|
+
await this.executeCommand(cmd, ctx);
|
|
583
|
+
cmdLogger.success("控制指令執行成功", {
|
|
584
|
+
topic: LOG_TOPIC_COMMAND,
|
|
585
|
+
data: { commandId: command.id, cmd },
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
catch (error) {
|
|
589
|
+
cmdLogger.warn("控制指令執行失敗", {
|
|
590
|
+
topic: LOG_TOPIC_COMMAND,
|
|
591
|
+
data: {
|
|
592
|
+
commandId: command.id,
|
|
593
|
+
cmd,
|
|
594
|
+
error: getErrorMessage(error),
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
cmdLogger.debug("控制指令批次處理完成", {
|
|
600
|
+
topic: LOG_TOPIC_COMMAND,
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
/** Shared commands, then {@link resolveNodeCommand} for subclass-specific handling. */
|
|
604
|
+
async executeCommand(cmd, ctx) {
|
|
605
|
+
if (cmd === CommandType.STOP_NODE) {
|
|
606
|
+
this.requestShutdown("cmd:STOP_NODE");
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
await this.resolveNodeCommand(cmd, ctx);
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Hook: handle node-type-specific {@link CommandType} values.
|
|
613
|
+
* Default rejects anything not handled in {@link executeCommand}.
|
|
614
|
+
*/
|
|
615
|
+
async resolveNodeCommand(cmd, _ctx) {
|
|
616
|
+
throw new Error(`無法解析的 cmd: ${cmd}`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Build argv for `node --import … --import tsx <entry>`.
|
|
622
|
+
* Use **relative** preload paths with `cwd: projectRoot` — Node 24 on Windows rejects
|
|
623
|
+
* absolute paths like `D:\…` for `--import` (ESM URL scheme error).
|
|
624
|
+
*/
|
|
625
|
+
function buildNodeImportArgs(input) {
|
|
626
|
+
const preloadModules = input.preloadModules ?? [];
|
|
627
|
+
const useTsx = input.useTsx ?? true;
|
|
628
|
+
const args = [];
|
|
629
|
+
for (const modulePath of preloadModules) {
|
|
630
|
+
args.push("--import", modulePath);
|
|
631
|
+
}
|
|
632
|
+
if (useTsx) {
|
|
633
|
+
args.push("--import", "tsx");
|
|
634
|
+
}
|
|
635
|
+
args.push(input.entryScript);
|
|
636
|
+
return args;
|
|
637
|
+
}
|
|
638
|
+
/** Spawn `node` with preload chain + tsx + TypeScript entry (relative to projectRoot). */
|
|
639
|
+
function spawnTsxChild(options) {
|
|
640
|
+
return spawn(process.execPath, buildNodeImportArgs({
|
|
641
|
+
preloadModules: options.preloadModules,
|
|
642
|
+
useTsx: options.useTsx,
|
|
643
|
+
entryScript: options.entryScript,
|
|
644
|
+
}), {
|
|
645
|
+
cwd: options.projectRoot,
|
|
646
|
+
env: options.env ?? process.env,
|
|
647
|
+
stdio: options.stdio ?? "inherit",
|
|
648
|
+
shell: false,
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
function isChildProcessRunning(child) {
|
|
652
|
+
return child != null && child.exitCode == null && !child.killed;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Process-local registry for tsx child processes (supervisor, worker, etc.).
|
|
657
|
+
* Each OS process has its own instance - do not expect cross-process PID tracking.
|
|
658
|
+
*/
|
|
659
|
+
class ManagedChildProcesses {
|
|
660
|
+
projectRoot;
|
|
661
|
+
preloadModules;
|
|
662
|
+
graceMs;
|
|
663
|
+
logger;
|
|
664
|
+
byLabel = new Map();
|
|
665
|
+
constructor(options) {
|
|
666
|
+
this.projectRoot = options.projectRoot;
|
|
667
|
+
this.preloadModules = options.preloadModules ?? [];
|
|
668
|
+
this.graceMs = options.graceMs ?? 5_000;
|
|
669
|
+
this.logger = options.logger;
|
|
670
|
+
}
|
|
671
|
+
get(label) {
|
|
672
|
+
return this.byLabel.get(label) ?? null;
|
|
673
|
+
}
|
|
674
|
+
getRunning(label) {
|
|
675
|
+
const child = this.byLabel.get(label);
|
|
676
|
+
return isChildProcessRunning(child) ? child : null;
|
|
677
|
+
}
|
|
678
|
+
runningLabels() {
|
|
679
|
+
return [...this.byLabel.entries()]
|
|
680
|
+
.filter((entry) => isChildProcessRunning(entry[1]))
|
|
681
|
+
.map(([label]) => label);
|
|
682
|
+
}
|
|
683
|
+
spawn(label, entryScript) {
|
|
684
|
+
const existing = this.getRunning(label);
|
|
685
|
+
if (existing != null) {
|
|
686
|
+
return { child: existing, created: false };
|
|
687
|
+
}
|
|
688
|
+
const child = spawnTsxChild({
|
|
689
|
+
projectRoot: this.projectRoot,
|
|
690
|
+
entryScript,
|
|
691
|
+
preloadModules: this.preloadModules,
|
|
692
|
+
});
|
|
693
|
+
child.on("exit", (code, signal) => {
|
|
694
|
+
this.logger?.info("child exited", { topic: "process", data: { label, code, signal } });
|
|
695
|
+
const current = this.byLabel.get(label);
|
|
696
|
+
if (current === child) {
|
|
697
|
+
this.byLabel.delete(label);
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
this.byLabel.set(label, child);
|
|
701
|
+
this.logger?.info("child started", {
|
|
702
|
+
topic: "process",
|
|
703
|
+
data: {
|
|
704
|
+
label,
|
|
705
|
+
pid: child.pid,
|
|
706
|
+
script: resolve(this.projectRoot, entryScript),
|
|
707
|
+
},
|
|
708
|
+
});
|
|
709
|
+
return { child, created: true };
|
|
710
|
+
}
|
|
711
|
+
async stopAll(options) {
|
|
712
|
+
const tracked = [...this.byLabel.values()].filter((child) => isChildProcessRunning(child));
|
|
713
|
+
await Promise.all(tracked.map((child) => this.stopChild(child)));
|
|
714
|
+
const extraPids = options?.extraPids ?? [];
|
|
715
|
+
for (const pid of extraPids) {
|
|
716
|
+
if (pid == null || tracked.some((child) => child.pid === pid)) {
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
try {
|
|
720
|
+
process.kill(pid, "SIGTERM");
|
|
721
|
+
}
|
|
722
|
+
catch {
|
|
723
|
+
// already exited
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
stopChild(child) {
|
|
728
|
+
return new Promise((resolvePromise) => {
|
|
729
|
+
child.once("exit", () => resolvePromise());
|
|
730
|
+
child.kill("SIGTERM");
|
|
731
|
+
setTimeout(() => {
|
|
732
|
+
if (isChildProcessRunning(child)) {
|
|
733
|
+
child.kill("SIGKILL");
|
|
734
|
+
}
|
|
735
|
+
}, this.graceMs);
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/** Factory: all listed env keys must be non-empty. */
|
|
741
|
+
function createRequiredEnvCheck(name, keys, options) {
|
|
742
|
+
return () => {
|
|
743
|
+
const env = options?.env ?? process.env;
|
|
744
|
+
try {
|
|
745
|
+
for (const key of keys) {
|
|
746
|
+
requireEnv(key, env);
|
|
747
|
+
}
|
|
748
|
+
return { name, ok: true };
|
|
749
|
+
}
|
|
750
|
+
catch (error) {
|
|
751
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
752
|
+
return { name, ok: false, detail: message };
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
/** Factory: every path must exist on disk. */
|
|
757
|
+
function createPathsExistCheck(name, paths, options) {
|
|
758
|
+
return async () => {
|
|
759
|
+
for (const filePath of paths) {
|
|
760
|
+
try {
|
|
761
|
+
await access(filePath);
|
|
762
|
+
}
|
|
763
|
+
catch {
|
|
764
|
+
return {
|
|
765
|
+
name,
|
|
766
|
+
ok: false,
|
|
767
|
+
detail: options?.missingDetail ?? `Missing ${filePath}`,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
const detail = options?.successDetail ?? paths[paths.length - 1] ?? paths.join(", ");
|
|
772
|
+
return { name, ok: true, detail };
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
/** Factory: lightweight Supabase query via {@link scanTargetList}. */
|
|
776
|
+
function createSupabaseReachableCheck(options) {
|
|
777
|
+
const name = options?.name ?? "supabase_connectivity";
|
|
778
|
+
const category = options?.category ?? "node";
|
|
779
|
+
const maxRows = options?.maxRows ?? 1;
|
|
780
|
+
return async () => {
|
|
781
|
+
try {
|
|
782
|
+
await scanTargetList({ category, maxRows });
|
|
783
|
+
return { name, ok: true };
|
|
784
|
+
}
|
|
785
|
+
catch (error) {
|
|
786
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
787
|
+
return { name, ok: false, detail: message };
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
function formatFailureMessage(checks) {
|
|
793
|
+
return checks
|
|
794
|
+
.filter((check) => !check.ok)
|
|
795
|
+
.map((check) => `${check.name}: ${check.detail ?? "failed"}`)
|
|
796
|
+
.join("; ");
|
|
797
|
+
}
|
|
798
|
+
/** Run checks in order and aggregate into a {@link ReadinessReport}. */
|
|
799
|
+
async function runReadinessChecks(checks) {
|
|
800
|
+
const results = await Promise.all(checks.map((check) => check()));
|
|
801
|
+
const failed = results.filter((check) => !check.ok);
|
|
802
|
+
return {
|
|
803
|
+
ok: failed.length === 0,
|
|
804
|
+
checks: results,
|
|
805
|
+
message: failed.length === 0 ? null : formatFailureMessage(results),
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function sleep(ms) {
|
|
810
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Poll {@link ServiceReadyGate} until ready, fail fast on `failed`, or timeout.
|
|
814
|
+
* Returns the final ready snapshot.
|
|
815
|
+
*/
|
|
816
|
+
async function waitForServiceReady(gate, options) {
|
|
817
|
+
const pollMs = options.pollMs ?? 500;
|
|
818
|
+
const defaultFailureMessage = options.failureMessage ?? "Readiness checks failed";
|
|
819
|
+
const deadline = Date.now() + options.timeoutMs;
|
|
820
|
+
let last = { failed: false, ready: false };
|
|
821
|
+
while (Date.now() < deadline) {
|
|
822
|
+
last = await gate.read();
|
|
823
|
+
if (last.failed) {
|
|
824
|
+
throw new Error(last.failureMessage ?? defaultFailureMessage);
|
|
825
|
+
}
|
|
826
|
+
if (last.ready) {
|
|
827
|
+
options.logger?.info("service ready", {
|
|
828
|
+
topic: "readiness",
|
|
829
|
+
data: last.readyDetail ?? {},
|
|
830
|
+
});
|
|
831
|
+
return last;
|
|
832
|
+
}
|
|
833
|
+
await sleep(pollMs);
|
|
834
|
+
}
|
|
835
|
+
const message = options.formatTimeoutError?.(last, options.timeoutMs) ??
|
|
836
|
+
`Service ready timeout after ${options.timeoutMs}ms (failed=${last.failed}, ready=${last.ready})`;
|
|
837
|
+
throw new Error(message);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/** Supabase `target.category` for rows that store task script payloads */
|
|
841
|
+
const TASK_REPO_SCRIPT_CATEGORY = "script";
|
|
842
|
+
/** Host app config directory (relative to `cwd`) — recommended for library consumers */
|
|
843
|
+
const TASK_RUNNER_CONFIG_DIR = "config";
|
|
844
|
+
const TASK_RUNNER_ROOT_CONFIG_FILENAME = "task.config.js";
|
|
845
|
+
/** Default root config path: `config/task.config.js` */
|
|
846
|
+
const TASK_RUNNER_ROOT_CONFIG_RELATIVE_PATH = `${TASK_RUNNER_CONFIG_DIR}/${TASK_RUNNER_ROOT_CONFIG_FILENAME}`;
|
|
847
|
+
/** Legacy fallback at project root */
|
|
848
|
+
const TASK_RUNNER_ROOT_CONFIG_LEGACY_RELATIVE_PATH = TASK_RUNNER_ROOT_CONFIG_FILENAME;
|
|
849
|
+
/** Per-task config filename inside each folder under `taskDir` */
|
|
850
|
+
const TASK_LOCAL_PACKAGE_CONFIG_FILENAME = "task.config.js";
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Directory containing a config file — anchor for paths declared **inside** that file.
|
|
854
|
+
*/
|
|
855
|
+
function getConfigFileDir(configFilePath) {
|
|
856
|
+
return dirname(configFilePath);
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Resolve a path from a config file's directory.
|
|
860
|
+
*
|
|
861
|
+
* - Absolute `pathInConfig` → returned as-is
|
|
862
|
+
* - Relative `pathInConfig` → `resolve(dirname(configFilePath), pathInConfig)`
|
|
863
|
+
*
|
|
864
|
+
* **Not** relative to `process.cwd()`. See `.cursor/skills/config-file-relative-paths/SKILL.md`.
|
|
865
|
+
*/
|
|
866
|
+
function resolvePathFromConfigFile(configFilePath, pathInConfig) {
|
|
867
|
+
const trimmed = pathInConfig.trim();
|
|
868
|
+
if (isAbsolute(trimmed)) {
|
|
869
|
+
return trimmed;
|
|
870
|
+
}
|
|
871
|
+
return resolve(getConfigFileDir(configFilePath), trimmed);
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Resolve a path from an explicit base directory (e.g. task package dir for `entry`).
|
|
875
|
+
* Same rules as {@link resolvePathFromConfigFile}, but base is already known.
|
|
876
|
+
*/
|
|
877
|
+
function resolvePathFromBaseDir(baseDir, path) {
|
|
878
|
+
const trimmed = path.trim();
|
|
879
|
+
if (isAbsolute(trimmed)) {
|
|
880
|
+
return trimmed;
|
|
881
|
+
}
|
|
882
|
+
return resolve(baseDir, trimmed);
|
|
883
|
+
}
|
|
884
|
+
/**
|
|
885
|
+
* Resolve a path from `cwd` — for **discovering** config files, not for fields inside them.
|
|
886
|
+
*/
|
|
887
|
+
function resolvePathFromCwd(cwd, path) {
|
|
888
|
+
const trimmed = path.trim();
|
|
889
|
+
if (isAbsolute(trimmed)) {
|
|
890
|
+
return trimmed;
|
|
891
|
+
}
|
|
892
|
+
return resolve(cwd, trimmed);
|
|
893
|
+
}
|
|
894
|
+
async function pathExists(path) {
|
|
895
|
+
try {
|
|
896
|
+
await access(path);
|
|
897
|
+
return true;
|
|
898
|
+
}
|
|
899
|
+
catch {
|
|
900
|
+
return false;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* Find the first existing file path: optional explicit path, else scan `candidatePaths` under `cwd`.
|
|
905
|
+
*/
|
|
906
|
+
async function resolveFirstExistingPath(cwd, options) {
|
|
907
|
+
const { explicitPath, candidatePaths } = options;
|
|
908
|
+
if (explicitPath != null && explicitPath.trim() !== "") {
|
|
909
|
+
const resolved = resolvePathFromCwd(cwd, explicitPath);
|
|
910
|
+
return (await pathExists(resolved)) ? resolved : null;
|
|
911
|
+
}
|
|
912
|
+
for (const candidate of candidatePaths) {
|
|
913
|
+
const resolved = join(cwd, candidate);
|
|
914
|
+
if (await pathExists(resolved)) {
|
|
915
|
+
return resolved;
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
920
|
+
/** `file://` href for Node dynamic `import()` of a local module. */
|
|
921
|
+
function toFileImportHref(filePath) {
|
|
922
|
+
const trimmed = filePath.trim();
|
|
923
|
+
if (trimmed.includes("://")) {
|
|
924
|
+
return trimmed;
|
|
925
|
+
}
|
|
926
|
+
return pathToFileURL(trimmed).href;
|
|
927
|
+
}
|
|
928
|
+
/** Native ESM import of a `.js` / `.mjs` config; supports `export default` or named export object. */
|
|
929
|
+
async function importJsConfigModule(configPath) {
|
|
930
|
+
const mod = await import(toFileImportHref(configPath));
|
|
931
|
+
return mod.default ?? mod;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function isTaskRepoContext(value) {
|
|
935
|
+
if (value == null || typeof value !== "object") {
|
|
936
|
+
return false;
|
|
937
|
+
}
|
|
938
|
+
const record = value;
|
|
939
|
+
return typeof record.taskParamsValidator === "function" && typeof record.taskFn === "function";
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Normalize a dynamically imported module into {@link TaskRepoContext}.
|
|
943
|
+
* Supported shapes:
|
|
944
|
+
* - `export default { taskParamsValidator, taskFn }`
|
|
945
|
+
* - `export { taskParamsValidator, taskFn }`
|
|
946
|
+
* - `export default taskFn` (validator becomes `() => true`)
|
|
947
|
+
*/
|
|
948
|
+
function normalizeRepoContextModule(moduleExports, taskTypeKey, exportName) {
|
|
949
|
+
if (moduleExports == null) {
|
|
950
|
+
return null;
|
|
951
|
+
}
|
|
952
|
+
const record = moduleExports;
|
|
953
|
+
const namedExport = exportName != null && exportName !== "" ? record[exportName] : undefined;
|
|
954
|
+
const defaultExport = record.default;
|
|
955
|
+
const candidate = namedExport ?? defaultExport ?? moduleExports;
|
|
956
|
+
let taskParamsValidator;
|
|
957
|
+
let taskFn;
|
|
958
|
+
if (typeof candidate === "function") {
|
|
959
|
+
taskFn = candidate;
|
|
960
|
+
taskParamsValidator = record.taskParamsValidator;
|
|
961
|
+
}
|
|
962
|
+
else if (typeof candidate === "object" && candidate != null) {
|
|
963
|
+
const ctx = candidate;
|
|
964
|
+
taskParamsValidator = ctx.taskParamsValidator;
|
|
965
|
+
taskFn = ctx.taskFn;
|
|
966
|
+
}
|
|
967
|
+
taskParamsValidator ??= record.taskParamsValidator;
|
|
968
|
+
taskFn ??= record.taskFn;
|
|
969
|
+
if (typeof taskFn !== "function") {
|
|
970
|
+
return null;
|
|
971
|
+
}
|
|
972
|
+
if (typeof taskParamsValidator !== "function") {
|
|
973
|
+
taskParamsValidator = () => true;
|
|
974
|
+
}
|
|
975
|
+
const fn = taskFn;
|
|
976
|
+
if (fn.displayName == null || fn.displayName === "") {
|
|
977
|
+
fn.displayName = taskTypeKey;
|
|
978
|
+
}
|
|
979
|
+
if (fn.taskTypeKey == null || fn.taskTypeKey === "") {
|
|
980
|
+
fn.taskTypeKey = taskTypeKey;
|
|
981
|
+
}
|
|
982
|
+
return {
|
|
983
|
+
taskParamsValidator: taskParamsValidator,
|
|
984
|
+
taskFn: fn,
|
|
985
|
+
};
|
|
986
|
+
}
|
|
987
|
+
function pickEntryScript(scripts) {
|
|
988
|
+
if (scripts.length === 0) {
|
|
989
|
+
return null;
|
|
990
|
+
}
|
|
991
|
+
const entry = scripts.find((script) => script.details.isEntry === true);
|
|
992
|
+
return entry ?? scripts[0];
|
|
993
|
+
}
|
|
994
|
+
function getScriptLoadKey(script) {
|
|
995
|
+
const { source, modulePath, hash } = script.details;
|
|
996
|
+
if (hash != null && hash !== "") {
|
|
997
|
+
return hash;
|
|
998
|
+
}
|
|
999
|
+
if (source != null && source !== "") {
|
|
1000
|
+
return `source:${source.length}:${source.slice(0, 64)}`;
|
|
1001
|
+
}
|
|
1002
|
+
if (modulePath != null && modulePath !== "") {
|
|
1003
|
+
return `path:${modulePath}`;
|
|
1004
|
+
}
|
|
1005
|
+
return script.id;
|
|
1006
|
+
}
|
|
1007
|
+
function assertScriptLoadable(details) {
|
|
1008
|
+
const hasSource = details.source != null && details.source.trim() !== "";
|
|
1009
|
+
const hasModulePath = details.modulePath != null && details.modulePath.trim() !== "";
|
|
1010
|
+
return hasSource || hasModulePath;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const importedModuleCache = new Map();
|
|
1014
|
+
async function importFromFilePath(modulePath) {
|
|
1015
|
+
return import(toFileImportHref(modulePath));
|
|
1016
|
+
}
|
|
1017
|
+
/** Resolve repo `details.url` to a value suitable for dynamic `import()`. */
|
|
1018
|
+
function resolveRepoEntryHref(url, cwd = process.cwd()) {
|
|
1019
|
+
const trimmed = url.trim();
|
|
1020
|
+
if (trimmed.includes("://")) {
|
|
1021
|
+
return trimmed;
|
|
1022
|
+
}
|
|
1023
|
+
return toFileImportHref(resolvePathFromCwd(cwd, trimmed));
|
|
1024
|
+
}
|
|
1025
|
+
async function importFromSource(source, cacheKey) {
|
|
1026
|
+
const hash = createHash("sha256").update(source).digest("hex").slice(0, 16);
|
|
1027
|
+
const dir = join(tmpdir(), "target-supabase-sdk", "repo-scripts");
|
|
1028
|
+
await mkdir(dir, { recursive: true });
|
|
1029
|
+
const filePath = join(dir, `${cacheKey}-${hash}.mjs`);
|
|
1030
|
+
await writeFile(filePath, source, "utf8");
|
|
1031
|
+
return importFromFilePath(filePath);
|
|
1032
|
+
}
|
|
1033
|
+
async function loadRepoContextFromScript({ logger, script, taskTypeKey, }) {
|
|
1034
|
+
const loadKey = getScriptLoadKey(script);
|
|
1035
|
+
const cacheKey = `${taskTypeKey}@${loadKey}`;
|
|
1036
|
+
const cached = importedModuleCache.get(cacheKey);
|
|
1037
|
+
if (cached != null) {
|
|
1038
|
+
logger.info("命中 Repo 脚本模块缓存", { topic: "repo", data: { cacheKey } });
|
|
1039
|
+
return cached;
|
|
1040
|
+
}
|
|
1041
|
+
const { source, modulePath, exportName } = script.details;
|
|
1042
|
+
try {
|
|
1043
|
+
let moduleExports;
|
|
1044
|
+
if (source != null && source.trim() !== "") {
|
|
1045
|
+
logger.info("从 source 动态加载 Repo 脚本", { topic: "repo", data: { scriptId: script.id } });
|
|
1046
|
+
moduleExports = await importFromSource(source, cacheKey);
|
|
1047
|
+
}
|
|
1048
|
+
else if (modulePath != null && modulePath.trim() !== "") {
|
|
1049
|
+
logger.info("从 modulePath 动态加载 Repo 脚本", {
|
|
1050
|
+
topic: "repo",
|
|
1051
|
+
data: { scriptId: script.id, modulePath },
|
|
1052
|
+
});
|
|
1053
|
+
moduleExports = await importFromFilePath(modulePath);
|
|
1054
|
+
}
|
|
1055
|
+
else {
|
|
1056
|
+
logger.warn("脚本缺少 source 或 modulePath", { topic: "repo", data: { scriptId: script.id } });
|
|
1057
|
+
return null;
|
|
1058
|
+
}
|
|
1059
|
+
const context = normalizeRepoContextModule(moduleExports, taskTypeKey, exportName);
|
|
1060
|
+
if (context == null) {
|
|
1061
|
+
logger.warn("脚本模块未导出合法的 TaskRepoContext", {
|
|
1062
|
+
topic: "repo",
|
|
1063
|
+
data: { scriptId: script.id, exportName: exportName ?? "default" },
|
|
1064
|
+
});
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
1067
|
+
const loaded = { context, contentHash: loadKey };
|
|
1068
|
+
importedModuleCache.set(cacheKey, loaded);
|
|
1069
|
+
return loaded;
|
|
1070
|
+
}
|
|
1071
|
+
catch (error) {
|
|
1072
|
+
logger.error("动态加载 Repo 脚本失败", {
|
|
1073
|
+
topic: "repo",
|
|
1074
|
+
data: { scriptId: script.id, error: error instanceof Error ? error.message : error },
|
|
1075
|
+
});
|
|
1076
|
+
return null;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
async function loadRepoContextFromUrl({ logger, url, taskTypeKey, exportName, }) {
|
|
1080
|
+
const href = resolveRepoEntryHref(url);
|
|
1081
|
+
const cacheKey = `${taskTypeKey}@url:${href}`;
|
|
1082
|
+
const cached = importedModuleCache.get(cacheKey);
|
|
1083
|
+
if (cached != null) {
|
|
1084
|
+
logger.info("命中 Repo URL 模块缓存", { topic: "repo", data: { cacheKey } });
|
|
1085
|
+
return cached;
|
|
1086
|
+
}
|
|
1087
|
+
try {
|
|
1088
|
+
logger.info("从 Repo URL 动态加载脚本", { topic: "repo", data: { url, href } });
|
|
1089
|
+
const moduleExports = await import(href);
|
|
1090
|
+
const context = normalizeRepoContextModule(moduleExports, taskTypeKey, exportName);
|
|
1091
|
+
if (context == null) {
|
|
1092
|
+
logger.warn("Repo URL 模块未导出合法的 TaskRepoContext", {
|
|
1093
|
+
topic: "repo",
|
|
1094
|
+
data: { url, exportName: exportName ?? "default" },
|
|
1095
|
+
});
|
|
1096
|
+
return null;
|
|
1097
|
+
}
|
|
1098
|
+
const loaded = { context, contentHash: cacheKey };
|
|
1099
|
+
importedModuleCache.set(cacheKey, loaded);
|
|
1100
|
+
return loaded;
|
|
1101
|
+
}
|
|
1102
|
+
catch (error) {
|
|
1103
|
+
logger.error("从 Repo URL 动态加载失败", {
|
|
1104
|
+
topic: "repo",
|
|
1105
|
+
data: { url, error: error instanceof Error ? error.message : error },
|
|
1106
|
+
});
|
|
1107
|
+
return null;
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
/** Clear in-memory imported module cache (tests / hot reload). */
|
|
1111
|
+
function clearRepoScriptModuleCache() {
|
|
1112
|
+
importedModuleCache.clear();
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
const localRepoRegistry = new Map();
|
|
1116
|
+
const remoteContextCache = new Map();
|
|
1117
|
+
function buildRemoteCacheKey(taskTypeKey, contentHash) {
|
|
1118
|
+
return `${taskTypeKey}@${contentHash}`;
|
|
1119
|
+
}
|
|
1120
|
+
async function fetchRemoteRepo(taskTypeKey) {
|
|
1121
|
+
const { data } = await getPossibleTarget({
|
|
1122
|
+
filterList: [
|
|
1123
|
+
{ field: "category", operator: "eq", value: CategoryRepo.REPO },
|
|
1124
|
+
{ field: "value", operator: "eq", value: taskTypeKey },
|
|
1125
|
+
],
|
|
1126
|
+
});
|
|
1127
|
+
return data ?? null;
|
|
1128
|
+
}
|
|
1129
|
+
async function fetchRemoteScripts(taskTypeKey) {
|
|
1130
|
+
const { data, error } = await supabase.client
|
|
1131
|
+
.from("target")
|
|
1132
|
+
.select("id, value, details")
|
|
1133
|
+
.eq("category", TASK_REPO_SCRIPT_CATEGORY)
|
|
1134
|
+
.eq("value", taskTypeKey);
|
|
1135
|
+
if (error) {
|
|
1136
|
+
throw error;
|
|
1137
|
+
}
|
|
1138
|
+
return (data ?? []);
|
|
1139
|
+
}
|
|
1140
|
+
async function loadLocalEntry(logger, entry, taskTypeKey) {
|
|
1141
|
+
if (entry.kind === "context") {
|
|
1142
|
+
return entry.context;
|
|
1143
|
+
}
|
|
1144
|
+
const loaded = await loadRepoContextFromScript({
|
|
1145
|
+
logger,
|
|
1146
|
+
script: {
|
|
1147
|
+
id: `local:${taskTypeKey}`,
|
|
1148
|
+
value: taskTypeKey,
|
|
1149
|
+
details: {
|
|
1150
|
+
manifestVersion: 0,
|
|
1151
|
+
modulePath: entry.modulePath,
|
|
1152
|
+
exportName: entry.exportName,
|
|
1153
|
+
isEntry: true,
|
|
1154
|
+
},
|
|
1155
|
+
},
|
|
1156
|
+
taskTypeKey,
|
|
1157
|
+
});
|
|
1158
|
+
return loaded?.context ?? null;
|
|
1159
|
+
}
|
|
1160
|
+
async function loadRemoteRepoContext(logger, taskTypeKey) {
|
|
1161
|
+
logger.info("從 Supabase 加載 Repo", { topic: "repo", data: { taskTypeKey } });
|
|
1162
|
+
const remoteRepo = await fetchRemoteRepo(taskTypeKey);
|
|
1163
|
+
if (remoteRepo == null) {
|
|
1164
|
+
const error = `Supabase 未找到 Repo (value=${taskTypeKey})`;
|
|
1165
|
+
logger.warn(error, { topic: "repo", data: { taskTypeKey } });
|
|
1166
|
+
return { context: null, error, failureReason: "REMOTE_REPO_NOT_FOUND" };
|
|
1167
|
+
}
|
|
1168
|
+
const repoUrl = remoteRepo.details?.url?.trim() ?? "";
|
|
1169
|
+
const repoHash = remoteRepo.details?.hash?.trim() ?? repoUrl;
|
|
1170
|
+
const cacheKey = buildRemoteCacheKey(taskTypeKey, repoHash);
|
|
1171
|
+
const cached = remoteContextCache.get(cacheKey);
|
|
1172
|
+
if (cached != null) {
|
|
1173
|
+
logger.info("命中遠程 Repo 上下文緩存", { topic: "repo", data: { cacheKey } });
|
|
1174
|
+
return { context: cached, resolvedVia: "remote" };
|
|
1175
|
+
}
|
|
1176
|
+
const scripts = await fetchRemoteScripts(taskTypeKey);
|
|
1177
|
+
const entryScript = pickEntryScript(scripts);
|
|
1178
|
+
let loaded = entryScript != null && assertScriptLoadable(entryScript.details)
|
|
1179
|
+
? await loadRepoContextFromScript({ logger, script: entryScript, taskTypeKey })
|
|
1180
|
+
: null;
|
|
1181
|
+
if (loaded == null && repoUrl !== "") {
|
|
1182
|
+
logger.info("未找到 script 行,嘗試從 Repo.details.url 加載", {
|
|
1183
|
+
topic: "repo",
|
|
1184
|
+
data: { taskTypeKey, url: repoUrl },
|
|
1185
|
+
});
|
|
1186
|
+
loaded = await loadRepoContextFromUrl({ logger, url: repoUrl, taskTypeKey });
|
|
1187
|
+
}
|
|
1188
|
+
if (loaded == null) {
|
|
1189
|
+
const error = `無法從 Repo 加載 TaskRepoContext (hasUrl=${repoUrl !== ""}, scriptCount=${scripts.length})`;
|
|
1190
|
+
logger.warn(error, {
|
|
1191
|
+
topic: "repo",
|
|
1192
|
+
data: { taskTypeKey, hasUrl: repoUrl !== "", scriptCount: scripts.length },
|
|
1193
|
+
});
|
|
1194
|
+
return { context: null, error, failureReason: "REMOTE_REPO_LOAD_FAILED" };
|
|
1195
|
+
}
|
|
1196
|
+
remoteContextCache.set(cacheKey, loaded.context);
|
|
1197
|
+
return { context: loaded.context, resolvedVia: "remote" };
|
|
1198
|
+
}
|
|
1199
|
+
const getRepoContext = async ({ logger, taskTypeKey, includeRemote = true, }) => {
|
|
1200
|
+
const localEntry = localRepoRegistry.get(taskTypeKey);
|
|
1201
|
+
if (localEntry != null) {
|
|
1202
|
+
logger.info("使用本地註冊的 Repo", { topic: "repo", data: { taskTypeKey } });
|
|
1203
|
+
const context = await loadLocalEntry(logger, localEntry, taskTypeKey);
|
|
1204
|
+
if (context != null && isTaskRepoContext(context)) {
|
|
1205
|
+
return { context: context, resolvedVia: "local" };
|
|
1206
|
+
}
|
|
1207
|
+
const error = "本地 Repo 模組加載失敗或未導出有效的 TaskRepoContext";
|
|
1208
|
+
logger.warn(error, { topic: "repo", data: { taskTypeKey } });
|
|
1209
|
+
return { context: null, error, failureReason: "LOCAL_REPO_LOAD_FAILED" };
|
|
1210
|
+
}
|
|
1211
|
+
if (!includeRemote) {
|
|
1212
|
+
const error = `本地未註冊 Repo (value=${taskTypeKey})`;
|
|
1213
|
+
logger.warn(error, { topic: "repo", data: { taskTypeKey, includeRemote: false } });
|
|
1214
|
+
return { context: null, error, failureReason: "LOCAL_REPO_NOT_FOUND" };
|
|
1215
|
+
}
|
|
1216
|
+
try {
|
|
1217
|
+
const result = await loadRemoteRepoContext(logger, taskTypeKey);
|
|
1218
|
+
if (result.context != null && isTaskRepoContext(result.context)) {
|
|
1219
|
+
return { context: result.context, resolvedVia: "remote" };
|
|
1220
|
+
}
|
|
1221
|
+
return {
|
|
1222
|
+
context: null,
|
|
1223
|
+
error: result.error ?? "遠程 Repo 上下文無效",
|
|
1224
|
+
failureReason: result.failureReason ?? "REMOTE_REPO_LOAD_FAILED",
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
catch (error) {
|
|
1228
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1229
|
+
logger.error("從 Supabase 加載 Repo 失敗", {
|
|
1230
|
+
topic: "repo",
|
|
1231
|
+
data: { taskTypeKey, error: message },
|
|
1232
|
+
});
|
|
1233
|
+
return { context: null, error: message, failureReason: "REMOTE_REPO_LOAD_FAILED" };
|
|
1234
|
+
}
|
|
1235
|
+
};
|
|
1236
|
+
/** Register an in-process TaskRepoContext (highest priority — no Supabase fetch). */
|
|
1237
|
+
function registerLocalRepoContext(taskTypeKey, context) {
|
|
1238
|
+
if (!isTaskRepoContext(context)) {
|
|
1239
|
+
throw new Error("[RepoManager] Invalid TaskRepoContext");
|
|
1240
|
+
}
|
|
1241
|
+
localRepoRegistry.set(taskTypeKey, { kind: "context", context });
|
|
1242
|
+
}
|
|
1243
|
+
/** Register a local ESM module path; loaded via dynamic import when the task runs. */
|
|
1244
|
+
function registerLocalModule(taskTypeKey, modulePath, exportName) {
|
|
1245
|
+
if (modulePath.trim() === "") {
|
|
1246
|
+
throw new Error("[RepoManager] modulePath must not be empty");
|
|
1247
|
+
}
|
|
1248
|
+
localRepoRegistry.set(taskTypeKey, { kind: "modulePath", modulePath, exportName });
|
|
1249
|
+
}
|
|
1250
|
+
function unregisterLocalRepo(taskTypeKey) {
|
|
1251
|
+
localRepoRegistry.delete(taskTypeKey);
|
|
1252
|
+
}
|
|
1253
|
+
function hasLocalRepo(taskTypeKey) {
|
|
1254
|
+
return localRepoRegistry.has(taskTypeKey);
|
|
1255
|
+
}
|
|
1256
|
+
function getRegisteredLocalTaskTypeKeys$1() {
|
|
1257
|
+
return [...localRepoRegistry.keys()];
|
|
1258
|
+
}
|
|
1259
|
+
function clearRemoteContextCache() {
|
|
1260
|
+
remoteContextCache.clear();
|
|
1261
|
+
}
|
|
1262
|
+
const RepoManager = {
|
|
1263
|
+
getRepoContext,
|
|
1264
|
+
registerLocalRepoContext,
|
|
1265
|
+
registerLocalModule,
|
|
1266
|
+
unregisterLocalRepo,
|
|
1267
|
+
hasLocalRepo,
|
|
1268
|
+
getRegisteredLocalTaskTypeKeys: getRegisteredLocalTaskTypeKeys$1,
|
|
1269
|
+
clearRemoteContextCache,
|
|
1270
|
+
clearRepoScriptModuleCache,
|
|
1271
|
+
};
|
|
1272
|
+
|
|
1273
|
+
const EMPTY_RESULT = {
|
|
1274
|
+
status: "not_configured",
|
|
1275
|
+
registered: [],
|
|
1276
|
+
skipped: [],
|
|
1277
|
+
errors: [],
|
|
1278
|
+
};
|
|
1279
|
+
let bootstrapCache = { fingerprint: null, lastResult: null };
|
|
1280
|
+
let bootstrapInFlight = null;
|
|
1281
|
+
function parseRootConfig(raw, configPath) {
|
|
1282
|
+
if (raw == null || typeof raw !== "object") {
|
|
1283
|
+
throw new Error(`Root config must export an object: ${configPath}`);
|
|
1284
|
+
}
|
|
1285
|
+
const record = raw;
|
|
1286
|
+
const taskDir = record.taskDir;
|
|
1287
|
+
if (typeof taskDir !== "string" || taskDir.trim() === "") {
|
|
1288
|
+
throw new Error(`Root config.taskDir must be a non-empty string: ${configPath}`);
|
|
1289
|
+
}
|
|
1290
|
+
return { taskDir: taskDir.trim() };
|
|
1291
|
+
}
|
|
1292
|
+
function parseTaskPackageConfig(raw, configPath) {
|
|
1293
|
+
if (raw == null || typeof raw !== "object") {
|
|
1294
|
+
throw new Error(`Task config must export an object: ${configPath}`);
|
|
1295
|
+
}
|
|
1296
|
+
const record = raw;
|
|
1297
|
+
const taskTypeKey = record.taskTypeKey;
|
|
1298
|
+
const entry = record.entry;
|
|
1299
|
+
if (typeof taskTypeKey !== "string" || taskTypeKey.trim() === "") {
|
|
1300
|
+
throw new Error(`taskTypeKey must be a non-empty string: ${configPath}`);
|
|
1301
|
+
}
|
|
1302
|
+
if (typeof entry !== "string" || entry.trim() === "") {
|
|
1303
|
+
throw new Error(`entry must be a non-empty string: ${configPath}`);
|
|
1304
|
+
}
|
|
1305
|
+
return {
|
|
1306
|
+
taskTypeKey: taskTypeKey.trim(),
|
|
1307
|
+
entry: entry.trim(),
|
|
1308
|
+
exportName: typeof record.exportName === "string" ? record.exportName : undefined,
|
|
1309
|
+
displayName: typeof record.displayName === "string" ? record.displayName : undefined,
|
|
1310
|
+
enabled: record.enabled !== false,
|
|
1311
|
+
};
|
|
1312
|
+
}
|
|
1313
|
+
function finalizeStatus(result) {
|
|
1314
|
+
if (result.errors.length > 0 && result.registered.length === 0) {
|
|
1315
|
+
return "failed";
|
|
1316
|
+
}
|
|
1317
|
+
if (result.registered.length === 0) {
|
|
1318
|
+
return "empty";
|
|
1319
|
+
}
|
|
1320
|
+
return "loaded";
|
|
1321
|
+
}
|
|
1322
|
+
async function computeConfigFingerprint(rootConfigPath, tasksRoot) {
|
|
1323
|
+
const rootStat = await stat(rootConfigPath);
|
|
1324
|
+
const tasksRootStat = await stat(tasksRoot);
|
|
1325
|
+
const entries = await readdir(tasksRoot, { withFileTypes: true });
|
|
1326
|
+
const configMtims = [];
|
|
1327
|
+
for (const entry of entries) {
|
|
1328
|
+
if (!entry.isDirectory()) {
|
|
1329
|
+
continue;
|
|
1330
|
+
}
|
|
1331
|
+
const configPath = join(tasksRoot, entry.name, TASK_LOCAL_PACKAGE_CONFIG_FILENAME);
|
|
1332
|
+
if (await pathExists(configPath)) {
|
|
1333
|
+
const configStat = await stat(configPath);
|
|
1334
|
+
configMtims.push(configStat.mtimeMs);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
configMtims.sort((a, b) => a - b);
|
|
1338
|
+
return `${rootConfigPath}:${rootStat.mtimeMs}:${tasksRoot}:${tasksRootStat.mtimeMs}:${configMtims.join(",")}`;
|
|
1339
|
+
}
|
|
1340
|
+
function canUseCachedBootstrap(fingerprint, forTaskTypeKey) {
|
|
1341
|
+
if (bootstrapCache.fingerprint !== fingerprint || bootstrapCache.lastResult == null) {
|
|
1342
|
+
return null;
|
|
1343
|
+
}
|
|
1344
|
+
if (forTaskTypeKey != null && forTaskTypeKey !== "") {
|
|
1345
|
+
if (!RepoManager.hasLocalRepo(forTaskTypeKey)) {
|
|
1346
|
+
return null;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
return { ...bootstrapCache.lastResult, cached: true };
|
|
1350
|
+
}
|
|
1351
|
+
async function runBootstrapScan(options) {
|
|
1352
|
+
const cwd = options.cwd ?? process.cwd();
|
|
1353
|
+
const rootConfigPath = await resolveFirstExistingPath(cwd, {
|
|
1354
|
+
explicitPath: options.rootConfigPath,
|
|
1355
|
+
candidatePaths: [TASK_RUNNER_ROOT_CONFIG_RELATIVE_PATH, TASK_RUNNER_ROOT_CONFIG_LEGACY_RELATIVE_PATH],
|
|
1356
|
+
});
|
|
1357
|
+
if (rootConfigPath == null) {
|
|
1358
|
+
const result = {
|
|
1359
|
+
...EMPTY_RESULT,
|
|
1360
|
+
message: `No root config (tried ${TASK_RUNNER_ROOT_CONFIG_RELATIVE_PATH}, ${TASK_RUNNER_ROOT_CONFIG_LEGACY_RELATIVE_PATH})`,
|
|
1361
|
+
};
|
|
1362
|
+
bootstrapCache = { fingerprint: null, lastResult: result };
|
|
1363
|
+
return result;
|
|
1364
|
+
}
|
|
1365
|
+
const result = {
|
|
1366
|
+
registered: [],
|
|
1367
|
+
skipped: [],
|
|
1368
|
+
errors: [],
|
|
1369
|
+
};
|
|
1370
|
+
try {
|
|
1371
|
+
const rootRaw = await importJsConfigModule(rootConfigPath);
|
|
1372
|
+
const { taskDir } = parseRootConfig(rootRaw, rootConfigPath);
|
|
1373
|
+
const tasksRoot = resolvePathFromConfigFile(rootConfigPath, taskDir);
|
|
1374
|
+
if (!(await pathExists(tasksRoot))) {
|
|
1375
|
+
const failed = {
|
|
1376
|
+
status: "failed",
|
|
1377
|
+
message: `taskDir does not exist: ${tasksRoot}`,
|
|
1378
|
+
...result,
|
|
1379
|
+
};
|
|
1380
|
+
bootstrapCache = { fingerprint: null, lastResult: failed };
|
|
1381
|
+
return failed;
|
|
1382
|
+
}
|
|
1383
|
+
const fingerprint = await computeConfigFingerprint(rootConfigPath, tasksRoot);
|
|
1384
|
+
const cached = canUseCachedBootstrap(fingerprint, options.forTaskTypeKey);
|
|
1385
|
+
if (cached != null) {
|
|
1386
|
+
return cached;
|
|
1387
|
+
}
|
|
1388
|
+
const entries = await readdir(tasksRoot, { withFileTypes: true });
|
|
1389
|
+
for (const entry of entries) {
|
|
1390
|
+
if (!entry.isDirectory()) {
|
|
1391
|
+
continue;
|
|
1392
|
+
}
|
|
1393
|
+
const taskPackageDir = join(tasksRoot, entry.name);
|
|
1394
|
+
const taskConfigPath = join(taskPackageDir, TASK_LOCAL_PACKAGE_CONFIG_FILENAME);
|
|
1395
|
+
if (!(await pathExists(taskConfigPath))) {
|
|
1396
|
+
result.skipped.push({ taskDir: taskPackageDir, reason: "missing task.config.js" });
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
try {
|
|
1400
|
+
const taskRaw = await importJsConfigModule(taskConfigPath);
|
|
1401
|
+
const taskConfig = parseTaskPackageConfig(taskRaw, taskConfigPath);
|
|
1402
|
+
if (taskConfig.enabled === false) {
|
|
1403
|
+
result.skipped.push({ taskDir: taskPackageDir, reason: "disabled in task.config.js" });
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
const entryPath = resolvePathFromBaseDir(taskPackageDir, taskConfig.entry);
|
|
1407
|
+
if (!(await pathExists(entryPath))) {
|
|
1408
|
+
throw new Error(`entry module not found: ${entryPath}`);
|
|
1409
|
+
}
|
|
1410
|
+
RepoManager.registerLocalModule(taskConfig.taskTypeKey, entryPath, taskConfig.exportName);
|
|
1411
|
+
result.registered.push(taskConfig.taskTypeKey);
|
|
1412
|
+
}
|
|
1413
|
+
catch (error) {
|
|
1414
|
+
result.errors.push({
|
|
1415
|
+
taskDir: taskPackageDir,
|
|
1416
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1417
|
+
});
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
const status = finalizeStatus(result);
|
|
1421
|
+
const finalResult = {
|
|
1422
|
+
status,
|
|
1423
|
+
message: status === "empty" ? `Root config loaded but no tasks registered under ${tasksRoot}` : undefined,
|
|
1424
|
+
...result,
|
|
1425
|
+
};
|
|
1426
|
+
bootstrapCache = { fingerprint, lastResult: finalResult };
|
|
1427
|
+
return finalResult;
|
|
1428
|
+
}
|
|
1429
|
+
catch (error) {
|
|
1430
|
+
const failed = {
|
|
1431
|
+
status: "failed",
|
|
1432
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1433
|
+
...result,
|
|
1434
|
+
};
|
|
1435
|
+
bootstrapCache = { fingerprint: null, lastResult: failed };
|
|
1436
|
+
return failed;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Discover local task packages from config files and register them with {@link RepoManager}.
|
|
1441
|
+
*
|
|
1442
|
+
* **Never throws** for missing root config — returns `{ status: "not_configured" }`.
|
|
1443
|
+
*
|
|
1444
|
+
* Re-scans are skipped when the config fingerprint is unchanged (root config + taskDir mtimes).
|
|
1445
|
+
* Concurrent calls share a single in-flight scan (single-flight).
|
|
1446
|
+
*
|
|
1447
|
+
* Host app layout (recommended):
|
|
1448
|
+
* ```
|
|
1449
|
+
* ./config/task.config.js → { taskDir: "../tasks" }
|
|
1450
|
+
* ./tasks/my-task/task.config.js
|
|
1451
|
+
* ./tasks/my-task/index.mjs
|
|
1452
|
+
* ```
|
|
1453
|
+
*
|
|
1454
|
+
* `taskDir` / `entry` paths are relative to **their config file's directory** (see config-file-relative-paths skill).
|
|
1455
|
+
*
|
|
1456
|
+
* Legacy fallback: `./task.config.js` at project root (`taskDir: "./tasks"`).
|
|
1457
|
+
*/
|
|
1458
|
+
async function bootstrapLocalTasks(options = {}) {
|
|
1459
|
+
if (bootstrapInFlight != null) {
|
|
1460
|
+
return bootstrapInFlight;
|
|
1461
|
+
}
|
|
1462
|
+
bootstrapInFlight = runBootstrapScan(options).finally(() => {
|
|
1463
|
+
bootstrapInFlight = null;
|
|
1464
|
+
});
|
|
1465
|
+
return bootstrapInFlight;
|
|
1466
|
+
}
|
|
1467
|
+
/** Clear fingerprint cache (tests / hot-reload tooling). */
|
|
1468
|
+
function clearBootstrapLocalTasksCache() {
|
|
1469
|
+
bootstrapCache = { fingerprint: null, lastResult: null };
|
|
1470
|
+
bootstrapInFlight = null;
|
|
1471
|
+
}
|
|
1472
|
+
function getRegisteredLocalTaskTypeKeys() {
|
|
1473
|
+
return RepoManager.getRegisteredLocalTaskTypeKeys();
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
function validateTaskParams(validator, taskParams, logger) {
|
|
1477
|
+
try {
|
|
1478
|
+
return validator(taskParams) === true;
|
|
1479
|
+
}
|
|
1480
|
+
catch (error) {
|
|
1481
|
+
logger.warn("taskParamsValidator 拋出異常", {
|
|
1482
|
+
topic: "task",
|
|
1483
|
+
data: { error: error instanceof Error ? error.message : error },
|
|
1484
|
+
});
|
|
1485
|
+
return false;
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
function invalid(params) {
|
|
1489
|
+
return { isValid: false, ...params };
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Resolve task Repo context and validate `params` via `taskParamsValidator`.
|
|
1493
|
+
*
|
|
1494
|
+
* Repo resolution uses {@link RepoManager.getRepoContext}:
|
|
1495
|
+
* local registry first; when no local entry and `includeRemote`, loads from Supabase.
|
|
1496
|
+
*/
|
|
1497
|
+
async function validateTaskRepoAndParams({ logger, taskTypeKey, params, bootstrapLocal = false, bootstrapLocalOptions, includeRemote = true, }) {
|
|
1498
|
+
if (params == null) {
|
|
1499
|
+
return invalid({
|
|
1500
|
+
reason: "MISSING_PARAMS",
|
|
1501
|
+
code: ResultCode.PARAMS_NOT_VALID,
|
|
1502
|
+
message: "任務缺少 params",
|
|
1503
|
+
step: "params",
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
if (bootstrapLocal) {
|
|
1507
|
+
const bootstrap = await bootstrapLocalTasks({
|
|
1508
|
+
...bootstrapLocalOptions,
|
|
1509
|
+
forTaskTypeKey: taskTypeKey,
|
|
1510
|
+
});
|
|
1511
|
+
logger.debug("本地任務 bootstrap 完成", {
|
|
1512
|
+
topic: "task",
|
|
1513
|
+
data: {
|
|
1514
|
+
taskTypeKey,
|
|
1515
|
+
step: "local_bootstrap",
|
|
1516
|
+
status: bootstrap.status,
|
|
1517
|
+
registered: bootstrap.registered,
|
|
1518
|
+
cached: bootstrap.cached ?? false,
|
|
1519
|
+
message: bootstrap.message,
|
|
1520
|
+
},
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
logger.debug("開始解析 Repo 上下文", {
|
|
1524
|
+
topic: "task",
|
|
1525
|
+
data: { taskTypeKey, step: "repo_load", includeRemote },
|
|
1526
|
+
});
|
|
1527
|
+
const { context: repoContext, error: repoError, failureReason, resolvedVia, } = await RepoManager.getRepoContext({
|
|
1528
|
+
logger,
|
|
1529
|
+
taskTypeKey,
|
|
1530
|
+
includeRemote,
|
|
1531
|
+
});
|
|
1532
|
+
if (repoContext == null) {
|
|
1533
|
+
return invalid({
|
|
1534
|
+
reason: failureReason ?? "REMOTE_REPO_LOAD_FAILED",
|
|
1535
|
+
code: ResultCode.REPO_NOT_VALID,
|
|
1536
|
+
message: repoError ?? "無法加載 Repo 上下文",
|
|
1537
|
+
step: "repo_load",
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
logger.debug("Repo 上下文解析完成", {
|
|
1541
|
+
topic: "task",
|
|
1542
|
+
data: { taskTypeKey, step: "repo_load", resolvedVia },
|
|
1543
|
+
});
|
|
1544
|
+
if (repoContext.taskFn.taskTypeKey !== taskTypeKey) {
|
|
1545
|
+
return invalid({
|
|
1546
|
+
reason: "TASK_TYPE_KEY_MISMATCH",
|
|
1547
|
+
code: ResultCode.REPO_NOT_VALID,
|
|
1548
|
+
message: `taskFn.taskTypeKey (${repoContext.taskFn.taskTypeKey}) 與 task.value (${taskTypeKey}) 不一致`,
|
|
1549
|
+
step: "task_type_key",
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
logger.debug("開始校驗任務參數", { topic: "task", data: { taskTypeKey, step: "params_validation" } });
|
|
1553
|
+
if (!validateTaskParams(repoContext.taskParamsValidator, params, logger)) {
|
|
1554
|
+
return invalid({
|
|
1555
|
+
reason: "PARAMS_VALIDATION_FAILED",
|
|
1556
|
+
code: ResultCode.PARAMS_NOT_VALID,
|
|
1557
|
+
message: "任務參數校驗失敗",
|
|
1558
|
+
step: "params_validation",
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1561
|
+
logger.debug("任務參數校驗通過", { topic: "task", data: { taskTypeKey, step: "params_validation" } });
|
|
1562
|
+
return { isValid: true, repoContext };
|
|
1563
|
+
}
|
|
1564
|
+
const TaskRepoValidation = {
|
|
1565
|
+
validate: validateTaskRepoAndParams,
|
|
1566
|
+
};
|
|
1567
|
+
|
|
1568
|
+
function logLocalBootstrap(logger, bootstrap) {
|
|
1569
|
+
switch (bootstrap.status) {
|
|
1570
|
+
case "not_configured":
|
|
1571
|
+
logger.info("未找到本地任務配置,跳過本地註冊", {
|
|
1572
|
+
topic: LOG_TOPIC_TASK,
|
|
1573
|
+
data: { message: bootstrap.message },
|
|
1574
|
+
});
|
|
1575
|
+
break;
|
|
1576
|
+
case "failed":
|
|
1577
|
+
logger.warn("本地任務註冊失敗", {
|
|
1578
|
+
topic: LOG_TOPIC_TASK,
|
|
1579
|
+
data: { message: bootstrap.message, errors: bootstrap.errors },
|
|
1580
|
+
});
|
|
1581
|
+
break;
|
|
1582
|
+
case "empty":
|
|
1583
|
+
logger.info("本地任務掃描完成,無已註冊任務", {
|
|
1584
|
+
topic: LOG_TOPIC_TASK,
|
|
1585
|
+
data: { message: bootstrap.message, skipped: bootstrap.skipped },
|
|
1586
|
+
});
|
|
1587
|
+
break;
|
|
1588
|
+
case "loaded":
|
|
1589
|
+
logger.info("本地任務註冊完成", {
|
|
1590
|
+
topic: LOG_TOPIC_TASK,
|
|
1591
|
+
data: {
|
|
1592
|
+
registered: bootstrap.registered,
|
|
1593
|
+
skipped: bootstrap.skipped,
|
|
1594
|
+
errors: bootstrap.errors,
|
|
1595
|
+
},
|
|
1596
|
+
});
|
|
1597
|
+
break;
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
/**
|
|
1601
|
+
* Worker startup: discover local task packages + remote Repo rows, merge into `availableTaskList`.
|
|
1602
|
+
*
|
|
1603
|
+
* - Local: {@link bootstrapLocalTasks} → registers modules in {@link RepoManager}
|
|
1604
|
+
* - Remote: {@link getScanRemoteRepoValues} with {@link TASK_REPO_USAGE}
|
|
1605
|
+
* - Merge: union of both; execution still prefers local registry in {@link RepoManager.getRepoContext}
|
|
1606
|
+
*
|
|
1607
|
+
* @throws When merged list is empty (no task types to claim).
|
|
1608
|
+
*/
|
|
1609
|
+
async function registerTasks({ logger, local: localOptions, includeRemote = true, }) {
|
|
1610
|
+
logger.debug("開始註冊任務(本地 + 遠程)", { topic: LOG_TOPIC_TASK });
|
|
1611
|
+
const local = await bootstrapLocalTasks(localOptions ?? {});
|
|
1612
|
+
logLocalBootstrap(logger, local);
|
|
1613
|
+
let remoteValues = [];
|
|
1614
|
+
if (includeRemote) {
|
|
1615
|
+
logger.debug("查詢遠程 Repo 列表", {
|
|
1616
|
+
topic: LOG_TOPIC_TASK,
|
|
1617
|
+
data: { usage: TASK_REPO_USAGE },
|
|
1618
|
+
});
|
|
1619
|
+
const { data, error: remoteError } = await getScanRemoteRepoValues({ usage: TASK_REPO_USAGE });
|
|
1620
|
+
if (remoteError) {
|
|
1621
|
+
throw new Error(remoteError.message);
|
|
1622
|
+
}
|
|
1623
|
+
remoteValues = data ?? [];
|
|
1624
|
+
logger.debug("遠程 Repo 查詢完成", {
|
|
1625
|
+
topic: LOG_TOPIC_TASK,
|
|
1626
|
+
data: { count: remoteValues.length, values: remoteValues },
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
const availableTaskList = [...new Set([...local.registered, ...remoteValues])];
|
|
1630
|
+
if (availableTaskList.length === 0) {
|
|
1631
|
+
throw new Error("任務註冊失敗:本地與遠程均無可用任務類型");
|
|
1632
|
+
}
|
|
1633
|
+
logger.success("任務註冊完成", {
|
|
1634
|
+
topic: LOG_TOPIC_TASK,
|
|
1635
|
+
data: {
|
|
1636
|
+
availableTaskList,
|
|
1637
|
+
localRegistered: local.registered,
|
|
1638
|
+
remoteValues,
|
|
1639
|
+
},
|
|
1640
|
+
});
|
|
1641
|
+
return {
|
|
1642
|
+
availableTaskList,
|
|
1643
|
+
local,
|
|
1644
|
+
remote: { values: remoteValues },
|
|
1645
|
+
includeRemote,
|
|
1646
|
+
};
|
|
1647
|
+
}
|
|
1648
|
+
/** 将 taskFn 与 params 闭包绑定,供 TaskNode 无参调用 `await taskFn()` */
|
|
1649
|
+
function bindTaskFn(taskFn, taskParams) {
|
|
1650
|
+
const run = async () => taskFn(taskParams);
|
|
1651
|
+
return Object.assign(run, {
|
|
1652
|
+
displayName: taskFn.displayName,
|
|
1653
|
+
taskTypeKey: taskFn.taskTypeKey,
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
/**
|
|
1657
|
+
* 將 Supabase 認領到的 Task 解析為可執行的 `ExecutableTaskFn`。
|
|
1658
|
+
*
|
|
1659
|
+
* 流程:
|
|
1660
|
+
* 1. 從 task.details 取出 `params`;`task.value` 作為 taskTypeKey(同 {@link Repo.value})
|
|
1661
|
+
* 2. {@link TaskRepoValidation.validate} — 本地註冊優先;未命中時可選遠端(`includeRemote`)
|
|
1662
|
+
* Worker 在 registry 未命中時會補掃本地(`local_bootstrap_on_miss`)
|
|
1663
|
+
* 3. bindTaskFn,返回閉包後的無參執行函數
|
|
1664
|
+
*
|
|
1665
|
+
* 失敗時返回 `{ isSuccess: false, code, message, reason, step }`(不 throw)。
|
|
1666
|
+
*/
|
|
1667
|
+
const prepareTask = async ({ logger, task, includeRemote = true, bootstrapLocalOptions, }) => {
|
|
1668
|
+
const fail = (params) => {
|
|
1669
|
+
logger.warn(params.message, {
|
|
1670
|
+
topic: LOG_TOPIC_TASK,
|
|
1671
|
+
data: {
|
|
1672
|
+
taskId: task.id,
|
|
1673
|
+
taskTypeKey: task.value,
|
|
1674
|
+
reason: params.reason,
|
|
1675
|
+
step: params.step,
|
|
1676
|
+
},
|
|
1677
|
+
});
|
|
1678
|
+
return {
|
|
1679
|
+
isSuccess: false,
|
|
1680
|
+
taskFn: null,
|
|
1681
|
+
code: params.code,
|
|
1682
|
+
message: params.message,
|
|
1683
|
+
reason: params.reason,
|
|
1684
|
+
step: params.step,
|
|
1685
|
+
};
|
|
1686
|
+
};
|
|
1687
|
+
const { id: taskId, name: taskName, value: taskTypeKey, details } = task;
|
|
1688
|
+
logger.debug(`開始準備任務 ${taskName}-${taskTypeKey}`, {
|
|
1689
|
+
topic: LOG_TOPIC_TASK,
|
|
1690
|
+
data: { taskId, taskName, taskTypeKey },
|
|
1691
|
+
});
|
|
1692
|
+
const { params: taskParams } = details;
|
|
1693
|
+
if (!getRegisteredLocalTaskTypeKeys().includes(taskTypeKey)) {
|
|
1694
|
+
const bootstrap = await bootstrapLocalTasks({
|
|
1695
|
+
...bootstrapLocalOptions,
|
|
1696
|
+
forTaskTypeKey: taskTypeKey,
|
|
1697
|
+
});
|
|
1698
|
+
logger.debug("本地 registry 未命中,補掃本地任務", {
|
|
1699
|
+
topic: LOG_TOPIC_TASK,
|
|
1700
|
+
data: {
|
|
1701
|
+
taskId,
|
|
1702
|
+
taskTypeKey,
|
|
1703
|
+
step: "local_bootstrap_on_miss",
|
|
1704
|
+
status: bootstrap.status,
|
|
1705
|
+
registered: bootstrap.registered,
|
|
1706
|
+
cached: bootstrap.cached ?? false,
|
|
1707
|
+
message: bootstrap.message,
|
|
1708
|
+
},
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
const validation = await TaskRepoValidation.validate({
|
|
1712
|
+
logger,
|
|
1713
|
+
taskTypeKey,
|
|
1714
|
+
params: taskParams,
|
|
1715
|
+
includeRemote,
|
|
1716
|
+
});
|
|
1717
|
+
if (!validation.isValid) {
|
|
1718
|
+
return fail({
|
|
1719
|
+
reason: validation.reason,
|
|
1720
|
+
code: validation.code,
|
|
1721
|
+
message: validation.message,
|
|
1722
|
+
step: validation.step,
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
const { repoContext } = validation;
|
|
1726
|
+
logger.debug("Repo 上下文加載成功", {
|
|
1727
|
+
topic: LOG_TOPIC_TASK,
|
|
1728
|
+
data: {
|
|
1729
|
+
taskTypeKey,
|
|
1730
|
+
taskFnDisplayName: repoContext.taskFn.displayName,
|
|
1731
|
+
step: "repo_load",
|
|
1732
|
+
},
|
|
1733
|
+
});
|
|
1734
|
+
const boundTaskFn = bindTaskFn(repoContext.taskFn, taskParams);
|
|
1735
|
+
logger.debug("任務準備完成", {
|
|
1736
|
+
topic: LOG_TOPIC_TASK,
|
|
1737
|
+
data: {
|
|
1738
|
+
taskId,
|
|
1739
|
+
taskTypeKey,
|
|
1740
|
+
displayName: boundTaskFn.displayName,
|
|
1741
|
+
step: "bind",
|
|
1742
|
+
},
|
|
1743
|
+
});
|
|
1744
|
+
return {
|
|
1745
|
+
isSuccess: true,
|
|
1746
|
+
taskFn: boundTaskFn,
|
|
1747
|
+
};
|
|
1748
|
+
};
|
|
1749
|
+
const TaskManager = {
|
|
1750
|
+
registerTasks,
|
|
1751
|
+
prepareTask,
|
|
1752
|
+
bootstrapLocalTasks,
|
|
1753
|
+
getRegisteredLocalTaskTypeKeys,
|
|
1754
|
+
clearBootstrapLocalTasksCache,
|
|
1755
|
+
};
|
|
1756
|
+
|
|
1757
|
+
/**
|
|
1758
|
+
* Task worker node: commands → heartbeat → claim & execute tasks.
|
|
1759
|
+
* Built on {@link BaseNodeRuntime}.
|
|
1760
|
+
*/
|
|
1761
|
+
class TaskNode extends BaseNodeRuntime {
|
|
1762
|
+
_availableTaskList = [];
|
|
1763
|
+
_includeRemote = true;
|
|
1764
|
+
get availableTaskList() {
|
|
1765
|
+
return [...this._availableTaskList];
|
|
1766
|
+
}
|
|
1767
|
+
constructor() {
|
|
1768
|
+
super("taskNode");
|
|
1769
|
+
}
|
|
1770
|
+
async onBeforeRegisterNode(logger) {
|
|
1771
|
+
const { availableTaskList, includeRemote } = await TaskManager.registerTasks({ logger });
|
|
1772
|
+
this._availableTaskList = availableTaskList;
|
|
1773
|
+
this._includeRemote = includeRemote;
|
|
1774
|
+
}
|
|
1775
|
+
async runLoopSteps(ctx, heartbeatOk) {
|
|
1776
|
+
if (!heartbeatOk) {
|
|
1777
|
+
const logger = createLogger({
|
|
1778
|
+
scope: createScope({
|
|
1779
|
+
module: `${this.runtimeModule}-runLoopSteps`,
|
|
1780
|
+
traceId: ctx.loopTraceId,
|
|
1781
|
+
labels: { nodeId: ctx.nodeId },
|
|
1782
|
+
}),
|
|
1783
|
+
});
|
|
1784
|
+
logger.warn("心跳失敗,本輪跳過任務認領", { topic: LOG_TOPIC_NODE });
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
await this.runAsWorker(ctx);
|
|
1788
|
+
}
|
|
1789
|
+
async runAsWorker(ctx) {
|
|
1790
|
+
const { loopTraceId, nodeId } = ctx;
|
|
1791
|
+
const loopScope = createScope({ module: "runAsWorker", traceId: loopTraceId, labels: { nodeId } });
|
|
1792
|
+
const logger = createLogger({ scope: loopScope });
|
|
1793
|
+
const availableTaskList = this.availableTaskList;
|
|
1794
|
+
if (availableTaskList.length === 0) {
|
|
1795
|
+
logger.warn("未配置可認領任務類型,跳過本輪", { topic: LOG_TOPIC_TASK });
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
logger.debug("向 Supabase 認領任務", {
|
|
1799
|
+
topic: LOG_TOPIC_TASK,
|
|
1800
|
+
data: { availableTaskList },
|
|
1801
|
+
});
|
|
1802
|
+
const { data: task, error: claimError } = await patchClaimTask({
|
|
1803
|
+
nodeId,
|
|
1804
|
+
availableTaskList: [...availableTaskList],
|
|
1805
|
+
traceId: loopTraceId,
|
|
1806
|
+
});
|
|
1807
|
+
if (claimError) {
|
|
1808
|
+
if (isOptimisticLockResponse(claimError)) {
|
|
1809
|
+
logger.debug("認領競爭失敗,本輪無可認領任務", {
|
|
1810
|
+
topic: LOG_TOPIC_TASK,
|
|
1811
|
+
data: { availableTaskList },
|
|
1812
|
+
});
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
logger.error("認領任務失敗", {
|
|
1816
|
+
topic: LOG_TOPIC_TASK,
|
|
1817
|
+
data: {
|
|
1818
|
+
availableTaskList,
|
|
1819
|
+
error: claimError.message,
|
|
1820
|
+
code: claimError.code,
|
|
1821
|
+
},
|
|
1822
|
+
});
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
if (task == null) {
|
|
1826
|
+
logger.debug("本輪無匹配的 TODO 任務", {
|
|
1827
|
+
topic: LOG_TOPIC_TASK,
|
|
1828
|
+
data: { availableTaskList },
|
|
1829
|
+
});
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
const taskTraceId = task.details.traceId ?? CRITICAL_UNKNOWN_TASK_TRACE_ID;
|
|
1833
|
+
const taskScope = createScope({
|
|
1834
|
+
module: "runAsWorker",
|
|
1835
|
+
traceId: loopTraceId,
|
|
1836
|
+
labels: { nodeId },
|
|
1837
|
+
traceParentId: taskTraceId,
|
|
1838
|
+
});
|
|
1839
|
+
const taskLogger = createLogger({ scope: taskScope });
|
|
1840
|
+
taskLogger.success("認領任務成功,準備執行", {
|
|
1841
|
+
topic: LOG_TOPIC_TASK,
|
|
1842
|
+
data: {
|
|
1843
|
+
taskId: task.id,
|
|
1844
|
+
taskName: task.name,
|
|
1845
|
+
taskTypeKey: task.value,
|
|
1846
|
+
taskStatus: task.details.status,
|
|
1847
|
+
},
|
|
1848
|
+
});
|
|
1849
|
+
const prepareLogger = createLogger({ scope: withModule(taskScope, "prepareTask") });
|
|
1850
|
+
const prepareResult = await TaskManager.prepareTask({
|
|
1851
|
+
logger: prepareLogger,
|
|
1852
|
+
task,
|
|
1853
|
+
includeRemote: this._includeRemote,
|
|
1854
|
+
});
|
|
1855
|
+
const { isSuccess: isPrepareSuccess, taskFn, code, message, reason, step } = prepareResult;
|
|
1856
|
+
if (!isPrepareSuccess || taskFn == null) {
|
|
1857
|
+
prepareLogger.critical("任務準備失敗,無法繼續執行,中止任務並 RESET 為 OPEN", {
|
|
1858
|
+
topic: LOG_TOPIC_TASK,
|
|
1859
|
+
data: {
|
|
1860
|
+
taskId: task.id,
|
|
1861
|
+
taskTypeKey: task.value,
|
|
1862
|
+
code,
|
|
1863
|
+
message,
|
|
1864
|
+
reason,
|
|
1865
|
+
step,
|
|
1866
|
+
},
|
|
1867
|
+
});
|
|
1868
|
+
await this.abortTaskRun({
|
|
1869
|
+
logger: prepareLogger,
|
|
1870
|
+
taskId: task.id,
|
|
1871
|
+
nodeId,
|
|
1872
|
+
traceId: taskTraceId,
|
|
1873
|
+
});
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
logger.success("任務準備完成", {
|
|
1877
|
+
topic: LOG_TOPIC_TASK,
|
|
1878
|
+
data: {
|
|
1879
|
+
taskId: task.id,
|
|
1880
|
+
taskName: task.name,
|
|
1881
|
+
taskTypeKey: task.value,
|
|
1882
|
+
displayName: taskFn.displayName,
|
|
1883
|
+
},
|
|
1884
|
+
});
|
|
1885
|
+
const executeLogger = createLogger({ scope: withModule(taskScope, "executeTask") });
|
|
1886
|
+
const onExecuteAbort = async (error) => {
|
|
1887
|
+
executeLogger.critical("任務執行遇到未知錯誤,中止任務並 RESET 為 OPEN", {
|
|
1888
|
+
topic: LOG_TOPIC_TASK,
|
|
1889
|
+
data: {
|
|
1890
|
+
taskId: task.id,
|
|
1891
|
+
taskTypeKey: task.value,
|
|
1892
|
+
taskName: task.name,
|
|
1893
|
+
displayName: taskFn.displayName,
|
|
1894
|
+
error: getErrorMessage(error),
|
|
1895
|
+
},
|
|
1896
|
+
});
|
|
1897
|
+
await this.abortTaskRun({
|
|
1898
|
+
logger: executeLogger,
|
|
1899
|
+
taskId: task.id,
|
|
1900
|
+
nodeId,
|
|
1901
|
+
traceId: taskTraceId,
|
|
1902
|
+
});
|
|
1903
|
+
};
|
|
1904
|
+
executeLogger.info("開始執行業務邏輯", {
|
|
1905
|
+
topic: LOG_TOPIC_TASK,
|
|
1906
|
+
data: { taskId: task.id, displayName: taskFn.displayName },
|
|
1907
|
+
});
|
|
1908
|
+
const startTime = Date.now();
|
|
1909
|
+
let result = null;
|
|
1910
|
+
try {
|
|
1911
|
+
result = await taskFn();
|
|
1912
|
+
}
|
|
1913
|
+
catch (error) {
|
|
1914
|
+
await onExecuteAbort(toError(error));
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
if (result == null) {
|
|
1918
|
+
await onExecuteAbort(new Error("任務執行結果為空"));
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
const { isSuccess: isTaskSuccess, cost, extra } = result ?? { isSuccess: false, cost: 0, extra: null };
|
|
1922
|
+
if (isTaskSuccess) {
|
|
1923
|
+
await this.finalizeTaskRun("success", {
|
|
1924
|
+
logger: executeLogger,
|
|
1925
|
+
taskId: task.id,
|
|
1926
|
+
taskTypeKey: task.value,
|
|
1927
|
+
nodeId,
|
|
1928
|
+
cost,
|
|
1929
|
+
extra,
|
|
1930
|
+
traceId: taskTraceId,
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
else {
|
|
1934
|
+
await this.finalizeTaskRun("failure", {
|
|
1935
|
+
logger: executeLogger,
|
|
1936
|
+
taskId: task.id,
|
|
1937
|
+
taskTypeKey: task.value,
|
|
1938
|
+
nodeId,
|
|
1939
|
+
cost,
|
|
1940
|
+
extra,
|
|
1941
|
+
traceId: taskTraceId,
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1944
|
+
const duration = Date.now() - startTime;
|
|
1945
|
+
executeLogger.info("業務邏輯執行結束", {
|
|
1946
|
+
topic: LOG_TOPIC_TASK,
|
|
1947
|
+
data: {
|
|
1948
|
+
taskId: task.id,
|
|
1949
|
+
taskName: task.name,
|
|
1950
|
+
taskTypeKey: task.value,
|
|
1951
|
+
displayName: taskFn.displayName,
|
|
1952
|
+
durationMs: duration,
|
|
1953
|
+
},
|
|
1954
|
+
});
|
|
1955
|
+
}
|
|
1956
|
+
async abortTaskRun(params) {
|
|
1957
|
+
const { logger, taskId, nodeId, traceId, extra } = params;
|
|
1958
|
+
const logData = { taskId, nodeId };
|
|
1959
|
+
const extraValue = typeof extra === "string" ? extra : undefined;
|
|
1960
|
+
const { error } = await patchChangeTaskStatus({
|
|
1961
|
+
id: taskId,
|
|
1962
|
+
action: TaskStatusAction.RESET,
|
|
1963
|
+
nodeId,
|
|
1964
|
+
extra: extraValue,
|
|
1965
|
+
traceId,
|
|
1966
|
+
});
|
|
1967
|
+
if (error) {
|
|
1968
|
+
logger.error(error.message, { topic: LOG_TOPIC_TASK, data: logData });
|
|
1969
|
+
return false;
|
|
1970
|
+
}
|
|
1971
|
+
logger.info("任務已中止並 RESET 為 OPEN", { topic: LOG_TOPIC_TASK, data: logData });
|
|
1972
|
+
return true;
|
|
1973
|
+
}
|
|
1974
|
+
async finalizeTaskRun(outcome, params) {
|
|
1975
|
+
const { logger, taskId, nodeId, taskTypeKey, cost, extra, traceId } = params;
|
|
1976
|
+
const outcomePrefix = outcome === "success" ? "任務成功" : "任務失敗";
|
|
1977
|
+
const logData = {
|
|
1978
|
+
taskTypeKey,
|
|
1979
|
+
taskId,
|
|
1980
|
+
nodeId,
|
|
1981
|
+
...(extra != null && extra !== "" ? { extra } : {}),
|
|
1982
|
+
};
|
|
1983
|
+
const extraValue = typeof extra === "string" ? extra : undefined;
|
|
1984
|
+
const { error: changeTaskStatusError } = outcome === "success"
|
|
1985
|
+
? await patchChangeTaskStatus({
|
|
1986
|
+
id: taskId,
|
|
1987
|
+
action: TaskStatusAction.FINISH,
|
|
1988
|
+
nodeId,
|
|
1989
|
+
cost,
|
|
1990
|
+
extra: extraValue,
|
|
1991
|
+
traceId,
|
|
1992
|
+
})
|
|
1993
|
+
: await patchChangeTaskStatus({
|
|
1994
|
+
id: taskId,
|
|
1995
|
+
action: TaskStatusAction.RESET,
|
|
1996
|
+
nodeId,
|
|
1997
|
+
extra: extraValue,
|
|
1998
|
+
traceId,
|
|
1999
|
+
});
|
|
2000
|
+
if (changeTaskStatusError) {
|
|
2001
|
+
const upperMessage = `${outcomePrefix}但是更新 TaskStatus 失敗,產生孤兒 Task,請儘快排查問題`;
|
|
2002
|
+
logger.critical(upperMessage, { topic: LOG_TOPIC_TASK, data: logData });
|
|
2003
|
+
// TODO: 需要處理孤兒 Task
|
|
2004
|
+
throw new Error(upperMessage);
|
|
2005
|
+
}
|
|
2006
|
+
logger.info("更新 TaskStatus 成功", { topic: LOG_TOPIC_TASK, data: logData });
|
|
2007
|
+
if (outcome === "success") {
|
|
2008
|
+
logger.success("任務執行結束,業務邏輯成功", { topic: LOG_TOPIC_TASK, data: logData });
|
|
2009
|
+
}
|
|
2010
|
+
else {
|
|
2011
|
+
logger.warn("任務執行結束,業務邏輯失敗", { topic: LOG_TOPIC_TASK, data: logData });
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
/**
|
|
2017
|
+
* Create a task row with publish-time Repo + params validation.
|
|
2018
|
+
* **Node entry only** — requires local Repo bootstrap and {@link TaskRepoValidation}.
|
|
2019
|
+
*
|
|
2020
|
+
* @see target-supabase-sdk/node
|
|
2021
|
+
*/
|
|
2022
|
+
const postTaskWithValidation = validateWithSchema(postTaskSchema, "postTaskSchema")(async ({ name, value, params, taskStatus, tagList, extra, traceId, traceParentId }) => {
|
|
2023
|
+
const taskTraceId = traceId?.trim() || logManager.generateTraceId();
|
|
2024
|
+
const logger = createLogger({
|
|
2025
|
+
module: "postTaskWithValidation",
|
|
2026
|
+
traceId: taskTraceId,
|
|
2027
|
+
traceParentId: traceParentId ?? null,
|
|
2028
|
+
});
|
|
2029
|
+
const validation = await TaskRepoValidation.validate({
|
|
2030
|
+
logger,
|
|
2031
|
+
taskTypeKey: value,
|
|
2032
|
+
params,
|
|
2033
|
+
bootstrapLocal: true,
|
|
2034
|
+
});
|
|
2035
|
+
if (!validation.isValid) {
|
|
2036
|
+
logger.warn(validation.message, {
|
|
2037
|
+
topic: "task",
|
|
2038
|
+
data: {
|
|
2039
|
+
taskTypeKey: value,
|
|
2040
|
+
reason: validation.reason,
|
|
2041
|
+
step: validation.step,
|
|
2042
|
+
},
|
|
2043
|
+
});
|
|
2044
|
+
return generateResponse.error(validation.message, undefined, String(validation.code));
|
|
2045
|
+
}
|
|
2046
|
+
return createTaskRow({ name, value, params, taskStatus, tagList, extra, traceParentId, taskTraceId }, logger);
|
|
2047
|
+
});
|
|
2048
|
+
|
|
2049
|
+
var CategoryTrigger;
|
|
2050
|
+
(function (CategoryTrigger) {
|
|
2051
|
+
CategoryTrigger["TRIGGER"] = "trigger";
|
|
2052
|
+
})(CategoryTrigger || (CategoryTrigger = {}));
|
|
2053
|
+
var TriggerStatus;
|
|
2054
|
+
(function (TriggerStatus) {
|
|
2055
|
+
TriggerStatus["ENABLED"] = "ENABLED";
|
|
2056
|
+
TriggerStatus["DISABLED"] = "DISABLED";
|
|
2057
|
+
TriggerStatus["PAUSED"] = "PAUSED";
|
|
2058
|
+
})(TriggerStatus || (TriggerStatus = {}));
|
|
2059
|
+
|
|
2060
|
+
const TRIGGER_STATUS_FIELD = "details->>status";
|
|
2061
|
+
const traceIdSchema = z.string().trim().min(1).optional();
|
|
2062
|
+
const dailyScheduleSchema = z.object({
|
|
2063
|
+
kind: z.literal("daily"),
|
|
2064
|
+
hour: z.number().int().min(0).max(23),
|
|
2065
|
+
minute: z.number().int().min(0).max(59),
|
|
2066
|
+
});
|
|
2067
|
+
const postTaskActionSchema = z.object({
|
|
2068
|
+
kind: z.literal("post_task"),
|
|
2069
|
+
taskTypeKey: z.string().trim().min(1),
|
|
2070
|
+
taskParams: z.unknown(),
|
|
2071
|
+
taskName: z.string().trim().min(1).optional(),
|
|
2072
|
+
taskStatus: z.enum([TaskStatus.OPEN, TaskStatus.TODO]).optional(),
|
|
2073
|
+
});
|
|
2074
|
+
const postTriggerSchema = z.object({
|
|
2075
|
+
name: z.string().trim().min(1),
|
|
2076
|
+
value: z.string().trim().min(1),
|
|
2077
|
+
status: z.nativeEnum(TriggerStatus).optional().default(TriggerStatus.ENABLED),
|
|
2078
|
+
schedule: dailyScheduleSchema,
|
|
2079
|
+
action: postTaskActionSchema,
|
|
2080
|
+
tagList: z.array(z.string()).optional().default([]),
|
|
2081
|
+
traceId: traceIdSchema,
|
|
2082
|
+
});
|
|
2083
|
+
const scanEnabledTriggersSchema = z.object({
|
|
2084
|
+
traceId: traceIdSchema,
|
|
2085
|
+
});
|
|
2086
|
+
const patchTriggerFiredSchema = z.object({
|
|
2087
|
+
triggerId: z.string().trim().min(1),
|
|
2088
|
+
fireKey: z.string().trim().min(1),
|
|
2089
|
+
expectedLastFireKey: z.string().nullable().optional(),
|
|
2090
|
+
traceId: traceIdSchema,
|
|
2091
|
+
});
|
|
2092
|
+
function enabledTriggerFilter() {
|
|
2093
|
+
return [{ field: TRIGGER_STATUS_FIELD, operator: "eq", value: TriggerStatus.ENABLED }];
|
|
2094
|
+
}
|
|
2095
|
+
/** List all ENABLED triggers (Phase 1: in-memory due check). */
|
|
2096
|
+
const scanEnabledTriggers = validateWithSchema(scanEnabledTriggersSchema, "scanEnabledTriggersSchema")(async ({ traceId }) => {
|
|
2097
|
+
const logger = createLogger({ module: "scanEnabledTriggers", traceId });
|
|
2098
|
+
try {
|
|
2099
|
+
const { data: triggerList } = await scanTargetList({
|
|
2100
|
+
category: CategoryTrigger.TRIGGER,
|
|
2101
|
+
filterList: enabledTriggerFilter(),
|
|
2102
|
+
orderBy: { field: "created_at", ascending: true },
|
|
2103
|
+
});
|
|
2104
|
+
const rows = triggerList ?? [];
|
|
2105
|
+
logger.debug("已掃描 ENABLED Trigger", {
|
|
2106
|
+
topic: "trigger",
|
|
2107
|
+
data: { count: rows.length },
|
|
2108
|
+
});
|
|
2109
|
+
return generateResponse.success(rows);
|
|
2110
|
+
}
|
|
2111
|
+
catch (error) {
|
|
2112
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2113
|
+
return generateResponse.error(message);
|
|
2114
|
+
}
|
|
2115
|
+
});
|
|
2116
|
+
/** Create a trigger row (scheduler / admin). */
|
|
2117
|
+
const postTrigger = validateWithSchema(postTriggerSchema, "postTriggerSchema")(async ({ name, value, status, schedule, action, tagList, traceId }) => {
|
|
2118
|
+
const logger = createLogger({ module: "postTrigger", traceId });
|
|
2119
|
+
const details = {
|
|
2120
|
+
manifestVersion: 0,
|
|
2121
|
+
status,
|
|
2122
|
+
schedule: schedule,
|
|
2123
|
+
action: action,
|
|
2124
|
+
lastFiredAt: null,
|
|
2125
|
+
lastFireKey: null,
|
|
2126
|
+
};
|
|
2127
|
+
const result = await createTarget({
|
|
2128
|
+
payload: { name, value, status, schedule, action, tagList, traceId },
|
|
2129
|
+
createFn: () => ({
|
|
2130
|
+
name,
|
|
2131
|
+
value,
|
|
2132
|
+
category: CategoryTrigger.TRIGGER,
|
|
2133
|
+
tagList,
|
|
2134
|
+
details,
|
|
2135
|
+
}),
|
|
2136
|
+
});
|
|
2137
|
+
logger.info("Trigger 已創建", {
|
|
2138
|
+
topic: "trigger",
|
|
2139
|
+
data: { triggerId: result.data?.id, triggerKey: value, status },
|
|
2140
|
+
});
|
|
2141
|
+
return result;
|
|
2142
|
+
});
|
|
2143
|
+
/**
|
|
2144
|
+
* Mark trigger as fired for `fireKey`.
|
|
2145
|
+
* When `expectedLastFireKey` is set, uses optimistic lock on that field.
|
|
2146
|
+
*/
|
|
2147
|
+
const patchTriggerFired = validateWithSchema(patchTriggerFiredSchema, "patchTriggerFiredSchema")(async ({ triggerId, fireKey, expectedLastFireKey, traceId }) => {
|
|
2148
|
+
const logger = createLogger({ module: "patchTriggerFired", traceId });
|
|
2149
|
+
const now = Date.now();
|
|
2150
|
+
try {
|
|
2151
|
+
const optimisticLockFilterList = expectedLastFireKey != null && expectedLastFireKey !== ""
|
|
2152
|
+
? [{ field: "details->>lastFireKey", operator: "eq", value: expectedLastFireKey }]
|
|
2153
|
+
: [];
|
|
2154
|
+
const data = await updateTargetDetails({
|
|
2155
|
+
id: triggerId,
|
|
2156
|
+
optimisticLockFilterList,
|
|
2157
|
+
updateFn: (current) => ({
|
|
2158
|
+
...current,
|
|
2159
|
+
lastFireKey: fireKey,
|
|
2160
|
+
lastFiredAt: now,
|
|
2161
|
+
}),
|
|
2162
|
+
});
|
|
2163
|
+
logger.success("Trigger 觸發記錄已更新", {
|
|
2164
|
+
topic: "trigger",
|
|
2165
|
+
data: { triggerId, fireKey },
|
|
2166
|
+
});
|
|
2167
|
+
return generateResponse.success(data);
|
|
2168
|
+
}
|
|
2169
|
+
catch (error) {
|
|
2170
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2171
|
+
logger.warn("Trigger 觸發記錄更新失敗", {
|
|
2172
|
+
topic: "trigger",
|
|
2173
|
+
data: { triggerId, fireKey, error: message },
|
|
2174
|
+
});
|
|
2175
|
+
return generateResponse.error(message);
|
|
2176
|
+
}
|
|
2177
|
+
});
|
|
2178
|
+
|
|
2179
|
+
const LOG_TOPIC_TRIGGER = "trigger";
|
|
2180
|
+
/** Fixed main-loop interval for {@link TriggerNode} (ms). */
|
|
2181
|
+
const TRIGGER_LOOP_INTERVAL_MS = 60_000;
|
|
2182
|
+
/** Default extra retries after first failure within one runner tick. */
|
|
2183
|
+
const TRIGGER_RUNNER_DEFAULT_RETRY_COUNT = 3;
|
|
2184
|
+
/** Default delay between retry attempts within the same tick (ms). */
|
|
2185
|
+
const TRIGGER_RUNNER_DEFAULT_RETRY_DELAY_MS = 0;
|
|
2186
|
+
|
|
2187
|
+
const runnerRegistry = new Map();
|
|
2188
|
+
let registrationClosed = false;
|
|
2189
|
+
function normalizeRunnerKey(key) {
|
|
2190
|
+
const normalized = key.trim();
|
|
2191
|
+
if (normalized.length === 0) {
|
|
2192
|
+
throw new Error("[TriggerManager] key must be a non-empty string");
|
|
2193
|
+
}
|
|
2194
|
+
return normalized;
|
|
2195
|
+
}
|
|
2196
|
+
function tryNormalizeRunnerKey(key) {
|
|
2197
|
+
const normalized = key.trim();
|
|
2198
|
+
return normalized.length > 0 ? normalized : null;
|
|
2199
|
+
}
|
|
2200
|
+
function validateRunnerOptions(options) {
|
|
2201
|
+
if (!Number.isFinite(options.intervalMs) || options.intervalMs < 1) {
|
|
2202
|
+
throw new Error(`[TriggerManager] intervalMs must be a positive number, got ${options.intervalMs}`);
|
|
2203
|
+
}
|
|
2204
|
+
const retryCount = options.retryCount ?? TRIGGER_RUNNER_DEFAULT_RETRY_COUNT;
|
|
2205
|
+
if (!Number.isInteger(retryCount) || retryCount < 0) {
|
|
2206
|
+
throw new Error(`[TriggerManager] retryCount must be a non-negative integer, got ${retryCount}`);
|
|
2207
|
+
}
|
|
2208
|
+
const initialDelayMs = options.initialDelayMs ?? 0;
|
|
2209
|
+
if (!Number.isFinite(initialDelayMs) || initialDelayMs < 0) {
|
|
2210
|
+
throw new Error(`[TriggerManager] initialDelayMs must be a non-negative number, got ${initialDelayMs}`);
|
|
2211
|
+
}
|
|
2212
|
+
const retryDelayMs = options.retryDelayMs ?? TRIGGER_RUNNER_DEFAULT_RETRY_DELAY_MS;
|
|
2213
|
+
if (!Number.isFinite(retryDelayMs) || retryDelayMs < 0) {
|
|
2214
|
+
throw new Error(`[TriggerManager] retryDelayMs must be a non-negative number, got ${retryDelayMs}`);
|
|
2215
|
+
}
|
|
2216
|
+
if (options.timeoutMs != null) {
|
|
2217
|
+
if (!Number.isFinite(options.timeoutMs) || options.timeoutMs < 1) {
|
|
2218
|
+
throw new Error(`[TriggerManager] timeoutMs must be a positive number when set, got ${options.timeoutMs}`);
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
function delay(ms) {
|
|
2223
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2224
|
+
}
|
|
2225
|
+
async function invokeRunnerFn(fn, ctx, timeoutMs) {
|
|
2226
|
+
if (timeoutMs == null) {
|
|
2227
|
+
await fn(ctx);
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
2230
|
+
let timeoutHandle;
|
|
2231
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
2232
|
+
timeoutHandle = setTimeout(() => {
|
|
2233
|
+
reject(new Error(`Runner timed out after ${timeoutMs}ms`));
|
|
2234
|
+
}, timeoutMs);
|
|
2235
|
+
});
|
|
2236
|
+
try {
|
|
2237
|
+
await Promise.race([Promise.resolve(fn(ctx)), timeoutPromise]);
|
|
2238
|
+
}
|
|
2239
|
+
finally {
|
|
2240
|
+
if (timeoutHandle != null) {
|
|
2241
|
+
clearTimeout(timeoutHandle);
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
/** In-process registry and tick loop for {@link TriggerNode}. */
|
|
2246
|
+
const TriggerManager = {
|
|
2247
|
+
registerRunner(options) {
|
|
2248
|
+
if (registrationClosed) {
|
|
2249
|
+
throw new Error("[TriggerManager] Registration closed — call registerRunner before TriggerNode.start()");
|
|
2250
|
+
}
|
|
2251
|
+
const key = normalizeRunnerKey(options.key);
|
|
2252
|
+
validateRunnerOptions(options);
|
|
2253
|
+
if (typeof options.fn !== "function") {
|
|
2254
|
+
throw new Error(`[TriggerManager] fn must be a function (runner key: ${key})`);
|
|
2255
|
+
}
|
|
2256
|
+
if (runnerRegistry.has(key)) {
|
|
2257
|
+
throw new Error(`[TriggerManager] Duplicate runner key: ${key}`);
|
|
2258
|
+
}
|
|
2259
|
+
const now = Date.now();
|
|
2260
|
+
runnerRegistry.set(key, {
|
|
2261
|
+
key,
|
|
2262
|
+
intervalMs: options.intervalMs,
|
|
2263
|
+
fn: options.fn,
|
|
2264
|
+
retryCount: options.retryCount ?? TRIGGER_RUNNER_DEFAULT_RETRY_COUNT,
|
|
2265
|
+
retryDelayMs: options.retryDelayMs ?? TRIGGER_RUNNER_DEFAULT_RETRY_DELAY_MS,
|
|
2266
|
+
timeoutMs: options.timeoutMs,
|
|
2267
|
+
nextRunAt: now + (options.initialDelayMs ?? 0),
|
|
2268
|
+
running: false,
|
|
2269
|
+
});
|
|
2270
|
+
},
|
|
2271
|
+
hasRunner(key) {
|
|
2272
|
+
const normalized = tryNormalizeRunnerKey(key);
|
|
2273
|
+
return normalized != null && runnerRegistry.has(normalized);
|
|
2274
|
+
},
|
|
2275
|
+
unregisterRunner(key) {
|
|
2276
|
+
const normalized = tryNormalizeRunnerKey(key);
|
|
2277
|
+
if (normalized == null) {
|
|
2278
|
+
return false;
|
|
2279
|
+
}
|
|
2280
|
+
const runner = runnerRegistry.get(normalized);
|
|
2281
|
+
if (runner == null) {
|
|
2282
|
+
return false;
|
|
2283
|
+
}
|
|
2284
|
+
if (runner.running) {
|
|
2285
|
+
throw new Error(`[TriggerManager] Cannot unregister running runner: ${normalized}`);
|
|
2286
|
+
}
|
|
2287
|
+
return runnerRegistry.delete(normalized);
|
|
2288
|
+
},
|
|
2289
|
+
getRunnerKeys() {
|
|
2290
|
+
return [...runnerRegistry.keys()];
|
|
2291
|
+
},
|
|
2292
|
+
getRunnersBelowLoopInterval() {
|
|
2293
|
+
return [...runnerRegistry.values()]
|
|
2294
|
+
.filter((runner) => runner.intervalMs < TRIGGER_LOOP_INTERVAL_MS)
|
|
2295
|
+
.map((runner) => ({ key: runner.key, intervalMs: runner.intervalMs }));
|
|
2296
|
+
},
|
|
2297
|
+
/** Called by TriggerNode during bootstrap — blocks further registerRunner calls. */
|
|
2298
|
+
closeRegistration() {
|
|
2299
|
+
registrationClosed = true;
|
|
2300
|
+
},
|
|
2301
|
+
/** Re-open registration (tests only). */
|
|
2302
|
+
_resetRegistrationForTests() {
|
|
2303
|
+
registrationClosed = false;
|
|
2304
|
+
},
|
|
2305
|
+
/**
|
|
2306
|
+
* Remove all runners. Throws if any runner is still executing.
|
|
2307
|
+
* Wait for in-flight ticks to finish before calling in tests.
|
|
2308
|
+
*/
|
|
2309
|
+
clearRunners() {
|
|
2310
|
+
for (const runner of runnerRegistry.values()) {
|
|
2311
|
+
if (runner.running) {
|
|
2312
|
+
throw new Error(`[TriggerManager] Cannot clearRunners while runner is running: ${runner.key}`);
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
runnerRegistry.clear();
|
|
2316
|
+
registrationClosed = false;
|
|
2317
|
+
},
|
|
2318
|
+
async tick(ctx) {
|
|
2319
|
+
const { loopTraceId, nodeId } = ctx;
|
|
2320
|
+
const loopScope = createScope({ module: "tickRunners", traceId: loopTraceId, labels: { nodeId } });
|
|
2321
|
+
const logger = createLogger({ scope: loopScope });
|
|
2322
|
+
const now = Date.now();
|
|
2323
|
+
const runners = [...runnerRegistry.values()];
|
|
2324
|
+
if (runners.length === 0) {
|
|
2325
|
+
logger.debug("無 runner 可調度", { topic: LOG_TOPIC_TRIGGER });
|
|
2326
|
+
return;
|
|
2327
|
+
}
|
|
2328
|
+
// Overlap: if fn is still running when due, skip that tick (no catch-up / backlog).
|
|
2329
|
+
// Safe while intervals are large; missed ticks increase if fn duration exceeds intervalMs.
|
|
2330
|
+
const dueRunners = runners.filter((runner) => !runner.running && now >= runner.nextRunAt);
|
|
2331
|
+
if (dueRunners.length === 0) {
|
|
2332
|
+
logger.debug("本輪無到期 runner", { topic: LOG_TOPIC_TRIGGER });
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2335
|
+
logger.debug("調度到期 runner", {
|
|
2336
|
+
topic: LOG_TOPIC_TRIGGER,
|
|
2337
|
+
data: { due: dueRunners.map((r) => r.key) },
|
|
2338
|
+
});
|
|
2339
|
+
const results = await Promise.all(dueRunners.map((runner) => runRunner(ctx, runner)));
|
|
2340
|
+
const executed = results.filter(Boolean).length;
|
|
2341
|
+
if (executed > 0) {
|
|
2342
|
+
logger.info("Runner 調度完成", {
|
|
2343
|
+
topic: LOG_TOPIC_TRIGGER,
|
|
2344
|
+
data: { due: dueRunners.length, executed },
|
|
2345
|
+
});
|
|
2346
|
+
}
|
|
2347
|
+
else {
|
|
2348
|
+
logger.warn("本輪到期 runner 均未成功", {
|
|
2349
|
+
topic: LOG_TOPIC_TRIGGER,
|
|
2350
|
+
data: {
|
|
2351
|
+
due: dueRunners.length,
|
|
2352
|
+
runnerKeys: dueRunners.map((r) => r.key),
|
|
2353
|
+
},
|
|
2354
|
+
});
|
|
2355
|
+
}
|
|
2356
|
+
},
|
|
2357
|
+
};
|
|
2358
|
+
async function runRunner(ctx, runner) {
|
|
2359
|
+
const { loopTraceId, nodeId } = ctx;
|
|
2360
|
+
if (runner.running) {
|
|
2361
|
+
return false;
|
|
2362
|
+
}
|
|
2363
|
+
runner.running = true;
|
|
2364
|
+
const runnerLogger = createLogger({
|
|
2365
|
+
scope: withModule(createScope({ module: "runRunner", traceId: loopTraceId, labels: { nodeId, runnerKey: runner.key } }), runner.key),
|
|
2366
|
+
});
|
|
2367
|
+
const maxAttempts = 1 + runner.retryCount;
|
|
2368
|
+
let succeeded = false;
|
|
2369
|
+
try {
|
|
2370
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
2371
|
+
try {
|
|
2372
|
+
runnerLogger.info("Runner 觸發", {
|
|
2373
|
+
topic: LOG_TOPIC_TRIGGER,
|
|
2374
|
+
data: { attempt, maxAttempts },
|
|
2375
|
+
});
|
|
2376
|
+
await invokeRunnerFn(runner.fn, {
|
|
2377
|
+
loopTraceId,
|
|
2378
|
+
nodeId,
|
|
2379
|
+
runnerKey: runner.key,
|
|
2380
|
+
logger: runnerLogger,
|
|
2381
|
+
attempt,
|
|
2382
|
+
maxAttempts,
|
|
2383
|
+
}, runner.timeoutMs);
|
|
2384
|
+
runnerLogger.success("Runner 完成", {
|
|
2385
|
+
topic: LOG_TOPIC_TRIGGER,
|
|
2386
|
+
data: { attempt, maxAttempts },
|
|
2387
|
+
});
|
|
2388
|
+
succeeded = true;
|
|
2389
|
+
break;
|
|
2390
|
+
}
|
|
2391
|
+
catch (error) {
|
|
2392
|
+
const message = getErrorMessage(error);
|
|
2393
|
+
if (attempt < maxAttempts) {
|
|
2394
|
+
runnerLogger.warn("Runner 失敗,準備重試", {
|
|
2395
|
+
topic: LOG_TOPIC_TRIGGER,
|
|
2396
|
+
data: { attempt, maxAttempts, error: message },
|
|
2397
|
+
});
|
|
2398
|
+
if (runner.retryDelayMs > 0) {
|
|
2399
|
+
await delay(runner.retryDelayMs);
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
else {
|
|
2403
|
+
runnerLogger.error("Runner 重試用盡仍失敗", {
|
|
2404
|
+
topic: LOG_TOPIC_TRIGGER,
|
|
2405
|
+
data: { attempt, maxAttempts, error: message },
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
}
|
|
2411
|
+
finally {
|
|
2412
|
+
runner.running = false;
|
|
2413
|
+
runner.nextRunAt = Date.now() + runner.intervalMs;
|
|
2414
|
+
}
|
|
2415
|
+
return succeeded;
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
/**
|
|
2419
|
+
* Local interval scheduler node.
|
|
2420
|
+
* Main loop: commands → heartbeat → {@link TriggerManager.tick}.
|
|
2421
|
+
*
|
|
2422
|
+
* Register runners via {@link TriggerManager.registerRunner} before {@link start}.
|
|
2423
|
+
* Does not scan Supabase trigger rows — see `trigger.api.ts` for remote admin APIs.
|
|
2424
|
+
*
|
|
2425
|
+
* Deploy **one** TriggerNode per logical scheduler — multiple processes duplicate runner work.
|
|
2426
|
+
*/
|
|
2427
|
+
class TriggerNode extends BaseNodeRuntime {
|
|
2428
|
+
requireRunners;
|
|
2429
|
+
constructor(options) {
|
|
2430
|
+
super("triggerNode");
|
|
2431
|
+
this.requireRunners = options?.requireRunners ?? false;
|
|
2432
|
+
}
|
|
2433
|
+
getLoopIntervalMs() {
|
|
2434
|
+
return TRIGGER_LOOP_INTERVAL_MS;
|
|
2435
|
+
}
|
|
2436
|
+
loopLoggerMinLevel() {
|
|
2437
|
+
return LogLevel.WARN;
|
|
2438
|
+
}
|
|
2439
|
+
async onBeforeRegisterNode(logger) {
|
|
2440
|
+
TriggerManager.closeRegistration();
|
|
2441
|
+
const runnerKeys = TriggerManager.getRunnerKeys();
|
|
2442
|
+
if (runnerKeys.length === 0) {
|
|
2443
|
+
if (this.requireRunners) {
|
|
2444
|
+
throw new Error("[TriggerNode] requireRunners is true but no runners are registered");
|
|
2445
|
+
}
|
|
2446
|
+
logger.warn("未注册任何 Trigger runner,主循環將空轉", {
|
|
2447
|
+
topic: LOG_TOPIC_TRIGGER,
|
|
2448
|
+
});
|
|
2449
|
+
return;
|
|
2450
|
+
}
|
|
2451
|
+
for (const { key, intervalMs } of TriggerManager.getRunnersBelowLoopInterval()) {
|
|
2452
|
+
logger.warn("Runner interval 小於主循環 tick 間隔,實際觸發精度受限", {
|
|
2453
|
+
topic: LOG_TOPIC_TRIGGER,
|
|
2454
|
+
data: {
|
|
2455
|
+
runnerKey: key,
|
|
2456
|
+
intervalMs,
|
|
2457
|
+
loopIntervalMs: TRIGGER_LOOP_INTERVAL_MS,
|
|
2458
|
+
},
|
|
2459
|
+
});
|
|
2460
|
+
}
|
|
2461
|
+
logger.success("Trigger 節點啟動", {
|
|
2462
|
+
topic: LOG_TOPIC_TRIGGER,
|
|
2463
|
+
data: { runnerKeys },
|
|
2464
|
+
});
|
|
2465
|
+
}
|
|
2466
|
+
async runLoopSteps(ctx, heartbeatOk) {
|
|
2467
|
+
if (!heartbeatOk) {
|
|
2468
|
+
const logger = createLogger({
|
|
2469
|
+
scope: createScope({
|
|
2470
|
+
module: `${this.runtimeModule}-runLoopSteps`,
|
|
2471
|
+
traceId: ctx.loopTraceId,
|
|
2472
|
+
labels: { nodeId: ctx.nodeId },
|
|
2473
|
+
}),
|
|
2474
|
+
});
|
|
2475
|
+
logger.warn("心跳失敗,本輪跳過 runner", { topic: LOG_TOPIC_NODE });
|
|
2476
|
+
return;
|
|
2477
|
+
}
|
|
2478
|
+
await TriggerManager.tick(ctx);
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
|
|
2482
|
+
export { BaseNodeRuntime, CategoryCommand, CategoryRepo, CategoryTrigger, CommandType, LOG_TOPIC_TRIGGER, LogLevel, MAX_POLL_TARGET_LIST_SIZE, ManagedChildProcesses, NodeStatus, RepoManager, ResultCode, TASK_LOCAL_PACKAGE_CONFIG_FILENAME, TASK_REPO_SCRIPT_CATEGORY, TASK_REPO_USAGE, TASK_RUNNER_CONFIG_DIR, TASK_RUNNER_ROOT_CONFIG_FILENAME, TASK_RUNNER_ROOT_CONFIG_LEGACY_RELATIVE_PATH, TASK_RUNNER_ROOT_CONFIG_RELATIVE_PATH, TRIGGER_LOOP_INTERVAL_MS, TRIGGER_RUNNER_DEFAULT_RETRY_COUNT, TRIGGER_RUNNER_DEFAULT_RETRY_DELAY_MS, TaskManager, TaskNode, TaskRepoValidation, TaskStatus, TaskStatusAction, TriggerManager, TriggerNode, TriggerStatus, buildNodeImportArgs, createLogger, createPathsExistCheck, createRequiredEnvCheck, createSupabaseReachableCheck, createTarget, generateResponse, getPollCommandList, getPollCommandListSchema, getPossibleTarget, getScanRemoteRepoValues, initSupabaseFromStandardEnv, isChildProcessRunning, isOptimisticLockResponse, loadEnvFiles, logManager, nodeBufferToArrayBuffer, patchChangeNodeStatus, patchChangeTaskStatus, patchClaimTask, patchNodeHeartBeat, patchStopNode, patchTriggerFired, patchTriggerFiredSchema, pollTargetList, postCommand, postCommandSchema, postRegisterNode, postTaskSchema, postTaskWithValidation, postTrigger, postTriggerSchema, publicBaseUrlFromEnv, readAndVerifySourceFile, readEnv, requireEnv, runReadinessChecks, scanEnabledTriggers, scanEnabledTriggersSchema, scanTargetList, sha256Hex, spawnTsxChild, supabase, updateTargetDetails, validateTaskRepoAndParams, validateWithSchema, waitForServiceReady };
|
|
2483
|
+
//# sourceMappingURL=node.js.map
|