@jetcrab/pi-desk 1.1.1 → 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/docs/pi-desk/README.md +47 -0
  2. package/docs/pi-desk/commands.md +76 -0
  3. package/docs/pi-desk/examples/local-application/browser/application.js +106 -0
  4. package/docs/pi-desk/examples/local-application/browser/entry.js +9 -0
  5. package/docs/pi-desk/examples/local-application/package.json +9 -0
  6. package/docs/pi-desk/examples/local-application/pi-desk.ts +11 -0
  7. package/docs/pi-desk/plugins.md +119 -0
  8. package/package.json +3 -2
  9. package/src/client/l2_biz/pi-settings/hooks/l2-use-pi-settings.ts +37 -21
  10. package/src/client/l2_biz/pi-settings/l2-pi-settings.tsx +37 -43
  11. package/src/client/l2_biz/pi-settings/views/l2-mcp-settings-editor.tsx +3 -1
  12. package/src/client/l2_biz/pi-settings/views/l2-mcp-settings-list.tsx +1 -1
  13. package/src/client/l2_biz/plugin-host/hooks/l2-use-plugin-catalog.ts +3 -1
  14. package/src/client/l2_biz/plugin-host/hooks/l2-use-plugin-download-source.ts +5 -1
  15. package/src/client/l2_biz/plugin-host/l2-plugin-capability-details.tsx +57 -41
  16. package/src/client/l2_biz/plugin-host/l2-plugin-management-controls.tsx +89 -116
  17. package/src/client/l2_biz/plugin-host/l2-plugin-management.tsx +492 -338
  18. package/src/client/l2_biz/plugin-host/l2-use-plugin-management.ts +72 -22
  19. package/src/client/l2_biz/plugin-host/views/l2-plugin-catalog-detail.tsx +104 -76
  20. package/src/client/l2_biz/plugin-host/views/l2-plugin-catalog-search.tsx +101 -56
  21. package/src/client/l2_biz/plugin-host/views/l2-plugin-direct-install.tsx +91 -59
  22. package/src/client/l2_biz/plugin-host/views/l2-plugin-download-source.tsx +36 -27
  23. package/src/client/l2_biz/plugin-host/views/l2-plugin-installed-list.tsx +218 -208
  24. package/src/client/l2_biz/plugin-host/views/l2-plugin-management-channel.tsx +12 -5
  25. package/src/client/l2_biz/plugin-host/views/l2-plugin-management-dialog.tsx +84 -106
  26. package/src/client/l2_biz/plugin-host/views/l2-plugin-readme.tsx +29 -15
  27. package/src/client/l2_biz/settings/hooks/l2-use-project-model-default-settings.ts +22 -11
  28. package/src/client/l2_biz/settings/l2-settings.tsx +6 -11
  29. package/src/client/l2_biz/settings/views/l2-project-model-default-settings.tsx +20 -14
  30. package/src/client/l2_biz/terminal/l2-terminal-workspace.tsx +35 -1
  31. package/src/client/l2_biz/workbench/file-workspace/l2-work-session-file-preview.tsx +6 -2
  32. package/src/client/l2_biz/workbench/hooks/l2-workbench-chat-viewport.ts +14 -1
  33. package/src/client/l2_biz/workbench/l2-workbench-branch-picker.tsx +33 -5
  34. package/src/client/l2_biz/workbench/l2-workbench-sidebar-layout.tsx +3 -1
  35. package/src/client/l2_biz/workbench/l2-workbench.tsx +20 -3
  36. package/src/client/l3_modules/conversation/l3-conversation-call-summary.tsx +0 -3
  37. package/src/client/l3_modules/project-files/l3-file-context-menu-content.tsx +1 -0
  38. package/src/client/l4_foundation/ui/l4-app-dialog.tsx +3 -1
  39. package/src/client/l4_foundation/ui/l4-image-viewer.tsx +21 -0
  40. package/src/client/l4_foundation/ui/shadcn/checkbox.tsx +27 -0
  41. package/src/client/l4_foundation/ui/shadcn/context-menu.tsx +8 -1
  42. package/src/client/l4_foundation/ui/shadcn/dropdown-menu.tsx +5 -0
  43. package/src/client/l4_foundation/ui/shadcn/popover.tsx +2 -0
  44. package/src/client/l4_foundation/ui/shadcn/select.tsx +2 -0
  45. package/src/client/l4_foundation/ui/shadcn/switch.tsx +31 -0
  46. package/src/client/l4_foundation/ui/shadcn/tabs.tsx +74 -0
  47. package/src/common/l2_biz/plugin/l2-plugin-management-messages.ts +52 -22
  48. package/src/common/l3_modules/conversation/l3-conversation-locale-messages.ts +2 -2
  49. package/src/common/l3_modules/conversation/l3-conversation-pidesk.ts +1 -1
  50. package/src/common/l4_foundation/plugin/l4-plugin-package.ts +1 -0
  51. package/src/server/l2_biz/plugin-management/l2-plugin-catalog.ts +16 -5
  52. package/src/server/l2_biz/plugin-management/l2-plugin-commands.ts +21 -1
  53. package/src/server/l2_biz/plugin-management/l2-plugin-management.ts +21 -2
  54. package/src/server/l2_biz/plugin-management/l2-plugin-updates.ts +17 -3
  55. package/src/server/l4_foundation/pi/l4-pi-basic-session-services.ts +3 -3
  56. package/src/server/l4_foundation/pi/l4-pi-package-maintenance-runner.mts +4 -11
  57. package/src/server/l4_foundation/pi/l4-pi-plugin-registry-config.ts +52 -34
  58. package/src/server/l4_foundation/pi/l4-pi-plugin-registry-network.ts +5 -3
  59. package/src/server/l4_foundation/pi/l4-pi-plugin-registry.ts +23 -8
  60. package/src/server/l4_foundation/pi/l4-pi-session-initialization.ts +2 -5
  61. package/src/server/l4_foundation/pidesk/l4-pidesk-command.ts +8 -1
  62. package/src/server/l4_foundation/pidesk/l4-pidesk-runtime.ts +24 -38
  63. package/src/server/l4_foundation/pidesk/l4-pidesk-system-prompt.ts +11 -0
  64. package/temp/build/pi-desk/release/.next/BUILD_ID +1 -1
  65. package/temp/build/pi-desk/release/.next/build-manifest.json +3 -3
  66. package/temp/build/pi-desk/release/.next/prerender-manifest.json +3 -3
  67. package/temp/build/pi-desk/release/.next/react-loadable-manifest.json +1 -1
  68. package/temp/build/pi-desk/release/.next/required-server-files.js +2 -2
  69. package/temp/build/pi-desk/release/.next/required-server-files.json +2 -2
  70. package/temp/build/pi-desk/release/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  71. package/temp/build/pi-desk/release/.next/server/app/_global-error.html +1 -1
  72. package/temp/build/pi-desk/release/.next/server/app/_global-error.rsc +3 -3
  73. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_full.segment.rsc +3 -3
  74. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +3 -3
  75. package/temp/build/pi-desk/release/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  76. package/temp/build/pi-desk/release/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  77. package/temp/build/pi-desk/release/.next/server/app/api/health/route.js +1 -1
  78. package/temp/build/pi-desk/release/.next/server/app/login/page.js +1 -1
  79. package/temp/build/pi-desk/release/.next/server/app/login/page_client-reference-manifest.js +1 -1
  80. package/temp/build/pi-desk/release/.next/server/app/page.js +79 -79
  81. package/temp/build/pi-desk/release/.next/server/app/page_client-reference-manifest.js +1 -1
  82. package/temp/build/pi-desk/release/.next/server/chunks/25406.js +8 -77
  83. package/temp/build/pi-desk/release/.next/server/chunks/{81107.js → 35348.js} +2 -2
  84. package/temp/build/pi-desk/release/.next/server/chunks/35985.js +4 -4
  85. package/temp/build/pi-desk/release/.next/server/chunks/36361.js +1 -1
  86. package/temp/build/pi-desk/release/.next/server/chunks/47757.js +6 -75
  87. package/temp/build/pi-desk/release/.next/server/chunks/50520.js +4 -4
  88. package/temp/build/pi-desk/release/.next/server/chunks/58330.js +1 -1
  89. package/temp/build/pi-desk/release/.next/server/chunks/84231.js +2 -2
  90. package/temp/build/pi-desk/release/.next/server/chunks/89236.js +2 -2
  91. package/temp/build/pi-desk/release/.next/server/chunks/92386.js +5 -5
  92. package/temp/build/pi-desk/release/.next/server/chunks/94518.js +1 -1
  93. package/temp/build/pi-desk/release/.next/server/chunks/9874.js +1 -1
  94. package/temp/build/pi-desk/release/.next/server/middleware-build-manifest.js +1 -1
  95. package/temp/build/pi-desk/release/.next/server/middleware-react-loadable-manifest.js +1 -1
  96. package/temp/build/pi-desk/release/.next/server/pages/500.html +1 -1
  97. package/temp/build/pi-desk/release/.next/server/server-reference-manifest.json +1 -1
  98. package/temp/build/pi-desk/release/.next/static/chunks/14197-3d3ddd426ac06c9c.js +3 -0
  99. package/temp/build/pi-desk/release/.next/static/chunks/34903-e4205c0becabea76.js +318 -0
  100. package/temp/build/pi-desk/release/.next/static/chunks/{54206.7c0792096df8a5cf.js → 54206.82a63ab971911eda.js} +3 -3
  101. package/temp/build/pi-desk/release/.next/static/chunks/74132-876f256c7a6101af.js +13 -0
  102. package/temp/build/pi-desk/release/.next/static/chunks/78923-17a9c337b29dcecd.js +1 -0
  103. package/temp/build/pi-desk/release/.next/static/chunks/96995-8f8313ea2ac3abd7.js +62 -0
  104. package/temp/build/pi-desk/release/.next/static/chunks/app/{layout-7c5409cced9c2501.js → layout-4faf3a52d219296d.js} +2 -2
  105. package/temp/build/pi-desk/release/.next/static/chunks/app/login/{page-8c87fb9e6c918848.js → page-bf893718bb151f3c.js} +1 -1
  106. package/temp/build/pi-desk/release/.next/static/chunks/app/page-c6c68b33156be10a.js +158 -0
  107. package/temp/build/pi-desk/release/.next/static/chunks/{webpack-45424d28099bbefa.js → webpack-7bfbf627379fb4c1.js} +1 -1
  108. package/temp/build/pi-desk/release/.next/static/css/3213ee28336bddc2.css +1 -0
  109. package/temp/build/pi-desk/release/.next/trace-build +1 -1
  110. package/src/server/l4_foundation/pidesk/l4-pidesk-skill-template.ts +0 -87
  111. package/temp/build/pi-desk/release/.next/static/chunks/27491-8b2d3e345b9142ea.js +0 -318
  112. package/temp/build/pi-desk/release/.next/static/chunks/7089-600a8f5593b7846c.js +0 -3
  113. package/temp/build/pi-desk/release/.next/static/chunks/74132-0884df6a6f11861a.js +0 -13
  114. package/temp/build/pi-desk/release/.next/static/chunks/78923-539a8b5f7af3c54a.js +0 -1
  115. package/temp/build/pi-desk/release/.next/static/chunks/96995-ae32167974365050.js +0 -62
  116. package/temp/build/pi-desk/release/.next/static/chunks/app/page-39b146293b6eb488.js +0 -158
  117. package/temp/build/pi-desk/release/.next/static/css/56486914a8ba0a1b.css +0 -1
  118. /package/temp/build/pi-desk/release/.next/static/{STVrBHA3z9KPtvBE4WcE2 → 1LdtGYUckBhYZy0R-6yG0}/_buildManifest.js +0 -0
  119. /package/temp/build/pi-desk/release/.next/static/{STVrBHA3z9KPtvBE4WcE2 → 1LdtGYUckBhYZy0R-6yG0}/_ssgManifest.js +0 -0
@@ -179,7 +179,7 @@ const infoSchema = z
179
179
  mode: z.enum(['basic', 'normal']),
180
180
  cwd: z.string(),
181
181
  agentDir: z.string(),
182
- skillDirectory: z.string()
182
+ docsDirectory: z.string()
183
183
  })
184
184
  .strict()
185
185
 
@@ -25,6 +25,7 @@ export const L4PluginRegistrySchema = z
25
25
  }, '插件源必须是 HTTP 或 HTTPS 仓库地址')
26
26
  export const L4PluginDownloadSourceSchema = z.discriminatedUnion('mode', [
27
27
  z.object({ mode: z.literal('auto') }).strict(),
28
+ z.object({ mode: z.literal('local') }).strict(),
28
29
  z.object({ mode: z.literal('official') }).strict(),
29
30
  z.object({ mode: z.literal('domestic') }).strict(),
30
31
  z.object({ mode: z.literal('custom'), registry: L4PluginRegistrySchema }).strict()
@@ -17,6 +17,7 @@ import {
17
17
  L4_PLUGIN_OFFICIAL_NAMES,
18
18
  L4PluginRegistryError,
19
19
  searchL4PluginCatalog,
20
+ readL4PluginInstallRegistry,
20
21
  resolveL4PluginInstall,
21
22
  type L4RegistryPackage
22
23
  } from '@server/l4_foundation/pi/l4-pi-plugin-registry'
@@ -181,15 +182,21 @@ export class L2PluginCatalog {
181
182
  }
182
183
 
183
184
  async get(request: L2PluginCatalogGetRequest): Promise<L2PluginCatalogDetail> {
184
- const resolved = request.registry
185
+ const downloadSource = readL4PiPluginPreferences().downloadSource
186
+ const registry =
187
+ downloadSource.mode === 'local'
188
+ ? (await readL4PluginInstallRegistry(request.name, { downloadSource })).registry
189
+ : request.registry
190
+ const resolved = registry
185
191
  ? null
186
192
  : await resolveL4PluginInstall(
187
- `npm:${request.name}${request.version ? `@${request.version}` : ''}`
193
+ `npm:${request.name}${request.version ? `@${request.version}` : ''}`,
194
+ { downloadSource }
188
195
  )
189
196
  const detail = await getL4PluginCatalogDetail(
190
197
  request.name,
191
198
  resolved ? resolved.source.slice(resolved.source.lastIndexOf('@') + 1) : request.version,
192
- request.registry ?? resolved?.registry
199
+ registry ?? resolved?.registry
193
200
  )
194
201
  const manifest = detail.manifest
195
202
  return {
@@ -211,9 +218,13 @@ export class L2PluginCatalog {
211
218
  }
212
219
 
213
220
  async getSettings(): Promise<L2PluginCatalogSettings> {
221
+ const downloadSource = readL4PiPluginPreferences().downloadSource
214
222
  return {
215
- downloadSource: readL4PiPluginPreferences().downloadSource,
216
- recommendedRegistry: await getL4PluginRecommendedRegistry()
223
+ downloadSource,
224
+ recommendedRegistry:
225
+ downloadSource.mode === 'local'
226
+ ? L4_PLUGIN_OFFICIAL_REGISTRY
227
+ : await getL4PluginRecommendedRegistry()
217
228
  }
218
229
  }
219
230
 
@@ -65,6 +65,12 @@ async function resolveTargets(
65
65
  const targets = new Map<string, CommandTarget>()
66
66
  for (const name of names) {
67
67
  const existing = findL2PluginCommandTarget(snapshot.plugins, name, agentDir)
68
+ if (command.action === 'reload') {
69
+ if (!existing) throw new Error(`插件不存在:${name}`)
70
+ if (existing.status === 'disabled') throw new Error(`插件已禁用,请先启用:${name}`)
71
+ targets.set(existing.source, { name, source: existing.source, local: false })
72
+ continue
73
+ }
68
74
  if (command.action === 'remove') {
69
75
  if (!existing) throw new Error(`插件不存在:${name}`)
70
76
  if (existing.kind !== 'package') {
@@ -102,6 +108,18 @@ async function acceptTargets(
102
108
  targets: CommandTarget[],
103
109
  management: CommandManagement
104
110
  ): Promise<CommandAcceptance[]> {
111
+ if (command.action === 'reload') {
112
+ const acceptances: CommandAcceptance[] = []
113
+ for (const target of targets) {
114
+ try {
115
+ await management.apply([target.source])
116
+ acceptances.push({ target, error: null })
117
+ } catch (error) {
118
+ acceptances.push({ target, error: failureMessage(error) })
119
+ }
120
+ }
121
+ return acceptances
122
+ }
105
123
  const results = new Map<string, string | null>()
106
124
  const packages = targets.filter((target) => !target.local)
107
125
  const locals = targets.filter((target) => target.local)
@@ -170,6 +188,7 @@ function completedResult(
170
188
  if (command.version && item.version !== command.version) {
171
189
  throw new Error(`实际版本 ${item.version ?? '未知'} 与目标版本 ${command.version} 不一致`)
172
190
  }
191
+ if (command.action === 'reload') return '成功:已重新加载本机代码,已有 Pi 会话保持不变'
173
192
  return target.local
174
193
  ? '成功:已加载当前代码,服务与界面已更新'
175
194
  : `成功:已安装${item.version ? ` ${item.version}` : ''}并完成宿主加载`
@@ -303,7 +322,8 @@ export async function executeL2PluginCommand(
303
322
  if (acceptedCount === 0) {
304
323
  throw new Error(acceptances.map(({ target, error }) => `${target.name}:${error}`).join('\n'))
305
324
  }
306
- const action = command.action === 'remove' ? '卸载' : '安装'
325
+ const action =
326
+ command.action === 'remove' ? '卸载' : command.action === 'reload' ? '重载' : '安装'
307
327
  const description = `${action} ${targets.map((target) => target.name).join('、')}${command.version ? `@${command.version}` : command.tag ? `(渠道 ${command.tag})` : ''}`
308
328
  console.info('[Pi Desk][Commands] 插件命令已接纳', {
309
329
  operation: description,
@@ -9,8 +9,8 @@ import {
9
9
  } from '@common/l4_foundation/locale/l4-localized-text'
10
10
 
11
11
  import { createHash, randomUUID } from 'node:crypto'
12
- import { mkdir, open, readFile, realpath, rm, stat, writeFile } from 'node:fs/promises'
13
- import { isAbsolute, join, relative, resolve } from 'node:path'
12
+ import { copyFile, mkdir, open, readFile, realpath, rm, stat, writeFile } from 'node:fs/promises'
13
+ import { dirname, isAbsolute, join, relative, resolve } from 'node:path'
14
14
  import {
15
15
  DefaultPackageManager,
16
16
  getAgentDir,
@@ -1009,6 +1009,25 @@ export class L2PluginManagement {
1009
1009
 
1010
1010
  console.info('[Pi Desk][PluginManagement] 开始在隔离 AgentDir 预检候选插件', { source })
1011
1011
  try {
1012
+ // npm 解析结果没有 registry 表示本机模式,预检需保留完整项目配置而非单个源地址。
1013
+ if (!registry && l2PluginNpmName(source)) {
1014
+ for (const configPath of ['.npmrc', join('etc', 'npmrc')]) {
1015
+ const target = join(stagingAgentDir, 'npm', configPath)
1016
+ await mkdir(dirname(target), { recursive: true })
1017
+ try {
1018
+ await copyFile(join(this.agentDir, 'npm', configPath), target)
1019
+ console.info('[Pi Desk][PluginManagement] 已为隔离预检继承本机 npm 配置', {
1020
+ source,
1021
+ configPath
1022
+ })
1023
+ } catch (error) {
1024
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT')
1025
+ throw new L2PluginManagementInvalidPackageError(
1026
+ `无法继承本机 npm 配置(${configPath}):${errorMessage(error)}`
1027
+ )
1028
+ }
1029
+ }
1030
+ }
1012
1031
  await runL4PiPackageMaintenance({
1013
1032
  maintenance: { action: 'install', source, ...(registry ? { registry } : {}) },
1014
1033
  agentDir: stagingAgentDir,
@@ -11,7 +11,10 @@ import {
11
11
  l4PluginNpmName,
12
12
  l4PluginSourceIdentity
13
13
  } from '@common/l4_foundation/plugin/l4-plugin-package'
14
- import { readL4PiPluginUpdateTag } from '@server/l4_foundation/pi/l4-pi-plugin-preferences'
14
+ import {
15
+ readL4PiPluginPreferences,
16
+ readL4PiPluginUpdateTag
17
+ } from '@server/l4_foundation/pi/l4-pi-plugin-preferences'
15
18
  import {
16
19
  readL4PluginInstallRegistry,
17
20
  resolveL4PluginInstall
@@ -109,12 +112,23 @@ export class L2PluginUpdates {
109
112
  continue
110
113
  let key: string | null = null
111
114
  try {
112
- const policy = await readL4PluginInstallRegistry(name, { agentDir: this.agentDir })
113
- key = JSON.stringify([identity, tag, policy.registry, policy.fallback])
115
+ const downloadSource = readL4PiPluginPreferences(this.agentDir).downloadSource
116
+ const policy = await readL4PluginInstallRegistry(name, {
117
+ agentDir: this.agentDir,
118
+ downloadSource
119
+ })
120
+ key = JSON.stringify([
121
+ identity,
122
+ tag,
123
+ downloadSource.mode,
124
+ policy.registry,
125
+ policy.fallback
126
+ ])
114
127
  let value = this.cache.get(key)?.value
115
128
  if (check) {
116
129
  if (!source.path || !source.version) throw new Error('无法读取插件已安装版本')
117
130
  const resolved = await resolveL4PluginInstall(`npm:${name}@${tag}`, {
131
+ downloadSource,
118
132
  agentDir: this.agentDir,
119
133
  signal: this.signal,
120
134
  cache: false
@@ -2,7 +2,7 @@ import 'server-only'
2
2
 
3
3
  import { existsSync, readFileSync } from 'node:fs'
4
4
  import { join } from 'node:path'
5
- import { readL4PiDeskSkills } from '@server/l4_foundation/pidesk/l4-pidesk-runtime'
5
+ import { appendL4PiDeskSystemPrompt } from '@server/l4_foundation/pidesk/l4-pidesk-runtime'
6
6
  import {
7
7
  CONFIG_DIR_NAME,
8
8
  createExtensionRuntime,
@@ -38,7 +38,7 @@ class BasicResourceLoader implements ResourceLoader {
38
38
  }
39
39
 
40
40
  getSkills(): ReturnType<ResourceLoader['getSkills']> {
41
- return { skills: readL4PiDeskSkills(), diagnostics: [] }
41
+ return { skills: [], diagnostics: [] }
42
42
  }
43
43
 
44
44
  getPrompts(): ReturnType<ResourceLoader['getPrompts']> {
@@ -62,7 +62,7 @@ class BasicResourceLoader implements ResourceLoader {
62
62
  }
63
63
 
64
64
  getAppendSystemPrompt(): string[] {
65
- return this.appendPrompt ? [this.appendPrompt.content] : []
65
+ return appendL4PiDeskSystemPrompt(this.appendPrompt ? [this.appendPrompt.content] : [])
66
66
  }
67
67
 
68
68
  getAppendSystemPromptSources(): Array<{ path: string }> {
@@ -64,23 +64,16 @@ async function main(): Promise<void> {
64
64
  if (!source || source.length > 2048) throw new Error('插件维护来源无效')
65
65
  const registry = process.argv[4] ? L4PluginRegistrySchema.parse(process.argv[4]) : undefined
66
66
  const scope = /^npm:(@[^/]+)\//.exec(source)?.[1]
67
+ // scoped 源只覆盖对应 scope,公共依赖继续使用原有默认源。
68
+ const registryKey = scope ? `npm_config_${scope}:registry` : 'npm_config_registry'
67
69
  const inheritedEnvironment = { ...process.env }
68
70
  if (registry) {
69
71
  // Windows 子进程的环境名不区分大小写,不能同时保留两种拼写。
70
- const replaced = new Set([
71
- 'npm_config_registry',
72
- ...(scope ? [`npm_config_${scope}:registry`.toLowerCase()] : [])
73
- ])
74
72
  for (const key of Object.keys(inheritedEnvironment)) {
75
- if (replaced.has(key.toLowerCase())) delete inheritedEnvironment[key]
73
+ if (key.toLowerCase() === registryKey.toLowerCase()) delete inheritedEnvironment[key]
76
74
  }
77
75
  }
78
- const registryEnvironment: Record<string, string> = registry
79
- ? {
80
- npm_config_registry: registry,
81
- ...(scope ? { [`npm_config_${scope}:registry`]: registry } : {})
82
- }
83
- : {}
76
+ const registryEnvironment: Record<string, string> = registry ? { [registryKey]: registry } : {}
84
77
 
85
78
  const configuredAgentDir = process.env.PI_CODING_AGENT_DIR
86
79
  const agentDir =
@@ -2,14 +2,15 @@ import 'server-only'
2
2
 
3
3
  import { execFile } from 'node:child_process'
4
4
  import { existsSync } from 'node:fs'
5
- import { readFile } from 'node:fs/promises'
5
+ import { mkdir, readFile } from 'node:fs/promises'
6
6
  import { dirname, join, resolve } from 'node:path'
7
7
  import { homedir } from 'node:os'
8
8
  import { promisify } from 'node:util'
9
+ import { getAgentDir } from '@earendil-works/pi-coding-agent'
9
10
 
10
11
  const execute = promisify(execFile)
11
12
  type NpmConfig = Record<string, string>
12
- let cached: { cwd: string; expires: number; value: Promise<NpmConfig> } | undefined
13
+ let cached: { cwd: string; prefix: string; expires: number; value: Promise<NpmConfig> } | undefined
13
14
 
14
15
  function npmCli(): string | undefined {
15
16
  const directories = [
@@ -49,17 +50,33 @@ async function readNpmrc(path: string, config: NpmConfig): Promise<void> {
49
50
  }
50
51
  }
51
52
 
52
- async function loadConfig(): Promise<NpmConfig> {
53
+ function npmConfigPath(path: string, cwd: string): string {
54
+ const homePattern = process.platform === 'win32' ? /^~[\\/]/ : /^~\//
55
+ return homePattern.test(path) ? resolve(homedir(), path.slice(2)) : resolve(cwd, path)
56
+ }
57
+
58
+ async function loadConfig(cwd: string, installRoot: string): Promise<NpmConfig> {
59
+ const environment: NpmConfig = {}
60
+ // 与 npm loadEnv 一致:大小写统一后按环境枚举顺序覆盖,空值不参与配置。
61
+ for (const [key, value] of Object.entries(process.env)) {
62
+ if (value !== undefined && value !== '' && /^npm_config_/i.test(key)) {
63
+ const original = key.slice(11)
64
+ const name = original.startsWith('//')
65
+ ? original
66
+ : original.replace(/(?!^)_/g, '-').toLowerCase()
67
+ environment[name] = expand(value)
68
+ }
69
+ }
53
70
  const cli = npmCli()
54
71
  let effective: Record<string, unknown> = {}
55
72
  if (cli) {
56
73
  try {
57
- const result = await execute(process.execPath, [cli, 'config', 'list', '--json'], {
58
- cwd: process.cwd(),
59
- timeout: 4000,
60
- maxBuffer: 512 * 1024,
61
- windowsHide: true
62
- })
74
+ await mkdir(cwd, { recursive: true })
75
+ const result = await execute(
76
+ process.execPath,
77
+ [cli, 'config', 'list', '--json', '--prefix', installRoot],
78
+ { cwd, timeout: 4000, maxBuffer: 512 * 1024, windowsHide: true }
79
+ )
63
80
  effective = JSON.parse(result.stdout) as Record<string, unknown>
64
81
  } catch {
65
82
  // 不输出 npm 的 stderr:其中可能包含注册表鉴权信息。
@@ -68,18 +85,25 @@ async function loadConfig(): Promise<NpmConfig> {
68
85
  }
69
86
  const config: NpmConfig = {}
70
87
  if (cli) await readNpmrc(resolve(dirname(cli), '../npmrc'), config)
71
- const prefix = typeof effective.prefix === 'string' ? effective.prefix : dirname(process.execPath)
88
+ // npm 默认 globalconfig 同样受 CLI --prefix 影响,不能回退到 Node 安装目录。
72
89
  const globalPath =
73
- typeof effective.globalconfig === 'string' ? effective.globalconfig : join(prefix, 'etc/npmrc')
90
+ typeof effective.globalconfig === 'string'
91
+ ? effective.globalconfig
92
+ : (environment.globalconfig ?? join(installRoot, 'etc/npmrc'))
74
93
  const userPath =
75
- process.env.NPM_CONFIG_USERCONFIG ??
76
- process.env.npm_config_userconfig ??
77
- (typeof effective.userconfig === 'string' ? effective.userconfig : join(homedir(), '.npmrc'))
78
- await readNpmrc(globalPath, config)
79
- await readNpmrc(userPath, config)
80
- const project =
81
- typeof effective['local-prefix'] === 'string' ? effective['local-prefix'] : process.cwd()
82
- await readNpmrc(join(project, '.npmrc'), config)
94
+ typeof effective.userconfig === 'string'
95
+ ? effective.userconfig
96
+ : (environment.userconfig ?? join(homedir(), '.npmrc'))
97
+ await readNpmrc(npmConfigPath(globalPath, cwd), config)
98
+ await readNpmrc(npmConfigPath(userPath, cwd), config)
99
+ // Pi user-scope 安装传入 --prefix,项目 npmrc 属于安装根,不属于应用或维护 cwd。
100
+ if (
101
+ effective.global !== true &&
102
+ environment.global !== 'true' &&
103
+ effective.location !== 'global' &&
104
+ environment.location !== 'global'
105
+ )
106
+ await readNpmrc(join(installRoot, '.npmrc'), config)
83
107
  for (const [key, value] of Object.entries(effective)) {
84
108
  if (
85
109
  typeof value === 'string' &&
@@ -88,23 +112,17 @@ async function loadConfig(): Promise<NpmConfig> {
88
112
  )
89
113
  config[key] = value
90
114
  }
91
- for (const [key, value] of Object.entries(process.env)) {
92
- if (value !== undefined && /^npm_config_/i.test(key)) {
93
- const original = key.slice(11)
94
- const name = original.startsWith('//')
95
- ? original
96
- : original.replace(/(?!^)_/g, '-').toLowerCase()
97
- config[name] = expand(value)
98
- }
99
- }
100
- return config
115
+ return { ...config, ...environment }
101
116
  }
102
117
 
103
- export function readRegistryNpmConfig(): Promise<NpmConfig> {
104
- const cwd = process.cwd()
105
- if (!cached || cached.cwd !== cwd || cached.expires <= Date.now()) {
106
- const value = loadConfig()
107
- cached = { cwd, expires: Date.now() + 30_000, value }
118
+ export function readRegistryNpmConfig(agentDir = getAgentDir()): Promise<NpmConfig> {
119
+ const cwd =
120
+ process.env.PI_DESK_PACKAGE_MAINTENANCE_CWD ??
121
+ join(agentDir, 'pi-desk', 'plugin-package-manager')
122
+ const prefix = join(agentDir, 'npm')
123
+ if (!cached || cached.cwd !== cwd || cached.prefix !== prefix || cached.expires <= Date.now()) {
124
+ const value = loadConfig(cwd, prefix)
125
+ cached = { cwd, prefix, expires: Date.now() + 30_000, value }
108
126
  void value.catch(() => {
109
127
  if (cached?.value === value) cached = undefined
110
128
  })
@@ -26,10 +26,11 @@ export async function fetchRegistryBytes(
26
26
  timeout?: number
27
27
  cache?: boolean
28
28
  auth?: boolean
29
+ agentDir?: string
29
30
  }
30
31
  ): Promise<Buffer> {
31
32
  options.signal?.throwIfAborted()
32
- const config = options.auth === false ? {} : await readRegistryNpmConfig()
33
+ const config = options.auth === false ? {} : await readRegistryNpmConfig(options.agentDir)
33
34
  const authorization = registryAuth(url, config)
34
35
  const key = `${url.href}:${options.limit}:${createHash('sha256')
35
36
  .update(authorization ?? '')
@@ -133,9 +134,10 @@ export async function fetchRegistryBytes(
133
134
 
134
135
  export async function fetchRegistryJson(
135
136
  url: URL,
136
- signal?: AbortSignal
137
+ signal?: AbortSignal,
138
+ agentDir?: string
137
139
  ): Promise<Record<string, unknown>> {
138
- const body = await fetchRegistryBytes(url, { limit: 8 * 1024 * 1024, signal })
140
+ const body = await fetchRegistryBytes(url, { limit: 8 * 1024 * 1024, signal, agentDir })
139
141
  try {
140
142
  const parsed: unknown = JSON.parse(body.toString('utf8'))
141
143
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('invalid')
@@ -102,16 +102,25 @@ export async function readL4PluginInstallRegistry(
102
102
  name: string,
103
103
  options: { downloadSource?: L4PluginDownloadSource; registry?: string; agentDir?: string }
104
104
  ): Promise<{ registry: string; fallback: boolean }> {
105
+ const preferences = readL4PiPluginPreferences(options.agentDir)
106
+ const source = options.downloadSource ?? preferences.downloadSource
107
+ if (source.mode === 'local') {
108
+ const config = await readRegistryNpmConfig(options.agentDir)
109
+ const scoped = name.startsWith('@') ? config[`${name.split('/')[0]}:registry`] : undefined
110
+ return {
111
+ registry: normalizeRegistry(scoped || config.registry || L4_PLUGIN_OFFICIAL_REGISTRY),
112
+ fallback: false
113
+ }
114
+ }
105
115
  if (options.registry) {
106
116
  const registry = normalizeRegistry(options.registry)
107
117
  if (![L4_PLUGIN_OFFICIAL_REGISTRY, L4_PLUGIN_DOMESTIC_REGISTRY].includes(registry))
108
118
  return { registry, fallback: false }
109
119
  }
110
- const preferences = readL4PiPluginPreferences(options.agentDir)
111
120
  const saved = preferences.registries[`npm:${name}`]
112
121
  if (saved) return { registry: normalizeRegistry(saved), fallback: false }
113
122
  if (name.startsWith('@')) {
114
- const config = await readRegistryNpmConfig()
123
+ const config = await readRegistryNpmConfig(options.agentDir)
115
124
  const scoped = config[`${name.split('/')[0]}:registry`]
116
125
  if (scoped) {
117
126
  const registry = normalizeRegistry(scoped)
@@ -119,7 +128,6 @@ export async function readL4PluginInstallRegistry(
119
128
  return { registry, fallback: false }
120
129
  }
121
130
  }
122
- const source = options.downloadSource ?? preferences.downloadSource
123
131
  if (source.mode === 'custom')
124
132
  return { registry: normalizeRegistry(source.registry), fallback: false }
125
133
  if (source.mode === 'official') return { registry: L4_PLUGIN_OFFICIAL_REGISTRY, fallback: false }
@@ -145,7 +153,9 @@ export async function resolveL4PluginInstall(
145
153
  const selector = match[2] ?? 'latest'
146
154
  if (selector.length > 128 || !/^[a-zA-Z0-9._+-]+$/.test(selector))
147
155
  throw new L4PluginRegistryError('npm 插件版本或标签无效', 400)
148
- const selected = await readL4PluginInstallRegistry(name, options)
156
+ const downloadSource =
157
+ options.downloadSource ?? readL4PiPluginPreferences(options.agentDir).downloadSource
158
+ const selected = await readL4PluginInstallRegistry(name, { ...options, downloadSource })
149
159
  let registry = selected.registry
150
160
  let target = selector
151
161
  let version: string
@@ -172,18 +182,23 @@ export async function resolveL4PluginInstall(
172
182
  version = chooseVersion(metadata, target)
173
183
  validManifest(registryObject(metadata.versions)[version], name, version)
174
184
  }
175
- return { source: `npm:${name}@${version}`, registry }
185
+ // 本机源仅供版本查询,安装必须继续沿用 npm 配置,也不能保存为逐包仓库归属。
186
+ return {
187
+ source: `npm:${name}@${version}`,
188
+ ...(downloadSource.mode === 'local' ? {} : { registry })
189
+ }
176
190
  }
177
191
 
178
192
  async function readInstallMetadata(
179
193
  url: URL,
180
- options: { signal?: AbortSignal; cache?: boolean }
194
+ options: { signal?: AbortSignal; cache?: boolean; agentDir?: string }
181
195
  ): Promise<Record<string, unknown>> {
182
- if (options.cache !== false) return fetchRegistryJson(url, options.signal)
196
+ if (options.cache !== false) return fetchRegistryJson(url, options.signal, options.agentDir)
183
197
  const body = await fetchRegistryBytes(url, {
184
198
  limit: 8 * 1024 * 1024,
185
199
  signal: options.signal,
186
- cache: false
200
+ cache: false,
201
+ agentDir: options.agentDir
187
202
  })
188
203
  try {
189
204
  const parsed: unknown = JSON.parse(body.toString('utf8'))
@@ -15,7 +15,7 @@ import {
15
15
  import type { CapabilityMode } from '@jetcrab/pi-desk-sdk/capabilities'
16
16
  import {
17
17
  createL4PiDeskTool,
18
- readL4PiDeskSkills,
18
+ appendL4PiDeskSystemPrompt,
19
19
  type L4PiDeskCommandResult
20
20
  } from '@server/l4_foundation/pidesk/l4-pidesk-runtime'
21
21
  import { L4PiCapabilityRuntime } from './l4-pi-capability-runtime'
@@ -313,10 +313,7 @@ export class L4PiSessionInitialization {
313
313
  : await this.options.resources.createServices(
314
314
  this.options.cwd,
315
315
  {
316
- skillsOverride: (loaded) => ({
317
- ...loaded,
318
- skills: readL4PiDeskSkills(loaded.skills)
319
- }),
316
+ appendSystemPromptOverride: appendL4PiDeskSystemPrompt,
320
317
  eventBus: this.capabilities.eventBus(this.plugins.eventBus),
321
318
  extensionsOverride: (loaded) => {
322
319
  if (!this.active) {
@@ -1,6 +1,6 @@
1
1
  import 'server-only'
2
2
 
3
- export type L4PiDeskPluginAction = 'list' | 'show' | 'install' | 'remove'
3
+ export type L4PiDeskPluginAction = 'list' | 'show' | 'install' | 'reload' | 'remove'
4
4
 
5
5
  export type L4PiDeskCommand =
6
6
  | { kind: 'help'; message: string }
@@ -54,6 +54,13 @@ const definitions: readonly CommandDefinition[] = [
54
54
  version: true,
55
55
  tag: true
56
56
  },
57
+ {
58
+ path: ['plugins', 'reload'],
59
+ description:
60
+ '重新加载一个或多个已安装插件;不下载、不重启宿主,不重载已有 Pi 会话;完成后向本会话汇总结果',
61
+ name: 'multiple',
62
+ scope: 'required'
63
+ },
57
64
  {
58
65
  path: ['plugins', 'remove'],
59
66
  description: '卸载一个或多个包来源;完成后向本会话汇总结果',
@@ -1,21 +1,14 @@
1
1
  import 'server-only'
2
2
 
3
- import { mkdir, readFile, writeFile } from 'node:fs/promises'
3
+ import { access, readFile, rm } from 'node:fs/promises'
4
4
  import { dirname, join } from 'node:path'
5
- import {
6
- defineTool,
7
- getAgentDir,
8
- getDocsPath,
9
- loadSkillsFromDir,
10
- truncateHead,
11
- type Skill
12
- } from '@earendil-works/pi-coding-agent'
5
+ import { defineTool, getAgentDir, truncateHead } from '@earendil-works/pi-coding-agent'
13
6
  import { Type } from 'typebox'
14
7
  import type { AgentToolResult } from '@earendil-works/pi-agent-core'
15
8
  import { getL4PiDeskDataDir } from '@server/l4_foundation/pi/l4-pi-desk-data-dir'
16
9
  import { isL4PiDeskSafeMode } from '@server/l4_foundation/pi/l4-pi-desk-mode'
17
10
  import { parseL4PiDeskCommand, type L4PiDeskCommand } from './l4-pidesk-command'
18
- import { renderL4PiDeskPluginGuide, renderL4PiDeskSkill } from './l4-pidesk-skill-template'
11
+ import { renderL4PiDeskSystemPrompt } from './l4-pidesk-system-prompt'
19
12
 
20
13
  export interface L4PiDeskCommandResult {
21
14
  mode: 'sync' | 'async'
@@ -36,7 +29,8 @@ export type L4PiDeskPluginCommandHandler = (
36
29
 
37
30
  interface CommandRuntime {
38
31
  handler: L4PiDeskPluginCommandHandler
39
- skill: Skill
32
+ systemPrompt: string
33
+ docsDirectory: string
40
34
  version: string
41
35
  development: boolean
42
36
  agentDir: string
@@ -66,45 +60,37 @@ export async function initializeL4PiDeskCommands(
66
60
  const sdkRoot = dirname(
67
61
  dirname(createRequire(join(hostRoot, 'package.json')).resolve('@jetcrab/pi-desk-sdk'))
68
62
  )
69
- const skillRoot = join(getL4PiDeskDataDir(), development ? 'dev-skills' : 'skills', 'pi-desk')
70
- await mkdir(join(skillRoot, 'references'), { recursive: true })
71
- await Promise.all([
72
- writeFile(
73
- join(skillRoot, 'SKILL.md'),
74
- renderL4PiDeskSkill({
75
- agentDir: portable(agentDir),
76
- sdkRoot: portable(sdkRoot),
77
- piDocs: portable(getDocsPath())
78
- }),
79
- 'utf8'
80
- ),
81
- writeFile(
82
- join(skillRoot, 'references', 'plugins.md'),
83
- renderL4PiDeskPluginGuide(portable(agentDir)),
84
- 'utf8'
85
- )
86
- ])
87
- const loaded = loadSkillsFromDir({ dir: skillRoot, source: 'pidesk' })
88
- const skill = loaded.skills.find((item) => item.name === 'pi-desk')
89
- if (!skill) throw new Error('Pi Desk Skill 生成后无法加载')
63
+ const docsDirectory = join(hostRoot, 'docs', 'pi-desk')
64
+ await Promise.all(
65
+ ['README.md', 'commands.md', 'plugins.md'].map((file) => access(join(docsDirectory, file)))
66
+ )
67
+ // 只清理当前环境曾生成的宿主 Skill,用户 Skills 和其他宿主环境保持不变。
68
+ await rm(join(getL4PiDeskDataDir(), development ? 'dev-skills' : 'skills', 'pi-desk'), {
69
+ recursive: true,
70
+ force: true
71
+ })
90
72
  const runtime: CommandRuntime = {
91
73
  handler,
92
- skill,
74
+ docsDirectory,
75
+ systemPrompt: renderL4PiDeskSystemPrompt({
76
+ docsDirectory: portable(docsDirectory),
77
+ sdkRoot: portable(sdkRoot)
78
+ }),
93
79
  version: manifest.version,
94
80
  development,
95
81
  agentDir
96
82
  }
97
83
  globalThis.__piDeskCommandRuntime = runtime
98
- console.info('[Pi Desk][Commands] 命令入口与 Skill 已准备完成', { skillPath: skill.filePath })
84
+ console.info('[Pi Desk][Commands] 命令入口与系统资料导航已准备完成', { docsDirectory })
99
85
  }
100
86
 
101
87
  export function disposeL4PiDeskCommands(): void {
102
88
  globalThis.__piDeskCommandRuntime = undefined
103
89
  }
104
90
 
105
- export function readL4PiDeskSkills(skills: readonly Skill[] = []): Skill[] {
106
- const skill = globalThis.__piDeskCommandRuntime?.skill
107
- return skill ? [...skills.filter((item) => item.name !== skill.name), skill] : [...skills]
91
+ export function appendL4PiDeskSystemPrompt(prompts: readonly string[] = []): string[] {
92
+ const prompt = globalThis.__piDeskCommandRuntime?.systemPrompt
93
+ return prompt ? [...prompts, prompt] : [...prompts]
108
94
  }
109
95
 
110
96
  export async function executeL4PiDeskCommand(
@@ -126,7 +112,7 @@ export async function executeL4PiDeskCommand(
126
112
  mode: isL4PiDeskSafeMode() ? 'basic' : 'normal',
127
113
  cwd: context.cwd,
128
114
  agentDir: runtime.agentDir,
129
- skillDirectory: runtime.skill.baseDir
115
+ docsDirectory: runtime.docsDirectory
130
116
  })
131
117
  }
132
118
  }
@@ -0,0 +1,11 @@
1
+ import 'server-only'
2
+
3
+ export function renderL4PiDeskSystemPrompt(paths: {
4
+ docsDirectory: string
5
+ sdkRoot: string
6
+ }): string {
7
+ return `Pi Desk 资料(仅在使用、管理 Pi Desk 或开发其插件时读取):
8
+ - 文档入口:${paths.docsDirectory}/README.md
9
+ - SDK 公开类型声明:${paths.sdkRoot}/dist/
10
+ 从入口选择并完整阅读对应文档及引用示例;原生 Pi 能力按入口引用查阅官方资料。相对路径从文档所在目录解析。`
11
+ }
@@ -1 +1 @@
1
- STVrBHA3z9KPtvBE4WcE2
1
+ 1LdtGYUckBhYZy0R-6yG0