From 819947984f2a6b048d5fb342afaeceaab5ff6dd7 Mon Sep 17 00:00:00 2001 From: muzimu217 <1278844978@qq.com> Date: Sat, 12 Sep 2026 04:34:34 -0700 Subject: [PATCH 1/4] feat(plugins): install imported pi extension dependencies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Importing a pi CLI extension stripped node_modules and copied only the sources, so any extension with npm dependencies failed to load (jiti reported "Cannot find module ..." and the whole extension was skipped). The generated plugin now keeps the source package.json (plus lockfile) at the plugin root and installs declared dependencies there before the first load: npm install --omit=dev --legacy-peer-deps --no-audit --no-fund --ignore-scripts. Kernel packages keep resolving through virtual modules, and --ignore-scripts means no third-party install script ever runs; a native module that needs one reports its own load error. A failed install never blocks the import — the renderer shows a warning toast with the npm stderr tail. Verified with pi-hermes-memory: the imported plugin loads in the sidecar runtime with its tools, slash commands, and turn hooks registered (previously a load_error). --- .../electron/main/agent-extensions-ipc.ts | 11 +- .../desktop/electron/main/agent-extensions.ts | 122 +++++++++++++++++- apps/desktop/src/lib/api.ts | 16 ++- apps/desktop/test/agent-extensions.test.mjs | 74 +++++++++++ packages/i18n/src/locales/de/index.ts | 3 +- packages/i18n/src/locales/en/index.ts | 3 +- packages/i18n/src/locales/es/index.ts | 3 +- packages/i18n/src/locales/fr/index.ts | 3 +- packages/i18n/src/locales/ko/index.ts | 3 +- packages/i18n/src/locales/tr/index.ts | 3 +- packages/i18n/src/locales/zh-CN/index.ts | 5 + packages/i18n/src/locales/zh-TW/index.ts | 5 + 12 files changed, 238 insertions(+), 13 deletions(-) diff --git a/apps/desktop/electron/main/agent-extensions-ipc.ts b/apps/desktop/electron/main/agent-extensions-ipc.ts index e6f367951..b5dfe2236 100644 --- a/apps/desktop/electron/main/agent-extensions-ipc.ts +++ b/apps/desktop/electron/main/agent-extensions-ipc.ts @@ -9,7 +9,11 @@ */ import { dialog, type BrowserWindow } from "electron"; import { ErrorCodes, IPC, type TrustedExtensionUiPromptResponse } from "@pi-desktop/shared"; -import { generateImportedExtensionPlugin, type AgentExtensionBridge } from "./agent-extensions.js"; +import { + generateImportedExtensionPlugin, + installExtensionDependencies, + type AgentExtensionBridge, +} from "./agent-extensions.js"; export type AgentExtensionIpcDeps = { handle: (channel: string, fn: (...args: any[]) => Promise) => void; @@ -71,7 +75,10 @@ export function registerAgentExtensionIpc(deps: AgentExtensionIpcDeps): void { const source = picked.canceled ? undefined : picked.filePaths[0]; if (!source) return { canceled: true }; const generated = generateImportedExtensionPlugin(source, deps.importRoot); + // Install before registration so the extension's first load already sees + // its dependencies; a failed install registers anyway and is reported. + const dependencies = await installExtensionDependencies(generated.path); const loaded = await deps.loadDevPlugin(generated.path); - return { canceled: false, ...generated, loaded }; + return { canceled: false, ...generated, loaded, dependencies }; }); } diff --git a/apps/desktop/electron/main/agent-extensions.ts b/apps/desktop/electron/main/agent-extensions.ts index d6bdee8a5..f4f20ab95 100644 --- a/apps/desktop/electron/main/agent-extensions.ts +++ b/apps/desktop/electron/main/agent-extensions.ts @@ -8,8 +8,9 @@ * the modal prompts between the sidecar and the renderer. Discovery and * enablement are the plugin system's job; nothing here touches the filesystem. */ +import { spawn } from "node:child_process"; import { createHash, randomUUID } from "node:crypto"; -import { cpSync, existsSync, lstatSync, mkdirSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs"; +import { copyFileSync, cpSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs"; import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path"; import { assertImportedPackagePath, discoverImportedPackageSkills } from "./imported-package-skills"; import { discoverManualPath } from "@pi-desktop/agent-runtime"; @@ -237,6 +238,115 @@ export class AgentExtensionBridge { } } +export type ExtensionDependencyInstallResult = + | { state: "skipped"; reason: "no-package-json" | "no-dependencies" } + | { state: "installed" } + | { state: "failed"; error: string }; + +/** Injectable so tests never run npm. Resolves with the exit code and captured stderr. */ +export type DependencyCommandRunner = ( + command: string, + args: string[], + cwd: string, + timeoutMs: number, +) => Promise<{ code: number; stderr: string }>; + +const NPM_INSTALL_TIMEOUT_MS = 120_000; +/** npm output is not toast-shaped; the tail carries the actual failure. */ +const DEPENDENCY_ERROR_TAIL_CHARS = 200; + +function dependencyErrorTail(text: string): string { + return text.length > DEPENDENCY_ERROR_TAIL_CHARS + ? `…${text.slice(-DEPENDENCY_ERROR_TAIL_CHARS)}` + : text; +} + +function defaultDependencyRunner( + command: string, + args: string[], + cwd: string, + timeoutMs: number, +): Promise<{ code: number; stderr: string }> { + return new Promise((resolve, reject) => { + // Shell only where npm is a .cmd shim (Windows); every arg is a literal. + const child = spawn(command, args, { + cwd, + shell: process.platform === "win32", + stdio: ["ignore", "ignore", "pipe"], + }); + let stderr = ""; + child.stderr?.on("data", (chunk: Buffer) => { + stderr += chunk.toString("utf8"); + }); + const timer = setTimeout(() => { + stderr += `\nnpm install exceeded ${timeoutMs}ms and was terminated`; + child.kill("SIGTERM"); + }, timeoutMs); + child.on("error", (err) => { + clearTimeout(timer); + reject(err); + }); + child.on("close", (code) => { + clearTimeout(timer); + resolve({ code: code ?? 1, stderr }); + }); + }); +} + +/** + * Install an imported extension's npm dependencies inside the generated plugin + * directory (spec 07-plugins/16 §3): the sidecar's jiti resolves bare imports + * from the plugin root's `node_modules`, and the kernel packages (`pi-ai`, + * `pi-coding-agent`, `pi-tui`) keep winning through virtual modules, so + * installing them is harmless. `--legacy-peer-deps` keeps `pi-coding-agent` + * peers out of the tree; `--ignore-scripts` means no third-party install + * script ever runs here — a native module that needs one fails to load with a + * diagnostic instead (documented workaround: rebuild against Electron + * headers). A failure never blocks the import; the extension reports its own + * load error and the renderer surfaces this result. + */ +export async function installExtensionDependencies( + pluginDir: string, + options?: { runner?: DependencyCommandRunner; timeoutMs?: number }, +): Promise { + const packageJsonPath = join(pluginDir, "package.json"); + if (!existsSync(packageJsonPath)) { + return { state: "skipped", reason: "no-package-json" }; + } + let manifest: { dependencies?: Record }; + try { + manifest = JSON.parse(readFileSync(packageJsonPath, "utf8")); + } catch (err) { + return { + state: "failed", + error: `package.json is not valid JSON: ${err instanceof Error ? err.message : String(err)}`, + }; + } + if (!manifest.dependencies || Object.keys(manifest.dependencies).length === 0) { + return { state: "skipped", reason: "no-dependencies" }; + } + try { + const result = await (options?.runner ?? defaultDependencyRunner)( + "npm", + ["install", "--omit=dev", "--legacy-peer-deps", "--no-audit", "--no-fund", "--ignore-scripts"], + pluginDir, + options?.timeoutMs ?? NPM_INSTALL_TIMEOUT_MS, + ); + if (result.code !== 0) { + return { + state: "failed", + error: dependencyErrorTail(`npm install exited ${result.code}: ${result.stderr.trim()}`), + }; + } + return { state: "installed" }; + } catch (err) { + return { + state: "failed", + error: dependencyErrorTail(err instanceof Error ? err.message : String(err)), + }; + } +} + const PLUGIN_ID_PREFIX = "imported."; function slugFor(path: string): string { @@ -250,7 +360,10 @@ function slugFor(path: string): string { /** * Build a plugin directory from a pi extension file or directory (spec §3): * copies the source under `src/`, writes a manifest that declares the entry - * files as `contributes.agentExtensions`, and a no-op `main.js`. + * files as `contributes.agentExtensions`, and a no-op `main.js`. A directory + * that ships a `package.json` also gets it (plus its lockfile) at the plugin + * root so {@link installExtensionDependencies} can resolve its dependencies + * there; `node_modules` itself is never copied — it is reinstalled. */ export function generateImportedExtensionPlugin( source: string, @@ -330,5 +443,10 @@ export function generateImportedExtensionPlugin( "// Generated by PI-Desktop: declarative skills and/or agent extensions.\nmodule.exports = {};\n", "utf8", ); + if (isDirectory) { + for (const file of ["package.json", "package-lock.json", "npm-shrinkwrap.json"]) { + if (existsSync(join(resolved, file))) copyFileSync(join(resolved, file), join(dir, file)); + } + } return { path: dir, id, entries }; } diff --git a/apps/desktop/src/lib/api.ts b/apps/desktop/src/lib/api.ts index 6bf40c2b7..da8518e65 100644 --- a/apps/desktop/src/lib/api.ts +++ b/apps/desktop/src/lib/api.ts @@ -789,9 +789,19 @@ export const api = { ), /** Import a pi CLI extension file or directory as a development plugin (spec 16 §3). */ importPiExtension: () => - invoke<{ canceled: true } | { canceled: false; id: string; path: string; entries: string[] }>( - IPC.invoke.pluginImportExtension, - ), + invoke< + | { canceled: true } + | { + canceled: false; + id: string; + path: string; + entries: string[]; + dependencies: + | { state: "skipped"; reason: "no-package-json" | "no-dependencies" } + | { state: "installed" } + | { state: "failed"; error: string }; + } + >(IPC.invoke.pluginImportExtension), runExtensionCommand: (input: { sessionId: string; name: string; args: string }) => invoke<{ ok: boolean }>(IPC.invoke.extensionsCommandRun, input), respondExtensionPrompt: (response: TrustedExtensionUiPromptResponse) => diff --git a/apps/desktop/test/agent-extensions.test.mjs b/apps/desktop/test/agent-extensions.test.mjs index 88044c9b6..43cb086f3 100644 --- a/apps/desktop/test/agent-extensions.test.mjs +++ b/apps/desktop/test/agent-extensions.test.mjs @@ -9,6 +9,7 @@ register(new URL("./helpers/ts-import-hooks.mjs", import.meta.url)); const { AgentExtensionBridge, generateImportedExtensionPlugin, + installExtensionDependencies, } = await import("../electron/main/agent-extensions.ts"); function bridge(overrides = {}) { @@ -119,3 +120,76 @@ test("importing a pi extension directory or file generates a plugin holding agen writeFileSync(join(root, "notes.md"), "# no"); assert.throws(() => generateImportedExtensionPlugin(join(root, "notes.md"), importRoot), /no extension entry/); }); + +test("importing a directory keeps its package.json at the plugin root and never copies node_modules", () => { + const root = mkdtempSync(join(tmpdir(), "pi-ax-pkg-")); + const extDir = join(root, "memory-ext"); + mkdirSync(join(extDir, "node_modules", "some-dep"), { recursive: true }); + writeFileSync(join(extDir, "node_modules", "some-dep", "index.js"), "module.exports = {};"); + writeFileSync(join(extDir, "index.ts"), "export default function () {}\n"); + writeFileSync( + join(extDir, "package.json"), + JSON.stringify({ name: "memory-ext", dependencies: { "some-dep": "^1.0.0" }, pi: { extensions: ["index.ts"] } }), + ); + writeFileSync(join(extDir, "package-lock.json"), "{}"); + + const generated = generateImportedExtensionPlugin(extDir, join(root, "imported")); + assert.equal(readFileSync(join(generated.path, "package.json"), "utf8"), readFileSync(join(extDir, "package.json"), "utf8"), "package.json lands at the plugin root for dependency install"); + assert.ok(existsSync(join(generated.path, "package-lock.json"))); + assert.ok(!existsSync(join(generated.path, "src", "node_modules")), "node_modules is reinstalled, never copied"); + + const file = join(root, "solo.ts"); + writeFileSync(file, "export default function () {}\n"); + const single = generateImportedExtensionPlugin(file, join(root, "imported")); + assert.ok(!existsSync(join(single.path, "package.json")), "a lone file has nothing to install from"); +}); + +test("dependency install: skips without a manifest or dependencies, runs npm with pinned flags, surfaces failures", async () => { + const root = mkdtempSync(join(tmpdir(), "pi-ax-deps-")); + const write = (name, json) => { + const dir = join(root, name); + mkdirSync(dir, { recursive: true }); + if (json !== null) writeFileSync(join(dir, "package.json"), json); + return dir; + }; + + const noPackage = write("no-package", null); + assert.deepEqual(await installExtensionDependencies(noPackage), { state: "skipped", reason: "no-package-json" }); + + const noDeps = write("no-deps", JSON.stringify({ name: "x" })); + assert.deepEqual(await installExtensionDependencies(noDeps), { state: "skipped", reason: "no-dependencies" }); + + const badJson = write("bad-json", "{ not json"); + const bad = await installExtensionDependencies(badJson); + assert.equal(bad.state, "failed"); + assert.match(bad.error, /package\.json is not valid JSON/); + + const calls = []; + const installed = write("installed", JSON.stringify({ name: "x", dependencies: { "some-dep": "^1" } })); + const runner = async (command, args, cwd, timeoutMs) => { + calls.push({ command, args, cwd, timeoutMs }); + return { code: 0, stderr: "" }; + }; + assert.deepEqual(await installExtensionDependencies(installed, { runner, timeoutMs: 1234 }), { state: "installed" }); + assert.equal(calls.length, 1); + assert.equal(calls[0].command, "npm"); + assert.deepEqual(calls[0].args, ["install", "--omit=dev", "--legacy-peer-deps", "--no-audit", "--no-fund", "--ignore-scripts"]); + assert.equal(calls[0].cwd, installed, "npm runs inside the plugin directory"); + assert.equal(calls[0].timeoutMs, 1234); + + const failing = write("failing", JSON.stringify({ name: "x", dependencies: { "some-dep": "^1" } })); + const result = await installExtensionDependencies(failing, { + runner: async () => ({ code: 1, stderr: "npm error code ENOTFOUND\nnpm error network unreachable" }), + }); + assert.equal(result.state, "failed"); + assert.match(result.error, /exited 1/); + assert.match(result.error, /ENOTFOUND/); + + const throwing = write("throwing", JSON.stringify({ name: "x", dependencies: { "some-dep": "^1" } })); + const thrown = await installExtensionDependencies(throwing, { + runner: async () => { + throw new Error("npm not found"); + }, + }); + assert.deepEqual(thrown, { state: "failed", error: "npm not found" }); +}); diff --git a/packages/i18n/src/locales/de/index.ts b/packages/i18n/src/locales/de/index.ts index 0bdecb92b..118b1f225 100644 --- a/packages/i18n/src/locales/de/index.ts +++ b/packages/i18n/src/locales/de/index.ts @@ -1383,9 +1383,10 @@ export const de = { "loadDevDone": "Lokales Plugin geladen", "importExtension": "pi-Erweiterung importieren", "importExtensionDone": "Als Plugin {{id}} importiert", + "importExtensionDepsFailed": "Als Plugin {{id}} importiert, aber die Abhängigkeiten konnten nicht installiert werden: {{error}}", "agentExtension": { "title": "Agent-Erweiterung", - "importConfirm": "Importierte Erweiterungen laufen im Agentenprozess mit denselben Rechten wie die Tools des Agenten. Deklarierte Skills können dem Agenten Anweisungen hinzufügen. Fortfahren?", +"importConfirm": "Importierte Erweiterungen laufen im Agentenprozess mit denselben Rechten wie die Tools des Agenten. Deklarierte Skills können dem Agenten Anweisungen hinzufügen, und deklarierte Abhängigkeiten werden mit npm installiert (Installationsskripte deaktiviert). Fortfahren?", "diagnostics": "Diagnosen", "commandNeedsSession": "Starte zuerst einen Chat, um einen Erweiterungsbefehl auszuführen.", "state": { diff --git a/packages/i18n/src/locales/en/index.ts b/packages/i18n/src/locales/en/index.ts index b075356f9..da929e7f7 100644 --- a/packages/i18n/src/locales/en/index.ts +++ b/packages/i18n/src/locales/en/index.ts @@ -1400,9 +1400,10 @@ export const en = { loadDevDone: "Local plugin loaded", importExtension: "Import pi extension", importExtensionDone: "Imported as plugin {{id}}", + importExtensionDepsFailed: "Imported as plugin {{id}}, but installing dependencies failed: {{error}}", agentExtension: { title: "Agent extension", - importConfirm: "Imported extensions run inside the agent process with the same access as the agent's own tools. Declared skills can add instructions to the agent. Continue?", +importConfirm: "Imported extensions run inside the agent process with the same access as the agent's own tools. Declared skills can add instructions to the agent, and declared dependencies are installed with npm (install scripts disabled). Continue?", diagnostics: "Diagnostics", commandNeedsSession: "Start a chat first to run an extension command.", state: { diff --git a/packages/i18n/src/locales/es/index.ts b/packages/i18n/src/locales/es/index.ts index 7731c2723..3b2b72f7b 100644 --- a/packages/i18n/src/locales/es/index.ts +++ b/packages/i18n/src/locales/es/index.ts @@ -1383,9 +1383,10 @@ export const es = { "loadDevDone": "Complemento local cargado", "importExtension": "Importar extensión de pi", "importExtensionDone": "Importada como complemento {{id}}", + "importExtensionDepsFailed": "Importada como complemento {{id}}, pero falló la instalación de dependencias: {{error}}", "agentExtension": { "title": "Extensión del agente", - "importConfirm": "Las extensiones importadas se ejecutan en el proceso del agente con el mismo acceso que sus propias herramientas. Las habilidades declaradas pueden añadir instrucciones al agente. ¿Continuar?", +"importConfirm": "Las extensiones importadas se ejecutan en el proceso del agente con el mismo acceso que sus propias herramientas. Las habilidades declaradas pueden añadir instrucciones al agente y las dependencias declaradas se instalan con npm (scripts de instalación desactivados). ¿Continuar?", "diagnostics": "Diagnósticos", "commandNeedsSession": "Inicia un chat antes de ejecutar un comando de extensión.", "state": { diff --git a/packages/i18n/src/locales/fr/index.ts b/packages/i18n/src/locales/fr/index.ts index 35414664e..0a12f4a53 100644 --- a/packages/i18n/src/locales/fr/index.ts +++ b/packages/i18n/src/locales/fr/index.ts @@ -1383,9 +1383,10 @@ export const fr = { "loadDevDone": "Plugin local chargé", "importExtension": "Importer une extension pi", "importExtensionDone": "Importée comme plugin {{id}}", + "importExtensionDepsFailed": "Importée comme plugin {{id}}, mais l'installation des dépendances a échoué : {{error}}", "agentExtension": { "title": "Extension de l'agent", - "importConfirm": "Les extensions importées s’exécutent dans le processus de l’agent avec les mêmes droits que ses outils. Les compétences déclarées peuvent ajouter des instructions à l’agent. Continuer ?", +"importConfirm": "Les extensions importées s’exécutent dans le processus de l’agent avec les mêmes droits que ses outils. Les compétences déclarées peuvent ajouter des instructions à l’agent et les dépendances déclarées sont installées avec npm (scripts d’installation désactivés). Continuer ?", "diagnostics": "Diagnostics", "commandNeedsSession": "Démarrez d'abord une discussion pour exécuter une commande d'extension.", "state": { diff --git a/packages/i18n/src/locales/ko/index.ts b/packages/i18n/src/locales/ko/index.ts index a0b58c33a..95f528521 100644 --- a/packages/i18n/src/locales/ko/index.ts +++ b/packages/i18n/src/locales/ko/index.ts @@ -1402,9 +1402,10 @@ export const ko = { loadDevDone: "로컬 플러그인 불러옴", importExtension: "pi 확장 가져오기", importExtensionDone: "플러그인 {{id}}(으)로 가져왔습니다", + importExtensionDepsFailed: "플러그인 {{id}}(으)로 가져왔지만 의존성 설치에 실패했습니다: {{error}}", agentExtension: { title: "에이전트 확장", - importConfirm: "가져온 확장은 에이전트 프로세스 안에서 에이전트 자체 도구와 같은 권한으로 실행됩니다. 선언된 스킬은 에이전트에 지침을 추가할 수 있습니다. 계속할까요?", +importConfirm: "가져온 확장은 에이전트 프로세스 안에서 에이전트 자체 도구와 같은 권한으로 실행됩니다. 선언된 스킬은 에이전트에 지침을 추가할 수 있고, 선언된 의존성은 npm으로 설치됩니다(설치 스크립트 비활성화). 계속할까요?", diagnostics: "진단", commandNeedsSession: "확장 명령을 실행하려면 먼저 채팅을 시작하세요.", state: { diff --git a/packages/i18n/src/locales/tr/index.ts b/packages/i18n/src/locales/tr/index.ts index 6c1b3688f..3d602b9cb 100644 --- a/packages/i18n/src/locales/tr/index.ts +++ b/packages/i18n/src/locales/tr/index.ts @@ -1402,9 +1402,10 @@ export const tr = { loadDevDone: "Yerel eklenti yüklendi", importExtension: "pi uzantısını içe aktar", importExtensionDone: "{{id}} eklentisi olarak içe aktarıldı", + importExtensionDepsFailed: "{{id}} eklentisi olarak içe aktarıldı, ancak bağımlılıklar yüklenemedi: {{error}}", agentExtension: { title: "Ajan uzantısı", - importConfirm: "İçe aktarılan uzantılar ajan sürecinde, ajanın kendi araçlarıyla aynı erişimle çalışır. Bildirilen beceriler ajana talimatlar ekleyebilir. Devam edilsin mi?", +importConfirm: "İçe aktarılan uzantılar ajan sürecinde, ajanın kendi araçlarıyla aynı erişimle çalışır. Bildirilen beceriler ajana talimatlar ekleyebilir ve bildirilen bağımlılıklar npm ile yüklenir (yükleme betikleri devre dışı). Devam edilsin mi?", diagnostics: "Tanılamalar", commandNeedsSession: "Uzantı komutu çalıştırmak için önce bir sohbet başlatın.", state: { diff --git a/packages/i18n/src/locales/zh-CN/index.ts b/packages/i18n/src/locales/zh-CN/index.ts index 2bfa7b6b8..ce22d95dc 100644 --- a/packages/i18n/src/locales/zh-CN/index.ts +++ b/packages/i18n/src/locales/zh-CN/index.ts @@ -1392,9 +1392,14 @@ export const zhCN = { loadDevDone: "本地插件已加载", importExtension: "导入 pi 扩展", importExtensionDone: "已导入为插件 {{id}}", + importExtensionDepsFailed: "已导入为插件 {{id}},但依赖安装失败:{{error}}", agentExtension: { title: "Agent 扩展", +<<<<<<< HEAD importConfirm: "导入的扩展会在 agent 进程内运行,拥有与 agent 自身工具相同的权限;包声明的技能可向 agent 提供指令。继续吗?", +======= + importConfirm: "导入的扩展将在 agent 进程内运行,拥有与 agent 自身工具相同的权限。声明的依赖将通过 npm 安装(禁用安装脚本)。继续吗?", +>>>>>>> 8e55819 (feat(plugins): install imported pi extension dependencies) diagnostics: "诊断", commandNeedsSession: "请先开始一个对话,再运行扩展命令。", state: { diff --git a/packages/i18n/src/locales/zh-TW/index.ts b/packages/i18n/src/locales/zh-TW/index.ts index 84327a3bd..ef0d68cb9 100644 --- a/packages/i18n/src/locales/zh-TW/index.ts +++ b/packages/i18n/src/locales/zh-TW/index.ts @@ -1392,9 +1392,14 @@ export const zhTW = { loadDevDone: "本地外掛已載入", importExtension: "匯入 pi 擴充", importExtensionDone: "已匯入為外掛 {{id}}", + importExtensionDepsFailed: "已匯入為外掛 {{id}},但相依套件安裝失敗:{{error}}", agentExtension: { title: "Agent 擴充", +<<<<<<< HEAD importConfirm: "匯入的擴充會在 agent 程序內執行,擁有與 agent 自身工具相同的權限;套件宣告的技能可向 agent 提供指令。要繼續嗎?", +======= + importConfirm: "匯入的擴充將在 agent 程序內執行,擁有與 agent 自身工具相同的權限。宣告的相依套件將透過 npm 安裝(停用安裝指令碼)。要繼續嗎?", +>>>>>>> 8e55819 (feat(plugins): install imported pi extension dependencies) diagnostics: "診斷", commandNeedsSession: "請先開始一個對話,再執行擴充命令。", state: { From 8593057ac42987bd558d6e87d408ec93b86ea573 Mon Sep 17 00:00:00 2001 From: muzimu217 <1278844978@qq.com> Date: Sat, 12 Sep 2026 10:11:53 -0700 Subject: [PATCH 2/4] docs(plugins): document imported extension dependency install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pair spec 07-plugins/16 §3.2 with the new import flow (package.json kept at the plugin root, workspaces stripped, pinned npm flags, failure never blocking), add the E2E-PLUGIN-import-extension-* scenarios to the test plan in both locales, spell out the dependency behavior and the Electron-headers rebuild workaround in the plugin development guide, and narrow the README claim to disclose the npm step. --- README.md | 2 +- README.zh-CN.md | 2 +- .../desktop/electron/main/agent-extensions.ts | 43 ++++++++++++++--- .../src/features/plugins/usePluginsPage.ts | 10 +++- apps/desktop/test/agent-extensions.test.mjs | 48 +++++++++++++++++++ docs/plugin-development.md | 11 ++++- docs/spec/06-delivery/04-e2e-test-plan.md | 40 ++++++++++++++++ docs/spec/07-plugins/16-trusted-extensions.md | 10 ++++ docs/zh-CN/plugin-development.md | 7 ++- .../spec/06-delivery/04-e2e-test-plan.md | 32 +++++++++++++ .../spec/07-plugins/16-trusted-extensions.md | 7 +++ 11 files changed, 200 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 3ee0625a6..016fdead2 100644 --- a/README.md +++ b/README.md @@ -231,7 +231,7 @@ The control endpoint supports project, session, and Agent workflows plus a revie Extensions written for the [pi](https://github.com/badlogic/pi-mono) CLI can run inside PI-Desktop's agent unchanged. -A plugin can list them under `contributes.agentExtensions`, or **Plugins → Import pi extension** can wrap an existing extension file or directory in a plugin. +A plugin can list them under `contributes.agentExtensions`, or **Plugins → Import pi extension** can wrap an existing extension file or directory in a plugin. If the directory declares npm `dependencies`, they are installed into the generated plugin before its first load (`--ignore-scripts`, so no third-party install script ever runs). They can register tools, slash commands, and hooks on every turn, tool call, and provider request. They run with the same access as the agent's own tools, gated by the `agent.extension` permission. diff --git a/README.zh-CN.md b/README.zh-CN.md index 4592f4290..1feb3b6d9 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -291,7 +291,7 @@ PI_DESKTOP_MCP_CONTROL=1 **Plugins → Import pi extension** -直接把现有 extension 文件或目录包装成 PI-Desktop 插件。 +直接把现有 extension 文件或目录包装成 PI-Desktop 插件。若目录声明了 npm `dependencies`,会在首次加载前安装到生成的插件中(`--ignore-scripts`,绝不运行第三方安装脚本)。 这些扩展可以注册: diff --git a/apps/desktop/electron/main/agent-extensions.ts b/apps/desktop/electron/main/agent-extensions.ts index f4f20ab95..1e5554515 100644 --- a/apps/desktop/electron/main/agent-extensions.ts +++ b/apps/desktop/electron/main/agent-extensions.ts @@ -254,6 +254,8 @@ export type DependencyCommandRunner = ( const NPM_INSTALL_TIMEOUT_MS = 120_000; /** npm output is not toast-shaped; the tail carries the actual failure. */ const DEPENDENCY_ERROR_TAIL_CHARS = 200; +/** Rolling cap so a chatty npm cannot balloon the main process's memory. */ +const DEPENDENCY_STDERR_KEEP_CHARS = 8192; function dependencyErrorTail(text: string): string { return text.length > DEPENDENCY_ERROR_TAIL_CHARS @@ -261,7 +263,7 @@ function dependencyErrorTail(text: string): string { : text; } -function defaultDependencyRunner( +export function defaultDependencyRunner( command: string, args: string[], cwd: string, @@ -269,6 +271,8 @@ function defaultDependencyRunner( ): Promise<{ code: number; stderr: string }> { return new Promise((resolve, reject) => { // Shell only where npm is a .cmd shim (Windows); every arg is a literal. + // A shell kill on Windows terminates the shim, possibly leaving npm + // itself running — accepted for v1, the timeout result still resolves. const child = spawn(command, args, { cwd, shell: process.platform === "win32", @@ -277,18 +281,26 @@ function defaultDependencyRunner( let stderr = ""; child.stderr?.on("data", (chunk: Buffer) => { stderr += chunk.toString("utf8"); + if (stderr.length > DEPENDENCY_STDERR_KEEP_CHARS * 2) { + stderr = stderr.slice(-DEPENDENCY_STDERR_KEEP_CHARS); + } }); + let killTimer: ReturnType | undefined; const timer = setTimeout(() => { stderr += `\nnpm install exceeded ${timeoutMs}ms and was terminated`; child.kill("SIGTERM"); + killTimer = setTimeout(() => child.kill("SIGKILL"), 5_000); }, timeoutMs); - child.on("error", (err) => { + const settle = (fn: () => void) => { clearTimeout(timer); - reject(err); + if (killTimer) clearTimeout(killTimer); + fn(); + }; + child.on("error", (err) => { + settle(() => reject(err)); }); child.on("close", (code) => { - clearTimeout(timer); - resolve({ code: code ?? 1, stderr }); + settle(() => resolve({ code: code ?? 1, stderr })); }); }); } @@ -444,8 +456,25 @@ export function generateImportedExtensionPlugin( "utf8", ); if (isDirectory) { - for (const file of ["package.json", "package-lock.json", "npm-shrinkwrap.json"]) { - if (existsSync(join(resolved, file))) copyFileSync(join(resolved, file), join(dir, file)); + const rootPackageJson = join(resolved, "package.json"); + if (existsSync(rootPackageJson)) { + // A `workspaces` field would send npm into the copied sources under + // src/; strip it so the install sees only the declared dependencies. + try { + const pkg = JSON.parse(readFileSync(rootPackageJson, "utf8")) as Record; + if (pkg && typeof pkg === "object" && "workspaces" in pkg) { + delete pkg.workspaces; + writeFileSync(join(dir, "package.json"), JSON.stringify(pkg, null, 2) + "\n", "utf8"); + } else { + copyFileSync(rootPackageJson, join(dir, "package.json")); + } + } catch { + // Not valid JSON: copy verbatim; the install step reports the failure. + copyFileSync(rootPackageJson, join(dir, "package.json")); + } + for (const file of ["package-lock.json", "npm-shrinkwrap.json"]) { + if (existsSync(join(resolved, file))) copyFileSync(join(resolved, file), join(dir, file)); + } } } return { path: dir, id, entries }; diff --git a/apps/desktop/src/features/plugins/usePluginsPage.ts b/apps/desktop/src/features/plugins/usePluginsPage.ts index a9ea6f1e7..16ed83204 100644 --- a/apps/desktop/src/features/plugins/usePluginsPage.ts +++ b/apps/desktop/src/features/plugins/usePluginsPage.ts @@ -294,13 +294,21 @@ export function usePluginsPage() { }); // A pi CLI extension becomes a development plugin holding `agent.extension` - // (spec 07-plugins/16 §3); the confirm is the trust decision. + // (spec 07-plugins/16 §3); the confirm is the trust decision. Declared npm + // dependencies are installed (scripts disabled) before the first load. const importExtension = () => run(async () => { if (!window.confirm(t("plugins.agentExtension.importConfirm"))) return; const result = await api.importPiExtension(); if (result.canceled) return; await refreshPlugins(); + if (result.dependencies.state === "failed") { + showToast( + t("plugins.importExtensionDepsFailed", { id: result.id, error: result.dependencies.error }), + { variant: "warning" }, + ); + return; + } showToast(t("plugins.importExtensionDone", { id: result.id }), { variant: "success" }); }); diff --git a/apps/desktop/test/agent-extensions.test.mjs b/apps/desktop/test/agent-extensions.test.mjs index 43cb086f3..85e051822 100644 --- a/apps/desktop/test/agent-extensions.test.mjs +++ b/apps/desktop/test/agent-extensions.test.mjs @@ -144,6 +144,54 @@ test("importing a directory keeps its package.json at the plugin root and never assert.ok(!existsSync(join(single.path, "package.json")), "a lone file has nothing to install from"); }); +test("importing strips workspaces from the copied package.json so npm never enters src/", () => { + const root = mkdtempSync(join(tmpdir(), "pi-ax-ws-")); + const extDir = join(root, "monorepo-ext"); + mkdirSync(extDir, { recursive: true }); + writeFileSync(join(extDir, "index.ts"), "export default function () {}\n"); + writeFileSync( + join(extDir, "package.json"), + JSON.stringify({ name: "monorepo-ext", workspaces: ["packages/*"], dependencies: { "some-dep": "^1" } }), + ); + + const generated = generateImportedExtensionPlugin(extDir, join(root, "imported")); + const pkg = JSON.parse(readFileSync(join(generated.path, "package.json"), "utf8")); + assert.ok(!("workspaces" in pkg), "workspaces is stripped from the plugin root copy"); + assert.deepEqual(pkg.dependencies, { "some-dep": "^1" }); + + const plainDir = join(root, "plain-ext"); + mkdirSync(plainDir, { recursive: true }); + writeFileSync(join(plainDir, "index.ts"), "export default function () {}\n"); + writeFileSync(join(plainDir, "package.json"), JSON.stringify({ name: "plain-ext", dependencies: {} })); + const plain = generateImportedExtensionPlugin(plainDir, join(root, "imported")); + assert.deepEqual(JSON.parse(readFileSync(join(plain.path, "package.json"), "utf8")), { + name: "plain-ext", + dependencies: {}, + }, "a package.json without workspaces is copied verbatim"); +}); + +test("default runner caps captured stderr and escalates the timeout kill", async () => { + const { defaultDependencyRunner } = await import("../electron/main/agent-extensions.ts"); + + const flooded = await defaultDependencyRunner( + process.execPath, + ["-e", "process.stderr.write('x'.repeat(40000)); process.exit(0)"], + process.cwd(), + 30_000, + ); + assert.equal(flooded.code, 0); + assert.ok(flooded.stderr.length <= 8192 + 64, "stderr is capped to a rolling tail"); + + const stalled = await defaultDependencyRunner( + process.execPath, + ["-e", "setTimeout(() => {}, 60000)"], + process.cwd(), + 300, + ); + assert.notEqual(stalled.code, 0, "a stalled install is killed"); + assert.match(stalled.stderr, /exceeded 300ms and was terminated/); +}); + test("dependency install: skips without a manifest or dependencies, runs npm with pinned flags, surfaces failures", async () => { const root = mkdtempSync(join(tmpdir(), "pi-ax-deps-")); const write = (name, json) => { diff --git a/docs/plugin-development.md b/docs/plugin-development.md index 2bfea4141..a4e1b7444 100644 --- a/docs/plugin-development.md +++ b/docs/plugin-development.md @@ -723,7 +723,16 @@ What to know before you use it: other terminal-only surfaces) are inert and reported in the plugin row's details, never thrown. - **Existing pi extensions** need no changes: Plugins → overflow menu → - "Import pi extension" wraps a file or directory in a generated plugin. + "Import pi extension" wraps a file or directory in a generated plugin. If + the directory ships a `package.json` with `dependencies`, they are + installed into the plugin root before the first load + (`npm install --omit=dev --legacy-peer-deps --no-audit --no-fund + --ignore-scripts`): no third-party install script ever runs, so a native + module that needs one fails to load with a diagnostic — rebuild it against + Electron headers (`npx @electron/rebuild -v `) inside + the plugin directory to fix it. A failed install never blocks the import; + you get a warning toast with the npm error and the row shows the load + error. ## 7. Permission design diff --git a/docs/spec/06-delivery/04-e2e-test-plan.md b/docs/spec/06-delivery/04-e2e-test-plan.md index 870cdadd0..3f2a98b26 100644 --- a/docs/spec/06-delivery/04-e2e-test-plan.md +++ b/docs/spec/06-delivery/04-e2e-test-plan.md @@ -10243,6 +10243,46 @@ sample extensions under `apps/desktop/test/fixtures/pi-extensions/`. - **Milestone**: Post-MVP (R7 v1, delivered first as the bundling spike) - **Status**: Unit-covered by `packages/agent-runtime/src/extensions/bundle.test.ts` (esbuild bundle run from a temp directory); packaged-app journey Draft +#### E2E-PLUGIN-import-extension-installs-dependencies: Importing an extension with npm dependencies installs them before first load + +- **Preconditions**: A pi extension directory shipping a `package.json` with + `dependencies` (a pure-JavaScript package is sufficient) and no + `node_modules`; npm reachable; the import confirm accepted. +- **Steps**: 1) Plugins → Import pi extension, pick the directory. 2) + Inspect `plugins/imported//`. 3) Send a prompt that exercises the + extension. +- **Expected**: The plugin root holds the copied `package.json` with any + `workspaces` field stripped and a `node_modules` directory created by + `npm install --omit=dev --legacy-peer-deps --no-audit --no-fund + --ignore-scripts` (no install script ran); the extension row reaches + `loaded` with its tools, commands, and hooks registered, and they take + effect in the turn. +- **Specs linked**: `07-plugins/16-trusted-extensions.md` §3.2, §10.2 +- **Acceptance**: Security, Quality +- **Milestone**: Post-MVP (R7 v1) +- **Status**: Unit-covered by `apps/desktop/test/agent-extensions.test.mjs` + and verified manually with `pi-hermes-memory` through the sidecar bundle + (tools, commands, and hooks registered, zero diagnostics); no CI journey yet + +#### E2E-PLUGIN-import-extension-reports-missing-dependency: A failed dependency install or unloadable dependency is surfaced, never silent + +- **Preconditions**: A pi extension directory whose `package.json` declares + a dependency that cannot install (npm offline or unresolvable); and one + whose dependency installs but fails to load (for example a native module + that needs a build script). +- **Steps**: 1) Import the first directory with npm failing. 2) Inspect the + toast and the plugin row. 3) Import the second directory and start a turn. +- **Expected**: The renderer shows a warning toast carrying the npm stderr + tail; the plugin still registers; the row reports the extension `error` + state with a `load_error` diagnostic; the session and all other extensions + keep working. +- **Specs linked**: `07-plugins/16-trusted-extensions.md` §3.2, §4.4, §10.2 +- **Acceptance**: Security, Quality +- **Milestone**: Post-MVP (R7 v1) +- **Status**: Unit-covered by `apps/desktop/test/agent-extensions.test.mjs` + (skip, failure, and invalid-manifest paths); no CI journey yet + + --- #### E2E-233: Icon-only actions explain their purpose in the active language diff --git a/docs/spec/07-plugins/16-trusted-extensions.md b/docs/spec/07-plugins/16-trusted-extensions.md index f9f98cfe8..ec1b2ff45 100644 --- a/docs/spec/07-plugins/16-trusted-extensions.md +++ b/docs/spec/07-plugins/16-trusted-extensions.md @@ -84,6 +84,16 @@ A package that explicitly declares `pi.skills` and has no `pi.extensions` (or an empty array) is skill-only: incidental scripts, including `index.js`, are copied as resources but never promoted to executable agent extensions. +A directory that ships a `package.json` also has it (plus its lockfile) copied +to the plugin root with any `workspaces` field stripped; if it declares +`dependencies`, main installs them into the plugin root before the first load +with `npm install --omit=dev --legacy-peer-deps --no-audit --no-fund +--ignore-scripts` (bounded time, no third-party install script ever runs, +kernel packages keep resolving through virtual modules). A failed install is +reported to the renderer and never blocks the import — the extension then +reports its own load error. The confirm discloses the npm step alongside the +skills disclosure. + | Source | Becomes | |---|---| | A pi extension directory or file | A local plugin under `plugins/imported`, id `imported.` | diff --git a/docs/zh-CN/plugin-development.md b/docs/zh-CN/plugin-development.md index 00b36d822..5a710e990 100644 --- a/docs/zh-CN/plugin-development.md +++ b/docs/zh-CN/plugin-development.md @@ -635,7 +635,12 @@ export default function (pi) { `registerMessageRenderer`、`navigateTree` 及其他仅终端可用的界面)是空操作,在插件行 的详情里报告,绝不抛出。 - **已有的 pi 扩展**无需修改:插件页 → 溢出菜单 →“导入 pi 扩展”会把文件或目录包成 - 生成的插件。 + 生成的插件。若目录自带声明了 `dependencies` 的 `package.json`,会在首次加载前把 + 依赖安装到插件根(`npm install --omit=dev --legacy-peer-deps --no-audit --no-fund + --ignore-scripts`):绝不运行第三方安装脚本,因此需要构建脚本的原生模块会以诊断的 + 形式加载失败——在该插件目录内用 Electron 头重建(`npx @electron/rebuild -v + `)即可修复。安装失败绝不阻塞导入;你会收到带 npm 错误的警告 + toast,插件行显示加载错误。 ## 7.权限设计 diff --git a/docs/zh-CN/spec/06-delivery/04-e2e-test-plan.md b/docs/zh-CN/spec/06-delivery/04-e2e-test-plan.md index b283ce708..fb26fbd11 100644 --- a/docs/zh-CN/spec/06-delivery/04-e2e-test-plan.md +++ b/docs/zh-CN/spec/06-delivery/04-e2e-test-plan.md @@ -6435,6 +6435,38 @@ IPC 请求无法关闭。 - **里程碑**:MVP 后(R7 v1,作为打包 spike 首先交付) - **状态**:由 `packages/agent-runtime/src/extensions/bundle.test.ts` 单元覆盖 (esbuild 打包产物在临时目录运行);打包应用旅程为草稿 +#### E2E-PLUGIN-import-extension-installs-dependencies:导入带 npm 依赖的扩展会在首次加载前安装依赖 + +- **前置条件**:一个自带 `package.json` 且声明了 `dependencies`(纯 JavaScript 包即可)、 + 无 `node_modules` 的 pi 扩展目录;npm 可达;导入确认已接受。 +- **步骤**:1)插件页 → 导入 pi 扩展,选择该目录。2)检查 `plugins/imported//`。 + 3)发送一个会用到该扩展的提示。 +- **预期**:插件根有复制来的 `package.json`(`workspaces` 字段已被剥离)和由 + `npm install --omit=dev --legacy-peer-deps --no-audit --no-fund --ignore-scripts` + 创建的 `node_modules`(没有运行任何安装脚本);扩展行达到 `loaded`,工具、命令与 + hooks 均已注册,并在回合中生效。 +- **链接规格**:`07-plugins/16-trusted-extensions.md` §3.2、§10.2 +- **验收**:安全、质量 +- **里程碑**:MVP 后(R7 v1) +- **状态**:由 `apps/desktop/test/agent-extensions.test.mjs` 单元覆盖,并已用 + `pi-hermes-memory` 经 sidecar 打包产物人工验证(工具、命令与 hooks 注册成功,零 + 诊断);暂无 CI 旅程 + +#### E2E-PLUGIN-import-extension-reports-missing-dependency:依赖安装失败或依赖无法加载会被呈现,绝不静默 + +- **前置条件**:一个 `package.json` 声明了无法安装依赖(npm 离线或无法解析)的 pi 扩展 + 目录;以及一个依赖可安装但无法加载(例如需要构建脚本的原生模块)的扩展目录。 +- **步骤**:1)在 npm 失败的情况下导入第一个目录。2)检查 toast 与插件行。3)导入 + 第二个目录并开始回合。 +- **预期**:渲染层出现携带 npm stderr 尾部的警告 toast;插件仍然注册;该行显示扩展 + `error` 状态与 `load_error` 诊断;会话与其他扩展均不受影响。 +- **链接规格**:`07-plugins/16-trusted-extensions.md` §3.2、§4.4、§10.2 +- **验收**:安全、质量 +- **里程碑**:MVP 后(R7 v1) +- **状态**:由 `apps/desktop/test/agent-extensions.test.mjs`(跳过、失败与无效 manifest + 路径)单元覆盖;暂无 CI 旅程 + + #### E2E-234:工作区安全拒绝名单与忽略层 - **前提条件**:一个项目包含 `.env`、`.env.example`、`server.pem`、`keys/id_rsa`、 diff --git a/docs/zh-CN/spec/07-plugins/16-trusted-extensions.md b/docs/zh-CN/spec/07-plugins/16-trusted-extensions.md index 10a3880fd..5547ee3a4 100644 --- a/docs/zh-CN/spec/07-plugins/16-trusted-extensions.md +++ b/docs/zh-CN/spec/07-plugins/16-trusted-extensions.md @@ -72,6 +72,13 @@ agent 循环上注册工具、命令和事件处理器。`ExtensionAPI` 契约 的包视为仅技能包:包括 `index.js` 在内的附带脚本作为资源复制,不会被提升为可执行的 Agent 扩展。 +目录若自带 `package.json`,会(连同其 lockfile)一并复制到插件根并剥离 `workspaces` 字段; +若声明了 `dependencies`,main 会在首次加载前把依赖安装到插件根,命令为 +`npm install --omit=dev --legacy-peer-deps --no-audit --no-fund --ignore-scripts` +(限时执行、绝不运行第三方安装脚本、内核包继续经 virtual modules 解析)。安装失败会上报 +渲染层且绝不阻塞导入——扩展随后上报自身的 load error。确认对话框会与技能披露一并说明 +npm 安装步骤。 + | 来源 | 结果 | |---|---| | 一个 pi 扩展目录或文件 | `plugins/imported` 下的本地插件,id 为 `imported.` | From 1b08c9790fd9935b0b778add17a8af3adff9cd63 Mon Sep 17 00:00:00 2001 From: muzimu217 <1278844978@qq.com> Date: Sat, 12 Sep 2026 10:20:10 -0700 Subject: [PATCH 3/4] test(plugins): make the stderr cap assertion platform-agnostic Linux pipes deliver the flood in smaller chunks, so the buffer can sit between the keep size and the 2x trim threshold when the child exits. Assert the actual invariant of the rolling cap (bounded by 2x the keep size after every chunk) instead of the single-chunk macOS result. --- apps/desktop/test/agent-extensions.test.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/desktop/test/agent-extensions.test.mjs b/apps/desktop/test/agent-extensions.test.mjs index 85e051822..fbdd3fd52 100644 --- a/apps/desktop/test/agent-extensions.test.mjs +++ b/apps/desktop/test/agent-extensions.test.mjs @@ -180,7 +180,9 @@ test("default runner caps captured stderr and escalates the timeout kill", async 30_000, ); assert.equal(flooded.code, 0); - assert.ok(flooded.stderr.length <= 8192 + 64, "stderr is capped to a rolling tail"); + // Invariant of the rolling cap: after every chunk the buffer is at most + // 2× the keep size, regardless of how the pipe chunks the writes. + assert.ok(flooded.stderr.length <= 16384, "stderr is capped to a bounded tail"); const stalled = await defaultDependencyRunner( process.execPath, From 0de399855bdee3c4e8875c5de54f5e2c3edb2eab Mon Sep 17 00:00:00 2001 From: muzimu217 <1278844978@qq.com> Date: Sat, 12 Sep 2026 10:20:10 -0700 Subject: [PATCH 4/4] chore(i18n): merge importConfirm copy with the 288 stack Both PRs rewrote the import confirmation in every locale; the combined copy discloses the skills addition and the npm dependency install. --- packages/i18n/src/locales/zh-CN/index.ts | 6 +----- packages/i18n/src/locales/zh-TW/index.ts | 6 +----- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/packages/i18n/src/locales/zh-CN/index.ts b/packages/i18n/src/locales/zh-CN/index.ts index ce22d95dc..c44cac3ef 100644 --- a/packages/i18n/src/locales/zh-CN/index.ts +++ b/packages/i18n/src/locales/zh-CN/index.ts @@ -1395,11 +1395,7 @@ export const zhCN = { importExtensionDepsFailed: "已导入为插件 {{id}},但依赖安装失败:{{error}}", agentExtension: { title: "Agent 扩展", -<<<<<<< HEAD - importConfirm: "导入的扩展会在 agent 进程内运行,拥有与 agent 自身工具相同的权限;包声明的技能可向 agent 提供指令。继续吗?", -======= - importConfirm: "导入的扩展将在 agent 进程内运行,拥有与 agent 自身工具相同的权限。声明的依赖将通过 npm 安装(禁用安装脚本)。继续吗?", ->>>>>>> 8e55819 (feat(plugins): install imported pi extension dependencies) + importConfirm: "导入的扩展会在 agent 进程内运行,拥有与 agent 自身工具相同的权限;包声明的技能可向 agent 提供指令,声明的依赖将通过 npm 安装(禁用安装脚本)。继续吗?", diagnostics: "诊断", commandNeedsSession: "请先开始一个对话,再运行扩展命令。", state: { diff --git a/packages/i18n/src/locales/zh-TW/index.ts b/packages/i18n/src/locales/zh-TW/index.ts index ef0d68cb9..764c57c82 100644 --- a/packages/i18n/src/locales/zh-TW/index.ts +++ b/packages/i18n/src/locales/zh-TW/index.ts @@ -1395,11 +1395,7 @@ export const zhTW = { importExtensionDepsFailed: "已匯入為外掛 {{id}},但相依套件安裝失敗:{{error}}", agentExtension: { title: "Agent 擴充", -<<<<<<< HEAD - importConfirm: "匯入的擴充會在 agent 程序內執行,擁有與 agent 自身工具相同的權限;套件宣告的技能可向 agent 提供指令。要繼續嗎?", -======= - importConfirm: "匯入的擴充將在 agent 程序內執行,擁有與 agent 自身工具相同的權限。宣告的相依套件將透過 npm 安裝(停用安裝指令碼)。要繼續嗎?", ->>>>>>> 8e55819 (feat(plugins): install imported pi extension dependencies) + importConfirm: "匯入的擴充會在 agent 程序內執行,擁有與 agent 自身工具相同的權限;套件宣告的技能可向 agent 提供指令,宣告的相依套件將透過 npm 安裝(停用安裝指令碼)。要繼續嗎?", diagnostics: "診斷", commandNeedsSession: "請先開始一個對話,再執行擴充命令。", state: {