diff --git a/cordis.patch.yml b/cordis.patch.yml index 1c44e5b..bf23083 100644 --- a/cordis.patch.yml +++ b/cordis.patch.yml @@ -7,8 +7,9 @@ # github: packages as transitive deps of this package). Install them in the # same `dsh plugin add` line — see docs/INSTALL.md. # -# After install, open Settings → Plugins to edit apiUrl / lang / groups -# (settings card client half; see docs/INSTALL.md). Token: credential NETX_API_TOKEN. +# After install, open Settings → Plugins to edit apiUrl / lang / thinking+reply +# language / groups (settings card client half; see docs/INSTALL.md). +# Token: credential NETX_API_TOKEN. Model language is Host-global (all presets). - insert: - id: netxops @@ -16,6 +17,8 @@ config: apiUrl: http://127.0.0.1:8890 lang: zh + thinkingLanguage: auto + replyLanguage: follow-user nmsProvider: zte-ume tokenCredentialRef: NETX_API_TOKEN toolCallTimeoutMs: 120000 diff --git a/docs/INSTALL.md b/docs/INSTALL.md index de502d0..bdf956b 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -8,7 +8,7 @@ One install wires **all of**: |-------|----------------| | Host tools | Capability groups **ops / topology** (one group ↔ one skill). Default ops in Ops preset; topology and all public off. Other agents may mount `dsh-netxops/tools-ops|topology` | | Companion plugins | **dsh-im-ops** + **dsh-ops-cron** — add as **direct** profile deps in the same command (DSH/pnpm blocks `github:` as transitive deps) | -| Plugins card | Settings → Plugins → **Netx Ops** (URL / lang / token / capability groups) | +| Plugins card | Settings → Plugins → **Netx Ops** (URL / API lang / thinking+reply language / token / capability groups) | | Agent preset + skills | Settings → Agent presets → **Custom → Netx Ops** (copied into `~/.dsh/.agent-presets` on first boot); playbooks follow the same group toggles | You do **not** run `link-preset.ps1` for normal use. That script is only a manual fallback. diff --git a/docs/PORTING.md b/docs/PORTING.md index 55f1836..3484525 100644 --- a/docs/PORTING.md +++ b/docs/PORTING.md @@ -11,7 +11,7 @@ Adaptations: removed oclaw Admin / WhatsApp / `ume_alarm_xlsx_report` / wiki capture / skill_auto_install; fiber/offline recipes use Raw/aggregate tools only. DSH no longer spawns `python -m netx_mcp`. Model-facing NMS tools renamed to `*Nms*`; ZTE UME remains the `nmsProvider=zte-ume` adapter. -**Config UX:** Settings → Plugins → **Netx Ops** card (`src/client/` → `lib/client.js`). Token via credentials `NETX_API_TOKEN`; `apiUrl` / `lang` / capability groups / `nmsProvider` via settings namespace `netxops`. +**Config UX:** Settings → Plugins → **Netx Ops** card (`src/client/` → `lib/client.js`). Token via credentials `NETX_API_TOKEN`; `apiUrl` / `lang` / `thinkingLanguage` / `replyLanguage` / capability groups / `nmsProvider` via settings namespace `netxops`. Thinking/reply language inject Host-global `systemPrompt` (all presets). ## Stay in oclaw (until retired) diff --git a/examples/local-debug/patch.cordis.yml b/examples/local-debug/patch.cordis.yml index f598583..62bc899 100644 --- a/examples/local-debug/patch.cordis.yml +++ b/examples/local-debug/patch.cordis.yml @@ -22,6 +22,8 @@ config: apiUrl: http://127.0.0.1:8890 lang: zh + thinkingLanguage: auto + replyLanguage: follow-user tokenCredentialRef: NETX_API_TOKEN toolCallTimeoutMs: 120000 installAgentPreset: true diff --git a/lib/client.js b/lib/client.js index da50b51..44e2215 100644 --- a/lib/client.js +++ b/lib/client.js @@ -667,11 +667,14 @@ var CSS = ` .dsh-nx-field-head>label,.dsh-nx-field-head>.dsh-nx-field-label{flex:1;min-width:0} .dsh-nx-settings-field input[type=text], .dsh-nx-settings-field input[type=password], -.dsh-nx-settings-field input:not([type]){ +.dsh-nx-settings-field input:not([type]), +.dsh-nx-settings-field select{ box-sizing:border-box;width:100%;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,#dfe1e5);border-radius:6px;font:inherit;font-size:13px;line-height:20px;background:var(--dsw-alias-bg-module-platform,#f4f5f7);color:inherit } -.dsh-nx-settings-field input:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#3370ff);outline-offset:1px} -.dsh-nx-settings-field input:disabled{opacity:.55;cursor:default} +.dsh-nx-settings-field input:focus-visible, +.dsh-nx-settings-field select:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary,#3370ff);outline-offset:1px} +.dsh-nx-settings-field input:disabled, +.dsh-nx-settings-field select:disabled{opacity:.55;cursor:default} .dsh-nx-inputInvalid{border-color:var(--dsw-alias-state-error-primary,#d54941)!important} .dsh-nx-hint{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary,#8f959e)} .dsh-nx-invalid{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-state-error-primary,#d54941)} @@ -967,6 +970,63 @@ function ValueField(props) { ] }); } +function SelectField(props) { + const known = props.options.some((row) => row.value === props.field.text); + return /* @__PURE__ */ jsx_runtime.jsxs("div", { + className: "dsh-nx-settings-field", + children: [ + /* @__PURE__ */ jsx_runtime.jsxs("div", { + className: "dsh-nx-field-head", + children: [ + /* @__PURE__ */ jsx_runtime.jsx("label", { + htmlFor: props.id, + children: props.label + }), + props.field.overridden ? /* @__PURE__ */ jsx_runtime.jsxs("span", { + className: "dsh-nx-badges", + children: [ + /* @__PURE__ */ jsx_runtime.jsx("span", { + className: "dsh-nx-badge", + children: props.overriddenLabel + }), + /* @__PURE__ */ jsx_runtime.jsx("button", { + type: "button", + className: "dsh-nx-reset", + disabled: props.disabled, + onClick: props.onReset, + children: props.resetLabel + }) + ] + }) : null + ] + }), + /* @__PURE__ */ jsx_runtime.jsxs("select", { + id: props.id, + className: props.field.invalid ? "dsh-nx-inputInvalid" : undefined, + value: props.field.text, + disabled: props.disabled, + "aria-invalid": props.field.invalid || undefined, + onChange: (event) => { + props.onEdit(event.target.value); + }, + children: [ + !known && props.field.text ? /* @__PURE__ */ jsx_runtime.jsx("option", { + value: props.field.text, + children: props.field.text + }) : null, + props.options.map((row) => /* @__PURE__ */ jsx_runtime.jsx("option", { + value: row.value, + children: row.label + }, row.value)) + ] + }), + /* @__PURE__ */ jsx_runtime.jsx("p", { + className: props.field.invalid ? "dsh-nx-invalid" : "dsh-nx-hint", + children: props.field.invalid ? props.invalidLabel : props.hint + }) + ] + }); +} function ImDeliveryPicker(props) { const options = props.catalog.options; const selected = resolveImTargets({ @@ -1275,6 +1335,48 @@ function NetxopsCard(props) { props.resetField("lang"); } }), + /* @__PURE__ */ jsx_runtime.jsx(SelectField, { + id: "netxops-thinking-language", + label: t("thinkingLanguage"), + hint: t("thinkingLanguageHint"), + field: state.thinkingLanguage, + options: [ + { value: "auto", label: "auto" }, + { value: "zh-CN", label: "zh-CN" }, + { value: "en", label: "en" } + ], + overriddenLabel: t("overridden"), + resetLabel: t("reset"), + invalidLabel: t("invalid"), + disabled, + onEdit: (text) => { + props.edit("thinkingLanguage", text); + }, + onReset: () => { + props.resetField("thinkingLanguage"); + } + }), + /* @__PURE__ */ jsx_runtime.jsx(SelectField, { + id: "netxops-reply-language", + label: t("replyLanguage"), + hint: t("replyLanguageHint"), + field: state.replyLanguage, + options: [ + { value: "follow-user", label: "follow-user" }, + { value: "zh", label: "zh" }, + { value: "en", label: "en" } + ], + overriddenLabel: t("overridden"), + resetLabel: t("reset"), + invalidLabel: t("invalid"), + disabled, + onEdit: (text) => { + props.edit("replyLanguage", text); + }, + onReset: () => { + props.resetField("replyLanguage"); + } + }), /* @__PURE__ */ jsx_runtime.jsx(ValueField, { id: "netxops-nms-provider", label: t("nmsProvider"), @@ -1860,6 +1962,8 @@ class NetxopsCardController { this.form = new CardForm(scope, [ textField("apiUrl"), textField("lang"), + textField("thinkingLanguage"), + textField("replyLanguage"), textField("nmsProvider"), textField("kbRoot"), booleanFieldPersistFalse("groupOpsInPreset"), @@ -2127,6 +2231,8 @@ class NetxopsCardController { ...this.form.shell(), apiUrl: this.form.field("apiUrl"), lang: this.form.field("lang"), + thinkingLanguage: this.form.field("thinkingLanguage"), + replyLanguage: this.form.field("replyLanguage"), nmsProvider: this.form.field("nmsProvider"), kbRoot: this.form.field("kbRoot"), groupOpsInPreset: this.form.field("groupOpsInPreset"), @@ -2281,8 +2387,12 @@ var en = { sectionExport: "Session export", apiUrl: "API URL", apiUrlHint: "e.g. http://127.0.0.1:8890", - lang: "Language", - langHint: "zh / en", + lang: "API / alarm language", + langHint: "zh / en — netx API query + alarm prompt copy only (not model reply)", + thinkingLanguage: "Thinking language", + thinkingLanguageHint: "auto / zh-CN / en — model chain-of-thought for all presets", + replyLanguage: "Reply language", + replyLanguageHint: "follow-user / zh / en — final answers for all presets", capabilityGroups: "Capability groups", nmsProvider: "NMS provider", nmsProviderHint: "zte-ume", @@ -2354,8 +2464,12 @@ var zh = { sectionExport: "会话导出", apiUrl: "API 地址", apiUrlHint: "例如 http://127.0.0.1:8890", - lang: "语言", - langHint: "zh / en", + lang: "API / 告警语言", + langHint: "zh / en — 仅影响 netx API 与告警文案,不控制模型回复", + thinkingLanguage: "思考语言", + thinkingLanguageHint: "auto / zh-CN / en — 全预设模型思考过程语言", + replyLanguage: "回复语言", + replyLanguageHint: "follow-user / zh / en — 全预设最终回答语言", capabilityGroups: "能力组", nmsProvider: "NMS 提供方", nmsProviderHint: "zte-ume", diff --git a/lib/index.js b/lib/index.js index 1d34b3f..a5e7909 100644 --- a/lib/index.js +++ b/lib/index.js @@ -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 = () => { diff --git a/package.json b/package.json index 97646af..e34d852 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dsh-netxops", - "version": "0.1.30", + "version": "0.1.31", "description": "DeepSeek Harness Netx Ops: ops/topology + IM delivery + session export + knowledge-base MANIFEST/_skills + localSkills", "license": "MIT", "type": "module", diff --git a/src/client/NetxopsCard.tsx b/src/client/NetxopsCard.tsx index 3050e5c..7943898 100644 --- a/src/client/NetxopsCard.tsx +++ b/src/client/NetxopsCard.tsx @@ -155,6 +155,57 @@ function ValueField(props: { ) } +function SelectField(props: { + id: string + label: string + hint: string + field: CardFieldState + options: ReadonlyArray<{ value: string, label: string }> + overriddenLabel: string + resetLabel: string + invalidLabel: string + disabled: boolean + onEdit: (text: string) => void + onReset: () => void +}) { + const known = props.options.some((row) => row.value === props.field.text) + return ( +
+ {props.field.invalid ? props.invalidLabel : props.hint} +
+