按 TODO.md 的优先级把待办全部落地为代码,详见 CHANGELOG 的 [1.7.0]。 - 「走不动」攻关:地面状态修正(syncGroundToPhysics / trustSetbackOnGround)、 出站包类型分布、备选执行器 manualMover(只借 A* 规划、自己按键执行) - 原点自救分两轮并统计成败;回城前评估「原地是否更差」并记录前后坐标 - 新增目标区块租约表(targetLease)避免多机器人撞同一块 - 定时消息支持 cron 与「期望回复」确认;新增人工介入 Webhook 通知(notifier) - 大地图后台预热与视野裁剪;控制台日志按帧批量写入、配置面板支持搜索 顺带修掉两个影响部署的细节: - 部署脚本(deploy.js / ssh-helper.js)改为也能从 .env 读 SSH 凭据 —— 在命令行传密码会留在 shell 历史与进程列表里,写进已被忽略的 .env 更安全; 显式环境变量仍然优先。.env.example 补上占位符并说明这些只在本机用。 - package.json / package-lock.json 版本对齐到 1.7.0(此前停留在 1.1.0, 与 CHANGELOG 的语义化版本声明脱节)。 测试:368 → 436 个用例,`npm test` 与 `npm run check` 全绿。
109 行
4.5 KiB
JavaScript
109 行
4.5 KiB
JavaScript
'use strict';
|
|
|
|
const test = require('node:test');
|
|
const assert = require('node:assert');
|
|
|
|
const { Notifier } = require('../src/core/notifier');
|
|
const { defaultConfig } = require('../src/config/expansionConfig');
|
|
|
|
/** 造一个记录请求的假 fetch */
|
|
function setup(options) {
|
|
const opts = options || {};
|
|
const config = defaultConfig();
|
|
Object.assign(config.notify, opts.notify || {});
|
|
const calls = [];
|
|
const fetchImpl = opts.fetchImpl || (async (url, init) => {
|
|
calls.push({ url, init });
|
|
return { ok: true, status: 200 };
|
|
});
|
|
const notifier = new Notifier(config, null, { fetchImpl, minIntervalMs: opts.minIntervalMs });
|
|
return { notifier, config, calls };
|
|
}
|
|
|
|
test('默认关闭:没启用或没配地址时什么都不发', async () => {
|
|
const off = setup();
|
|
assert.strictEqual(off.notifier.isEnabled(), false);
|
|
assert.strictEqual(await off.notifier.notify('hi'), false);
|
|
assert.strictEqual(off.calls.length, 0);
|
|
|
|
const noUrl = setup({ notify: { enabled: true } });
|
|
assert.strictEqual(noUrl.notifier.isEnabled(), false, '没有地址等同于没启用');
|
|
assert.strictEqual(await noUrl.notifier.notify('hi'), false);
|
|
});
|
|
|
|
test('启用后按飞书自定义机器人的文本格式 POST', async () => {
|
|
const { notifier, calls } = setup({
|
|
notify: { enabled: true, webhookUrl: 'https://open.feishu.cn/open-apis/bot/v2/hook/abc' }
|
|
});
|
|
assert.strictEqual(notifier.isEnabled(), true);
|
|
assert.strictEqual(await notifier.notify('需要验证码'), true);
|
|
assert.strictEqual(calls.length, 1);
|
|
assert.strictEqual(calls[0].url, 'https://open.feishu.cn/open-apis/bot/v2/hook/abc');
|
|
const body = JSON.parse(calls[0].init.body);
|
|
assert.strictEqual(body.msgtype, 'text');
|
|
assert.strictEqual(body.content.text, '需要验证码');
|
|
assert.strictEqual(notifier.sent, 1);
|
|
});
|
|
|
|
test('同一个去重 key 在最小间隔内只发一次(避免反复打扰)', async () => {
|
|
const { notifier, calls } = setup({
|
|
notify: { enabled: true, webhookUrl: 'https://example.com/hook' },
|
|
minIntervalMs: 60000
|
|
});
|
|
assert.strictEqual(await notifier.notify('a', { key: 'verification:BotA' }), true);
|
|
assert.strictEqual(await notifier.notify('a', { key: 'verification:BotA' }), false, '同一问题不重复提醒');
|
|
assert.strictEqual(calls.length, 1);
|
|
// 不同 key 互不影响
|
|
assert.strictEqual(await notifier.notify('b', { key: 'rescue-failed:BotA' }), true);
|
|
assert.strictEqual(calls.length, 2);
|
|
// force 可以跳过去重(手动重发用)
|
|
assert.strictEqual(await notifier.notify('a', { key: 'verification:BotA', force: true }), true);
|
|
assert.strictEqual(calls.length, 3);
|
|
});
|
|
|
|
test('Webhook 地址可回退环境变量 NOTIFY_WEBHOOK_URL', async () => {
|
|
const previous = process.env.NOTIFY_WEBHOOK_URL;
|
|
process.env.NOTIFY_WEBHOOK_URL = 'https://example.com/from-env';
|
|
try {
|
|
const { notifier, calls } = setup({ notify: { enabled: true } });
|
|
assert.strictEqual(notifier.isEnabled(), true, '配置里没填地址时应回退环境变量');
|
|
await notifier.notify('hi');
|
|
assert.strictEqual(calls[0].url, 'https://example.com/from-env');
|
|
} finally {
|
|
if (previous === undefined) delete process.env.NOTIFY_WEBHOOK_URL;
|
|
else process.env.NOTIFY_WEBHOOK_URL = previous;
|
|
}
|
|
});
|
|
|
|
test('发送失败与异常都被记下来,不会抛出', async () => {
|
|
const { notifier } = setup({
|
|
notify: { enabled: true, webhookUrl: 'https://example.com/hook' },
|
|
fetchImpl: async () => ({ ok: false, status: 500 })
|
|
});
|
|
assert.strictEqual(await notifier.notify('hi'), false);
|
|
assert.strictEqual(notifier.failed, 1);
|
|
assert.strictEqual(notifier.lastError, 'HTTP 500');
|
|
|
|
const throwing = setup({
|
|
notify: { enabled: true, webhookUrl: 'https://example.com/hook' },
|
|
fetchImpl: async () => {
|
|
throw new Error('网络不通');
|
|
}
|
|
});
|
|
assert.strictEqual(await throwing.notifier.notify('hi'), false, '异常必须被吞掉');
|
|
assert.strictEqual(throwing.notifier.failed, 1);
|
|
assert.ok(throwing.notifier.lastError.includes('网络不通'));
|
|
});
|
|
|
|
test('snapshot 只暴露域名,不泄露带凭证的完整地址', async () => {
|
|
const { notifier } = setup({
|
|
notify: { enabled: true, webhookUrl: 'https://open.feishu.cn/open-apis/bot/v2/hook/secret-token' }
|
|
});
|
|
await notifier.notify('hi');
|
|
const snap = notifier.snapshot();
|
|
assert.strictEqual(snap.enabled, true);
|
|
assert.strictEqual(snap.configured, true);
|
|
assert.strictEqual(snap.host, 'open.feishu.cn');
|
|
assert.strictEqual(snap.sent, 1);
|
|
assert.ok(!JSON.stringify(snap).includes('secret-token'), '完整地址不能出现在接口响应里');
|
|
}); |