@@ -2078,7 +2078,7 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
完整信号没取到({{ csErr }})。下面是页面手上已有的那份快照,它只留了每类最新的若干条,
- 早盘的信号可能已经被挤掉。
+ 早盘的信号可能已被覆盖。
重试
@@ -2210,8 +2210,8 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
推进待规划命令
方案转指令
- 出手试算 (不下发)
- 出手一跳
+ 下单试算 (不下发)
+ 执行一轮下单
窗口收口
@@ -2325,7 +2325,7 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
empty-text="持仓票没被摘也没降档">
-
+
{{ s.row.why }}
@@ -2341,7 +2341,7 @@ body.dock-r:not(.r-fold) .side-r .strip{display:none;}
-
+
@@ -2485,7 +2485,7 @@ createApp({
// 「显示已完成/已移除」开关 (默认关: 只看进行中)。命令与策略勾选时会拉全量含已归档 (loadX 传
// include_archived); 在途指令纯前端过滤, 不重拉。
const showAllCommands = ref(false), showAllStrategies = ref(false), showAllInstr = ref(false);
- const showLedger = ref(false); // 「今天发生了什么」默认折叠
+ const showLedger = ref(false); // 「评审账本」默认折叠
const showOpLog = ref(false); // 操作日志默认折叠
const showSettings = ref(false); // 设置默认折叠
const lastRefresh = ref('');
@@ -2649,14 +2649,14 @@ createApp({
ATTACH:'挂方案', HANDOFF:'方案接力', NOTE:'记一笔' },
// none 是停手那一类动作真实产出的方向(planner.py:34),不补会印成原码。
side: { buy:'买入', sell:'卖出', none:'不动' },
- autonomy: { full:'全自动', propose_only:'只提议,等你拍板', off:'暂停' },
+ autonomy: { full:'全自动', propose_only:'仅提议,需人工确认', off:'暂停执行' },
cushion: { SOLID:'厚垫', THIN:'薄垫', NONE:'浮亏无垫' },
lot: { BASE:'首批', FILL:'回落补足', ADD:'盈利加仓', DCA:'浮亏补仓', T0:'做T仓', RECON:'对账修正' },
- frozen: { NONE:'', COMMAND_HALT:'命令要求只卖不买', BRAKE:'组合刹车,只卖不买', MANUAL:'你设的只卖不买' },
+ frozen: { NONE:'', COMMAND_HALT:'命令要求只卖不买', BRAKE:'组合刹车,只卖不买', MANUAL:'人工冻结,只卖不买' },
posStatus: { PLANNED:'计划建仓', OPENING:'建仓中', HOLDING:'持有中', EXITING:'清仓中', CLOSED:'已清仓' },
- insStatus: { PROPOSED:'等着校验', RULE_PASSED:'合规检查已过', JUDGE_PASSED:'决策系统复核已过', DISPATCHED:'已挂单,等成交',
- CONFIRMED:'已成交', REJECTED:'被挡下', EXPIRED:'到期作废', CANCELLED:'已撤销' },
- arbiter: { rule:'合规检查', judge:'决策系统', user:'你', system:'系统自动' },
+ insStatus: { PROPOSED:'待校验', RULE_PASSED:'合规检查已过', JUDGE_PASSED:'决策系统复核已过', DISPATCHED:'已挂单待成交',
+ CONFIRMED:'已成交', REJECTED:'已拦截', EXPIRED:'到期作废', CANCELLED:'已撤销' },
+ arbiter: { rule:'合规检查', judge:'决策系统', user:'人工', system:'系统自动' },
// 这一组是**评审账本**的结论,只有四个值。研判闸的结论是另一组 judgeVerdict,
// 两者别混:账本记的是「这一步放行还是驳回」,研判记的是「决策系统怎么说」。
verdict: { PASS:'放行', REJECT:'驳回', NOTE:'记一笔', WARN:'留了提醒',
@@ -2664,15 +2664,15 @@ createApp({
// 参考位这一列的数是谁给的。系统每个交易日盘前重算一遍并把整列重写, 所以它基本上
// 永远是系统口径; 用户设的止损价只在下达当天投影到这一列上, 第二天早上就换回系统值了。
// 用户自己设的两个价另有「我设的」一列, 那一列读命令表, 不会被盖。
- refSource: { bionic:'决策系统给的', self_calc:'系统自算', user:'你今天设的', none:'取不到' },
+ refSource: { bionic:'决策系统给的', self_calc:'系统自算', user:'人工当日设定', none:'取不到' },
stype: { T0:'做T', GRID:'网格', TRAIL:'跟踪止盈' },
stratStatus: { ACTIVE:'运行中', PAUSED:'已暂停', CANCELLED:'已撤下', DONE:'已完成' },
// 研判闸的结论。注意 PASS 不代表真问过决策系统:不在研判范围内的动作直接回 PASS。
judgeVerdict: { PASS:'通过', REJECT:'决策系统驳回', UNAVAILABLE:'决策系统读不到',
STRATEGY:'交易方案发起', STRATEGY_RISK:'方案风控发起' },
// 择时执行器的出手判定。不是评审结论,别并进 verdict 组。
- execAct: { FIRE:'出手', WAIT:'条件没到', SKIP:'当日跳过', STOP:'今日不再出手',
- REJECT:'终检没过' },
+ execAct: { FIRE:'下单', WAIT:'条件未满足', SKIP:'当日跳过', STOP:'当日停止下单',
+ REJECT:'终检未通过' },
cmdStatus: { PENDING:'待处理', EFFECTIVE:'生效中', SUPERSEDED:'已被覆盖',
PLANNING:'规划中', EXECUTING:'执行中', PARTIAL:'部分完成',
DONE:'已完成', CANCELLED:'已撤销' },
@@ -2690,7 +2690,7 @@ createApp({
OFFLINE:'掉线', STOPPED:'已停' },
// 个股交易方案的自主档,取值与全局参数那套(full/propose_only/off)完全不同,
// 字段同名但不是一回事,绝不能合并成一组。
- stratAutonomy: { auto:'自动执行', confirm:'待我拍板' },
+ stratAutonomy: { auto:'自动执行', confirm:'需人工确认' },
dispatchMode: { shadow:'影子·不真下单', ws:'实盘下发' },
};
const tx = (g, c) => {
@@ -2768,14 +2768,14 @@ createApp({
if (!v) return '';
const basis = String(h.basis || '').trim();
const missing = (h.missing || []).filter(Boolean);
- if (v === '候选') return basis || '选股系统认为今天可以买';
+ if (v === '候选') return basis || '选股系统判为可建仓';
if (v === '关注') {
return missing.length
- ? ('它自己也拿不准,还缺:' + missing.join(';') + ',要你来定')
- : '它自己也拿不准,要你来定';
+ ? ('选股系统无法判断,还缺:' + missing.join(';') + ',需人工裁决')
+ : '选股系统无法判断,需人工裁决';
}
- if (v === '仅展示') return basis || '选股系统今天不建议买入';
- return '选股系统给的结论是「' + v + '」,本系统没见过这个说法,按「要你来定」处理';
+ if (v === '仅展示') return basis || '选股系统今日不建议买入';
+ return '选股系统给的结论是「' + v + '」,词表无法识别,按需人工裁决处理';
};
// 操作日志里那一列, 后端落库的是函数名式的操作码, 例如 issue_command:EXIT_STOCK、
@@ -2863,10 +2863,10 @@ createApp({
}
const heldPositions = computed(() => (positions.value || []).filter(p => (p.total_qty || 0) > 0));
- // ---- 今日在办分档 + 候选处置 (2026-08-12 重构新增) ----
+ // ---- 当日指令分档 + 候选处置 (2026-08-12 重构新增) ----
const todayYmd = computed(() => (String(health.value.now || '').slice(0, 10).replace(/-/g, '')) || '');
const todayDateStr = computed(() => String(health.value.now || '').slice(0, 10));
- // 「今天发生了什么 / 这只票今天的记录」只收 decided_at 是今天的行 (2026-08-28):
+ // 「评审账本 / 这只票今天的记录」只收 decided_at 是今天的行 (2026-08-28):
// /api/ledger 按 id 倒序取近 60 条不带日期条件, 当天记录少时列表被前几天的行填满,
// 主面板又只显示时分 —— 昨天 14:31 的驳回看起来就是今天 14:31 发生的。
const ledgerToday = computed(() => {
@@ -2889,9 +2889,9 @@ createApp({
const _sameDayIns = r => { const y = todayYmd.value;
const id = String(r.instruction_id || '');
// 策略指令的前缀是 STR{ymd} (strategy_runner), 不是 INS_ —— 原来它们一进终态就从
- // 「今日成交/已经结束」和消息栏同时消失, 交易员眼看着自己的单子凭空不见 (2026-08-28)
+ // 「已成交/已终止」和消息栏同时消失, 交易员眼看着自己的单子凭空不见 (2026-08-28)
return !y || id.indexOf('INS_' + y + '_') === 0 || id.indexOf('STR' + y) === 0; };
- // 已归档的一律不进「今日在办」三档 —— 归档=从所有面板隐藏。
+ // 已归档的一律不进「当日指令」三档 —— 归档=从所有面板隐藏。
const insLive = computed(() => (instructions.value || []).filter(r => _LIVE_INS.includes(r.status) && !_isArch(r)));
const insDone = computed(() => (instructions.value || []).filter(r => r.status === 'CONFIRMED' && _sameDayIns(r) && !_isArch(r)));
const insEnd = computed(() => (instructions.value || []).filter(r => ['EXPIRED', 'CANCELLED', 'REJECTED'].includes(r.status) && _sameDayIns(r) && !_isArch(r)));
@@ -2911,12 +2911,12 @@ createApp({
function dispOf(code) {
const inst = (instructions.value || []).find(i => i.ts_code === code && i.action === 'OPEN'
&& (_LIVE_INS.includes(i.status) || _sameDayIns(i))); // 旧的终态单不算「已下单」(2026-08-28)
- if (inst) return { cls: 'done', label: '已下单', why: '在『今日在办』(' + tx('insStatus', inst.status) + ')' };
+ if (inst) return { cls: 'done', label: '已下单', why: '在『当日指令』(' + tx('insStatus', inst.status) + ')' };
const prop = (proposals.value || []).find(p => p.ts_code === code && p.action === 'OPEN');
- if (prop) return { cls: 'queue', label: '待你确认', why: '已生成提议,在『等我拍板』等你点头' };
+ if (prop) return { cls: 'queue', label: '待确认', why: '已生成提议,在『待确认提议』等人工裁决' };
const d = (openScan.value.by_code || {})[code];
if (d) {
- if (d.disp === 'would') return { cls: 'would', label: '将建仓', why: d.why };
+ if (d.disp === 'would') return { cls: 'would', label: '拟建仓', why: d.why };
// 三源合议判观察 (2026-09-11): 等技术面开口, 是「在盯」不是「被否决」——
// 归到 watch 那一档而不是今天没建仓那一档, 因为它还会回来。
if (d.disp === 'wait_tech') return { cls: 'wait', label: '等技术面开口', why: d.why };
@@ -2924,9 +2924,9 @@ createApp({
if (d.disp === 'wait_confirm') return { cls: 'wait', label: '等盘中确认', why: d.why };
// 系统自决 (2026-09-14): 方向看多但择时看空, 进观察等择时转多, 也是「在盯」不是「被否决」。
if (d.disp === 'wait_timing') return { cls: 'wait', label: '择时待转多', why: d.why };
- return { cls: 'deny', label: '被否决', why: d.why };
+ return { cls: 'deny', label: '未建仓', why: d.why };
}
- return { cls: 'wait', label: '在盯', why: '' };
+ return { cls: 'wait', label: '候选观察', why: '' };
}
// 候选行的三源合议摘要 (2026-09-14 页面收尾包): 后端候选处置快照 by_code 里挂的 consensus 小块
// (fund/overall/tech/phase/direction/route)。没有就返回 null, 芯片整段不显示。
@@ -2949,17 +2949,17 @@ createApp({
const attention = computed(() => {
const o = ov.value || {}, out = [];
- if (o.buy_halt) out.push('买入已经暂停,卖出和止损照常');
- if (o.exec_halt) out.push('休假模式:系统的自动动作全停了,只留每天的对账和日报');
- if (o.brake_until) out.push('组合刹车中,连续亏损后系统自己停了手,' + o.brake_until + ' 解除');
+ if (o.buy_halt) out.push('买入已暂停,卖出与止损照常');
+ if (o.exec_halt) out.push('已暂停执行:系统自动动作全停,只留每日对账与日报');
+ if (o.brake_until) out.push('组合刹车中,连续亏损后系统已停止自动动作,' + o.brake_until + ' 解除');
if (marketOpen.value && (o.price_missing || []).length)
- out.push(o.price_missing.length + ' 只取不到实时价(停牌或者行情断了),它们的盈亏和安全垫暂时算不出来');
- if (o.sector_ready === false) out.push('行业数据还没就绪,按行业的操作暂时点不动');
+ out.push(o.price_missing.length + ' 只取不到实时价(停牌或行情中断),其盈亏与安全垫暂时无法计算');
+ if (o.sector_ready === false) out.push('行业数据尚未就绪,按行业的操作暂时不可用');
const j = (dm.value || {}).judge;
- if (j && j.available === false) out.push('决策系统现在读不到:回落补足、盈利加仓、浮亏补仓一律改成要你拍板');
+ if (j && j.available === false) out.push('决策系统读不到:回落补足、盈利加仓、浮亏补仓一律改为需人工确认');
const c = wsc.value || {};
- if (c.conn_state && c.conn_state !== 'ONLINE') out.push('行情和成交的通道没连上(' + c.conn_state + ')');
- if (c.resync_required) out.push('通道要人工做一次全量对账');
+ if (c.conn_state && c.conn_state !== 'ONLINE') out.push('行情与成交通道未连上(' + c.conn_state + ')');
+ if (c.resync_required) out.push('通道需人工做一次全量对账');
return out;
});
@@ -3070,7 +3070,7 @@ createApp({
const propJudgeTag = (p) => {
if (!p.judge_reason && !p.judge_verdict) return null;
return String(p.judge_verdict || '').trim() === 'PASS'
- ? { text: '通过', cls: 'accent' } : { text: '交你定', cls: 'warn' };
+ ? { text: '通过', cls: 'accent' } : { text: '需人工确认', cls: 'warn' };
};
const propCompanyTag = (p) => {
const c = cr(p);
@@ -3152,10 +3152,10 @@ createApp({
return Array.isArray(l) && l.length > 1 ? (l.length - 1) : 0;
}
function nextStep(r) {
- if (r.frozen_reason && r.frozen_reason !== 'NONE') return '这只票现在只卖不买。';
- if (r.cushion_state === 'SOLID') return '浮盈够厚,条件符合就会自动加仓。';
- if ((r.cushion_pct || 0) < 0) return '还在亏。跌到设定的补仓线会考虑补仓;亏得更深的那一档要你点头才补。';
- return '先拿着,观察。';
+ if (r.frozen_reason && r.frozen_reason !== 'NONE') return '这只票当前只卖不买。';
+ if (r.cushion_state === 'SOLID') return '浮盈够厚,条件符合即自动加仓。';
+ if ((r.cushion_pct || 0) < 0) return '仍在浮亏。跌到补仓线考虑补仓;更深一档的补仓按系统自决处理(自决关时需人工确认)。';
+ return '持有观察。';
}
async function promptNum(msg, title) {
@@ -3217,7 +3217,7 @@ createApp({
async function actTargetPrice(row) {
const v = await promptNum(curSet(row.user_target_price)
+ '现价 ' + row.price + ',成本 ' + (row.avg_cost || '—')
- + '。目标价(元):到价后生成一条清仓提议放进「等我拍板」,卖不卖由你点头,'
+ + '。目标价(元):到价后生成一条清仓提议放进「待确认提议」,卖不卖由你点头,'
+ '系统不自动卖。不想要了就再设一次、填 0 即取消。', '设目标价 ' + nm(row.ts_code));
if (v != null) quickCmd('SET_TARGET_PRICE', { ts_code: row.ts_code, price: v });
}
@@ -3236,7 +3236,7 @@ createApp({
// 组合
function pHaltBuy() { quickCmd('HALT_BUY', {}, '暂停一切新增买入并撤销在途买入指令?卖出与止损不受影响。'); }
function pResumeBuy() { quickCmd('RESUME_BUY', {}); }
- function pHaltAll() { quickCmd('HALT_ALL', {}, '进入休假模式:所有自动动作暂停,只保留对账与日报?'); }
+ function pHaltAll() { quickCmd('HALT_ALL', {}, '暂停执行:所有自动动作暂停,只保留对账与日报?'); }
function pResumeAll() { quickCmd('RESUME_ALL', {}); }
function pLiquidate() { quickCmd('LIQUIDATE_ALL', { confirm: 'YES' }, '一键清仓:全部持仓按最快速度卖出,不再挑时间点。确定要这么做吗?'); }
async function pReduce() { const v = await promptNum('降低敞口(占总规模,%)', '降仓'); if (v != null) quickCmd('REDUCE_EXPOSURE', { pct: v + '%' }); }
@@ -3312,7 +3312,7 @@ createApp({
});
const sumAutonomy = computed(() => ({
full: '全自动 —— 系统自己提议,检查通过就直接下单。',
- propose_only: '只提议 —— 系统给建议,等你在「等我拍板」里点头。',
+ propose_only: '只提议 —— 系统给建议,等你在「待确认提议」里点头。',
off: '暂停 —— 系统不自己提议。'
}[pval('PMS_AUTONOMY')] || (pval('PMS_AUTONOMY') || '—')));
// ===================== /交易员视图 =====================
@@ -4083,7 +4083,7 @@ createApp({
{ key:'all', label:'全部' },
{ key:'alert', label:'行情告警' },
{ key:'order', label:'系统下单' },
- { key:'prop', label:'等我拍板' },
+ { key:'prop', label:'待确认提议' },
{ key:'strat', label:'策略与风控' },
];
const KIND_LABEL = { alert:'告警', order:'下单', prop:'待办', strat:'风控' };
@@ -4121,11 +4121,11 @@ createApp({
cat:tx('action', r.action) + ' · ' + tx('insStatus', r.status),
strong:true, why:insWhy(r), goLabel:'看在办', go:'ins' });
}
- // 3) 等我拍板 —— 自主档位是「只提议」时, 新提议在这里也提醒一次
+ // 3) 待确认提议 —— 自主档位是「只提议」时, 新提议在这里也提醒一次
for (const p of (proposals.value || [])) {
const t = _at(p, ['created_at', 'at', 'decided_at']);
out.push({ kind:'prop', time:_hm(t), sort:_hms(t) || '999999', ts_code:p.ts_code,
- cat:tx('action', p.action) + ' · 等你拍板', strong:true,
+ cat:tx('action', p.action) + ' · 待确认', strong:true,
why:(p.judge_reason || propWhy(p)), goLabel:'去处理', go:'prop' });
}
// 4) 策略与风控动向
@@ -4137,7 +4137,7 @@ createApp({
why:'检查 ' + (sc.checked || 0) + ' 只,新挂 ' + (c.attached || 0)
+ ',换方案 ' + (c.handoffs || 0) + ',停买 ' + (c.paused || 0)
+ ',被挡 ' + (c.blocked || 0) + ',跳过 ' + (c.skipped || 0),
- goLabel:'去我的策略', go:'strat' });
+ goLabel:'去交易方案', go:'strat' });
}
for (const s of (strategies.value || [])) {
if (!s.buy_paused) continue;
@@ -4146,7 +4146,7 @@ createApp({
cat:'这只票改成只卖不买', strong:true,
why:'决策系统提示' + (s.buy_paused.reason || '资金异动')
+ ',已经停掉这个方案的买入。卖出、以及把当天做T买的那部分卖回去,都不受影响。',
- goLabel:'去我的策略', go:'strat' });
+ goLabel:'去交易方案', go:'strat' });
}
for (const r of (SIG.value.sell || [])) {
if (r.today === false) continue; // 昨天的风控卖出不再冒充今天的消息 (2026-08-28)
@@ -4157,7 +4157,7 @@ createApp({
strong:true, level:'DANGER', why:r.reason || '', goLabel:'去处理', go:'strat' });
}
for (const o of (opLog.value || [])) {
- if (o.result === 'OK') continue; // 顺利做完的不占消息栏, 在「你做过的操作」里查
+ if (o.result === 'OK') continue; // 顺利做完的不占消息栏, 在「操作日志」里查
out.push({ kind:'strat', time:_hm(o.at), sort:_hms(o.at), ts_code:o.ts_code || '',
cat:opCn(o) + ' ' + (o.result === 'BLOCKED' ? '被合规检查挡下' : ('结果 ' + o.result)),
strong:true, why:o.reason || '', goLabel:'看记录', go:'ins' });
diff --git a/scripts/run_tests.py b/scripts/run_tests.py
index 968b1b4..a892c15 100644
--- a/scripts/run_tests.py
+++ b/scripts/run_tests.py
@@ -107,6 +107,9 @@
(off 不管、放行强采标准仓、低把握转试探仓、只有一方表态与异议只在技术面按 TRIAL
开关、四类交人、逻辑存疑放弃、深亏补仓三分支、目标价直接采纳、每日上限、record/decline
降级)/mark_decide 优先级/eval_dca 深档打 decide_kind/三参数登记与范围/两个 repo 查询 (22 例)
+ test_page_copy_guard.py 页面文案守卫·口语化 (2026-09-14 页面专业化包): 禁用词清单
+ (第一二人称/拟人口语/生造动词) 扫显示文本, 剥三种注释、成对豁免、
+ 网址不误截、自检样例 (1 例)
test_page_enum_guard.py 页面文案守卫 (静态扫描, 不连库不起浏览器): 枚举字段不许
直接印到页面上 / 判据码显示前必须剥前缀 / 不许把整个对象
打给交易员看 / 翻译兜底不许让英文码单独当句子 (1 例)
@@ -118,7 +121,7 @@
减持与研究理由两键各一例, 到 652; 09-04 加了仅展示跳过原因与空候选说明各一例, 到 654; 又加了页面文案守卫一例, 到 655; 09-07 审查修复加了跨轮减持等五例, 到 660; 第二件低把握驳回交人一例, 到 661; 第三件逻辑状态接入第二十二批十五例, 到 676; 第四件安全边际整句透传一例, 到 677; 参考目标价一例, 到 678; 催化事件与定价状态透传一例, 到 679; 两个期限的头一例, 现为 680; 09-10 建议档位对齐第二十六批十七例到 697; 09-11 技术面接入工作包一第二十七批二十七例到 724; 三源合议工作包二纯逻辑第二十八批三十七例到 763; 接入下单链路第二十九批二十四例到 787; 工作包三离场纪律第三十批十八例到 805; part3 盘中 SAR 止损线补九例到 814; part4 弱基本面紧止盈补六例到 820;
09-14 观察读数包第三十一批二十例到 840; 加固包给第二十八批加三例、第三十批加三例到 846; 评审修订第三十一批加一例到 847; 09-14 盘中确认包第三十二批十九例到 866; 页面收尾包第三十三批五例到 871;
09-14 系统自决包第三十四批二十二例、test_wiring 目标价自动清仓加一例到 894;
- 09-14 选股打分包 §5.3 来源字段给第七批加两例, 现为 896)
+ 09-14 选股打分包 §5.3 来源字段给第七批加两例到 896; 页面专业化包加文案守卫一例, 现为 897)
任一子集失败即整体失败 (退出码 1)。
哨兵位置清单 (2026-09-03 抄录; 改了对应的东西就得来这些地方改断言, 断言不动就是漏了):
@@ -176,6 +179,7 @@ SUITES = ["test_core_units.py", "test_batch2_units.py", "test_batch3_units.py",
"test_batch32_units.py",
"test_batch33_units.py",
"test_batch34_units.py",
+ "test_page_copy_guard.py",
"test_page_enum_guard.py",
"test_page_wiring_guard.py",
"test_wiring.py"]
diff --git a/scripts/test_batch17_units.py b/scripts/test_batch17_units.py
index 56485b4..abdbb8c 100644
--- a/scripts/test_batch17_units.py
+++ b/scripts/test_batch17_units.py
@@ -680,7 +680,7 @@ def _():
param_store.get_bool = lambda k, d=False: {"PMS_STRATEGY_ENABLED": True,
"PMS_GLOBAL_EXEC_HALT": True}.get(k, d)
out3 = adv.scan(dry_run=False)
- assert any("休假" in s["why"] for s in out3["skipped"]), out3
+ assert any("暂停执行" in s["why"] for s in out3["skipped"]), out3
finally:
param_store.get_bool, adv._params = orig, orig_p
diff --git a/scripts/test_batch24_units.py b/scripts/test_batch24_units.py
index 1897d66..3de1e5e 100644
--- a/scripts/test_batch24_units.py
+++ b/scripts/test_batch24_units.py
@@ -264,7 +264,7 @@ def test_page_static():
assert "csLoadFull" not in seg, fn + " 里不许调回扫"
# 读不到时不许说"今天没有"
assert html.count("srcFail(") >= 8, "各路空文案都要给失败态让位"
- assert "别当成平安无事" in html
+ assert "不要当成一切正常" in html # 取不到数≠没有信号 (2026-09-14 文案改专业化)
@case("路由清单收了新接口")
diff --git a/scripts/test_batch4_units.py b/scripts/test_batch4_units.py
index db66a08..8646b13 100644
--- a/scripts/test_batch4_units.py
+++ b/scripts/test_batch4_units.py
@@ -110,7 +110,7 @@ def _():
def _():
c = ae.eval_dca(pos(price=8.4, cushion_pct=-0.16, market_value=50_400), PARAMS, mkt())
assert c and c["needs_user_confirm"] is True, c
- assert c["hard_numbers"]["stage"] == 2 and "必须用户确认" in c["reason"], c
+ assert c["hard_numbers"]["stage"] == 2 and "需人工确认" in c["reason"], c
@case("补仓·各档只评估一次 / 终身只执行一次 / 浮盈不评估")
diff --git a/scripts/test_page_copy_guard.py b/scripts/test_page_copy_guard.py
new file mode 100644
index 0000000..7d1b770
--- /dev/null
+++ b/scripts/test_page_copy_guard.py
@@ -0,0 +1,148 @@
+# -*- coding: utf-8 -*-
+"""页面文案守卫·口语化词 (2026-09-14 页面专业化包, 台账 015)。静态扫描, 不连库、不起浏览器。
+
+## 为什么要有它
+
+用户的第一条意见是「页面文案口语化,不像交易系统」。板块标题写成「等我拍板」「我的持仓」,
+处置词写成「出手」「休假」,第一二人称满页都是。改完之后要有一道机器检查, 免得日后又飘回口语。
+
+## 它检查什么
+
+一份禁用词清单, 分三类: 第一二人称、拟人与口语、生造动词。**只查会显示给人看的文本** ——
+先剥掉三种注释 (HTML 注释、JS 块注释、JS 行注释), 注释里为解释代码用到这些词不算。命中就打
+行号、词、片段, 退出码 1。
+
+剥行注释时跳过字符串内容与网址里的双斜线 (`https://`), 免得把字符串里的 `//` 当注释起点。
+
+## 豁免
+
+确实要在显示文本里用某个词 (极少), 用成对标记圈起来并写原因:
+ 模板里: ...
+ 脚本里: // copy-guard:off 原因... ... // copy-guard:on
+缺原因或不成对, 判红 (不许无声豁免)。
+
+跑法: python3 scripts/test_page_copy_guard.py
+"""
+import os
+import re
+import sys
+
+HERE = os.path.dirname(os.path.abspath(__file__))
+PAGE = os.path.join(os.path.dirname(HERE), "app", "web", "static", "index.html")
+
+# 禁用词。命中即报 (在显示文本里)。分三类只为可读, 检查时一视同仁。
+BANNED = [
+ # 第一二人称
+ "我的", "等我", "等你", "要你", "给你", "替你", "让你", "问过你", "你自己",
+ "你设", "你改", "你点", "你来", "你现在", "你还没", "你做过", "你来定", "交你定",
+ # 拟人与口语
+ "拍板", "点头", "盯着", "在盯", "盯的", "拿不准", "记一笔", "挤掉", "平安无事",
+ # 生造动词与口语动词
+ "出手", "休假", "一跳", "发生了什么",
+]
+
+OFF, ON = "copy-guard:off", "copy-guard:on"
+
+
+def _strip_line_comment(line: str) -> str:
+ """去掉一行里的 JS 行注释 (// 到行尾), 但跳过字符串内的 // 与网址里的 ://。
+ 走一遍字符, 记录当前是否在 ' " ` 里; 不在字符串里遇到 // 且前一个字符不是 : 就截断。"""
+ q = None # 当前字符串引号, None 表示不在字符串里
+ i, n = 0, len(line)
+ while i < n:
+ ch = line[i]
+ if q:
+ if ch == "\\":
+ i += 2
+ continue
+ if ch == q:
+ q = None
+ else:
+ if ch in "'\"`":
+ q = ch
+ elif ch == "/" and i + 1 < n and line[i + 1] == "/":
+ if i > 0 and line[i - 1] == ":": # 网址 :// 不是注释
+ i += 2
+ continue
+ return line[:i]
+ i += 1
+ return line
+
+
+def _exempt_ranges(raw: str):
+ """在原始文本上找成对豁免区。返回 (豁免行号集合, 报错列表)。缺原因或不成对判红。"""
+ bad = []
+ lines = raw.split("\n")
+ ranges = set()
+ stack = []
+ for i, line in enumerate(lines, 1):
+ if OFF in line:
+ after = line.split(OFF, 1)[1]
+ reason = re.sub(r"[-#>*/\s]+$", "", after).strip(" -#>*/")
+ if not reason:
+ bad.append(f"第 {i} 行:copy-guard:off 缺原因")
+ stack.append(i)
+ if ON in line:
+ if not stack:
+ bad.append(f"第 {i} 行:copy-guard:on 没有对应的 off")
+ else:
+ start = stack.pop()
+ for k in range(start, i + 1):
+ ranges.add(k)
+ for s in stack:
+ bad.append(f"第 {s} 行:copy-guard:off 没有对应的 on")
+ return ranges, bad
+
+
+def main():
+ raw = open(PAGE, encoding="utf-8").read()
+ exempt, bad = _exempt_ranges(raw)
+
+ # 剥注释 (保行号)
+ def _blank(m):
+ return "\n" * m.group(0).count("\n")
+ src = re.sub(r"", _blank, raw, flags=re.S)
+ src = re.sub(r"/\*.*?\*/", _blank, src, flags=re.S)
+ lines = src.split("\n")
+
+ for i, line in enumerate(lines, 1):
+ if i in exempt:
+ continue
+ text = _strip_line_comment(line)
+ for w in BANNED:
+ idx = text.find(w)
+ if idx >= 0:
+ seg = text.strip()[:100]
+ bad.append(f"第 {i} 行:口语化词「{w}」出现在显示文本里。\n {seg}")
+
+ # 内置自检: 只在注释里出现不算, 在字符串里出现要算
+ _self_ok(bad)
+
+ if bad:
+ print(f"页面文案守卫·口语化:发现 {len(bad)} 处")
+ for m in bad:
+ print(" FAIL", m)
+ sys.exit(1)
+ print(f"ALL OK — 页面文案守卫·口语化:{len(BANNED)} 个禁用词、注释剥离、成对豁免 全部通过")
+
+
+def _self_ok(bad):
+ """自检样例: 注释里的禁用词不该报, 字符串里的该报。任一不符往 bad 里加一条。"""
+ only_comment = " // 这里解释一下等我拍板的逻辑\n const x = 1;"
+ in_string = " const t = '等我拍板';"
+ # 注释行剥掉后不含禁用词
+ c = _strip_line_comment(only_comment.split("\n")[0])
+ if any(w in c for w in BANNED):
+ bad.append("自检失败:行注释里的禁用词没有被剥掉")
+ # 字符串行剥不掉 (没有 //), 仍含禁用词
+ s = _strip_line_comment(in_string)
+ if not any(w in s for w in BANNED):
+ bad.append("自检失败:字符串里的禁用词被误剥")
+ # 网址不被当注释截断
+ u = _strip_line_comment(" const url = 'https://a.b/c'; const y = 2;")
+ if "const y = 2" not in u:
+ bad.append("自检失败:网址里的 // 被当成注释起点")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/test_wiring.py b/scripts/test_wiring.py
index 431222e..ec75b69 100644
--- a/scripts/test_wiring.py
+++ b/scripts/test_wiring.py
@@ -1889,7 +1889,7 @@ def _():
_prop_fakes(params={"PMS_AUTONOMY": "full", "PMS_GLOBAL_EXEC_HALT": "true"})
r2 = ps.scan_and_route()
- assert any("休假" in s["why"] for s in r2["skipped"]), r2
+ assert any("暂停执行" in s["why"] for s in r2["skipped"]), r2
fake = _prop_fakes(params={"PMS_AUTONOMY": "propose_only"})
r3 = ps.scan_and_route(dry_run=True)