fix: portable 绿色单机版 SSH 连接 + 主机数据 + 打包体积
Some checks failed
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled

SSH 连接修复:
- main.cjs 顶部在所有 ssh2 require 之前安装 boringSslDhCompat 补丁
- 5 条 SSH 连接路径统一 legacyAlgorithms 默认值为 true

portable 版主机数据修复:
- main.cjs portable 模式设置 NETCATTY_PORTABLE=1
- preload.cjs 暴露 window.netcatty.isPortable
- useVaultState portable 模式跳过 safeStorage DPAPI 加密的 hosts_v1
- inspection saveVaultHosts handler 补 legacyAlgorithms/tags/os 字段映射

打包体积修复:
- 恢复 electron-builder.config.cjs 原始 40+ 条排除规则
- package.json 加 pend 运行时依赖
This commit is contained in:
2026-10-21 11:50:00 +08:00
committed by trae-solo-agent
parent 2e71e03d71
commit 2514e509c6
11 changed files with 206 additions and 29 deletions

View File

@@ -1442,7 +1442,13 @@ export const useVaultState = () => {
recoverPluginImporterTransaction(localStorageAdapter, PLUGIN_IMPORT_TRANSACTION_KEYS); recoverPluginImporterTransaction(localStorageAdapter, PLUGIN_IMPORT_TRANSACTION_KEYS);
}); });
if (cancelled) return; if (cancelled) return;
const savedHosts = localStorageAdapter.read<Host[]>(STORAGE_KEY_HOSTS); // portable 版 safeStorage DPAPI 绑定 userData 路径,解不开 dev 复制过来的
// LS 加密 blob。强制跳过 hosts_v1,直接走 else 分支从 inspection-data.json
// 读明文 _vault_hosts(含 password + legacyAlgorithms)。
const isPortable = (window as any).netcatty?.isPortable === true;
const savedHosts = isPortable
? undefined
: localStorageAdapter.read<Host[]>(STORAGE_KEY_HOSTS);
if (savedHosts) { if (savedHosts) {
// Capture version before the async gap so that any write occurring // Capture version before the async gap so that any write occurring
@@ -1452,6 +1458,17 @@ export const useVaultState = () => {
const decrypted = await decryptHosts(savedHosts); const decrypted = await decryptHosts(savedHosts);
if (cancelled) return; if (cancelled) return;
if (ver === hostsWriteVersion.current) { if (ver === hostsWriteVersion.current) {
// safeStorage 解密失败时 decryptField 会返回 enc:v1 占位符,
// sanitizeHost 再把它过滤成 undefined → 密码/私钥消失。
// 检查解密后是否大部分凭证都被清空了,是的话强制 fallback
// 到 inspectionStorage 的明文 _vault_hosts(打包版 portable 场景)。
const hasAnyPassword = decrypted.some((h: Host) =>
h.password && !h.password.startsWith("enc:v1:"));
const hasAnyPrivateKey = decrypted.some((h: Host) =>
h.privateKey && !h.privateKey.startsWith("enc:v1:"));
const credentialsLikelyLost =
decrypted.length > 0 && !hasAnyPassword && !hasAnyPrivateKey;
const sanitized = normalizeVaultOrder( const sanitized = normalizeVaultOrder(
migrateHostsFromLegacyLineTimestamps( migrateHostsFromLegacyLineTimestamps(
decrypted.map((host) => sanitizeHost(host)), decrypted.map((host) => sanitizeHost(host)),
@@ -1473,6 +1490,41 @@ export const useVaultState = () => {
return commitEncryptedHostsUnderVaultLock(ver, sanitized, enc); return commitEncryptedHostsUnderVaultLock(ver, sanitized, enc);
}); });
hostsWritePendingRef.current = writePromise; hostsWritePendingRef.current = writePromise;
// 解密后凭证丢失 → 用 inspectionStorage 的明文覆盖
if (credentialsLikelyLost) {
const nc = (window as any).netcatty;
if (nc?.inspection?.getVaultHosts) {
try {
const vaultHosts = await nc.inspection.getVaultHosts();
if (Array.isArray(vaultHosts) && vaultHosts.length > 0) {
const fallback: Host[] = vaultHosts.map((v: any) => ({
id: v.id,
label: v.label || "",
hostname: v.hostname || "",
port: v.port || 22,
username: v.username || "",
password: v.password || undefined,
identityFileId: v.identityFileId || undefined,
privateKey: v.privateKey || "",
passphrase: v.passphrase || "",
tags: v.tags || [],
os: v.os || "linux",
type: v.type || "",
distro: v.distro || "",
group: v.group || "",
legacyAlgorithms: v.legacyAlgorithms,
}));
const fbSanitized = normalizeVaultOrder(
fallback.map((h) => sanitizeHost(h)),
);
if (cancelled) return;
hostsRef.current = fbSanitized;
setHosts(fbSanitized);
}
} catch {}
}
}
} }
} else { } else {
// 本地 Local Storage 为空(可能是 origin 隔离导致的打包版环境) // 本地 Local Storage 为空(可能是 origin 隔离导致的打包版环境)
@@ -1499,6 +1551,7 @@ export const useVaultState = () => {
type: v.type || "", type: v.type || "",
distro: v.distro || "", distro: v.distro || "",
group: v.group || "", group: v.group || "",
legacyAlgorithms: v.legacyAlgorithms,
})); }));
if (cancelled) return; if (cancelled) return;
const sanitized = normalizeVaultOrder( const sanitized = normalizeVaultOrder(

View File

@@ -1,4 +1,4 @@
/** /**
* 运行监控 - 3D 拓扑大屏 * 运行监控 - 3D 拓扑大屏
* *
* 移植自 ITYUNWEI TopologyView.vue * 移植自 ITYUNWEI TopologyView.vue

View File

@@ -1,10 +1,6 @@
const path = require('node:path'); const path = require('node:path');
const { moshExtraResources } = require('./scripts/mosh-extra-resources.cjs'); const { moshExtraResources } = require('./scripts/mosh-extra-resources.cjs');
const { etExtraResources } = require('./scripts/et-extra-resources.cjs'); const { etExtraResources } = require('./scripts/et-extra-resources.cjs');
// 说明:electron-builder 依赖收集器在 monorepo/workspace 项目里会漏掉
// workspace 子包引用但根 package.json 没声明的运行时依赖(hoist 上来的),
// 且追踪传递依赖时容易漏包。这里直接把整个 node_modules 目录纳入 files,
// 确保运行时 require 什么都能找到。用排除列表去掉前端/构建工具/文档/测试代码。
/** /**
* @type {import('electron-builder').Configuration} * @type {import('electron-builder').Configuration}
@@ -58,19 +54,117 @@ module.exports = {
output: 'release' output: 'release'
}, },
files: [ files: [
// electron-builder 收集器只读根 package.json,但 workspace 子包的运行时依赖 'dist/**/*',
// 会 hoist 到根 node_modules 却没在根 package.json 里声明。 'electron/**/*',
// 收集器会漏掉这些包,所以把漏的补进根 package.json 后它就能追踪完整传递依赖树。 // The Windows Hello helper is shipped once through win.extraResources.
"dist/**/*", // Exclude local per-arch build output from app.asar on every platform.
"electron/**/*", '!electron/bridges/windowsHelloHelper/build/**/*',
"!electron/bridges/windowsHelloHelper/build/**/*", // Runtime smoke fixtures are built test packages, not host resources.
"!electron/plugins/fixtures/**/*", // Keep them out of production ASARs so no example plugin can be
"infrastructure/config/terminalFlowConstants.cjs", // mistaken for an installed or host-trusted package.
"infrastructure/config/terminalFlowConstants.json", '!electron/plugins/fixtures/**/*',
"domain/terminalPromptSecurity.shared.cjs", // Main-process terminal flow control reads shared thresholds from here
"lib/**/*.cjs", // (terminalFlowAck.cjs). Must ship beside electron/ in app.asar.
"lib/**/*.json", 'infrastructure/config/terminalFlowConstants.cjs',
"skills/**/*" 'infrastructure/config/terminalFlowConstants.json',
// Renderer and Mosh's main-process bootstrap share the same
// fail-closed prompt classifier at runtime.
'domain/terminalPromptSecurity.shared.cjs',
'lib/**/*.cjs',
'lib/**/*.json',
'skills/**/*',
'!public/**/*',
'!**/*.map',
'!**/*.d.ts',
'!**/*.d.mts',
'!**/*.d.cts',
'!**/*.ts',
'!**/*.tsx',
'!**/*.test.*',
'!**/*.spec.*',
'!**/__tests__/**/*',
'!**/test/**/*',
'!**/tests/**/*',
'!**/example/**/*',
'!**/examples/**/*',
'!node_modules/**/docs/**/*',
'!node_modules/**/doc/**/*',
'!node_modules/**/benchmark/**/*',
'!node_modules/**/benchmarks/**/*',
// Renderer-only packages are compiled into dist by Vite. Keep them
// installed for npm run dev/build, but do not ship the duplicate source
// packages in release artifacts.
'!node_modules/@fontsource/**/*',
'!node_modules/@monaco-editor/**/*',
'!node_modules/@radix-ui/**/*',
'!node_modules/@xterm/**/*',
'!node_modules/lucide-react/**/*',
'!node_modules/monaco-editor/**/*',
'!node_modules/react/**/*',
'!node_modules/react-dom/**/*',
'!node_modules/ai/**/*',
'!node_modules/@ai-sdk/**/*',
'!node_modules/@mdxeditor/**/*',
'!node_modules/streamdown/**/*',
'!node_modules/@streamdown/**/*',
'!node_modules/@tanstack/react-virtual/**/*',
'!node_modules/pinyin-pro/**/*',
'!node_modules/re2js/**/*',
'!node_modules/@eslint-community/regexpp/**/*',
'!node_modules/clsx/**/*',
'!node_modules/tailwind-merge/**/*',
'!node_modules/use-stick-to-bottom/**/*',
'!node_modules/lexical/**/*',
'!node_modules/@lexical/**/*',
'!node_modules/@codemirror/**/*',
'!node_modules/katex/**/*',
'!node_modules/shiki/**/*',
'!node_modules/@shiki/**/*',
// Heavy cloud completion specs are intentionally not bundled. The main
// process filters the same prefixes so dev and packaged builds behave
// consistently.
'!node_modules/@withfig/autocomplete/build/aws.js',
'!node_modules/@withfig/autocomplete/build/aws/**/*',
'!node_modules/@withfig/autocomplete/build/gcloud.js',
'!node_modules/@withfig/autocomplete/build/gcloud/**/*',
'!node_modules/@withfig/autocomplete/build/az/**/*',
// Fig specs are already compiled JavaScript; TypeScript is only pulled
// in by Fig helper packages as build tooling and is not needed at app
// runtime.
'!node_modules/typescript/**/*',
// ── Exclude per-platform native agent binaries (~100s of MB each). ──
// Netcatty is "bring your own CLI": each SDK is pointed at the user's
// system-installed CLI via an absolute path override (claude
// pathToClaudeCodeExecutable / codex codexPathOverride / copilot cliPath).
// Only the SDKs' JS is bundled; the heavy per-arch binaries are dropped.
// NOTE: claude-agent-sdk vendors the `claude` binary as a NESTED package
// (claude-agent-sdk/node_modules/@anthropic-ai/claude-agent-sdk-<arch>),
// so this exclusion must match at any depth (**/), not just top-level.
// The codex/copilot exclusions target only the per-arch binary packages
// (codex-<arch> / copilot-<arch>) — NOT @openai/codex-sdk / copilot-sdk,
// whose JS we DO bundle. @github/copilot is the full ~288MB CLI (with
// per-platform prebuilds); netcatty uses the user's copilot via cliPath,
// so it is excluded entirely.
'!**/@anthropic-ai/claude-agent-sdk-*/**/*',
'!node_modules/@anthropic-ai/claude-code-*/**/*',
'!node_modules/@openai/codex-{darwin,linux,linuxmusl,win32}-*/**/*',
'!node_modules/@github/copilot-{darwin,linux,linuxmusl,win32}-*/**/*',
'!node_modules/@github/copilot/**/*',
'!node_modules/opencode-{darwin,linux,linuxmusl,windows}-*/**/*',
'!node_modules/opencode-ai/**/*',
// CodeBuddy follows the same first-party integration model as the
// other coding agents: Netcatty discovers and passes the user's
// installed CLI path to the SDK. Keep the small SDK wrapper, but do not
// bundle the full CodeBuddy CLI payload (rg vendors + web UI).
'!node_modules/@tencent-ai/agent-sdk/cli/**/*',
// Netcatty loads Cursor SDK through ESM dynamic import, so the duplicate
// CommonJS build and type metadata are not needed at runtime.
'!node_modules/@cursor/sdk/dist/cjs/**/*',
'!node_modules/@cursor/sdk/dist/**/*.d.ts',
'!node_modules/@cursor/sdk/dist/**/*.d.ts.map',
// sqlite3 rebuilds a native module for Electron; its upstream source
// tarball is build-time payload only.
'!node_modules/sqlite3/deps/**/*'
], ],
asarUnpack: [ asarUnpack: [
'node_modules/node-pty/**/*', 'node_modules/node-pty/**/*',

View File

@@ -1620,9 +1620,12 @@ function registerInspectionHandlers(ipcMain, options) {
identityFileId: h.identityFileId, identityFileId: h.identityFileId,
privateKey: h.privateKey, privateKey: h.privateKey,
passphrase: h.passphrase, passphrase: h.passphrase,
tags: h.tags || [],
os: h.os || "linux",
type: h.type || "", type: h.type || "",
distro: h.distro || h.catalogId || "", distro: h.distro || h.catalogId || "",
group: h.group || "", group: h.group || "",
legacyAlgorithms: h.legacyAlgorithms,
})); }));
writeStorage("_vault_hosts", mapped); writeStorage("_vault_hosts", mapped);
return { ok: true, count: mapped.length }; return { ok: true, count: mapped.length };

View File

@@ -1388,7 +1388,7 @@ async function startPortForward(event, payload) {
keepaliveCountMax: keepalivePolicy.keepaliveCountMax, keepaliveCountMax: keepalivePolicy.keepaliveCountMax,
// Enable keyboard-interactive authentication (required for 2FA/MFA) // Enable keyboard-interactive authentication (required for 2FA/MFA)
tryKeyboard: true, tryKeyboard: true,
algorithms: buildAlgorithms(legacyAlgorithms, { skipEcdsaHostKey, algorithmOverrides }), algorithms: buildAlgorithms(legacyAlgorithms !== false, { skipEcdsaHostKey, algorithmOverrides }),
}; };
connectOpts.hostVerifier = hostKeyVerifier.createHostVerifier({ connectOpts.hostVerifier = hostKeyVerifier.createHostVerifier({
sender, sender,

View File

@@ -346,7 +346,7 @@ function createSshDiagnosticLogger(enabled) {
function shouldLogSshDebugMessage(msg) { function shouldLogSshDebugMessage(msg) {
if (typeof msg !== "string") return false; if (typeof msg !== "string") return false;
return /auth|publickey|keyboard|handshake|kex|newkeys|dh gex/i.test(msg); return /auth|publickey|keyboard|handshake|kex|newkeys|dh gex|Inbound|Outbound/i.test(msg);
} }
function attachSshDebugLogger(connectOpts, logger = log) { function attachSshDebugLogger(connectOpts, logger = log) {
@@ -586,8 +586,14 @@ async function connectThroughChain(event, options, jumpHosts, targetHost, target
// when the leaf's ECDSA skip is applied to it. Treat both as // when the leaf's ECDSA skip is applied to it. Treat both as
// strictly per-host: an unset hop value uses the hop's default // strictly per-host: an unset hop value uses the hop's default
// offer, not the leaf's narrower setting. // offer, not the leaf's narrower setting.
// Default legacyAlgorithms to true so NetMesh can connect to older
// network devices (Huawei, H3C, Cisco, etc.) that only support
// legacy KEX algorithms like diffie-hellman-group1-sha1. Modern
// devices still negotiate the strongest mutually supported algorithm
// first — offering legacy algorithms alongside modern ones does not
// weaken security on capable servers.
algorithms: buildAlgorithms( algorithms: buildAlgorithms(
jump.legacyAlgorithms ?? options.legacyAlgorithms, (jump.legacyAlgorithms ?? options.legacyAlgorithms) !== false,
{ {
skipEcdsaHostKey: jump.skipEcdsaHostKey, skipEcdsaHostKey: jump.skipEcdsaHostKey,
algorithmOverrides: jump.algorithmOverrides, algorithmOverrides: jump.algorithmOverrides,
@@ -607,7 +613,7 @@ async function connectThroughChain(event, options, jumpHosts, targetHost, target
logSshAlgorithms("Jump host", connOpts.algorithms, { logSshAlgorithms("Jump host", connOpts.algorithms, {
hostname: jump.hostname, hostname: jump.hostname,
port: jump.port || 22, port: jump.port || 22,
legacyAlgorithms: !!(jump.legacyAlgorithms ?? options.legacyAlgorithms), legacyAlgorithms: ((jump.legacyAlgorithms ?? options.legacyAlgorithms) !== false),
skipEcdsaHostKey: !!jump.skipEcdsaHostKey, skipEcdsaHostKey: !!jump.skipEcdsaHostKey,
hasAlgorithmOverrides: !!jump.algorithmOverrides, hasAlgorithmOverrides: !!jump.algorithmOverrides,
}, sshDiagnosticLogger); }, sshDiagnosticLogger);

View File

@@ -135,7 +135,7 @@ function createExecCommandApi(ctx) {
// KEX / cipher / host-key set as the interactive terminal. Without // KEX / cipher / host-key set as the interactive terminal. Without
// this, a host that needs the ECDSA skip or legacy algorithms would // this, a host that needs the ECDSA skip or legacy algorithms would
// connect in the terminal but still fail the same handshake here. // connect in the terminal but still fail the same handshake here.
algorithms: buildAlgorithms(payload.legacyAlgorithms, { algorithms: buildAlgorithms(payload.legacyAlgorithms !== false, {
skipEcdsaHostKey: payload.skipEcdsaHostKey, skipEcdsaHostKey: payload.skipEcdsaHostKey,
algorithmOverrides: payload.algorithmOverrides, algorithmOverrides: payload.algorithmOverrides,
}), }),

View File

@@ -362,7 +362,7 @@ function createStartSessionApi(ctx) {
authMethod: options.authMethod || undefined, authMethod: options.authMethod || undefined,
knownHosts: Array.isArray(options.knownHosts) ? options.knownHosts : undefined, knownHosts: Array.isArray(options.knownHosts) ? options.knownHosts : undefined,
verifyHostKeys: options.verifyHostKeys !== false, verifyHostKeys: options.verifyHostKeys !== false,
legacyAlgorithms: !!options.legacyAlgorithms, legacyAlgorithms: options.legacyAlgorithms !== false,
skipEcdsaHostKey: !!options.skipEcdsaHostKey, skipEcdsaHostKey: !!options.skipEcdsaHostKey,
algorithmOverrides: options.algorithmOverrides, algorithmOverrides: options.algorithmOverrides,
useSshAgent: options.useSshAgent, useSshAgent: options.useSshAgent,
@@ -1470,7 +1470,11 @@ function createStartSessionApi(ctx) {
keepaliveCountMax: keepalivePolicy.keepaliveCountMax, keepaliveCountMax: keepalivePolicy.keepaliveCountMax,
// Enable keyboard-interactive authentication (required for 2FA/MFA) // Enable keyboard-interactive authentication (required for 2FA/MFA)
tryKeyboard: true, tryKeyboard: true,
algorithms: buildAlgorithms(options.legacyAlgorithms, { // Default legacyAlgorithms to true so older network devices (Huawei,
// H3C, Cisco, etc.) that only support legacy KEX algorithms like
// diffie-hellman-group1-sha1 can still connect. Modern devices still
// negotiate the strongest mutually supported algorithm first.
algorithms: buildAlgorithms(options.legacyAlgorithms !== false, {
skipEcdsaHostKey: options.skipEcdsaHostKey, skipEcdsaHostKey: options.skipEcdsaHostKey,
algorithmOverrides: options.algorithmOverrides, algorithmOverrides: options.algorithmOverrides,
}), }),
@@ -1479,7 +1483,7 @@ function createStartSessionApi(ctx) {
logSshAlgorithms("Target host", connectOpts.algorithms, { logSshAlgorithms("Target host", connectOpts.algorithms, {
hostname: options.hostname, hostname: options.hostname,
port: options.port || 22, port: options.port || 22,
legacyAlgorithms: !!options.legacyAlgorithms, legacyAlgorithms: options.legacyAlgorithms !== false,
skipEcdsaHostKey: !!options.skipEcdsaHostKey, skipEcdsaHostKey: !!options.skipEcdsaHostKey,
hasAlgorithmOverrides: !!options.algorithmOverrides, hasAlgorithmOverrides: !!options.algorithmOverrides,
}, log); }, log);

View File

@@ -18,6 +18,14 @@ if (process.env.ELECTRON_RUN_AS_NODE) {
delete process.env.ELECTRON_RUN_AS_NODE; delete process.env.ELECTRON_RUN_AS_NODE;
} }
// BoringSSL DH compatibility shim — MUST be installed before ANY ssh2 require.
// Multiple bridges (sshBridge, sftpBridge, hostKeyVerifier, privateKeyNormalizer,
// sshAuthHelper, portForwardingBridge, inspectionExec, systemSshAgent...) each require
// ssh2 at module load. ssh2 destructes crypto.createDiffieHellmanGroup at load time,
// so the monkey-patch has to run before even the first ssh2 module load.
// Installing here (top of main, before any bridge requires) guarantees ordering.
require("./bridges/boringSslDhCompat.cjs").installBoringSslDhCompat();
// Load crash log bridge early so process-level error handlers can use it // Load crash log bridge early so process-level error handlers can use it
const crashLogBridge = require("./bridges/crashLogBridge.cjs"); const crashLogBridge = require("./bridges/crashLogBridge.cjs");
const { const {
@@ -267,6 +275,9 @@ const { applyPortableDataDirectory } = require("./portableData.cjs");
const portableData = applyPortableDataDirectory({ app }); const portableData = applyPortableDataDirectory({ app });
if (portableData) { if (portableData) {
console.info(`[Main] Portable data directory: ${portableData.dataDirectory}`); console.info(`[Main] Portable data directory: ${portableData.dataDirectory}`);
// portable 版 safeStorage DPAPI 绑定 userData 路径,解不开 dev 版 LS 加密 blob。
// 设置环境变量让 useVaultState 跳过 LS hosts_v1,直接从 inspection-data.json 读明文。
process.env.NETCATTY_PORTABLE = "1";
} }
const appPath = path.join(__dirname, ".."); const appPath = path.join(__dirname, "..");
const preload = path.join(__dirname, "preload.cjs"); const preload = path.join(__dirname, "preload.cjs");

View File

@@ -945,7 +945,12 @@ function isTrustedRendererLocation(allowedOrigins) {
const allowedOrigins = getAllowedRendererOrigins(); const allowedOrigins = getAllowedRendererOrigins();
if (isTrustedRendererLocation(allowedOrigins)) { if (isTrustedRendererLocation(allowedOrigins)) {
contextBridge.exposeInMainWorld("netcatty", { ...existing, ...api, ...figSpecApi }); contextBridge.exposeInMainWorld("netcatty", {
...existing, ...api, ...figSpecApi,
// portable 版标志:renderer 据此跳过 LS hosts_v1 加密读取,
// 直接走 inspection-data.json 明文(DPAPI 绑定 userData 路径解不开 dev 加密 blob)。
isPortable: process.env.NETCATTY_PORTABLE === "1",
});
} else { } else {
// If a window navigates to an untrusted origin, do NOT expose the bridge. // If a window navigates to an untrusted origin, do NOT expose the bridge.
try { try {

View File

@@ -126,6 +126,7 @@
"micromark-extension-gfm-strikethrough": "^2.1.0", "micromark-extension-gfm-strikethrough": "^2.1.0",
"monaco-editor": "^0.55.1", "monaco-editor": "^0.55.1",
"node-pty": "^1.1.0", "node-pty": "^1.1.0",
"pend": "^1.2.0",
"pinyin-pro": "^3.28.1", "pinyin-pro": "^3.28.1",
"re2js": "2.8.6", "re2js": "2.8.6",
"react": "^19.2.1", "react": "^19.2.1",