Add Host-global thinking and reply language controls.

Inject systemPrompt sections from Netx Ops settings so every preset can switch CoT and final-reply language without changing API/alarm lang.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-15 20:15:54 +08:00
parent 53c47fbc6a
commit d52c05e774
14 changed files with 652 additions and 25 deletions

View file

@ -1245,6 +1245,112 @@ function resetKbContext() {
applyKbEnv(unconfiguredKbSnapshot());
}
// src/netx/model-language.ts
var THINKING_LANGUAGE_DEFAULT = "auto";
var REPLY_LANGUAGE_DEFAULT = "follow-user";
var THINKING_LANGUAGE_IDS = ["zh-CN", "en"];
var REPLY_LANGUAGE_IDS = ["zh", "en"];
var THINKING_META = {
"zh-CN": { id: "zh-CN", name: "Simplified Chinese", native: "简体中文" },
en: { id: "en", name: "English", native: "English" }
};
var REPLY_META = {
zh: { id: "zh", name: "Simplified Chinese", native: "简体中文" },
en: { id: "en", name: "English", native: "English" }
};
function normalizeThinkingLanguage(raw) {
const value = String(raw ?? "").trim();
if (!value || value === THINKING_LANGUAGE_DEFAULT)
return THINKING_LANGUAGE_DEFAULT;
const lower = value.toLowerCase().replace(/_/g, "-");
if (lower === "zh" || lower === "zh-cn" || lower === "zh-hans" || lower === "chinese")
return "zh-CN";
if (lower === "en" || lower.startsWith("en-") || lower === "english")
return "en";
if (THINKING_LANGUAGE_IDS.includes(value)) {
return value;
}
return THINKING_LANGUAGE_DEFAULT;
}
function normalizeReplyLanguage(raw) {
const value = String(raw ?? "").trim();
if (!value || value === REPLY_LANGUAGE_DEFAULT || value === "auto" || value === "default") {
return REPLY_LANGUAGE_DEFAULT;
}
const lower = value.toLowerCase().replace(/_/g, "-");
if (lower === "zh" || lower === "zh-cn" || lower === "zh-hans" || lower === "chinese")
return "zh";
if (lower === "en" || lower.startsWith("en-") || lower === "english")
return "en";
if (REPLY_LANGUAGE_IDS.includes(value)) {
return value;
}
return REPLY_LANGUAGE_DEFAULT;
}
function resolveThinkingLanguage(setting, systemLocale) {
const normalized = normalizeThinkingLanguage(setting);
if (normalized !== THINKING_LANGUAGE_DEFAULT)
return normalized;
return languageFromLocale(systemLocale) ?? "en";
}
function languageFromLocale(locale) {
const tag = String(locale ?? "").trim().toLowerCase().replace(/_/g, "-");
if (!tag)
return;
if (tag === "zh" || tag.startsWith("zh-hans") || tag.startsWith("zh-cn") || tag === "zh-sg") {
return "zh-CN";
}
if (tag.startsWith("zh"))
return "zh-CN";
if (tag === "en" || tag.startsWith("en-"))
return "en";
return;
}
function readSystemLocalePreference(settings) {
if (!settings || typeof settings.get !== "function")
return;
let section;
try {
section = settings.get("locale");
} catch {
return;
}
if (section === null || typeof section !== "object" || Array.isArray(section))
return;
const preference = section.preference;
return typeof preference === "string" && preference.trim() ? preference.trim() : undefined;
}
function thinkingInstruction(language) {
const meta = THINKING_META[language];
return [
`The operator configured the language of your thinking/reasoning process: ${meta.native} (${meta.name}).`,
`Write your entire internal reasoning — chain-of-thought, analysis, planning, and deliberation — in ${meta.name}.`,
"Keep code, identifiers, file paths, commands, and technical terms as they are.",
"Your final answer to the user follows the reply-language setting (or the user language when reply language is follow-user); this thinking setting never alone changes the final answer language."
].join(" ");
}
function thinkingReminder(language) {
const meta = THINKING_META[language];
return [
`[Thinking language] The operator wants your reasoning/thinking process written in ${meta.native} (${meta.name}).`,
`Continue your entire chain-of-thought in ${meta.name}.`,
"Final answer language follows the reply-language setting / user language."
].join(" ");
}
function replyInstruction(setting) {
const normalized = normalizeReplyLanguage(setting);
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."
].join(" ");
}
// src/netx/runtime.ts
var STORE_KEY3 = Symbol.for("dsh-netxops.connection-store");
function store3() {
@ -3837,6 +3943,8 @@ var DEFAULT_TOKEN_REF = "NETX_API_TOKEN";
var Config = z.object({
apiUrl: z.string().default("http://127.0.0.1:8890"),
lang: z.string().default("zh"),
thinkingLanguage: z.string().default("auto"),
replyLanguage: z.string().default("follow-user"),
tokenCredentialRef: z.string().role("credential-ref").default(DEFAULT_TOKEN_REF),
toolCallTimeoutMs: z.number().step(1).min(1000).default(120000),
installAgentPreset: z.boolean().default(true),
@ -4009,6 +4117,40 @@ function apply(ctx, config = Config({})) {
if (String(ref) === source().tokenCredentialRef)
publish();
});
ctx.inject(["systemPrompt", "settings"], (promptCtx) => {
const systemPrompt = promptCtx.systemPrompt;
const effectiveThinking = () => {
const settings = promptCtx.get("settings");
const current = source();
return resolveThinkingLanguage(normalizeThinkingLanguage(current.thinkingLanguage), readSystemLocalePreference(settings));
};
promptCtx.effect(() => {
const disposers = [];
const push = (dispose) => {
if (typeof dispose === "function")
disposers.push(dispose);
};
push(systemPrompt.section({
name: "netxops:thinking-language",
order: 85,
text: () => thinkingInstruction(effectiveThinking())
}));
push(systemPrompt.context({
name: "netxops:thinking-language-reminder",
order: 1000,
text: () => thinkingReminder(effectiveThinking())
}));
push(systemPrompt.section({
name: "netxops:reply-language",
order: 890,
text: () => replyInstruction(normalizeReplyLanguage(source().replyLanguage))
}));
return () => {
for (const dispose of disposers.splice(0))
dispose();
};
}, "netxops: model language prompts");
});
ctx.inject(["tools"], (toolsCtx) => {
let unregisterTools;
const remountPublicTools = () => {