mirror of
https://github.com/hansjone/netxops.git
synced 2026-10-09 02:40:45 +08:00
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:
parent
d52c05e774
commit
91f2e9a5c8
7 changed files with 108 additions and 19 deletions
49
lib/index.js
49
lib/index.js
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
## 原则
|
||||
1. 先用工具拿证据(告警、清单、CLI),再下结论。
|
||||
2. 涉及破坏性变更:先说清影响与回滚(当前 CLI 仅只读:show / display / ping 等)。
|
||||
3. 跟随用户语言(含标题与说明)。现场默认:简洁、可扫读的运维口吻。
|
||||
3. 回复语言以 Host「Netx Ops」设置里的「回复语言」为准(强制 zh/en 时不得跟随用户语言);未强制(follow-user)时再跟随用户语言。现场默认:简洁、可扫读的运维口吻。
|
||||
|
||||
## 终稿骨架(告警 / 网元 / CLI 必须)
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
## 原则
|
||||
1. 先用工具拿证据(告警、清单、CLI),再下结论。
|
||||
2. 涉及破坏性变更:先说清影响与回滚(当前 CLI 仅只读:show / display / ping 等)。
|
||||
3. 跟随用户语言(含标题与说明)。现场默认:简洁、可扫读的运维口吻。
|
||||
3. 回复语言以 Host「Netx Ops」设置里的「回复语言」为准(强制 zh/en 时不得跟随用户语言);未强制(follow-user)时再跟随用户语言。现场默认:简洁、可扫读的运维口吻。
|
||||
|
||||
## 终稿骨架(告警 / 网元 / CLI 必须)
|
||||
```
|
||||
|
|
|
|||
38
src/index.ts
38
src/index.ts
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(' ')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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/)
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue