Fix reply language ignored when user writes Chinese.

Strengthen Host prompt injection with a per-step reply reminder, read live settings, and stop persona from forcing follow-user language.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-15 20:38:51 +08:00
parent d52c05e774
commit 91f2e9a5c8
7 changed files with 108 additions and 19 deletions

View file

@ -1343,11 +1343,23 @@ function replyInstruction(setting) {
return "";
const meta = REPLY_META[normalized];
return [
"## Reply language requirement",
`Regardless of the language the user writes in, you must always write the entire reply in ${meta.native} (${meta.name}) —`,
"including explanations, summaries, body text, titles, and error descriptions.",
"Keep code, commands, file paths, API names, host names, and proper nouns in their original form; do not translate them.",
"This requirement outranks language hints in the user message or persona until the Netx Ops reply-language setting changes."
"## Reply language requirement (mandatory)",
`The operator set Netx Ops reply language to ${meta.native} (${meta.name}).`,
`You MUST write the entire final reply in ${meta.name} only.`,
`Do not switch to the user's language even if they write in Chinese, English, or any other language.`,
"This includes titles, bullet labels (Result / Evidence / Next may stay as English keywords), explanations, summaries, warnings, and error text.",
"Keep code, commands, file paths, API names, host names, UUIDs, and proper nouns unchanged.",
'This rule outranks persona wording such as "follow the user language" and outranks language hints in the user message.'
].join(" ");
}
function replyReminder(setting) {
const normalized = normalizeReplyLanguage(setting);
if (normalized === REPLY_LANGUAGE_DEFAULT)
return "";
const meta = REPLY_META[normalized];
return [
`[Reply language] Mandatory: write your entire final answer in ${meta.native} (${meta.name}).`,
"Do not answer in the user's language. Code/paths/host names stay original."
].join(" ");
}
@ -4119,11 +4131,26 @@ function apply(ctx, config = Config({})) {
});
ctx.inject(["systemPrompt", "settings"], (promptCtx) => {
const systemPrompt = promptCtx.systemPrompt;
const liveConfig = () => {
const settings = promptCtx.get("settings");
const section = typeof settings?.get === "function" ? (() => {
try {
return settings.get(NETXOPS_SETTINGS_NAMESPACE);
} catch {
return;
}
})() : undefined;
if (section !== null && typeof section === "object" && !Array.isArray(section)) {
return { ...source(), ...section };
}
return source();
};
const effectiveThinking = () => {
const settings = promptCtx.get("settings");
const current = source();
const current = liveConfig();
return resolveThinkingLanguage(normalizeThinkingLanguage(current.thinkingLanguage), readSystemLocalePreference(settings));
};
const replySetting = () => normalizeReplyLanguage(liveConfig().replyLanguage);
promptCtx.effect(() => {
const disposers = [];
const push = (dispose) => {
@ -4142,9 +4169,15 @@ function apply(ctx, config = Config({})) {
}));
push(systemPrompt.section({
name: "netxops:reply-language",
order: 890,
text: () => replyInstruction(normalizeReplyLanguage(source().replyLanguage))
order: 10150,
text: () => replyInstruction(replySetting())
}));
push(systemPrompt.context({
name: "netxops:reply-language-reminder",
order: 1100,
text: () => replyReminder(replySetting())
}));
promptCtx.logger.info("netxops: model language prompts registered (thinking=%s reply=%s)", normalizeThinkingLanguage(liveConfig().thinkingLanguage), replySetting());
return () => {
for (const dispose of disposers.splice(0))
dispose();

View file

@ -1,6 +1,6 @@
{
"name": "dsh-netxops",
"version": "0.1.31",
"version": "0.1.32",
"description": "DeepSeek Harness Netx Ops: ops/topology + IM delivery + session export + knowledge-base MANIFEST/_skills + localSkills",
"license": "MIT",
"type": "module",

View file

@ -9,7 +9,7 @@
## 原则
1. 先用工具拿证据(告警、清单、CLI),再下结论。
2. 涉及破坏性变更:先说清影响与回滚(当前 CLI 仅只读:show / display / ping 等)。
3. 跟随用户语言(含标题与说明)。现场默认:简洁、可扫读的运维口吻。
3. 回复语言以 Host「Netx Ops」设置里的「回复语言」为准(强制 zh/en 时不得跟随用户语言);未强制(follow-user)时再跟随用户语言。现场默认:简洁、可扫读的运维口吻。
## 终稿骨架(告警 / 网元 / CLI 必须)

View file

@ -18,7 +18,7 @@
## 原则
1. 先用工具拿证据(告警、清单、CLI),再下结论。
2. 涉及破坏性变更:先说清影响与回滚(当前 CLI 仅只读:show / display / ping 等)。
3. 跟随用户语言(含标题与说明)。现场默认:简洁、可扫读的运维口吻。
3. 回复语言以 Host「Netx Ops」设置里的「回复语言」为准(强制 zh/en 时不得跟随用户语言);未强制(follow-user)时再跟随用户语言。现场默认:简洁、可扫读的运维口吻。
## 终稿骨架(告警 / 网元 / CLI 必须)
```

View file

@ -59,6 +59,7 @@ import {
normalizeThinkingLanguage,
readSystemLocalePreference,
replyInstruction,
replyReminder,
resolveThinkingLanguage,
thinkingInstruction,
thinkingReminder,
@ -412,14 +413,34 @@ export function apply(ctx: Context, config: Config = Config({})): void {
context: (spec: { name: string, order: number, text: string | (() => string) }) => (() => void) | void
}
const systemPrompt = (promptCtx as { systemPrompt: PromptSurface }).systemPrompt
/** Prefer live settings document over cordis base config (survives UI saves). */
const liveConfig = (): Config => {
const settings = promptCtx.get('settings') as {
get?: (ns: string) => unknown
} | undefined
const section = typeof settings?.get === 'function'
? (() => {
try {
return settings.get!(NETXOPS_SETTINGS_NAMESPACE)
} catch {
return undefined
}
})()
: undefined
if (section !== null && typeof section === 'object' && !Array.isArray(section)) {
return { ...source(), ...(section as Partial<Config>) }
}
return source()
}
const effectiveThinking = (): ReturnType<typeof resolveThinkingLanguage> => {
const settings = promptCtx.get('settings') as { get?: (ns: string) => unknown } | undefined
const current = source()
const current = liveConfig()
return resolveThinkingLanguage(
normalizeThinkingLanguage(current.thinkingLanguage),
readSystemLocalePreference(settings),
)
}
const replySetting = (): string => normalizeReplyLanguage(liveConfig().replyLanguage)
promptCtx.effect(() => {
const disposers: Array<() => void> = []
const push = (dispose: (() => void) | void): void => {
@ -435,11 +456,22 @@ export function apply(ctx: Context, config: Config = Config({})): void {
order: 1000,
text: () => thinkingReminder(effectiveThinking()),
}))
// Late section + per-step context so reply language beats persona / user language.
push(systemPrompt.section({
name: 'netxops:reply-language',
order: 890,
text: () => replyInstruction(normalizeReplyLanguage(source().replyLanguage)),
order: 10150,
text: () => replyInstruction(replySetting()),
}))
push(systemPrompt.context({
name: 'netxops:reply-language-reminder',
order: 1100,
text: () => replyReminder(replySetting()),
}))
promptCtx.logger.info(
'netxops: model language prompts registered (thinking=%s reply=%s)',
normalizeThinkingLanguage(liveConfig().thinkingLanguage),
replySetting(),
)
return () => {
for (const dispose of disposers.splice(0)) dispose()
}

View file

@ -135,10 +135,26 @@ export function replyInstruction(setting: string | undefined | null): string {
if (normalized === REPLY_LANGUAGE_DEFAULT) return ''
const meta = REPLY_META[normalized]
return [
'## Reply language requirement',
`Regardless of the language the user writes in, you must always write the entire reply in ${meta.native} (${meta.name}) —`,
'including explanations, summaries, body text, titles, and error descriptions.',
'Keep code, commands, file paths, API names, host names, and proper nouns in their original form; do not translate them.',
'This requirement outranks language hints in the user message or persona until the Netx Ops reply-language setting changes.',
'## Reply language requirement (mandatory)',
`The operator set Netx Ops reply language to ${meta.native} (${meta.name}).`,
`You MUST write the entire final reply in ${meta.name} only.`,
`Do not switch to the user's language even if they write in Chinese, English, or any other language.`,
'This includes titles, bullet labels (Result / Evidence / Next may stay as English keywords), explanations, summaries, warnings, and error text.',
'Keep code, commands, file paths, API names, host names, UUIDs, and proper nouns unchanged.',
'This rule outranks persona wording such as "follow the user language" and outranks language hints in the user message.',
].join(' ')
}
/**
* Per-step reminder for forced reply language.
* Empty when `follow-user`. Appended after the user message each model call.
*/
export function replyReminder(setting: string | undefined | null): string {
const normalized = normalizeReplyLanguage(setting)
if (normalized === REPLY_LANGUAGE_DEFAULT) return ''
const meta = REPLY_META[normalized]
return [
`[Reply language] Mandatory: write your entire final answer in ${meta.native} (${meta.name}).`,
'Do not answer in the user\'s language. Code/paths/host names stay original.',
].join(' ')
}

View file

@ -6,6 +6,7 @@ import {
normalizeReplyLanguage,
normalizeThinkingLanguage,
replyInstruction,
replyReminder,
resolveThinkingLanguage,
thinkingInstruction,
thinkingReminder,
@ -48,5 +49,12 @@ test('thinkingInstruction and reminder mention the target language', () => {
test('replyInstruction is empty for follow-user and forces otherwise', () => {
assert.equal(replyInstruction('follow-user'), '')
assert.match(replyInstruction('en'), /English/)
assert.match(replyInstruction('en'), /Do not switch to the user's language/)
assert.match(replyInstruction('zh'), /简体中文/)
})
test('replyReminder is empty for follow-user and reminds otherwise', () => {
assert.equal(replyReminder('follow-user'), '')
assert.match(replyReminder('en'), /Mandatory/)
assert.match(replyReminder('en'), /English/)
})