diff --git a/scripts/lib/scheduled-tasks.mjs b/scripts/lib/scheduled-tasks.mjs index b70d19d..acb6ad4 100644 --- a/scripts/lib/scheduled-tasks.mjs +++ b/scripts/lib/scheduled-tasks.mjs @@ -18,6 +18,26 @@ /** nodeagent 自己注册过的计划任务名(含历史遗留)。只增不改。 */ export const TASK_WHITELIST = ['nodeagent', 'na-gui', 'nodeagent-console']; +/** + * 自重启任务前缀 —— `system.agent.restart` 每次重启都注册 + * `nodeagent-selfrestart-<毫秒时间戳>`(动作是「杀旧 PID → 前台拉起新进程」)。 + * + * 2026-10-27 真机发现:这类任务不在上面的白名单里,于是**卸载时漏删**—— + * 留下的任务会继续尝试拉起已被删除的 agent.mjs,形成每次登录都失败一次的僵尸。 + * 但它也不能被当成垃圾随手清:**正在 Running 的那一个往往就是当前 agent 的 + * 父任务**(删除会连带杀掉 agent)。故只把它纳入「卸载清理」的判定集合, + * 由调用方在「已经停掉 agent」之后使用。 + */ +export const SELF_RESTART_PREFIX = 'nodeagent-selfrestart-'; + +/** `nodeagent-selfrestart-<纯数字>` —— 严格到毫秒时间戳形态,不接受任何通配。 */ +const SELF_RESTART_RE = /^nodeagent-selfrestart-\d{6,}$/; + +/** 是否是本框架的自重启任务(严格命名空间,非模糊匹配)。 */ +export function isSelfRestartTask(name) { + return typeof name === 'string' && SELF_RESTART_RE.test(name); +} + /** * 计划删除名单:白名单内**且实际存在**的任务,按白名单固定顺序返回。 * @@ -37,5 +57,11 @@ export function planTaskCleanup(existing) { for (const name of TASK_WHITELIST) { if (present.has(name)) out.push(name); } + // 自重启任务:仅接受严格命名空间(`nodeagent-selfrestart-<数字>`), + // 按名字排序保证输出稳定;与白名单重名时去重(实际不会发生,防御性)。 + const selfRestart = [...present].filter(isSelfRestartTask).sort(); + for (const name of selfRestart) { + if (!out.includes(name)) out.push(name); + } return out; } diff --git a/tests/unit/na-gui-cleanup.test.mjs b/tests/unit/na-gui-cleanup.test.mjs index 158a571..6ae8eda 100644 --- a/tests/unit/na-gui-cleanup.test.mjs +++ b/tests/unit/na-gui-cleanup.test.mjs @@ -2,7 +2,7 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; -import { TASK_WHITELIST, planTaskCleanup } from '../../scripts/lib/scheduled-tasks.mjs'; +import { TASK_WHITELIST, isSelfRestartTask, planTaskCleanup } from '../../scripts/lib/scheduled-tasks.mjs'; /** * na-gui 陈旧计划任务清理(ARCH B-P1-6 / T05)。 @@ -100,3 +100,36 @@ test('control.cmd 菜单暴露 cleanup 入口(纯 ASCII + CRLF 由 ps1-encodin assert.match(cmd, /call :RUN cleanup/, 'control.cmd 菜单必须有 cleanup 项转发'); assert.match(cmd, /8\)\s*Cleanup stale tasks/, 'control.cmd 菜单应展示 cleanup 项'); }); + +// =========================================================================== +// 自重启任务(2026-10-27 真机发现:卸载漏删 → 僵尸任务反复拉起已删文件) +// =========================================================================== + +test('isSelfRestartTask:只认严格命名空间 nodeagent-selfrestart-<数字>', () => { + assert.equal(isSelfRestartTask('nodeagent-selfrestart-1793077118968'), true); + assert.equal(isSelfRestartTask('nodeagent-selfrestart-123456'), true, '6 位时间戳级仍算'); + // 以下都不是我们注册的名字,绝不能被当成自重启任务 + assert.equal(isSelfRestartTask('nodeagent-selfrestart-'), false, '没有数字'); + assert.equal(isSelfRestartTask('nodeagent-selfrestart-abc123'), false, '非纯数字'); + assert.equal(isSelfRestartTask('nodeagent-selfrestart-123'), false, '位数过短(非时间戳形态)'); + assert.equal(isSelfRestartTask('mynodeagent-selfrestart-123456'), false, '前缀必须从头匹配'); + assert.equal(isSelfRestartTask('nodeagent-selfrestart-123456-extra'), false, '不得带额外后缀'); + assert.equal(isSelfRestartTask(''), false); + assert.equal(isSelfRestartTask(null), false); +}); + +test('planTaskCleanup:白名单外的自重启任务纳入清理,且仍然只增不误删', () => { + const plan = planTaskCleanup([ + 'nodeagent', + 'nodeagent-selfrestart-1793077118968', + 'nodeagent-selfrestart-1793077118999', + 'Adobe Acrobat Update Task', // 用户/第三方任务,绝不能碰 + 'OneDrive Standalone Update', + 'nodeagent-selfrestart-notatask', + ]); + assert.deepEqual(plan, [ + 'nodeagent', + 'nodeagent-selfrestart-1793077118968', + 'nodeagent-selfrestart-1793077118999', + ], '白名单先出、自重启按名字排序,第三方任务一律不出现'); +});