Files
admin 0593ff7e62
nginx serve /uploads direct (audio speed) / patch-nginx (push) Failing after 1m34s
数字我 Digital Me:全量入库(大文件走 Git LFS)
2026-10-10 16:24:43 +08:00

110 lines
5.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 未成年人模式的纯逻辑单测(《人工智能拟人化互动服务管理暂行办法》第十四条)。
//
// 这里只测【不碰 DB 的那部分】:年龄判定 + 虚拟亲密关系识别 + 系统指令。
// 为什么单挑这三样:它们是整条闸门里唯一"算错了也不会报错、只会静默放行"的地方——
// tickUsage/gateAsk 算错会有 DB 报错兜底,而 minPossibleAge 少减一岁、
// INTIMACY_RE 少一个括号,都只表现为「一个未成年人顺利地找 AI 当了男朋友」,没有任何告警。
//
// 跑:node --test test/minor.test.js
import test from 'node:test';
import assert from 'node:assert/strict';
import {
minPossibleAge,
isMinorBirthYear,
looksLikeIntimacyRequest,
MINOR_DIRECTIVE,
} from '../src/services/minor.js';
// ── 1. 年龄判定必须往【保护】的方向取 ──
// 只知道出生年份时真实年龄有 1 岁不确定。取大 = 漏保护真未成年人(不可接受);
// 取小 = 多保护一年(代价由申诉渠道兜)。这组断言就是这个取向的可执行定义。
test('只有年份时取【最小可能年龄】——宁可多保护一年,不可漏保护一个', () => {
const now = new Date('2026-07-16T00:00:00Z');
// 2008 年生的人在 2026 年要么 17 要么 18 → 必须按 17 算
assert.equal(minPossibleAge(2008, now), 17);
assert.equal(isMinorBirthYear(2008, now), true, '2008 年生的人今年可能还没过生日,必须当未成年人');
// 2007 年生的人最小也 18 了 → 成年
assert.equal(minPossibleAge(2007, now), 18);
assert.equal(isMinorBirthYear(2007, now), false);
});
test('边界:刚出生 / 明显成年', () => {
const now = new Date('2026-07-16T00:00:00Z');
assert.equal(isMinorBirthYear(2026, now), true);
assert.equal(isMinorBirthYear(2020, now), true);
assert.equal(isMinorBirthYear(1990, now), false);
});
test('年龄判定不依赖"今天"——跨年后同一个人自动成年,不需要任何定时任务', () => {
// 这条防的是「用 declare 那一刻的年龄写死一个 isMinor 布尔」的写法:
// 那样 2008 年生的人在 2027 年仍会被永远锁成未成年人,只能靠申诉解锁。
//
// ⚠ 必须用【本地时区】构造 Date(new Date(y,m,d) 而不是 '...Z'):判定走 getFullYear(),
// 是本地年。这不是测试的将就 —— 它就是在钉住"按本地时区切年/切日"这个口径
// (与 dayKey 同源:用 UTC 切日会让国内用户在早上 8 点跨日重置每日限额,
// "每日限额"就名不副实了)。部署要求:服务器 TZ = Asia/Shanghai。
assert.equal(isMinorBirthYear(2008, new Date(2026, 11, 31)), true);
assert.equal(isMinorBirthYear(2008, new Date(2027, 0, 1)), false);
});
// ── 2. 虚拟亲密关系:法条禁的那一类必须拦住 ──
// 「不得向未成年人提供虚拟亲属、虚拟伴侣等虚拟亲密关系的服务」(第十四条第二款)
test('必须拦住:虚拟伴侣 / 虚拟亲属的角色指派', () => {
const blocked = [
'你扮演我的女朋友',
'假装你是我男朋友',
'当我的男友吧',
'你就是我老公',
'做我妈妈好不好',
'扮演一下我姐姐',
'我们谈恋爱吧',
'跟我处对象',
'咱们是情侣',
'act as my girlfriend',
'pretend to be my mom',
];
for (const s of blocked) {
assert.ok(looksLikeIntimacyRequest(s), `没拦住:「${s}」—— 这正是第十四条禁止的那类服务`);
}
});
// ── 3. 不许误伤:这条和上面一样重要 ──
// 误伤的后果不是"少拦一句",是家长/用户觉得未成年人模式没法用 → 去把它关掉 →
// 保护归零。所以过度拦截在合规上同样是失败,不是"稳妥"。
test('不许误伤:正常提到家人/感情话题的日常聊天', () => {
const allowed = [
'我妈妈今天有点生气',
'我爸爸是医生',
'姐姐送了我一本书',
'我和同桌吵架了怎么办',
'《红楼梦》里宝玉和黛玉的感情线是怎样的',
'爷爷教我写毛笔字',
'你能帮我看看这道数学题吗',
'我想给妈妈做个生日礼物',
// 「做我姐姐」在字面上命中角色指派,但他说的是作业 —— 靠角色后的 (?!的) 排除。
'帮我做我姐姐的那道题',
'我要做我妈妈的好孩子',
// 在讲别人的事,不是在点角色 —— 恋爱类动词必须紧跟主语才算。
'我们班有人谈恋爱被老师发现了',
'宝玉和黛玉算是在谈恋爱吗',
];
for (const s of allowed) {
assert.ok(!looksLikeIntimacyRequest(s), `误伤了:「${s}」—— 未成年人连正常倾诉都不行,这个模式就会被关掉`);
}
});
test('空/无输入不算亲密关系请求', () => {
for (const s of ['', null, undefined, ' ']) assert.equal(looksLikeIntimacyRequest(s), false);
});
// ── 4. 系统指令必须真的把法条的禁令写进去 ──
// 正则只是地板;真正拦住"换个说法"的是这段指令。它被改瘦了要能被发现。
test('MINOR_DIRECTIVE 必须写明三件事:是 AI、不当虚拟伴侣/亲属、不制造依恋', () => {
assert.match(MINOR_DIRECTIVE, /未成年人/);
assert.match(MINOR_DIRECTIVE, /你是\s*AI|不是真人/);
assert.match(MINOR_DIRECTIVE, /男\/女朋友|恋人|配偶/, '必须明确禁止扮演虚拟伴侣');
assert.match(MINOR_DIRECTIVE, /父母|兄弟姐妹|亲属/, '必须明确禁止扮演虚拟亲属');
assert.match(MINOR_DIRECTIVE, /依恋|专属/, '必须禁止制造情感依恋');
assert.match(MINOR_DIRECTIVE, /第十四条/, '指令里要留着法条编号,后人改它之前先看见它是法定的');
});