feat: 动态插件系统 M0-M3(两层能力模型 + pi 原生契约 + Obsidian 式 SDK)
This commit is contained in:
@@ -1,7 +1,27 @@
|
||||
import { copyFileSync, mkdirSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import type { Plugin } from 'vite'
|
||||
|
||||
/** 构建期把主题 token 样式表拷到 out/renderer/tokens.css:
|
||||
* 生产环境 src/ 不进包,插件页经 peer://assets/tokens.css 引用它保证深浅色/accent 连续 */
|
||||
function copyTokensCss(): Plugin {
|
||||
return {
|
||||
name: 'aso-copy-tokens-css',
|
||||
closeBundle() {
|
||||
const src = resolve(__dirname, 'src/renderer/src/styles/beautifului.css')
|
||||
const outDir = resolve(__dirname, 'out/renderer')
|
||||
try {
|
||||
mkdirSync(outDir, { recursive: true })
|
||||
copyFileSync(src, resolve(outDir, 'tokens.css'))
|
||||
} catch (err) {
|
||||
this.warn(`tokens.css 拷贝失败: ${err instanceof Error ? err.message : String(err)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
main: {
|
||||
@@ -49,8 +69,12 @@ export default defineConfig({
|
||||
// 默认 5173 常被圈进保留段导致 EACCES;固定用 5323 避开(若日后再撞,
|
||||
// 可换端口,或管理员执行 net stop winnat && net start winnat 后
|
||||
// netsh int ipv4 add excludedportrange protocol=tcp startport=<端口> numberofports=1 永久占用)
|
||||
server: { port: 5323 },
|
||||
plugins: [react(), tailwindcss()],
|
||||
server: {
|
||||
port: 5323,
|
||||
// 允许 dev server 读取工作区源码包(@aso/plugin-sdk 从 packages/ 链接进来)
|
||||
fs: { allow: [resolve(__dirname, '../..')] }
|
||||
},
|
||||
plugins: [react(), tailwindcss(), copyTokensCss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src/renderer/src'),
|
||||
|
||||
Generated
-5624
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,7 @@
|
||||
"tailwind-merge": "3.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@aso/plugin-sdk": "workspace:*",
|
||||
"@tailwindcss/vite": "4.3.3",
|
||||
"@types/node": "26.4.1",
|
||||
"@types/react": "19.2.18",
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import { app, BrowserWindow, ipcMain, Menu, nativeTheme, shell } from 'electron'
|
||||
import { app, BrowserWindow, ipcMain, Menu, nativeTheme, protocol, shell } from 'electron'
|
||||
import { existsSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { registerIpc } from './ipc'
|
||||
import { getPluginHost } from './plugins/plugin-host'
|
||||
import { getBrowserController } from './browser/browser-controller'
|
||||
|
||||
process.env.APP_ROOT = join(__dirname, '..')
|
||||
|
||||
// 外置插件页的特权协议(必须在 app ready 之前注册):
|
||||
// - plugin://<id>/<file> → userData/plugins/<id>/ 内文件(宿主做路径穿越防护)
|
||||
// - peer://assets/tokens.css → 主题 token 样式表(插件页 <link> 引用,保证深浅色/accent 连续)
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{ scheme: 'plugin', privileges: { standard: true, secure: true, supportFetchAPI: true, stream: true } },
|
||||
{ scheme: 'peer', privileges: { standard: true, secure: true, supportFetchAPI: true } }
|
||||
])
|
||||
|
||||
export const VITE_DEV_SERVER_URL = process.env.ELECTRON_RENDERER_URL ?? process.env.VITE_DEV_SERVER_URL
|
||||
export const RENDERER_DIST = join(process.env.APP_ROOT, 'renderer')
|
||||
|
||||
@@ -134,6 +143,13 @@ if (!app.requestSingleInstanceLock()) {
|
||||
app.setAppUserModelId('com.inkreach.agent')
|
||||
}
|
||||
Menu.setApplicationMenu(null)
|
||||
|
||||
// 插件宿主:扫描/装载 + 协议映射 + 插件启停后重建 agent 运行时
|
||||
const pluginHost = getPluginHost()
|
||||
pluginHost.init()
|
||||
pluginHost.registerProtocols()
|
||||
pluginHost.setRuntimeRebuilder(() => agentService.rebuildRuntime())
|
||||
|
||||
void createWindow()
|
||||
|
||||
app.on('activate', () => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { app, BrowserWindow, dialog, ipcMain, shell } from 'electron'
|
||||
import { mkdirSync } from 'node:fs'
|
||||
import { userInfo } from 'node:os'
|
||||
import { AgentService } from './pi/agent-service'
|
||||
import { getPluginHost } from './plugins/plugin-host'
|
||||
import { getBrowserController } from './browser/browser-controller'
|
||||
import { detectBrowserSources, importBrowserProfile } from './browser/browser-profile'
|
||||
|
||||
@@ -380,5 +381,29 @@ export function registerIpc(): AgentService {
|
||||
return browserPass(() => importBrowserProfile(sourceId, profileDir))
|
||||
})
|
||||
|
||||
// ---- 插件系统 ----
|
||||
void app.whenReady().then(() => {
|
||||
// 插件启停/重载会重建 agent 运行时(extensionFactories 重跑)
|
||||
getPluginHost().setRuntimeRebuilder(() => service.rebuildRuntime())
|
||||
})
|
||||
|
||||
ipcMain.handle('plugins:list', () => getPluginHost().list())
|
||||
ipcMain.handle('plugins:uiState', () => getPluginHost().uiState())
|
||||
ipcMain.handle('plugins:setEnabled', (_e, id: unknown, enabled: unknown) => {
|
||||
if (typeof id !== 'string' || typeof enabled !== 'boolean') throw new Error('invalid plugin toggle')
|
||||
return getPluginHost().setEnabled(id, enabled)
|
||||
})
|
||||
ipcMain.handle('plugins:reload', () => getPluginHost().reload())
|
||||
ipcMain.handle('plugins:openView', (_e, view: unknown, params: unknown) => {
|
||||
if (typeof view !== 'string' || !view) throw new Error('invalid view')
|
||||
getPluginHost().openView(view, params)
|
||||
})
|
||||
// 插件页(iframe)经渲染层桥转发来的状态胶囊写请求;pluginId 键控防越权
|
||||
ipcMain.handle('plugins:setStatus', (_e, pluginId: unknown, itemId: unknown, item: unknown) => {
|
||||
if (typeof pluginId !== 'string' || typeof itemId !== 'string') throw new Error('invalid status item')
|
||||
getPluginHost().setStatusItem(pluginId, itemId, (item ?? null) as never)
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ import {
|
||||
type SessionInfo
|
||||
} from '@earendil-works/pi-coding-agent'
|
||||
import { ApprovalService, createApprovalExtension } from './approval'
|
||||
import { browserTools } from '../tools/browser-tools'
|
||||
import { getPluginHost } from '../plugins/plugin-host'
|
||||
import { browserToolNames } from '../tools/browser-tools'
|
||||
import type {
|
||||
AgentUiEvent,
|
||||
AppSettings,
|
||||
@@ -47,8 +48,9 @@ import type {
|
||||
/** 增量事件合批的刷新间隔(ms),参考 pi-web 的 60ms 快照节流经验取略低值 */
|
||||
const DELTA_FLUSH_MS = 40
|
||||
|
||||
/** SDK 注册的自定义工具名(用量卡片中与内置「系统工具」分开统计) */
|
||||
const CUSTOM_TOOL_NAMES = new Set<string>(browserTools.map((tool) => tool.name))
|
||||
/** SDK 注册的自定义工具名(用量卡片中与内置「系统工具」分开统计)。
|
||||
* browserTools 已迁入浏览器插件(pi.registerTool 注册),这里同步其名单。 */
|
||||
const CUSTOM_TOOL_NAMES = new Set<string>(browserToolNames)
|
||||
|
||||
/** 需要人工审批的工具名单(高危内置工具;业务演示工具已随 demo-tools.ts 移除) */
|
||||
const TOOLS_REQUIRING_APPROVAL: ReadonlySet<string> = new Set(['bash', 'write', 'edit'])
|
||||
@@ -270,7 +272,9 @@ export class AgentService {
|
||||
agentDir: targetAgentDir,
|
||||
resourceLoaderOptions: {
|
||||
extensionFactories: [
|
||||
createApprovalExtension(this.approvals, TOOLS_REQUIRING_APPROVAL, () => this.permissionMode)
|
||||
createApprovalExtension(this.approvals, TOOLS_REQUIRING_APPROVAL, () => this.permissionMode),
|
||||
// 插件聚合 extension:全部已启用插件在同一真实 ExtensionAPI 上下文里激活
|
||||
getPluginHost().createAggregatedExtension()
|
||||
]
|
||||
}
|
||||
})
|
||||
@@ -290,8 +294,9 @@ export class AgentService {
|
||||
const result = await createAgentSessionFromServices({
|
||||
services,
|
||||
sessionManager,
|
||||
sessionStartEvent,
|
||||
customTools: browserTools
|
||||
sessionStartEvent
|
||||
// 业务工具(browserTools 等)由插件经 pi.registerTool 注册(聚合 extension),
|
||||
// 此处不再传 customTools
|
||||
})
|
||||
return { ...result, services, diagnostics: services.diagnostics }
|
||||
}
|
||||
@@ -1125,7 +1130,8 @@ export class AgentService {
|
||||
}
|
||||
}
|
||||
|
||||
private async rebuildRuntime(): Promise<void> {
|
||||
/** 重建运行时(插件重载/启停后由 PluginHost 调用,extensionFactories 重跑) */
|
||||
async rebuildRuntime(): Promise<void> {
|
||||
const old = this.runtime
|
||||
this.runtime = null
|
||||
this.initPromise = null
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 浏览器插件(内置,组件级:UI 仍由宿主树 BrowserPanel 承载,面板 tab 见 App.tsx)。
|
||||
* 主进程侧:browserTools 经 pi.registerTool 注册(原生路径,提示词注入/审批拦截
|
||||
* 与 pi 生态一致),并示范内联 UI——受管浏览器打开时推状态胶囊。
|
||||
*/
|
||||
import { getBrowserController } from '../../browser/browser-controller'
|
||||
import { browserTools } from '../../tools/browser-tools'
|
||||
import type { BuiltinPlugin } from './index'
|
||||
|
||||
export const browserPlugin: BuiltinPlugin = {
|
||||
manifest: {
|
||||
id: 'browser',
|
||||
name: '浏览器',
|
||||
version: '0.1.0',
|
||||
minContract: '0.1.0',
|
||||
panel: true,
|
||||
order: 10,
|
||||
description: '受管浏览器(CDP 驱动,Agent 可操作,右侧面板查看)'
|
||||
},
|
||||
main: (api) => {
|
||||
for (const tool of browserTools) {
|
||||
api.pi.registerTool(tool)
|
||||
}
|
||||
|
||||
const pushStatus = (): void => {
|
||||
const state = getBrowserController().getState()
|
||||
if (!state.open || state.tabs.length === 0) {
|
||||
api.ui.removeStatusItem('tabs')
|
||||
return
|
||||
}
|
||||
const active = state.tabs.find((t) => t.tabId === state.activeTabId)
|
||||
const title = active?.title?.trim()
|
||||
api.ui.setStatusItem('tabs', {
|
||||
text: `浏览器 ${state.tabs.length} 页${title ? ` · ${title.slice(0, 12)}` : ''}`,
|
||||
tone: 'default'
|
||||
})
|
||||
}
|
||||
|
||||
const off = getBrowserController().onState(pushStatus)
|
||||
pushStatus()
|
||||
return () => {
|
||||
off()
|
||||
api.ui.removeStatusItem('tabs')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 内置插件注册表(编译进宿主 bundle 的"特权实现"):
|
||||
* - main:第一方工厂(不走动态 import;pi ExtensionAPI 语义与外置一致)
|
||||
* - component:渲染层组件 key(plugins/builtin-components.ts 里登记的宿主树组件;
|
||||
* 外置插件没有这项能力,遮蔽组件级内置需 manifest.replacesNative 声明)
|
||||
* 浏览器面板(M3)在此登记。
|
||||
*/
|
||||
import type { HarnessPluginFactory, PluginManifest } from '@aso/plugin-sdk'
|
||||
import { browserPlugin } from './browser'
|
||||
|
||||
export interface BuiltinPlugin {
|
||||
manifest: PluginManifest
|
||||
/** 主进程工厂(注册 pi 工具/事件、内联 UI) */
|
||||
main?: HarnessPluginFactory
|
||||
/** 渲染层宿主树组件 key(renderer/src/plugins/builtin-components.ts) */
|
||||
component?: string
|
||||
}
|
||||
|
||||
export const BUILTIN_PLUGINS: BuiltinPlugin[] = [browserPlugin]
|
||||
@@ -0,0 +1,495 @@
|
||||
/**
|
||||
* 插件宿主(主进程):
|
||||
* - 内置插件:代码注册(manifest + 第一方工厂 + 组件 key),编译进 bundle
|
||||
* - 外置插件:userData/plugins/<id>/manifest.json + 可选 main.mjs(动态 import)
|
||||
* - 同 id 高 semver 胜、平级外置胜;外置遮蔽组件级内置需 manifest.replacesNative
|
||||
* - 聚合为一个 InlineExtension 挂进 pi extensionFactories:插件工厂收到
|
||||
* { pi, ui, storage, manifest },pi 为真实 ExtensionAPI(工具/事件/命令走原生路径)
|
||||
* - 内联 UI(状态胶囊/工具栏按钮)数据存主进程(单一事实源),变更广播渲染层
|
||||
* - plugin:// 协议把插件目录映射给 iframe 页(含路径穿越防护)
|
||||
*
|
||||
* 签名校验(在线更新管道)尚未接入:当前外置插件直接装载(TODO: M5 接入
|
||||
* ed25519 验签 + 开发者模式豁免)。
|
||||
*/
|
||||
import { app, BrowserWindow, net, protocol } from 'electron'
|
||||
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
||||
import { join, resolve, sep } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import type { ExtensionAPI, InlineExtension } from '@earendil-works/pi-coding-agent'
|
||||
import type { HarnessPluginFactory, PluginManifest, StatusItem, ToolbarButtonDef } from '@aso/plugin-sdk'
|
||||
import type { PluginInfo, PluginStatusEntry, PluginToolbarEntry, PluginsUiState } from '@shared/types'
|
||||
import { BUILTIN_PLUGINS, type BuiltinPlugin } from './builtin'
|
||||
|
||||
/** harness 插件契约版本(manifest.minContract 与之比对;破坏性变更时提升) */
|
||||
export const HARNESS_CONTRACT_VERSION = '0.1.0'
|
||||
|
||||
interface ResolvedPlugin {
|
||||
manifest: PluginManifest
|
||||
source: 'builtin' | 'external'
|
||||
/** 外置插件根目录(plugin:// 映射根) */
|
||||
dir?: string
|
||||
/** 内置:第一方工厂 / 渲染层组件 key */
|
||||
builtinFactory?: HarnessPluginFactory
|
||||
componentKey?: string
|
||||
/** 外置 main.mjs 动态导入的工厂(reload 时替换) */
|
||||
externalFactory?: HarnessPluginFactory
|
||||
/** 装载/激活错误(管理页展示;不阻塞其他插件) */
|
||||
error?: string
|
||||
}
|
||||
|
||||
/** 最小 semver(x.y.z 数值三元组;忽略预发布段) */
|
||||
function parseSemver(v: string): [number, number, number] | null {
|
||||
const m = /^(\d+)\.(\d+)\.(\d+)/.exec(v.trim())
|
||||
if (!m) return null
|
||||
return [Number(m[1]), Number(m[2]), Number(m[3])]
|
||||
}
|
||||
|
||||
function compareSemver(a: string, b: string): number {
|
||||
const pa = parseSemver(a)
|
||||
const pb = parseSemver(b)
|
||||
if (!pa || !pb) return 0
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (pa[i] !== pb[i]) return pa[i] < pb[i] ? -1 : 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
class PluginHost {
|
||||
private resolved = new Map<string, ResolvedPlugin>()
|
||||
private disabled = new Set<string>()
|
||||
/** runtime 重建回调(由 ipc 层注入:extensionFactories 重跑 = 插件重新激活) */
|
||||
private rebuildRuntime: (() => Promise<void>) | null = null
|
||||
/** 状态胶囊/工具栏按钮:pluginId → (itemId → data) */
|
||||
private statusItems = new Map<string, Map<string, StatusItem>>()
|
||||
private toolbarButtons = new Map<string, Map<string, ToolbarButtonDef>>()
|
||||
/** 插件 KV 存储(pluginId → key → value) */
|
||||
private storage = new Map<string, Map<string, unknown>>()
|
||||
private storageSaveTimer: NodeJS.Timeout | null = null
|
||||
|
||||
private get externalRoot(): string {
|
||||
return join(app.getPath('userData'), 'plugins')
|
||||
}
|
||||
|
||||
private get settingsFile(): string {
|
||||
return join(app.getPath('userData'), 'aso-plugin-settings.json')
|
||||
}
|
||||
|
||||
private get storageFile(): string {
|
||||
return join(app.getPath('userData'), 'aso-plugin-storage.json')
|
||||
}
|
||||
|
||||
// ---- 生命周期 ----
|
||||
|
||||
init(): void {
|
||||
try {
|
||||
mkdirSync(this.externalRoot, { recursive: true })
|
||||
} catch {
|
||||
// 已存在或创建失败都继续(扫描时再报)
|
||||
}
|
||||
try {
|
||||
const raw = JSON.parse(readFileSync(this.settingsFile, 'utf8')) as { disabled?: unknown }
|
||||
if (Array.isArray(raw.disabled)) this.disabled = new Set(raw.disabled.filter((d): d is string => typeof d === 'string'))
|
||||
} catch {
|
||||
// 无设置文件 = 全部启用
|
||||
}
|
||||
try {
|
||||
const raw = JSON.parse(readFileSync(this.storageFile, 'utf8')) as Record<string, unknown>
|
||||
for (const [pid, kv] of Object.entries(raw)) {
|
||||
if (kv && typeof kv === 'object' && !Array.isArray(kv)) {
|
||||
this.storage.set(pid, new Map(Object.entries(kv as Record<string, unknown>)))
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// 无存储文件 = 空
|
||||
}
|
||||
void this.scan()
|
||||
}
|
||||
|
||||
setRuntimeRebuilder(fn: () => Promise<void>): void {
|
||||
this.rebuildRuntime = fn
|
||||
}
|
||||
|
||||
/** 扫描内置注册表 + 外置目录并合并(不动已激活状态;reload 才重建) */
|
||||
async scan(): Promise<void> {
|
||||
const byId = new Map<string, ResolvedPlugin>()
|
||||
|
||||
for (const builtin of BUILTIN_PLUGINS) {
|
||||
byId.set(builtin.manifest.id, {
|
||||
manifest: builtin.manifest,
|
||||
source: 'builtin',
|
||||
builtinFactory: builtin.main,
|
||||
componentKey: builtin.component
|
||||
})
|
||||
}
|
||||
|
||||
if (existsSync(this.externalRoot)) {
|
||||
let entries: import('node:fs').Dirent[]
|
||||
try {
|
||||
entries = readdirSync(this.externalRoot, { withFileTypes: true })
|
||||
} catch {
|
||||
entries = []
|
||||
}
|
||||
for (const entry of entries) {
|
||||
if (!entry.isDirectory()) continue
|
||||
const dir = join(this.externalRoot, entry.name)
|
||||
const manifest = this.readManifest(dir)
|
||||
if (!manifest) continue
|
||||
const existing = byId.get(manifest.id)
|
||||
if (existing) {
|
||||
const cmp = compareSemver(manifest.version, existing.manifest.version)
|
||||
// 外置仅在严格更高版本时遮蔽内置;平级外置胜仅适用于同为外置的场景(此处 existing 必为内置)
|
||||
if (cmp <= 0) continue
|
||||
// 组件级内置只能被显式声明降级替代的外置遮蔽
|
||||
if (existing.componentKey && !manifest.replacesNative) {
|
||||
existing.error = `外置 ${manifest.version} 未声明 replacesNative,继续使用内置 ${existing.manifest.version}`
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (compareSemver(manifest.minContract, HARNESS_CONTRACT_VERSION) > 0) {
|
||||
byId.set(manifest.id, {
|
||||
manifest,
|
||||
source: 'external',
|
||||
dir,
|
||||
error: `需要契约版本 >= ${manifest.minContract}(当前 ${HARNESS_CONTRACT_VERSION})`
|
||||
})
|
||||
continue
|
||||
}
|
||||
byId.set(manifest.id, { manifest, source: 'external', dir })
|
||||
}
|
||||
}
|
||||
|
||||
this.resolved = byId
|
||||
await this.loadExternalMains()
|
||||
this.send('plugins:listChanged', null)
|
||||
}
|
||||
|
||||
private readManifest(dir: string): PluginManifest | null {
|
||||
const file = join(dir, 'manifest.json')
|
||||
try {
|
||||
const raw = JSON.parse(readFileSync(file, 'utf8')) as Partial<PluginManifest>
|
||||
if (typeof raw.id !== 'string' || typeof raw.name !== 'string' || typeof raw.version !== 'string') return null
|
||||
if (!/^[a-z][a-z0-9-]*$/.test(raw.id)) return null
|
||||
if (!parseSemver(raw.version) || !parseSemver(raw.minContract ?? '0.0.0')) return null
|
||||
return {
|
||||
id: raw.id,
|
||||
name: raw.name,
|
||||
version: raw.version,
|
||||
minContract: raw.minContract ?? '0.0.0',
|
||||
description: typeof raw.description === 'string' ? raw.description : undefined,
|
||||
icon: typeof raw.icon === 'string' ? raw.icon : undefined,
|
||||
order: typeof raw.order === 'number' ? raw.order : undefined,
|
||||
panel: raw.panel === true,
|
||||
entry:
|
||||
raw.entry && typeof raw.entry === 'object'
|
||||
? {
|
||||
page: typeof raw.entry.page === 'string' ? raw.entry.page : undefined,
|
||||
main: typeof raw.entry.main === 'string' ? raw.entry.main : undefined
|
||||
}
|
||||
: undefined,
|
||||
replacesNative: raw.replacesNative === true,
|
||||
updateURL: typeof raw.updateURL === 'string' ? raw.updateURL : undefined
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/** 动态导入外置 main.mjs(cache-bust 保证重载拿到新代码) */
|
||||
private async loadExternalMains(): Promise<void> {
|
||||
for (const plugin of this.resolved.values()) {
|
||||
if (plugin.source !== 'external' || !plugin.manifest.entry?.main || plugin.error) continue
|
||||
const file = resolve(plugin.dir!, plugin.manifest.entry.main)
|
||||
try {
|
||||
const url = pathToFileURL(file).href + `?t=${Date.now()}`
|
||||
const mod = (await import(url)) as { default?: unknown }
|
||||
if (typeof mod.default !== 'function') {
|
||||
plugin.error = 'main.mjs 默认导出必须是插件工厂函数'
|
||||
continue
|
||||
}
|
||||
plugin.externalFactory = mod.default as HarnessPluginFactory
|
||||
plugin.error = undefined
|
||||
} catch (err) {
|
||||
plugin.externalFactory = undefined
|
||||
plugin.error = `main 加载失败: ${err instanceof Error ? err.message : String(err)}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 重新扫描 + 重新导入 + 清 UI 状态 + 重建 runtime(= pi /reload 等效) */
|
||||
async reload(): Promise<void> {
|
||||
this.statusItems.clear()
|
||||
this.toolbarButtons.clear()
|
||||
await this.scan()
|
||||
this.send('plugins:uiState', this.uiState())
|
||||
if (this.rebuildRuntime) await this.rebuildRuntime()
|
||||
}
|
||||
|
||||
async setEnabled(id: string, enabled: boolean): Promise<void> {
|
||||
if (enabled) this.disabled.delete(id)
|
||||
else this.disabled.add(id)
|
||||
try {
|
||||
writeFileSync(this.settingsFile, JSON.stringify({ disabled: [...this.disabled] }, null, 2), 'utf8')
|
||||
} catch (err) {
|
||||
console.error('[plugins] save settings failed:', err)
|
||||
}
|
||||
await this.reload()
|
||||
}
|
||||
|
||||
// ---- 查询 ----
|
||||
|
||||
isEnabled(id: string): boolean {
|
||||
return !this.disabled.has(id)
|
||||
}
|
||||
|
||||
list(): PluginInfo[] {
|
||||
return [...this.resolved.values()]
|
||||
.map((p) => ({
|
||||
manifest: p.manifest,
|
||||
source: p.source,
|
||||
enabled: this.isEnabled(p.manifest.id),
|
||||
hasMain: Boolean(p.builtinFactory ?? p.externalFactory),
|
||||
hasPage: Boolean(p.source === 'external' && p.manifest.entry?.page),
|
||||
hasComponent: Boolean(p.componentKey),
|
||||
error: p.error
|
||||
}))
|
||||
.sort((a, b) => a.manifest.id.localeCompare(b.manifest.id))
|
||||
}
|
||||
|
||||
getComponentKey(id: string): string | undefined {
|
||||
const p = this.resolved.get(id)
|
||||
return p?.componentKey
|
||||
}
|
||||
|
||||
uiState(): PluginsUiState {
|
||||
const status: PluginStatusEntry[] = []
|
||||
for (const [pluginId, items] of this.statusItems) {
|
||||
for (const [itemId, item] of items) status.push({ pluginId, itemId, item })
|
||||
}
|
||||
const toolbar: PluginToolbarEntry[] = []
|
||||
for (const [pluginId, buttons] of this.toolbarButtons) {
|
||||
for (const [buttonId, button] of buttons) toolbar.push({ pluginId, buttonId, button })
|
||||
}
|
||||
return { status, toolbar }
|
||||
}
|
||||
|
||||
// ---- 内联 UI(数据驱动;单一事实源在主进程) ----
|
||||
|
||||
setStatusItem(pluginId: string, itemId: string, item: StatusItem | null): void {
|
||||
if (!this.resolved.has(pluginId)) return
|
||||
let items = this.statusItems.get(pluginId)
|
||||
if (!items) {
|
||||
items = new Map()
|
||||
this.statusItems.set(pluginId, items)
|
||||
}
|
||||
if (item) items.set(itemId, item)
|
||||
else items.delete(itemId)
|
||||
if (items.size === 0) this.statusItems.delete(pluginId)
|
||||
this.send('plugins:uiState', this.uiState())
|
||||
}
|
||||
|
||||
registerToolbarButton(pluginId: string, button: ToolbarButtonDef): void {
|
||||
if (!this.resolved.has(pluginId)) return
|
||||
let buttons = this.toolbarButtons.get(pluginId)
|
||||
if (!buttons) {
|
||||
buttons = new Map()
|
||||
this.toolbarButtons.set(pluginId, buttons)
|
||||
}
|
||||
buttons.set(button.id, button)
|
||||
this.send('plugins:uiState', this.uiState())
|
||||
}
|
||||
|
||||
removeToolbarButton(pluginId: string, buttonId: string): void {
|
||||
const buttons = this.toolbarButtons.get(pluginId)
|
||||
if (!buttons) return
|
||||
buttons.delete(buttonId)
|
||||
if (buttons.size === 0) this.toolbarButtons.delete(pluginId)
|
||||
this.send('plugins:uiState', this.uiState())
|
||||
}
|
||||
|
||||
/** 清空某插件的全部内联 UI(禁用/重载路径) */
|
||||
private clearPluginUi(pluginId: string): void {
|
||||
this.statusItems.delete(pluginId)
|
||||
this.toolbarButtons.delete(pluginId)
|
||||
this.send('plugins:uiState', this.uiState())
|
||||
}
|
||||
|
||||
// ---- 深链 ----
|
||||
|
||||
openView(view: string, params?: unknown): void {
|
||||
this.send('plugins:view', { view, params })
|
||||
}
|
||||
|
||||
// ---- 插件 KV 存储 ----
|
||||
|
||||
storageFor(pluginId: string) {
|
||||
return {
|
||||
get: <T>(key: string): T | undefined => {
|
||||
const kv = this.storage.get(pluginId)
|
||||
const v = kv?.get(key)
|
||||
return v === undefined ? undefined : (structuredClone(v) as T)
|
||||
},
|
||||
set: (key: string, value: unknown): void => {
|
||||
let kv = this.storage.get(pluginId)
|
||||
if (!kv) {
|
||||
kv = new Map()
|
||||
this.storage.set(pluginId, kv)
|
||||
}
|
||||
kv.set(key, structuredClone(value))
|
||||
this.scheduleStorageSave()
|
||||
},
|
||||
delete: (key: string): void => {
|
||||
this.storage.get(pluginId)?.delete(key)
|
||||
this.scheduleStorageSave()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleStorageSave(): void {
|
||||
if (this.storageSaveTimer) return
|
||||
this.storageSaveTimer = setTimeout(() => {
|
||||
this.storageSaveTimer = null
|
||||
const doc: Record<string, Record<string, unknown>> = {}
|
||||
for (const [pid, kv] of this.storage) doc[pid] = Object.fromEntries(kv)
|
||||
try {
|
||||
writeFileSync(this.storageFile, JSON.stringify(doc, null, 2), 'utf8')
|
||||
} catch (err) {
|
||||
console.error('[plugins] save storage failed:', err)
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
|
||||
// ---- 聚合 pi extension ----
|
||||
|
||||
/** 挂进 AgentService 的 extensionFactories;工厂内激活全部已启用插件 */
|
||||
createAggregatedExtension(): InlineExtension {
|
||||
return {
|
||||
name: 'aso-plugins',
|
||||
factory: (pi: ExtensionAPI) => {
|
||||
const disposers: Array<() => void> = []
|
||||
for (const plugin of this.resolved.values()) {
|
||||
if (!this.isEnabled(plugin.manifest.id) || plugin.error) continue
|
||||
const factory = plugin.builtinFactory ?? plugin.externalFactory
|
||||
if (!factory) continue
|
||||
try {
|
||||
const result = factory(this.buildApi(plugin, pi))
|
||||
if (typeof result === 'function') {
|
||||
disposers.push(result)
|
||||
} else if (result && typeof (result as Promise<unknown>).then === 'function') {
|
||||
void (result as Promise<void | (() => void)>).then((d) => {
|
||||
if (typeof d === 'function') disposers.push(d)
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
plugin.error = `激活失败: ${err instanceof Error ? err.message : String(err)}`
|
||||
this.clearPluginUi(plugin.manifest.id)
|
||||
this.send('plugins:listChanged', null)
|
||||
}
|
||||
}
|
||||
pi.on('session_shutdown', () => {
|
||||
for (const dispose of disposers) {
|
||||
try {
|
||||
dispose()
|
||||
} catch (err) {
|
||||
console.error('[plugins] dispose error:', err)
|
||||
}
|
||||
}
|
||||
disposers.length = 0
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private buildApi(plugin: ResolvedPlugin, pi: ExtensionAPI) {
|
||||
const id = plugin.manifest.id
|
||||
return {
|
||||
pi,
|
||||
manifest: plugin.manifest,
|
||||
storage: this.storageFor(id),
|
||||
ui: {
|
||||
openView: (view: string, params?: unknown) => this.openView(view, params),
|
||||
setStatusItem: (itemId: string, item: StatusItem) => this.setStatusItem(id, itemId, item),
|
||||
removeStatusItem: (itemId: string) => this.setStatusItem(id, itemId, null),
|
||||
registerToolbarButton: (button: ToolbarButtonDef) => this.registerToolbarButton(id, button),
|
||||
removeToolbarButton: (buttonId: string) => this.removeToolbarButton(id, buttonId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- plugin:// 协议 ----
|
||||
|
||||
/** plugin://<id>/<rel> → 插件目录内文件;路径必须落在该插件根内(防穿越) */
|
||||
resolvePluginFile(id: string, rel: string): string | null {
|
||||
const plugin = this.resolved.get(id)
|
||||
if (!plugin || plugin.source !== 'external' || !plugin.dir) return null
|
||||
const root = resolve(plugin.dir)
|
||||
const target = resolve(root, rel)
|
||||
if (target !== root && !target.startsWith(root + sep)) return null
|
||||
return target
|
||||
}
|
||||
|
||||
registerProtocols(): void {
|
||||
protocol.handle('plugin', (request) => this.serveFile(() => {
|
||||
const url = new URL(request.url)
|
||||
const id = url.hostname
|
||||
const rel = decodeURIComponent(url.pathname).replace(/^\/+/, '')
|
||||
const file = this.resolvePluginFile(id, rel)
|
||||
return file ?? ''
|
||||
}))
|
||||
|
||||
// peer:// 只暴露固定白名单资产(tokens.css 主题变量表,插件页 <link> 引用)
|
||||
protocol.handle('peer', (request) => this.serveFile(() => {
|
||||
const url = new URL(request.url)
|
||||
const asset = `${url.hostname}${url.pathname}`.replace(/\/+$/, '')
|
||||
if (asset !== 'assets/tokens.css') return ''
|
||||
return this.tokensCssPath()
|
||||
}))
|
||||
}
|
||||
|
||||
private tokensCssPath(): string {
|
||||
// dev:源文件;prod:构建期拷贝到 out/renderer/tokens.css(vite closeBundle 插件)
|
||||
const src = resolve(app.getAppPath(), 'src', 'renderer', 'src', 'styles', 'beautifului.css')
|
||||
if (existsSync(src)) return src
|
||||
return resolve(app.getAppPath(), 'out', 'renderer', 'tokens.css')
|
||||
}
|
||||
|
||||
private async serveFile(resolvePath: () => string): Promise<Response> {
|
||||
let file = ''
|
||||
try {
|
||||
file = resolvePath()
|
||||
} catch {
|
||||
file = ''
|
||||
}
|
||||
if (!file || !existsSync(file)) return new Response('not found', { status: 404 })
|
||||
try {
|
||||
const res = await net.fetch(pathToFileURL(file).toString())
|
||||
// no-store:热重载换 src 即取新文件,避免缓存妨碍开发预览
|
||||
return new Response(res.body, {
|
||||
status: res.status,
|
||||
headers: {
|
||||
'content-type': res.headers.get('content-type') ?? 'application/octet-stream',
|
||||
'cache-control': 'no-store'
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('[plugins] serve failed:', file, err)
|
||||
return new Response('serve error', { status: 500 })
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 渲染层广播 ----
|
||||
|
||||
private send(channel: string, payload: unknown): void {
|
||||
for (const win of BrowserWindow.getAllWindows()) {
|
||||
if (!win.isDestroyed()) win.webContents.send(channel, payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let host: PluginHost | null = null
|
||||
|
||||
export function getPluginHost(): PluginHost {
|
||||
if (!host) host = new PluginHost()
|
||||
return host
|
||||
}
|
||||
|
||||
export type { BuiltinPlugin }
|
||||
@@ -13,6 +13,9 @@ import type {
|
||||
ModelCatalog,
|
||||
NavigateTreeOptions,
|
||||
PermissionMode,
|
||||
PluginInfo,
|
||||
PluginsUiState,
|
||||
PluginViewChange,
|
||||
ResourceDetails,
|
||||
SendImage,
|
||||
SessionMeta,
|
||||
@@ -129,6 +132,39 @@ const api = {
|
||||
return () => {
|
||||
ipcRenderer.removeListener('browser:event', listener)
|
||||
}
|
||||
},
|
||||
|
||||
// ---- 插件系统 ----
|
||||
pluginsList: (): Promise<PluginInfo[]> => ipcRenderer.invoke('plugins:list'),
|
||||
pluginsUiState: (): Promise<PluginsUiState> => ipcRenderer.invoke('plugins:uiState'),
|
||||
pluginsSetEnabled: (id: string, enabled: boolean): Promise<void> =>
|
||||
ipcRenderer.invoke('plugins:setEnabled', id, enabled),
|
||||
pluginsReload: (): Promise<void> => ipcRenderer.invoke('plugins:reload'),
|
||||
pluginsOpenView: (view: string, params?: unknown): Promise<void> =>
|
||||
ipcRenderer.invoke('plugins:openView', view, params),
|
||||
/** 插件页桥转发(渲染层桥校验来源后调用;pluginId 键控写主进程单一事实源) */
|
||||
pluginsSetStatus: (pluginId: string, itemId: string, item: unknown): Promise<void> =>
|
||||
ipcRenderer.invoke('plugins:setStatus', pluginId, itemId, item),
|
||||
onPluginsListChanged: (callback: () => void): (() => void) => {
|
||||
const listener = (): void => callback()
|
||||
ipcRenderer.on('plugins:listChanged', listener)
|
||||
return () => {
|
||||
ipcRenderer.removeListener('plugins:listChanged', listener)
|
||||
}
|
||||
},
|
||||
onPluginsUiState: (callback: (state: PluginsUiState) => void): (() => void) => {
|
||||
const listener = (_e: Electron.IpcRendererEvent, state: PluginsUiState): void => callback(state)
|
||||
ipcRenderer.on('plugins:uiState', listener)
|
||||
return () => {
|
||||
ipcRenderer.removeListener('plugins:uiState', listener)
|
||||
}
|
||||
},
|
||||
onPluginsView: (callback: (change: PluginViewChange) => void): (() => void) => {
|
||||
const listener = (_e: Electron.IpcRendererEvent, change: PluginViewChange): void => callback(change)
|
||||
ipcRenderer.on('plugins:view', listener)
|
||||
return () => {
|
||||
ipcRenderer.removeListener('plugins:view', listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'"
|
||||
content="default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-src 'self' plugin:"
|
||||
/>
|
||||
<title>Inkreach Agent</title>
|
||||
</head>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import type { JSX } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import type { JSX, ComponentType } from 'react'
|
||||
import { LuGlobe } from 'react-icons/lu'
|
||||
import { useAgent } from './hooks/useAgent'
|
||||
import { useBrowser } from './hooks/useBrowser'
|
||||
@@ -12,7 +12,13 @@ import { SettingsModal } from './components/SettingsModal'
|
||||
import { Search } from './components/Search'
|
||||
import { BrowserPanel } from './components/browser/BrowserPanel'
|
||||
import { SidePanel, type PanelTabIcon } from './components/SidePanel'
|
||||
import { PluginPageFrame, PluginIcon } from './plugins/PluginPageFrame'
|
||||
import { PluginErrorBoundary } from './plugins/PluginErrorBoundary'
|
||||
import { PluginStatusCapsules } from './plugins/PluginStatusCapsules'
|
||||
import { usePluginList, usePluginsUiState, sidebarPlugins } from './plugins/store'
|
||||
import { BUILTIN_PLUGIN_COMPONENTS } from './plugins/builtin-components'
|
||||
import { applyThemeColor, watchColorScheme } from './lib/theme'
|
||||
import type { PluginInfo } from '@shared/types'
|
||||
|
||||
// ---- 侧边面板(TabPanel):开合 / 标签 / 宽度,跨启动持久化 ----
|
||||
|
||||
@@ -29,17 +35,14 @@ interface SideState {
|
||||
active: string | null
|
||||
}
|
||||
|
||||
/** 面板标签注册表:新标签在这里登记即可进入 TabPanel 通用机制 */
|
||||
const PANEL_TAB_META: Record<string, { title: string; icon: PanelTabIcon }> = {
|
||||
browser: { title: '浏览器', icon: LuGlobe }
|
||||
}
|
||||
|
||||
/** 面板标签元数据:browser 为宿主内置;插件标签由 manifest(panel:true)动态注册。
|
||||
* loadSideState 只做形状校验,非法 id(已卸载插件等)在装配 panelTabs 时跳过。 */
|
||||
function loadSideState(): SideState {
|
||||
try {
|
||||
const raw = localStorage.getItem(SIDE_STORAGE_KEY)
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw) as Partial<SideState>
|
||||
const tabs = Array.isArray(parsed.tabs) ? parsed.tabs.filter((t) => t in PANEL_TAB_META) : []
|
||||
const tabs = Array.isArray(parsed.tabs) ? parsed.tabs.filter((t) => typeof t === 'string') : []
|
||||
return {
|
||||
open: Boolean(parsed.open),
|
||||
width:
|
||||
@@ -93,6 +96,83 @@ export default function App(): JSX.Element {
|
||||
const [sideOverlay, setSideOverlay] = useState(false)
|
||||
const { open: sideOpen, tabs: sideTabIds, active: sideActive, width: sideWidth } = side
|
||||
|
||||
// ---- 插件系统:视图切换('chat' | 插件 id)+ 深链参数 ----
|
||||
const plugins = usePluginList()
|
||||
const pluginsUi = usePluginsUiState()
|
||||
const [activeView, setActiveView] = useState<{ view: string; params?: unknown }>({ view: 'chat' })
|
||||
const activeViewRef = useRef(activeView)
|
||||
activeViewRef.current = activeView
|
||||
const pluginById = useMemo(() => new Map(plugins.map((p) => [p.manifest.id, p])), [plugins])
|
||||
const pluginByIdRef = useRef(pluginById)
|
||||
pluginByIdRef.current = pluginById
|
||||
const entryPlugins = useMemo(() => sidebarPlugins(plugins), [plugins])
|
||||
const panelPlugins = useMemo(
|
||||
() => plugins.filter((p) => p.enabled && p.manifest.panel && p.hasPage),
|
||||
[plugins]
|
||||
)
|
||||
/** 热重载令牌:变化即让插件页 iframe 换 src 整页重载(手动重载/开发期 watch 触发) */
|
||||
const [pluginReloadToken, setPluginReloadToken] = useState(0)
|
||||
|
||||
/** 打开视图('chat' 或插件 id,携深链参数);插件声明 panel 时联动展开其面板标签 */
|
||||
const handleOpenView = useCallback((view: string, params?: unknown) => {
|
||||
if (view === 'chat') {
|
||||
setActiveView({ view: 'chat' })
|
||||
return
|
||||
}
|
||||
const plugin = pluginByIdRef.current.get(view)
|
||||
if (!plugin || !plugin.enabled || (!plugin.hasComponent && !plugin.hasPage)) return
|
||||
setActiveView({ view, params })
|
||||
if (plugin.manifest.panel) {
|
||||
setSide((s) => ({
|
||||
...s,
|
||||
open: true,
|
||||
tabs: s.tabs.includes(view) ? s.tabs : [...s.tabs, view],
|
||||
active: view
|
||||
}))
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 主进程侧 openView(插件 main 工厂 / 深链按钮)→ 渲染层切视图
|
||||
useEffect(() => window.api.onPluginsView((change) => handleOpenView(change.view, change.params)), [handleOpenView])
|
||||
|
||||
// 插件列表变化(扫描/重载/启停)→ 刷新外置插件页(iframe 换 src 整页重载)
|
||||
useEffect(
|
||||
() =>
|
||||
window.api.onPluginsListChanged(() => {
|
||||
setPluginReloadToken((t) => t + 1)
|
||||
}),
|
||||
[]
|
||||
)
|
||||
|
||||
// 插件列表变化时校正当前视图(插件被禁用/卸载后回落聊天)
|
||||
useEffect(() => {
|
||||
setActiveView((current) => {
|
||||
if (current.view === 'chat') return current
|
||||
const plugin = pluginById.get(current.view)
|
||||
return plugin && plugin.enabled ? current : { view: 'chat' }
|
||||
})
|
||||
}, [pluginById])
|
||||
|
||||
const handleNewSession = useCallback(() => {
|
||||
void newSession()
|
||||
setActiveView({ view: 'chat' })
|
||||
}, [newSession])
|
||||
|
||||
const handleOpenSession = useCallback(
|
||||
(path: string) => {
|
||||
void openSession(path)
|
||||
setActiveView({ view: 'chat' })
|
||||
},
|
||||
[openSession]
|
||||
)
|
||||
|
||||
const handleOpenPlugin = useCallback(
|
||||
(id: string) => {
|
||||
handleOpenView(id, undefined)
|
||||
},
|
||||
[handleOpenView]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
localStorage.setItem(SIDE_STORAGE_KEY, JSON.stringify(side))
|
||||
@@ -147,16 +227,18 @@ export default function App(): JSX.Element {
|
||||
return stopWatching
|
||||
}, [])
|
||||
|
||||
// 全局快捷键:Ctrl+N 新会话 / Ctrl+K 搜索 / Ctrl+, 设置 / Ctrl+= / Ctrl+- 界面缩放(Ctrl+0 复位)/ Esc 关闭弹窗
|
||||
// 全局快捷键:Ctrl+N 新会话(全局,切回聊天)/ Ctrl+K 搜索 / Ctrl+, 设置(后两者仅聊天视图)
|
||||
// Ctrl+= / Ctrl+- 界面缩放(Ctrl+0 复位)/ Esc 关闭弹窗(不切视图,回聊天走侧栏点击)
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent): void => {
|
||||
const inChat = activeViewRef.current.view === 'chat'
|
||||
if (e.ctrlKey && e.key.toLowerCase() === 'n') {
|
||||
e.preventDefault()
|
||||
newSession()
|
||||
} else if (e.ctrlKey && e.key.toLowerCase() === 'k') {
|
||||
handleNewSession()
|
||||
} else if (inChat && e.ctrlKey && e.key.toLowerCase() === 'k') {
|
||||
e.preventDefault()
|
||||
setSearchOpen(true)
|
||||
} else if (e.ctrlKey && e.key === ',') {
|
||||
} else if (inChat && e.ctrlKey && e.key === ',') {
|
||||
e.preventDefault()
|
||||
setSettingsOpen(true)
|
||||
} else if (e.ctrlKey && (e.key === '=' || e.key === '+')) {
|
||||
@@ -175,7 +257,7 @@ export default function App(): JSX.Element {
|
||||
}
|
||||
window.addEventListener('keydown', onKey)
|
||||
return () => window.removeEventListener('keydown', onKey)
|
||||
}, [newSession])
|
||||
}, [handleNewSession])
|
||||
|
||||
const handlePickSuggestion = (text: string): void => {
|
||||
prefillRef.current = text
|
||||
@@ -206,15 +288,32 @@ export default function App(): JSX.Element {
|
||||
activeMeta?.firstMessage?.trim() ||
|
||||
''
|
||||
|
||||
// 面板标签:注册表 → PanelTab(内容按 id 装配)
|
||||
const panelTabs = sideTabIds.map((id) => {
|
||||
const meta = PANEL_TAB_META[id]
|
||||
return {
|
||||
// 插件面板标签图标(svg 字符串 → 组件;identity 稳定防标签重挂载)
|
||||
const pluginTabIcons = useMemo(() => {
|
||||
const map = new Map<string, PanelTabIcon>()
|
||||
for (const p of panelPlugins) {
|
||||
const svg = p.manifest.icon
|
||||
const Icon = ({ size, className }: { size?: number; className?: string }): JSX.Element => (
|
||||
<PluginIcon svg={svg} size={size ?? 16} className={className} />
|
||||
)
|
||||
map.set(p.manifest.id, Icon)
|
||||
}
|
||||
return map
|
||||
}, [panelPlugins])
|
||||
|
||||
// 浏览器插件(内置):面板 tab 按其启用态门控(禁用后 tab 与可开列表同步消失)
|
||||
const browserEnabled = plugins.find((p) => p.manifest.id === 'browser')?.enabled ?? true
|
||||
|
||||
// 面板标签:browser(宿主内置组件)+ manifest 声明 panel 的外置插件(iframe 页)
|
||||
const panelTabs = sideTabIds.flatMap((id) => {
|
||||
if (id === 'browser') {
|
||||
if (!browserEnabled) return []
|
||||
return [
|
||||
{
|
||||
id,
|
||||
title: meta.title,
|
||||
icon: meta.icon,
|
||||
content:
|
||||
id === 'browser' ? (
|
||||
title: '浏览器',
|
||||
icon: LuGlobe as PanelTabIcon,
|
||||
content: (
|
||||
<BrowserPanel
|
||||
state={browser.state}
|
||||
suspended={settingsOpen || searchOpen || sideOverlay}
|
||||
@@ -230,18 +329,46 @@ export default function App(): JSX.Element {
|
||||
onDetectSources={browser.detectSources}
|
||||
onImportProfile={browser.importProfile}
|
||||
/>
|
||||
) : null
|
||||
)
|
||||
}
|
||||
]
|
||||
}
|
||||
const plugin = pluginById.get(id)
|
||||
if (!plugin || !plugin.enabled || !plugin.manifest.panel) return []
|
||||
return [
|
||||
{
|
||||
id,
|
||||
title: plugin.manifest.name,
|
||||
icon: pluginTabIcons.get(id) ?? (LuGlobe as PanelTabIcon),
|
||||
content: (
|
||||
<PluginPageFrame plugin={plugin} onOpenView={handleOpenView} reloadToken={pluginReloadToken} />
|
||||
)
|
||||
}
|
||||
]
|
||||
})
|
||||
const panelAvailable = Object.entries(PANEL_TAB_META).map(([id, m]) => ({ id, title: m.title, icon: m.icon }))
|
||||
const panelAvailable = [
|
||||
...(browserEnabled ? [{ id: 'browser', title: '浏览器', icon: LuGlobe as PanelTabIcon }] : []),
|
||||
...panelPlugins.map((p) => ({
|
||||
id: p.manifest.id,
|
||||
title: p.manifest.name,
|
||||
icon: pluginTabIcons.get(p.manifest.id) ?? (LuGlobe as PanelTabIcon)
|
||||
}))
|
||||
]
|
||||
|
||||
// 当前插件视图(非 chat 时渲染其 Main:内置组件 or 外置 iframe 页)
|
||||
const activePlugin: PluginInfo | undefined =
|
||||
activeView.view === 'chat' ? undefined : pluginById.get(activeView.view)
|
||||
const ActivePluginComponent: ComponentType | undefined = activePlugin?.hasComponent
|
||||
? BUILTIN_PLUGIN_COMPONENTS[activeView.view]
|
||||
: undefined
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full bg-page text-ink antialiased">
|
||||
<SidebarNav
|
||||
sessionState={state.sessionState}
|
||||
sessions={sessions}
|
||||
onNewSession={newSession}
|
||||
onOpenSession={openSession}
|
||||
onNewSession={handleNewSession}
|
||||
onOpenSession={handleOpenSession}
|
||||
onRenameSession={renameSession}
|
||||
onDeleteSession={deleteSession}
|
||||
onOpenSearch={() => setSearchOpen(true)}
|
||||
@@ -249,12 +376,30 @@ export default function App(): JSX.Element {
|
||||
workspaces={workspaces}
|
||||
activeWorkspace={workspaces.find((w) => w.path === state.sessionState?.workspace)}
|
||||
onOpenWorkspace={openWorkspace}
|
||||
plugins={entryPlugins}
|
||||
activeView={activeView.view}
|
||||
onOpenPlugin={handleOpenPlugin}
|
||||
chatBadge={{
|
||||
approvals: state.approvals.length,
|
||||
error: Boolean(state.error || state.sessionError || state.retry),
|
||||
streaming: state.streaming
|
||||
}}
|
||||
/>
|
||||
<main className="relative flex min-w-0 flex-1 flex-col">
|
||||
<TitleBar title={sessionTitle} panelOpen={sideOpen} onTogglePanel={toggleSide} />
|
||||
<TitleBar
|
||||
title={activeView.view === 'chat' ? sessionTitle : activePlugin?.manifest.name}
|
||||
panelOpen={sideOpen}
|
||||
onTogglePanel={toggleSide}
|
||||
extra={<PluginStatusCapsules entries={pluginsUi.status} onOpenView={handleOpenView} />}
|
||||
/>
|
||||
<div className="flex min-h-0 flex-1">
|
||||
<div className="relative flex min-w-0 flex-1 flex-col">
|
||||
{/* 聊天列:切到插件视图时 display:none 保活(草稿/流式/滚动全保留) */}
|
||||
<div
|
||||
className="relative flex min-w-0 flex-1 flex-col"
|
||||
style={{ display: activeView.view === 'chat' ? undefined : 'none' }}
|
||||
>
|
||||
<ChatView
|
||||
visible={activeView.view === 'chat'}
|
||||
messages={state.messages}
|
||||
tools={state.tools}
|
||||
streaming={state.streaming}
|
||||
@@ -296,7 +441,7 @@ export default function App(): JSX.Element {
|
||||
autoCompactionEnabled={state.sessionState?.autoCompactionEnabled ?? true}
|
||||
onSend={send}
|
||||
onStop={stop}
|
||||
onNewSession={newSession}
|
||||
onNewSession={handleNewSession}
|
||||
onSetModel={setModel}
|
||||
onOpenSettings={() => setSettingsOpen(true)}
|
||||
onPermissionMode={setPermissionMode}
|
||||
@@ -311,6 +456,32 @@ export default function App(): JSX.Element {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 插件整页视图(同一时刻一个;错误边界兜底,崩溃不拖垮宿主) */}
|
||||
{activePlugin && (
|
||||
<div className="relative flex min-w-0 flex-1 flex-col">
|
||||
<PluginErrorBoundary
|
||||
pluginName={activePlugin.manifest.name}
|
||||
onBack={() => setActiveView({ view: 'chat' })}
|
||||
>
|
||||
{ActivePluginComponent ? (
|
||||
<ActivePluginComponent />
|
||||
) : activePlugin.hasPage ? (
|
||||
<PluginPageFrame
|
||||
plugin={activePlugin}
|
||||
params={activeView.params}
|
||||
reloadToken={pluginReloadToken}
|
||||
onOpenView={handleOpenView}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-[12.5px] text-ink-3">
|
||||
插件未提供页面
|
||||
</div>
|
||||
)}
|
||||
</PluginErrorBoundary>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sideOpen && (
|
||||
<SidePanel
|
||||
tabs={panelTabs}
|
||||
@@ -331,10 +502,10 @@ export default function App(): JSX.Element {
|
||||
open={searchOpen}
|
||||
onClose={() => setSearchOpen(false)}
|
||||
sessions={sessions}
|
||||
onNewSession={newSession}
|
||||
onNewSession={handleNewSession}
|
||||
onRefreshSessions={refreshSessions}
|
||||
onOpenSettings={() => setSettingsOpen(true)}
|
||||
onOpenSession={openSession}
|
||||
onOpenSession={handleOpenSession}
|
||||
/>
|
||||
<SettingsModal
|
||||
open={settingsOpen}
|
||||
|
||||
@@ -15,7 +15,8 @@ import {
|
||||
import { LuPanelLeft } from 'react-icons/lu'
|
||||
import { Button } from '@/components/atoms/Button'
|
||||
import { GlideMenu } from '@/components/GlideMenu'
|
||||
import type { SessionMeta, SessionState, WorkspaceMeta } from '@shared/types'
|
||||
import { PluginIcon } from '@/plugins/PluginPageFrame'
|
||||
import type { PluginInfo, SessionMeta, SessionState, WorkspaceMeta } from '@shared/types'
|
||||
|
||||
/** 可折叠工作区导航(beautifului.dev Sidebar Nav)。
|
||||
* 224px ↔ 56px 宽度过渡,文案随折叠淡出;导航行与会话行共用一个
|
||||
@@ -171,7 +172,11 @@ export function SidebarNav({
|
||||
onOpenSettings,
|
||||
workspaces,
|
||||
activeWorkspace,
|
||||
onOpenWorkspace
|
||||
onOpenWorkspace,
|
||||
plugins,
|
||||
activeView,
|
||||
onOpenPlugin,
|
||||
chatBadge
|
||||
}: {
|
||||
sessionState: SessionState | null
|
||||
sessions: SessionMeta[]
|
||||
@@ -184,6 +189,13 @@ export function SidebarNav({
|
||||
workspaces: WorkspaceMeta[]
|
||||
activeWorkspace?: WorkspaceMeta
|
||||
onOpenWorkspace: (path: string) => void
|
||||
/** 侧栏插件入口(已排序:order 升序,核心入口之后) */
|
||||
plugins: PluginInfo[]
|
||||
/** 当前视图('chat' 或插件 id;插件入口高亮用) */
|
||||
activeView: string
|
||||
onOpenPlugin: (id: string) => void
|
||||
/** 代理活动徽标(插件视图期间聊天列隐藏,会话区头提示 待审批>出错>流式) */
|
||||
chatBadge?: { approvals: number; error: boolean; streaming: boolean }
|
||||
}): ReactNode {
|
||||
const [collapsed, setCollapsed] = useState(() => {
|
||||
try {
|
||||
@@ -407,7 +419,7 @@ export function SidebarNav({
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 主导航(glide hover) */}
|
||||
{/* 主导航(glide hover):核心入口 + 插件入口 */}
|
||||
<GlideMenu className="gap-px" highlightClassName="sidebar-glide-highlight rounded-[7px] bg-hover-2" rowSelector="[data-row]">
|
||||
<button type="button" data-row onClick={onNewSession} className={navRowClass}>
|
||||
<span className="flex size-5 shrink-0 items-center justify-center text-ink-2">
|
||||
@@ -421,6 +433,26 @@ export function SidebarNav({
|
||||
</span>
|
||||
<span className="sidebar-copy ml-1.5 min-w-0 flex-1 truncate text-[13px] font-medium text-ink-3">搜索</span>
|
||||
</button>
|
||||
{plugins.map((plugin) => {
|
||||
const active = activeView === plugin.manifest.id
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={plugin.manifest.id}
|
||||
data-row
|
||||
title={plugin.manifest.description || plugin.manifest.name}
|
||||
onClick={() => onOpenPlugin(plugin.manifest.id)}
|
||||
className={`${navRowClass} ${active ? 'bg-hover-2 text-ink' : ''}`}
|
||||
>
|
||||
<span className="flex size-5 shrink-0 items-center justify-center">
|
||||
<PluginIcon svg={plugin.manifest.icon} size={18} />
|
||||
</span>
|
||||
<span className="sidebar-copy ml-1.5 min-w-0 flex-1 truncate text-[13px] font-medium text-ink-3">
|
||||
{plugin.manifest.name}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</GlideMenu>
|
||||
|
||||
{/* 会话列表区 */}
|
||||
@@ -439,6 +471,25 @@ export function SidebarNav({
|
||||
className={`shrink-0 transition-transform duration-150 ${chatsOpen ? '' : '-rotate-90'}`}
|
||||
/>
|
||||
<span>会话</span>
|
||||
{/* 插件视图期间聊天列隐藏:会话区头徽标提示代理活动(待审批>出错>流式) */}
|
||||
{chatBadge && activeView !== 'chat' && (chatBadge.approvals > 0 || chatBadge.error || chatBadge.streaming) && (
|
||||
<span
|
||||
title={
|
||||
chatBadge.approvals > 0
|
||||
? `${chatBadge.approvals} 个待审批`
|
||||
: chatBadge.error
|
||||
? 'Agent 出错/重试'
|
||||
: 'Agent 回复中'
|
||||
}
|
||||
className={`ml-0.5 inline-block size-[7px] shrink-0 rounded-full ${
|
||||
chatBadge.approvals > 0
|
||||
? 'animate-pulse bg-orange'
|
||||
: chatBadge.error
|
||||
? 'bg-red'
|
||||
: 'bg-accent'
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* 内联搜索:放大镜 ↔ 输入框宽度过渡 */}
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
import { useEffect, useState, type JSX } from 'react'
|
||||
import { useEffect, useState, type JSX, type ReactNode } from 'react'
|
||||
import { LuPanelRight } from 'react-icons/lu'
|
||||
|
||||
/**
|
||||
* 自绘标题栏(配合主进程 titleBarStyle:'hidden')。
|
||||
* 整条是拖拽区;左上角展示当前会话名称(无名称时显示「新会话」);
|
||||
* 右侧自绘窗口控制按钮(最小化/最大化还原/关闭),高度 40px。
|
||||
* 不使用系统 WCO overlay —— 其原生背景会盖住标题栏分隔线。
|
||||
* 整条是拖拽区;左上角展示当前会话/插件名称(无名称时显示「新会话」);
|
||||
* title 之后可挂 extra 槽(插件状态胶囊);右侧自绘窗口控制按钮(最小化/
|
||||
* 最大化还原/关闭),高度 40px。不使用系统 WCO overlay —— 其原生背景会盖住标题栏分隔线。
|
||||
*/
|
||||
export function TitleBar({
|
||||
title,
|
||||
panelOpen,
|
||||
onTogglePanel
|
||||
onTogglePanel,
|
||||
extra
|
||||
}: {
|
||||
title?: string
|
||||
panelOpen: boolean
|
||||
onTogglePanel: () => void
|
||||
/** 标题右侧插槽(插件状态胶囊等;聊天/插件视图下均可见) */
|
||||
extra?: ReactNode
|
||||
}): JSX.Element {
|
||||
const name = title?.trim()
|
||||
const [maximized, setMaximized] = useState(false)
|
||||
@@ -24,6 +27,7 @@ export function TitleBar({
|
||||
return (
|
||||
<div className="relative z-20 flex h-10 shrink-0 select-none items-center border-b border-line bg-surface pl-4 [-webkit-app-region:drag]">
|
||||
<div className="min-w-0 truncate text-[12.5px] font-medium text-ink">{name || '新会话'}</div>
|
||||
{extra}
|
||||
<div className="ml-auto flex h-full shrink-0 items-center gap-1 pr-1.5 [-webkit-app-region:no-drag]">
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -13,6 +13,9 @@ import type {
|
||||
ModelInfo,
|
||||
NavigateTreeOptions,
|
||||
PermissionMode,
|
||||
PluginInfo,
|
||||
PluginsUiState,
|
||||
PluginViewChange,
|
||||
ResourceDetails,
|
||||
SendImage,
|
||||
SessionMeta,
|
||||
@@ -111,6 +114,16 @@ declare global {
|
||||
browserDetectSources: () => Promise<BrowserSource[]>
|
||||
browserImportProfile: (sourceId: string, profileDir: string) => Promise<BrowserImportResult>
|
||||
onBrowserEvent: (callback: (event: BrowserUiEvent) => void) => () => void
|
||||
// ---- 插件系统 ----
|
||||
pluginsList: () => Promise<PluginInfo[]>
|
||||
pluginsUiState: () => Promise<PluginsUiState>
|
||||
pluginsSetEnabled: (id: string, enabled: boolean) => Promise<void>
|
||||
pluginsReload: () => Promise<void>
|
||||
pluginsOpenView: (view: string, params?: unknown) => Promise<void>
|
||||
pluginsSetStatus: (pluginId: string, itemId: string, item: unknown) => Promise<void>
|
||||
onPluginsListChanged: (callback: () => void) => () => void
|
||||
onPluginsUiState: (callback: (state: PluginsUiState) => void) => () => void
|
||||
onPluginsView: (callback: (change: PluginViewChange) => void) => () => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@ export const THEME_MODES: Array<{ id: ThemeMode; label: string }> = [
|
||||
{ id: 'system', label: '跟随系统' }
|
||||
]
|
||||
|
||||
/** 应用主题预设(undefined/未知 id = 恢复默认,移除内联覆盖) */
|
||||
/** 应用主题预设(undefined/未知 id = 恢复默认,移除内联覆盖)。
|
||||
* 变化时派发 aso-theme-changed(插件页桥据此把新 accent 下发给 iframe 页) */
|
||||
export function applyThemeColor(id: string | undefined): void {
|
||||
const preset = THEME_PRESETS.find((p) => p.id === id)
|
||||
const root = document.documentElement
|
||||
@@ -40,12 +41,13 @@ export function applyThemeColor(id: string | undefined): void {
|
||||
root.style.removeProperty('--accent')
|
||||
root.style.removeProperty('--accent-ink')
|
||||
root.style.removeProperty('--accent-tint')
|
||||
return
|
||||
}
|
||||
} else {
|
||||
root.style.setProperty('--accent', preset.accent)
|
||||
root.style.setProperty('--accent-ink', preset.ink)
|
||||
root.style.setProperty('--accent-tint', preset.tint)
|
||||
}
|
||||
window.dispatchEvent(new CustomEvent('aso-theme-changed'))
|
||||
}
|
||||
|
||||
/**
|
||||
* 深浅色模式应用:主进程把用户选择写入 Electron nativeTheme.themeSource,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Component, type ReactNode } from 'react'
|
||||
|
||||
/** 插件视图错误边界:单个插件崩溃显示占位 + 返回聊天,不拖垮整个应用 */
|
||||
export class PluginErrorBoundary extends Component<
|
||||
{ pluginName: string; children: ReactNode; onBack: () => void },
|
||||
{ error: Error | null }
|
||||
> {
|
||||
state: { error: Error | null } = { error: null }
|
||||
|
||||
static getDerivedStateFromError(error: Error): { error: Error } {
|
||||
return { error }
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error): void {
|
||||
console.error('[plugins] 视图渲染错误:', error)
|
||||
}
|
||||
|
||||
render(): ReactNode {
|
||||
if (this.state.error) {
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 px-8 text-center">
|
||||
<div className="text-[14px] font-medium text-red">插件「{this.props.pluginName}」渲染出错</div>
|
||||
<div className="max-w-md break-all text-[12px] leading-5 text-ink-3">{this.state.error.message}</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={this.props.onBack}
|
||||
className="rounded-control border border-line px-3 py-1.5 text-[12.5px] text-ink-2 transition-colors hover:bg-hover"
|
||||
>
|
||||
返回聊天
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* 外置插件页宿主(iframe + 桥宿主侧)。
|
||||
* - src 走 plugin://<id>/<page>?aso_t=<token>,token 变化即整页重载(热重载预览)
|
||||
* - 桥安全:消息来源必须同时满足 source === iframe.contentWindow 且
|
||||
* origin === 'plugin://<id>'(特权协议 standard+secure 下 iframe 有干净 origin)
|
||||
* - invoke 白名单:getState / send / getSettings(其余一律拒绝)
|
||||
* - 主题连续性:握手与主题变化时下发 { dark, accent }(accent 取自当前计算样式)
|
||||
*/
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { PluginInfo } from '@shared/types'
|
||||
import { HARNESS_BRIDGE_VERSION, type PluginToHostMessage } from '@aso/plugin-sdk/client'
|
||||
|
||||
/** 插件页可调用的宿主 API 白名单(window.api 的受控子集) */
|
||||
const INVOKE_ALLOWLIST = {
|
||||
getState: () => window.api.getState(),
|
||||
send: (text: string, images?: { mimeType: string; data: string }[]) => window.api.send(text, images),
|
||||
getSettings: () => window.api.getSettings()
|
||||
} as const
|
||||
|
||||
function currentTheme(): { dark: boolean; accent?: string } {
|
||||
const root = document.documentElement
|
||||
let accent = getComputedStyle(root).getPropertyValue('--accent').trim()
|
||||
if (accent) accent = accent.trim()
|
||||
return { dark: root.classList.contains('dark'), accent: accent || undefined }
|
||||
}
|
||||
|
||||
/** 去除 svg 字符串里的事件属性与 script(插件签名前的最小防线) */
|
||||
function sanitizeSvg(svg: string): string {
|
||||
return svg
|
||||
.replace(/<script[\s\S]*?<\/script>/gi, '')
|
||||
.replace(/\son\w+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, '')
|
||||
}
|
||||
|
||||
export function PluginIcon({
|
||||
svg,
|
||||
size = 18,
|
||||
className
|
||||
}: {
|
||||
svg?: string
|
||||
size?: number
|
||||
className?: string
|
||||
}): ReactNode {
|
||||
if (!svg) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
className={`block rounded-[5px] border border-line-strong ${className ?? ''}`}
|
||||
style={{ width: size - 6, height: size - 6 }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
className={`flex shrink-0 items-center justify-center text-ink-2 [&_svg]:h-[1em] [&_svg]:w-[1em] ${className ?? ''}`}
|
||||
style={{ fontSize: size, lineHeight: 1 }}
|
||||
dangerouslySetInnerHTML={{ __html: sanitizeSvg(svg) }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function PluginPageFrame({
|
||||
plugin,
|
||||
params,
|
||||
reloadToken = 0,
|
||||
onOpenView
|
||||
}: {
|
||||
plugin: PluginInfo
|
||||
/** 深链参数:握手后下发;存活期间变化即时下发 harness:deep-link */
|
||||
params?: unknown
|
||||
reloadToken?: number
|
||||
/** 插件页发起的 openView(含 'chat' / 其他插件 id),由 App 切换视图 */
|
||||
onOpenView: (view: string, params?: unknown) => void
|
||||
}): ReactNode {
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null)
|
||||
const [loaded, setLoaded] = useState(false)
|
||||
const handshake = useRef(false)
|
||||
const lastDeliveredParams = useRef<unknown>(undefined)
|
||||
const paramsRef = useRef<unknown>(undefined)
|
||||
paramsRef.current = params
|
||||
|
||||
const page = plugin.manifest.entry?.page || 'index.html'
|
||||
const src = useMemo(
|
||||
() => `plugin://${plugin.manifest.id}/${page}?aso_t=${reloadToken}`,
|
||||
[plugin.manifest.id, page, reloadToken]
|
||||
)
|
||||
|
||||
const post = (msg: unknown): void => {
|
||||
iframeRef.current?.contentWindow?.postMessage(msg, `plugin://${plugin.manifest.id}`)
|
||||
}
|
||||
|
||||
// 深链参数:握手后首次下发 + 存活期间变化即时下发
|
||||
useEffect(() => {
|
||||
if (!handshake.current) return
|
||||
if (lastDeliveredParams.current === params) return
|
||||
lastDeliveredParams.current = params
|
||||
post({ v: HARNESS_BRIDGE_VERSION, type: 'harness:deep-link', params })
|
||||
}, [params, plugin.manifest.id])
|
||||
|
||||
// 桥:接收插件页消息(source + origin 双重校验)
|
||||
useEffect(() => {
|
||||
const expectedOrigin = `plugin://${plugin.manifest.id}`
|
||||
const onMessage = (event: MessageEvent): void => {
|
||||
if (event.source !== iframeRef.current?.contentWindow) return
|
||||
if (event.origin !== expectedOrigin) return
|
||||
const msg = event.data as PluginToHostMessage | undefined
|
||||
if (!msg || typeof msg !== 'object' || typeof msg.type !== 'string' || msg.v !== HARNESS_BRIDGE_VERSION) return
|
||||
switch (msg.type) {
|
||||
case 'plugin:hello': {
|
||||
handshake.current = true
|
||||
post({ v: HARNESS_BRIDGE_VERSION, type: 'harness:hello', contract: '0.1.0' })
|
||||
post({ v: HARNESS_BRIDGE_VERSION, type: 'harness:theme', ...currentTheme() })
|
||||
if (paramsRef.current !== undefined) {
|
||||
lastDeliveredParams.current = paramsRef.current
|
||||
post({ v: HARNESS_BRIDGE_VERSION, type: 'harness:deep-link', params: paramsRef.current })
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'plugin:open-view':
|
||||
onOpenView(msg.view, msg.params)
|
||||
break
|
||||
case 'plugin:set-status':
|
||||
void window.api.pluginsSetStatus(plugin.manifest.id, msg.id, msg.item)
|
||||
break
|
||||
case 'plugin:invoke': {
|
||||
const fn = (INVOKE_ALLOWLIST as Record<string, (...args: unknown[]) => unknown>)[msg.method]
|
||||
if (!fn) {
|
||||
post({ v: HARNESS_BRIDGE_VERSION, type: 'harness:invoke-result', seq: msg.seq, ok: false, error: `方法不在白名单: ${msg.method}` })
|
||||
break
|
||||
}
|
||||
Promise.resolve()
|
||||
.then(() => fn(...msg.args))
|
||||
.then((value) =>
|
||||
post({ v: HARNESS_BRIDGE_VERSION, type: 'harness:invoke-result', seq: msg.seq, ok: true, value: value ?? null })
|
||||
)
|
||||
.catch((err: unknown) =>
|
||||
post({
|
||||
v: HARNESS_BRIDGE_VERSION,
|
||||
type: 'harness:invoke-result',
|
||||
seq: msg.seq,
|
||||
ok: false,
|
||||
error: err instanceof Error ? err.message : String(err)
|
||||
})
|
||||
)
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
window.addEventListener('message', onMessage)
|
||||
return () => window.removeEventListener('message', onMessage)
|
||||
}, [plugin.manifest.id, onOpenView])
|
||||
|
||||
// 主题变化(深浅色 / accent)下发
|
||||
useEffect(() => {
|
||||
const media = window.matchMedia('(prefers-color-scheme: dark)')
|
||||
const push = (): void => {
|
||||
if (handshake.current) post({ v: HARNESS_BRIDGE_VERSION, type: 'harness:theme', ...currentTheme() })
|
||||
}
|
||||
media.addEventListener('change', push)
|
||||
window.addEventListener('aso-theme-changed', push)
|
||||
return () => {
|
||||
media.removeEventListener('change', push)
|
||||
window.removeEventListener('aso-theme-changed', push)
|
||||
}
|
||||
}, [plugin.manifest.id])
|
||||
|
||||
// 换 src(热重载)时重置握手/加载态
|
||||
useEffect(() => {
|
||||
handshake.current = false
|
||||
lastDeliveredParams.current = undefined
|
||||
setLoaded(false)
|
||||
}, [src])
|
||||
|
||||
return (
|
||||
<div className="relative min-h-0 w-full flex-1">
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
title={plugin.manifest.name}
|
||||
src={src}
|
||||
onLoad={() => setLoaded(true)}
|
||||
// 无 allow-top-navigation(插件页不能劫持宿主导航);allow-same-origin
|
||||
// 保留 plugin://<id> 干净 origin 供桥校验
|
||||
sandbox="allow-scripts allow-forms allow-popups allow-same-origin"
|
||||
className="h-full w-full border-0 bg-page"
|
||||
/>
|
||||
{!loaded && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center text-[12px] text-ink-3">
|
||||
加载插件页…
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 插件状态胶囊(TitleBar B2 槽位):数据驱动渲染主进程广播的状态条目。
|
||||
* 点击条目 = 执行其深链(openView)。聊天视图与插件视图下均可见。
|
||||
*/
|
||||
import type { ReactNode } from 'react'
|
||||
import type { PluginStatusEntry } from '@shared/types'
|
||||
|
||||
const TONE_CLASS: Record<string, string> = {
|
||||
default: 'border-line text-ink-2',
|
||||
accent: 'border-accent/40 text-accent-ink bg-accent-tint',
|
||||
warn: 'border-orange/40 text-orange bg-orange-tint',
|
||||
danger: 'border-red/40 text-red bg-red-tint'
|
||||
}
|
||||
|
||||
export function PluginStatusCapsules({
|
||||
entries,
|
||||
onOpenView
|
||||
}: {
|
||||
entries: PluginStatusEntry[]
|
||||
onOpenView: (view: string, params?: unknown) => void
|
||||
}): ReactNode {
|
||||
if (entries.length === 0) return null
|
||||
return (
|
||||
<div className="ml-3 flex min-w-0 items-center gap-1.5 [-webkit-app-region:no-drag]">
|
||||
{entries.slice(0, 4).map(({ pluginId, itemId, item }) => {
|
||||
const tone = item.tone ?? 'default'
|
||||
const clickable = Boolean(item.openView)
|
||||
return (
|
||||
<button
|
||||
key={`${pluginId}:${itemId}`}
|
||||
type="button"
|
||||
title={item.text}
|
||||
onClick={() => {
|
||||
if (item.openView) onOpenView(item.openView.view, item.openView.params)
|
||||
}}
|
||||
className={`max-w-44 shrink-0 truncate rounded-full border px-2.5 py-0.5 text-[11px] leading-[18px] transition-transform ${
|
||||
TONE_CLASS[tone] ?? TONE_CLASS.default
|
||||
} ${clickable ? 'hover:opacity-80 active:scale-[0.97]' : 'cursor-default'}`}
|
||||
>
|
||||
{item.text}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
{entries.length > 4 && (
|
||||
<span className="shrink-0 rounded-full border border-line px-2 py-0.5 text-[11px] leading-[18px] text-ink-3">
|
||||
+{entries.length - 4}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* 内置插件的宿主树组件注册表(componentKey → React 组件)。
|
||||
* 仅内置插件可用(外置插件 UI 只能走 iframe 页);浏览器面板(M3)在此登记。
|
||||
*/
|
||||
import type { ComponentType } from 'react'
|
||||
|
||||
export const BUILTIN_PLUGIN_COMPONENTS: Record<string, ComponentType> = {}
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 渲染层插件状态 hooks:插件列表 + 内联 UI 快照(单一事实源在主进程,事件驱动同步)。
|
||||
*/
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { PluginInfo, PluginsUiState } from '@shared/types'
|
||||
|
||||
const EMPTY_UI: PluginsUiState = { status: [], toolbar: [] }
|
||||
|
||||
export function usePluginList(): PluginInfo[] {
|
||||
const [plugins, setPlugins] = useState<PluginInfo[]>([])
|
||||
useEffect(() => {
|
||||
let alive = true
|
||||
const refresh = (): void => {
|
||||
window.api
|
||||
.pluginsList()
|
||||
.then((list) => {
|
||||
if (alive) setPlugins(list)
|
||||
})
|
||||
.catch(() => undefined)
|
||||
}
|
||||
refresh()
|
||||
const off = window.api.onPluginsListChanged(refresh)
|
||||
return () => {
|
||||
alive = false
|
||||
off()
|
||||
}
|
||||
}, [])
|
||||
return plugins
|
||||
}
|
||||
|
||||
export function usePluginsUiState(): PluginsUiState {
|
||||
const [state, setState] = useState<PluginsUiState>(EMPTY_UI)
|
||||
useEffect(() => {
|
||||
let alive = true
|
||||
const refresh = (): void => {
|
||||
window.api
|
||||
.pluginsUiState()
|
||||
.then((snapshot) => {
|
||||
if (alive) setState(snapshot)
|
||||
})
|
||||
.catch(() => undefined)
|
||||
}
|
||||
refresh()
|
||||
const off = window.api.onPluginsUiState(setState)
|
||||
return () => {
|
||||
alive = false
|
||||
off()
|
||||
}
|
||||
}, [])
|
||||
return state
|
||||
}
|
||||
|
||||
/** 仅返回已启用且有侧栏入口的插件(入口排序:order 升序,其次 id) */
|
||||
export function sidebarPlugins(plugins: PluginInfo[]): PluginInfo[] {
|
||||
return plugins
|
||||
.filter((p) => p.enabled && (p.hasComponent || p.hasPage))
|
||||
.sort(
|
||||
(a, b) =>
|
||||
(a.manifest.order ?? 100) - (b.manifest.order ?? 100) ||
|
||||
a.manifest.id.localeCompare(b.manifest.id)
|
||||
)
|
||||
}
|
||||
@@ -350,3 +350,48 @@ export type AgentUiEvent =
|
||||
/** 压缩后的最新消息列表(含 compactionSummary 通知行,替代整次 session_switched) */
|
||||
messages: unknown[]
|
||||
}
|
||||
|
||||
// ===== 插件系统(契约类型来自 @aso/plugin-sdk) =====
|
||||
|
||||
import type { PluginManifest, StatusItem, ToolbarButtonDef } from '@aso/plugin-sdk'
|
||||
|
||||
/** 插件条目(管理页/侧栏入口数据源) */
|
||||
export interface PluginInfo {
|
||||
manifest: PluginManifest
|
||||
source: 'builtin' | 'external'
|
||||
enabled: boolean
|
||||
/** 有主进程工厂(pi 工具/事件/内联 UI 注册能力) */
|
||||
hasMain: boolean
|
||||
/** 有 iframe 插件页 */
|
||||
hasPage: boolean
|
||||
/** 有宿主树组件(仅内置;外置遮蔽需 replacesNative 声明) */
|
||||
hasComponent: boolean
|
||||
/** 装载/激活错误(不影响其他插件) */
|
||||
error?: string
|
||||
}
|
||||
|
||||
/** 状态胶囊条目(主进程单一事实源广播) */
|
||||
export interface PluginStatusEntry {
|
||||
pluginId: string
|
||||
itemId: string
|
||||
item: StatusItem
|
||||
}
|
||||
|
||||
/** 工具栏按钮条目 */
|
||||
export interface PluginToolbarEntry {
|
||||
pluginId: string
|
||||
buttonId: string
|
||||
button: ToolbarButtonDef
|
||||
}
|
||||
|
||||
/** 内联 UI 快照(plugins:uiState 事件与查询共用) */
|
||||
export interface PluginsUiState {
|
||||
status: PluginStatusEntry[]
|
||||
toolbar: PluginToolbarEntry[]
|
||||
}
|
||||
|
||||
/** 深链指令(主进程 openView / 插件页桥 → 渲染层切换视图) */
|
||||
export interface PluginViewChange {
|
||||
view: string
|
||||
params?: unknown
|
||||
}
|
||||
|
||||
@@ -67,3 +67,15 @@
|
||||
- ✅ Electron 桌面端(本仓库 `apps/desktop`)
|
||||
- ⛔ NestJS 后端、Ant Design Pro 后台、PostgreSQL —— 未设计,不创建、不实现
|
||||
- 业务插件中依赖服务器 API 的部分,先留接口占位(不依赖后端即可运行的本地能力可先行)
|
||||
|
||||
## 5. D7 动态插件系统(两层能力模型)
|
||||
|
||||
harness 收敛为「壳 + 聊天 + 插件系统」,业务功能以插件交付(详见 docs/plugins.md):
|
||||
|
||||
- **两层能力**:内置插件(代码注册,宿主树组件 + 第一方工厂 + 全量宿主能力,浏览器面板即首个实例)/ 外置插件(`userData/plugins/`,iframe 隔离页 + 动态 import 的 main.mjs + 仅 SDK 桥接 API)。同 id 高 semver 胜、外置可遮蔽内置(组件级需 `replacesNative` 声明)→ **内置插件也能在线热更而不发安装包**。
|
||||
- **pi 原生契约**:插件工厂收到 `{ pi, ui, storage }`——`pi` 为真实 `ExtensionAPI`,工具/事件/命令/消息走 `pi.registerTool` 等原生路径(harness 挂一个聚合 extension 扇出);`ui` 为 Electron 版槽位(对位 pi TUI 的 `ctx.ui.*`):侧栏入口/整页视图/面板 tab/状态胶囊/深链,内联 UI 一律数据驱动。
|
||||
- **iframe 隔离的动机**:外置 UI 与宿主无 React 实例共享问题、插件崩溃不拖垮宿主、热重载 = 换 src;代价(跨桥交互、主题靠 tokens.css 连续)已接受。`plugin://`/`peer://` 特权协议 + CSP `frame-src plugin:` + 路径穿越防护 + origin/source 双重校验。
|
||||
- **SDK 即 npm 包**(`packages/plugin-sdk`,Obsidian 模式):类型 + `definePlugin` + 页面桥 client(postMessage 白名单 invoke)+ hooks;仓库根 pnpm workspace(apps/* + packages/*)。
|
||||
- **在线更新(M5)**:索引检查 → 下载 → ed25519 验签(公钥钉 harness)→ minContract 兼容检查 → 原子替换 → 热重载;自动/手动并存。
|
||||
- **pi 原生目录(M7)**:pi resource-loader 默认已加载 `~/.pi/agent/extensions`(事实),M7 补开关与可见性管控。
|
||||
|
||||
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
# 插件系统(harness 插件契约)
|
||||
|
||||
> 状态:v0.8 进行中(M0–M3 已落地;M4 更新管道 / M6 模板与外置示例 / M7 pi 原生目录管控见 roadmap)
|
||||
> 契约版本:`HARNESS_CONTRACT_VERSION = 0.1.0`(`src/main/plugins/plugin-host.ts`;manifest.minContract 与之比对,不满足拒载)
|
||||
|
||||
## 两层能力模型
|
||||
|
||||
| | 内置插件 | 外置插件 |
|
||||
|---|---|---|
|
||||
| 位置 | 代码注册(`src/main/plugins/builtin/index.ts`),UI 组件编译进宿主 bundle | `userData/plugins/<id>/`(manifest.json + 页面资产 + main.mjs) |
|
||||
| UI 形态 | 宿主树 React 组件(`renderer/src/plugins/builtin-components.ts`,key=插件 id) | iframe 页 `plugin://<id>/<entry.page>`(自带 React,物理隔离) |
|
||||
| main 侧 | 第一方工厂(不动态 import) | 动态 `import()`(cache-bust 重载) |
|
||||
| 宿主能力 | 全量(BrowserSlot/原生视图协调/window.api) | 仅 SDK 桥接 API(postMessage,白名单 invoke) |
|
||||
| 信任 | 随包分发,免签名 | 签名校验(M5 接入;当前未签名也可装载——TODO) |
|
||||
| 管理 | 同一份 manifest/生命周期/管理页(可禁用) | 同左 + zip 导入/在线更新(M4/M5) |
|
||||
|
||||
同 id 冲突:高 semver 胜、平级外置胜;**外置遮蔽"组件级"内置必须声明 `replacesNative: true`**(能力降级声明,防原生视图能力被页面级外置静默顶掉)。
|
||||
|
||||
## manifest.json
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "hotwords", // ^[a-z][a-z0-9-]*$
|
||||
"name": "热点词",
|
||||
"version": "1.2.0", // semver(x.y.z)
|
||||
"minContract": "0.1.0",
|
||||
"description": "…",
|
||||
"icon": "<svg…/>", // 侧栏/面板图标(svg 字符串;宿主清除事件属性与 script)
|
||||
"order": 10, // 侧栏排序(只在插件间;核心入口恒在前)
|
||||
"panel": true, // 在右侧 SidePanel 出现一个 tab
|
||||
"entry": { "page": "index.html", "main": "main.mjs" }, // 内置插件省略 entry
|
||||
"replacesNative": false,
|
||||
"updateURL": "https://…/index.json"
|
||||
}
|
||||
```
|
||||
|
||||
## 主进程插件契约(`@aso/plugin-sdk` 主入口)
|
||||
|
||||
```ts
|
||||
import { definePlugin } from '@aso/plugin-sdk'
|
||||
|
||||
export default definePlugin((api) => {
|
||||
// pi 原生扩展 API:工具/事件/命令/消息(语义与 pi CLI 扩展一致)
|
||||
api.pi.registerTool(myTool)
|
||||
api.pi.on('agent_end', () => { /* … */ })
|
||||
|
||||
// harness UI 槽位(数据驱动)
|
||||
api.ui.setStatusItem('task', { text: '任务 3/5', tone: 'accent', openView: { view: 'hotwords', params: { id: 1 } } })
|
||||
api.ui.registerToolbarButton({ id: 'gen', title: '生成标题', openView: { view: 'hotwords' } })
|
||||
api.ui.openView('hotwords', { tab: 2 }) // 深链打开插件页
|
||||
|
||||
// 插件私有 KV(userData/aso-plugin-storage.json,按 pluginId 隔离)
|
||||
api.storage.set('lastQuery', 'xxx')
|
||||
|
||||
return () => { /* dispose:宿主在 session_shutdown / 禁用 / 重载时调用 */ }
|
||||
})
|
||||
```
|
||||
|
||||
实现:harness 在 pi `extensionFactories` 挂**一个聚合 extension**,工厂内创建真实 `ExtensionAPI` 上下文后扇出给所有已启用插件——工具注册走 pi 原生路径(提示词注入、审批拦截、事件时序由 pi 管)。
|
||||
|
||||
## 插件页(iframe)契约(`@aso/plugin-sdk/client`)
|
||||
|
||||
```ts
|
||||
import { getHarnessClient, useHarnessTheme, useDeepLinkParams } from '@aso/plugin-sdk/client'
|
||||
|
||||
const harness = getHarnessClient('hotwords')
|
||||
harness.openView('chat') // 深链返回聊天/其他插件
|
||||
harness.setStatus('task', { text: '进行中' }) // 状态胶囊(写主进程单一事实源)
|
||||
harness.invoke('getState') // 白名单:getState / send / getSettings
|
||||
useDeepLinkParams('hotwords') // openView 携带的参数(含挂起重放)
|
||||
useHarnessTheme('hotwords') // { dark, accent }(深浅色/accent 变化推送)
|
||||
```
|
||||
|
||||
- 通信:postMessage(页 → `window.parent`;宿主 → iframe.contentWindow)
|
||||
- 安全:宿主校验 `event.source === iframe.contentWindow` **且** `event.origin === 'plugin://<id>'`;sandbox 无 `allow-top-navigation`;iframe 页**无** window.api 直达
|
||||
- 主题连续性:插件页 `<link rel="stylesheet" href="peer://assets/tokens.css">`(宿主 beautifului.css 的 token 表,深浅色由 `.dark` 类 + 下发的 accent 生效);建议插件自带 Tailwind 按同 token 编译
|
||||
|
||||
## UI 槽位总览(对位 pi 的 ctx.ui)
|
||||
|
||||
| 槽位 | 机制 | 状态 |
|
||||
|---|---|---|
|
||||
| 侧栏入口(B1) | manifest icon/name/order;点进插件整页视图 | ✅ |
|
||||
| 插件整页 Main | 内置组件级 / 外置 iframe;聊天列 display:none 保活 | ✅ |
|
||||
| 面板 tab(B6) | `panel: true`;SidePanel 零改动承载(内置组件或 iframe) | ✅ |
|
||||
| 状态胶囊(B2) | `setStatusItem` 数据驱动,TitleBar 渲染,两种视图均可见 | ✅ |
|
||||
| 深链 | `openView(view, params)`(main 侧 IPC 广播 / 页侧桥直通) | ✅ |
|
||||
| 工具栏按钮(B5) | `registerToolbarButton` 数据已通,Composer 工具行渲染 | ⏳ 下版本 |
|
||||
| 聊天流富组件(B4) | ```类型``` 围栏块 → 宿主元组件(data-table/stat-grid/step-list/kv-card) | ⏳ 下版本 |
|
||||
| 聊天区浮动胶囊(B3) | 与 B2 同源数据,折叠/展开双态 | ⏳ 下版本 |
|
||||
|
||||
## 交互基线
|
||||
|
||||
- 整页替换:点插件入口 → 主列(聊天)隐藏保活;TitleBar 常驻(显示插件名 + 窗口控制 + 胶囊)
|
||||
- 返回聊天:点会话/新会话/Ctrl+N(全局);Esc 只关弹窗、**不**切视图
|
||||
- Ctrl+K / Ctrl+, 仅聊天视图;切回聊天自动重新吸底(ChatView `visible`)
|
||||
- 侧栏"会话"区头徽标:插件视图期间提示代理活动(待审批 > 出错 > 流式中)
|
||||
- 重载:主进程 `plugins:reload`(重扫 manifest + 重导入 main.mjs + 清内联 UI + 重建 runtime);渲染层收到列表变化后 iframe 换 src 整页重载(= pi `/reload` 等效)
|
||||
|
||||
## 安全清单
|
||||
|
||||
- `plugin://` 特权协议:路径解析后必须落在该插件目录内(防穿越);`peer://` 仅暴露白名单资产(tokens.css)
|
||||
- CSP:宿主页只加 `frame-src plugin:`(iframe 方案不动 script-src)
|
||||
- 外置 main.mjs 全权运行于主进程(与 pi 扩展同信任级)——**签名校验前,仅自行放置的插件目录可装载**(M5 后强制)
|
||||
|
||||
## pi 原生目录(M7,未实现)
|
||||
|
||||
事实:pi resource-loader **默认自动加载** `~/.pi/agent/extensions/*.ts` 与 `<cwd>/.pi/extensions/*.ts`(`noExtensions` 可关)——即本应用当前已在运行用户为 pi CLI 安装的扩展(逻辑部分;其 TUI UI 调用按 pi 非 TUI 模式语义降级)。M7 计划:设置开关(默认关 → `noExtensions`)+ 管理页可见/单禁 + 参与统一重载。
|
||||
|
||||
## 开发工作流(外置插件,M6 模板落地后)
|
||||
|
||||
```
|
||||
packages/plugin-template → npm i(workspace 引 @aso/plugin-sdk)
|
||||
页面:vite build --watch → dist/ # 改即重编
|
||||
宿主:开发者模式开 fs.watch → 自动 plugins:reload → iframe 换 src
|
||||
```
|
||||
+22
-1
@@ -11,7 +11,28 @@
|
||||
| v0.3.0 | 模型与设置 | 模型选择、Provider/API key 管理(写入 ~/.pi auth)、系统提示词自定义、连通性测试 |
|
||||
| v0.4.0 | 工具与审批 | 工具调用卡片(参数/结果折叠展示)、tool_call 拦截 → Approval Card、defineTool 业务工具骨架(本地可跑的演示工具) |
|
||||
| v0.5.0 | 打磨 | 深浅主题、GSAP 动效细节、快捷键、错误态/空态、队列可视化、自动滚动策略 |
|
||||
| 未来 | | electron-builder 打包与自动更新、i18n(中/英)、sqlite 持久化与迁移、本地备份恢复、CDP 插件、热点词/生成/模板业务插件、手动模式编排 UI、RPC 子进程隔离模式 |
|
||||
| v0.8.0 | 动态插件系统(M0–M3) | 两层能力模型(内置组件级/外置 iframe)、pi 原生契约聚合 extension、@aso/plugin-sdk、plugin://peer:// 协议、视图切换+聊天保活+徽标、侧栏/面板/胶囊/深链槽位、浏览器面板插件化、插件重载 |
|
||||
| 未来 | | 插件管理与 zip 导入(M4)、ed25519 签名在线更新(M5)、外置插件模板与开发工作流(M6)、pi 原生扩展目录管控(M7)、B3/B4/B5 槽位、electron-builder 打包与自动更新、i18n(中/英)、sqlite 持久化与迁移、本地备份恢复、热点词/生成/模板业务插件、手动模式编排 UI、RPC 子进程隔离模式 |
|
||||
|
||||
---
|
||||
|
||||
## v0.8.0 — 动态插件系统(M0–M3)
|
||||
|
||||
**交付**:
|
||||
- 仓库根 pnpm workspace(apps/* + packages/*)+ `@aso/plugin-sdk`(类型 + definePlugin + 页面桥 client/hooks;源码形态被 workspace 消费,无构建步骤)
|
||||
- 主进程 `PluginHost`:内置(代码注册)+ 外置(`userData/plugins/`,manifest 校验/契约版本检查/main.mjs 动态 import)双目录合并(高 semver 胜、外置遮蔽组件级内置需 replacesNative);聚合 InlineExtension 扇出真实 pi `ExtensionAPI`;内联 UI(状态胶囊/工具栏按钮)主进程单一事实源 + 事件广播;插件 KV 存储;`plugin://`(路径穿越防护)/`peer://`(tokens.css)特权协议;CSP 加 `frame-src plugin:`;插件启停/重载联动 runtime 重建
|
||||
- 渲染层:插件列表/内联 UI hooks;`PluginPageFrame`(iframe + 桥宿主侧:origin+source 双校验、白名单 invoke、主题下发、深链参数、热重载换 src);`PluginErrorBoundary`;`PluginStatusCapsules`(TitleBar 槽,两视图可见);App 视图切换(activeView{view,params}、聊天列 display:none 保活、Ctrl+N 全局/Ctrl+K,Ctrl+, 仅聊天、插件禁用自动回落);SidebarNav 插件入口 + 会话区头代理活动徽标(待审批>出错>流式);SidePanel 零改动承载插件 tab
|
||||
- 浏览器面板 → 内置插件:browserTools 迁 `pi.registerTool`(customTools 摘除)、面板 tab 按启用态门控、状态胶囊示范("浏览器 N 页")
|
||||
- 验证:typecheck/build 通过(含 SDK 打包、tokens.css 构建期拷贝);dev 启动截图核验(聊天布局无回归、浏览器面板正常、无报错)
|
||||
- 关键事实澄清:pi resource-loader **默认自动加载** `~/.pi/agent/extensions`——M7(原生目录管控)从"建设加载"降为"开关+可见性"
|
||||
|
||||
**自省**(v0.8.0 → 指导 M4+):
|
||||
1. **外置插件尚未强制签名**——当前 `userData/plugins/` 直接装载(代码里已留 TODO);M5 验签落地前不对外分发任何外置插件。
|
||||
2. **B3/B4/B5 槽位数据已通、渲染未接**——状态胶囊已在 TitleBar 生效;Composer 工具行按钮、聊天流围栏块元组件(data-table 等)、聊天区浮动胶囊排 M4 同批或下版本。
|
||||
3. **切换回聊天未自动聚焦输入框**——保活后焦点丢失,需 Composer 暴露 focus 入口(data 属性 + App effect)。
|
||||
4. **插件管理 UI 仍占位**——SettingsModal"插件"tab 转正(列表/启停/重载/导入)排 M4。
|
||||
5. **TS7(typescript 7 native)增量缓存坑**——改 `declare global` 后 typecheck 可能读到旧程序,删 `out/tsconfig.{node,web}/` 下 tsbuildinfo 即恢复。
|
||||
6. **workspace 迁移副作用**——根 workspace 化后 electron postinstall 偶发不跑("Electron uninstall"),手动 `node node_modules/.pnpm/electron@*/node_modules/electron/install.js` 修复;新克隆/重装后注意。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"name": "@aso/harness",
|
||||
"private": true,
|
||||
"version": "0.7.0",
|
||||
"description": "Inkreach Agent Harness(壳 + 聊天 + 插件系统;业务功能以插件形式交付)",
|
||||
"scripts": {
|
||||
"typecheck": "pnpm -r --if-present typecheck",
|
||||
"build": "pnpm -r --if-present build",
|
||||
"dev": "pnpm --dir apps/desktop dev"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "@aso/plugin-sdk",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "Inkreach harness 插件契约:清单/主进程工厂类型 + definePlugin + 插件页桥接 client(主入口给 main.mjs 作者,./client 给 iframe 页作者)",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./client": "./src/page.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@earendil-works/pi-coding-agent": "0.85.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=19"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "19.2.18",
|
||||
"react": "19.2.8",
|
||||
"typescript": "7.0.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/**
|
||||
* 插件页(iframe)侧桥接 client。
|
||||
* 用法(插件页入口):`import { getHarnessClient } from '@aso/plugin-sdk/client'`
|
||||
* 通信:postMessage —— 页 → window.parent;宿主 → iframe.contentWindow。
|
||||
* 宿主严格校验消息来源 origin(plugin://<id>);invoke 仅暴露白名单方法。
|
||||
*/
|
||||
import type { OpenViewAction, StatusItem } from './status'
|
||||
|
||||
/** 桥协议版本(宿主与插件页握手比对;不匹配时宿主拒绝 invoke) */
|
||||
export const HARNESS_BRIDGE_VERSION = 1
|
||||
|
||||
/** 宿主 → 插件页 */
|
||||
export type HostToPluginMessage =
|
||||
| { v: number; type: 'harness:hello'; contract: string }
|
||||
| { v: number; type: 'harness:theme'; dark: boolean; accent?: string }
|
||||
| { v: number; type: 'harness:deep-link'; params?: unknown }
|
||||
| { v: number; type: 'harness:agent'; event: HarnessAgentEvent }
|
||||
| { v: number; type: 'harness:invoke-result'; seq: number; ok: true; value: unknown }
|
||||
| { v: number; type: 'harness:invoke-result'; seq: number; ok: false; error: string }
|
||||
|
||||
/** 插件页 → 宿主 */
|
||||
export type PluginToHostMessage =
|
||||
| { v: number; type: 'plugin:hello'; pluginId: string }
|
||||
| { v: number; type: 'plugin:open-view'; view: string; params?: unknown }
|
||||
| { v: number; type: 'plugin:set-status'; id: string; item: StatusItem | null }
|
||||
| { v: number; type: 'plugin:invoke'; seq: number; method: string; args: unknown[] }
|
||||
|
||||
/** 宿主中继的 agent 事件(宽松类型:具体事件形状由宿主版本决定,插件页防御性读取) */
|
||||
export interface HarnessAgentEvent {
|
||||
type: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/** invoke 白名单(宿主桥逐方法放行;后续按需扩充) */
|
||||
export interface HarnessInvokeMap {
|
||||
getState(): unknown
|
||||
send(text: string, images?: { mimeType: string; data: string }[]): void
|
||||
getSettings(): unknown
|
||||
}
|
||||
|
||||
export interface HarnessTheme {
|
||||
dark: boolean
|
||||
accent?: string
|
||||
}
|
||||
|
||||
export interface HarnessClient {
|
||||
/** 握手完成(收到 harness:hello)后 resolve */
|
||||
ready: Promise<void>
|
||||
/** 宿主契约版本(ready 后可用) */
|
||||
contractVersion(): string
|
||||
/** 深链打开视图 */
|
||||
openView(view: string, params?: unknown): void
|
||||
/** 写/清一条状态胶囊(item=null 移除) */
|
||||
setStatus(id: string, item: StatusItem | null): void
|
||||
/** 深链参数:页面被 openView 打开/更新时回调(含挂起期间的一次重放) */
|
||||
onDeepLink(cb: (params: unknown) => void): () => void
|
||||
/** 主题变化(深浅色/accent) */
|
||||
onTheme(cb: (theme: HarnessTheme) => void): () => void
|
||||
/** agent 事件中继(宿主挑选的子集:state/agent_start/agent_end/stats…) */
|
||||
onAgentEvent(cb: (event: HarnessAgentEvent) => void): () => void
|
||||
/** 调用宿主白名单 API */
|
||||
invoke<M extends keyof HarnessInvokeMap>(
|
||||
method: M,
|
||||
...args: Parameters<HarnessInvokeMap[M]>
|
||||
): Promise<Awaited<ReturnType<HarnessInvokeMap[M]>>>
|
||||
}
|
||||
|
||||
type DeepLinkCb = (params: unknown) => void
|
||||
type ThemeCb = (theme: HarnessTheme) => void
|
||||
type AgentCb = (event: HarnessAgentEvent) => void
|
||||
|
||||
let cached: HarnessClient | null = null
|
||||
|
||||
/** 获取桥接 client(单例;首个调用即开始握手) */
|
||||
export function getHarnessClient(pluginId: string): HarnessClient {
|
||||
if (cached) return cached
|
||||
|
||||
let contract = ''
|
||||
let resolveReady: (() => void) | null = null
|
||||
const ready = new Promise<void>((resolve) => {
|
||||
resolveReady = resolve
|
||||
})
|
||||
|
||||
const deepLinkCbs = new Set<DeepLinkCb>()
|
||||
const themeCbs = new Set<ThemeCb>()
|
||||
const agentCbs = new Set<AgentCb>()
|
||||
const pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }>()
|
||||
let seq = 0
|
||||
|
||||
window.addEventListener('message', (event: MessageEvent) => {
|
||||
const msg = event.data as HostToPluginMessage | undefined
|
||||
if (!msg || typeof msg !== 'object' || typeof msg.type !== 'string' || !msg.type.startsWith('harness:')) return
|
||||
switch (msg.type) {
|
||||
case 'harness:hello':
|
||||
contract = msg.contract
|
||||
resolveReady?.()
|
||||
resolveReady = null
|
||||
break
|
||||
case 'harness:theme':
|
||||
for (const cb of themeCbs) cb({ dark: msg.dark, accent: msg.accent })
|
||||
break
|
||||
case 'harness:deep-link':
|
||||
for (const cb of deepLinkCbs) cb(msg.params)
|
||||
break
|
||||
case 'harness:agent':
|
||||
for (const cb of agentCbs) cb(msg.event)
|
||||
break
|
||||
case 'harness:invoke-result': {
|
||||
const waiter = pending.get(msg.seq)
|
||||
if (!waiter) break
|
||||
pending.delete(msg.seq)
|
||||
if (msg.ok) waiter.resolve(msg.value)
|
||||
else waiter.reject(new Error(msg.error))
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
const post = (msg: PluginToHostMessage): void => {
|
||||
// 宿主 origin 随运行模式不同(file:// 或 dev server),用 '*' 发送;
|
||||
// 方向安全由宿主侧的 origin 白名单校验保证
|
||||
window.parent?.postMessage({ ...msg, v: HARNESS_BRIDGE_VERSION }, '*')
|
||||
}
|
||||
|
||||
const invokeRaw = (method: string, ...args: unknown[]): Promise<unknown> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const id = ++seq
|
||||
pending.set(id, { resolve, reject })
|
||||
post({ v: HARNESS_BRIDGE_VERSION, type: 'plugin:invoke', seq: id, method, args })
|
||||
})
|
||||
|
||||
post({ v: HARNESS_BRIDGE_VERSION, type: 'plugin:hello', pluginId })
|
||||
|
||||
cached = {
|
||||
ready,
|
||||
contractVersion: () => contract,
|
||||
openView: (view, params) => post({ v: HARNESS_BRIDGE_VERSION, type: 'plugin:open-view', view, params }),
|
||||
setStatus: (id, item) => post({ v: HARNESS_BRIDGE_VERSION, type: 'plugin:set-status', id, item }),
|
||||
onDeepLink(cb) {
|
||||
deepLinkCbs.add(cb)
|
||||
return () => void deepLinkCbs.delete(cb)
|
||||
},
|
||||
onTheme(cb) {
|
||||
themeCbs.add(cb)
|
||||
return () => void themeCbs.delete(cb)
|
||||
},
|
||||
onAgentEvent(cb) {
|
||||
agentCbs.add(cb)
|
||||
return () => void agentCbs.delete(cb)
|
||||
},
|
||||
invoke: invokeRaw as HarnessClient['invoke']
|
||||
}
|
||||
return cached
|
||||
}
|
||||
|
||||
export type { OpenViewAction, StatusItem }
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 插件页 React hooks(peer react,可选使用)。
|
||||
* import { useHarnessTheme, useDeepLinkParams } from '@aso/plugin-sdk/client'
|
||||
*/
|
||||
import { useEffect, useState } from 'react'
|
||||
import { getHarnessClient, type HarnessAgentEvent, type HarnessTheme } from './client'
|
||||
|
||||
/** 当前主题(握手完成前为 null) */
|
||||
export function useHarnessTheme(pluginId: string): HarnessTheme | null {
|
||||
const [theme, setTheme] = useState<HarnessTheme | null>(null)
|
||||
useEffect(() => {
|
||||
const client = getHarnessClient(pluginId)
|
||||
let latest: HarnessTheme | null = null
|
||||
const off = client.onTheme((t) => {
|
||||
latest = t
|
||||
setTheme(t)
|
||||
})
|
||||
void client.ready.then(() => setTheme(latest))
|
||||
return off
|
||||
}, [pluginId])
|
||||
return theme
|
||||
}
|
||||
|
||||
/** 最近一次深链参数(页面被 openView 打开/更新时变化;无深链为 undefined) */
|
||||
export function useDeepLinkParams(pluginId: string): unknown {
|
||||
const [params, setParams] = useState<unknown>(undefined)
|
||||
useEffect(() => {
|
||||
const client = getHarnessClient(pluginId)
|
||||
const off = client.onDeepLink(setParams)
|
||||
return off
|
||||
}, [pluginId])
|
||||
return params
|
||||
}
|
||||
|
||||
/** 订阅宿主中继的 agent 事件(handler 变化不重订阅) */
|
||||
export function useAgentEvent(pluginId: string, handler: (event: HarnessAgentEvent) => void): void {
|
||||
useEffect(() => {
|
||||
const client = getHarnessClient(pluginId)
|
||||
const ref = { handler }
|
||||
const off = client.onAgentEvent((event) => ref.handler(event))
|
||||
return () => {
|
||||
ref.handler = () => undefined
|
||||
off()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pluginId])
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* @aso/plugin-sdk 主入口(插件 main.mjs / 内置插件工厂使用)。
|
||||
* 插件页(iframe)请改用 '@aso/plugin-sdk/client' 子入口(不含 pi SDK 类型依赖路径)。
|
||||
*/
|
||||
export * from './manifest'
|
||||
export * from './status'
|
||||
export * from './plugin'
|
||||
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* 插件清单类型(插件根目录 manifest.json,内置/外置共用一份契约)。
|
||||
* 本模块不得引入 pi SDK 或 React 类型——它同时被主进程与插件页消费。
|
||||
*/
|
||||
|
||||
/** 插件 id 命名规则:小写字母开头,小写字母/数字/连字符 */
|
||||
export const PLUGIN_ID_PATTERN = /^[a-z][a-z0-9-]*$/
|
||||
|
||||
export interface PluginManifest {
|
||||
/** 唯一 id(内置/外置同 id 时高 semver 胜、平级外置胜) */
|
||||
id: string
|
||||
name: string
|
||||
/** semver;在线更新与遮蔽判定依据 */
|
||||
version: string
|
||||
/** 最低 harness 契约版本(semver),不满足拒载 */
|
||||
minContract: string
|
||||
description?: string
|
||||
/** 侧栏入口图标:svg 字符串;缺省用宿主默认字形 */
|
||||
icon?: string
|
||||
/** 侧栏入口排序(升序,只在插件之间排序;核心入口恒在前) */
|
||||
order?: number
|
||||
/** 在右侧 SidePanel 出现为一个 tab */
|
||||
panel?: boolean
|
||||
/** 外置插件入口(相对插件根的路径)。
|
||||
* 内置插件留空:UI 组件与 main 工厂编译进宿主 bundle。 */
|
||||
entry?: {
|
||||
/** 插件页入口(iframe 加载 `plugin://<id>/<page>`) */
|
||||
page?: string
|
||||
/** 主进程模块(动态 import,默认导出 definePlugin 工厂) */
|
||||
main?: string
|
||||
}
|
||||
/** 外置插件遮蔽"组件级"内置插件时必须为 true(能力降级声明,
|
||||
* 防止带原生视图能力的内置被页面级外置静默顶掉) */
|
||||
replacesNative?: boolean
|
||||
/** 更新索引地址(JSON;格式见 docs/plugins.md) */
|
||||
updateURL?: string
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
/** '@aso/plugin-sdk/client' 子入口:插件页(iframe)桥接 client + hooks */
|
||||
export * from './client'
|
||||
export * from './hooks'
|
||||
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* 主进程侧插件契约(main.mjs / 内置插件工厂共用)。
|
||||
* 生命周期对齐 pi 原生扩展:工厂函数收到 api,返回 dispose(可选)。
|
||||
*/
|
||||
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent'
|
||||
import type { PluginManifest } from './manifest'
|
||||
import type { StatusItem, ToolbarButtonDef } from './status'
|
||||
|
||||
/** 插件私有 KV 存储(主进程落盘 userData/aso-plugin-storage.json,按 pluginId 命名空间隔离) */
|
||||
export interface PluginStorage {
|
||||
get<T>(key: string): T | undefined
|
||||
set(key: string, value: unknown): void
|
||||
delete(key: string): void
|
||||
}
|
||||
|
||||
/** harness 侧 UI 能力(Electron 版槽位,语义对位 pi TUI 的 ctx.ui.*)。
|
||||
* 内联槽位全部数据驱动;插件页内 UI 由插件页自理,不经此 API。 */
|
||||
export interface HarnessUiApi {
|
||||
/** 打开视图('chat' 或插件 id),携可选深链参数 */
|
||||
openView(view: string, params?: unknown): void
|
||||
/** 写/更新一条状态胶囊(按 id 键控;插件 dispose 时宿主自动清理其全部条目) */
|
||||
setStatusItem(id: string, item: StatusItem): void
|
||||
removeStatusItem(id: string): void
|
||||
/** 注册输入区工具行按钮(按 id 键控;dispose 自动清理) */
|
||||
registerToolbarButton(button: ToolbarButtonDef): void
|
||||
removeToolbarButton(id: string): void
|
||||
}
|
||||
|
||||
/** 插件工厂收到的完整 API */
|
||||
export interface HarnessPluginApi {
|
||||
/** pi 原生扩展 API:工具/事件/命令/消息全走这里(pi.registerTool 等,语义与 pi CLI 扩展一致) */
|
||||
pi: ExtensionAPI
|
||||
ui: HarnessUiApi
|
||||
storage: PluginStorage
|
||||
manifest: PluginManifest
|
||||
}
|
||||
|
||||
/** 插件工厂:同步或异步返回 dispose */
|
||||
export type HarnessPluginFactory = (api: HarnessPluginApi) => void | (() => void) | Promise<void | (() => void)>
|
||||
|
||||
/** 身份函数:为插件作者提供完整类型推断 */
|
||||
export function definePlugin(factory: HarnessPluginFactory): HarnessPluginFactory {
|
||||
return factory
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* 内联槽位的数据类型(B2 状态胶囊 / B5 工具栏按钮)。
|
||||
* 契约原则:内联 UI 一律"数据 + 深链",不注册组件——外置插件只有
|
||||
* iframe 页有能力渲染自己的 React;宿主统一按数据渲染这些小 UI。
|
||||
*/
|
||||
|
||||
/** 深链:点击行为 = 打开某个视图('chat' 或插件 id)并携带参数 */
|
||||
export interface OpenViewAction {
|
||||
view: string
|
||||
params?: unknown
|
||||
}
|
||||
|
||||
/** 状态胶囊条目(TitleBar 胶囊区 / 聊天区浮动胶囊共用一份数据) */
|
||||
export interface StatusItem {
|
||||
text: string
|
||||
tone?: 'default' | 'accent' | 'warn' | 'danger'
|
||||
/** 点击行为:深链;缺省不可点击 */
|
||||
openView?: OpenViewAction
|
||||
}
|
||||
|
||||
/** 输入区工具行按钮 */
|
||||
export interface ToolbarButtonDef {
|
||||
id: string
|
||||
title: string
|
||||
/** svg 字符串(16-20px 线条风格,宿主按 currentColor 渲染) */
|
||||
icon?: string
|
||||
/** 点击行为:深链;缺省不可点击 */
|
||||
openView?: OpenViewAction
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
+98
-77
@@ -6,7 +6,9 @@ settings:
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
.: {}
|
||||
|
||||
apps/desktop:
|
||||
dependencies:
|
||||
'@earendil-works/pi-ai':
|
||||
specifier: 0.85.0
|
||||
@@ -69,6 +71,9 @@ importers:
|
||||
specifier: 3.6.0
|
||||
version: 3.6.0
|
||||
devDependencies:
|
||||
'@aso/plugin-sdk':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/plugin-sdk
|
||||
'@tailwindcss/vite':
|
||||
specifier: 4.3.3
|
||||
version: 4.3.3(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0))
|
||||
@@ -103,6 +108,22 @@ importers:
|
||||
specifier: 8.2.2
|
||||
version: 8.2.2(@types/node@26.4.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)
|
||||
|
||||
packages/plugin-sdk:
|
||||
dependencies:
|
||||
'@earendil-works/pi-coding-agent':
|
||||
specifier: 0.85.0
|
||||
version: 0.85.0(ws@8.21.3)
|
||||
devDependencies:
|
||||
'@types/react':
|
||||
specifier: 19.2.18
|
||||
version: 19.2.18
|
||||
react:
|
||||
specifier: 19.2.8
|
||||
version: 19.2.8
|
||||
typescript:
|
||||
specifier: 7.0.2
|
||||
version: 7.0.2
|
||||
|
||||
packages:
|
||||
|
||||
'@anthropic-ai/sdk@0.123.0':
|
||||
@@ -818,8 +839,8 @@ packages:
|
||||
resolution: {integrity: sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==}
|
||||
engines: {node: '>= 20.19.0'}
|
||||
|
||||
'@oxc-project/types@0.148.0':
|
||||
resolution: {integrity: sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==}
|
||||
'@oxc-project/types@0.149.0':
|
||||
resolution: {integrity: sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA==}
|
||||
|
||||
'@peculiar/asn1-schema@2.9.4':
|
||||
resolution: {integrity: sha512-GjzePcT9Iw8NzeOPf73iNS9xM+TBhd/FilAfP+RQGkTMQJTVWtytN3JHJACCjf/ABNau5S7mS3g+DcuxmRgYEg==}
|
||||
@@ -863,92 +884,92 @@ packages:
|
||||
'@protobufjs/utf8@1.1.2':
|
||||
resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==}
|
||||
|
||||
'@rolldown/binding-android-arm-eabi@1.2.7':
|
||||
resolution: {integrity: sha512-EypzgnYCwyVY4NDHKzGmNJT5b+XaQEBniHxsMdeIQLB/tcCzZnhqrzHpZFbX9iaxx+5RiB8caATBtfvZP7zVxQ==}
|
||||
'@rolldown/binding-android-arm-eabi@1.2.8':
|
||||
resolution: {integrity: sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@rolldown/binding-android-arm64@1.2.7':
|
||||
resolution: {integrity: sha512-l17HE9EweWaqJZhuUuNBN/FzM62xw+DECVnJyvMsxn8vJFAGLy5QfLDoYAcronkAN8VxKZHezDpulHDPx95vFw==}
|
||||
'@rolldown/binding-android-arm64@1.2.8':
|
||||
resolution: {integrity: sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@rolldown/binding-darwin-arm64@1.2.7':
|
||||
resolution: {integrity: sha512-8ED8ELFvHXc6OCETIn4gXObPiaR6bckM/ipXtbzlPVDRMBfEGjCKgO90F9YtfdpDatVx/ZQw7aZ1vUMf/+T3Mw==}
|
||||
'@rolldown/binding-darwin-arm64@1.2.8':
|
||||
resolution: {integrity: sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@rolldown/binding-darwin-x64@1.2.7':
|
||||
resolution: {integrity: sha512-/WPripjtiAIZ2tWY7ddijORT0Ujg87wxWW/qcoFVCKAWVDPhtY0xr7Dj0M3GyNGz60jGwTElhro/mkF9dT7dDQ==}
|
||||
'@rolldown/binding-darwin-x64@1.2.8':
|
||||
resolution: {integrity: sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@rolldown/binding-freebsd-x64@1.2.7':
|
||||
resolution: {integrity: sha512-14DI4NcqpvbICxSnGLx3PmtDaWqRP/KGSGb6C+JLLVPeZRl6dKdHba3pGsqT3vpdTqhEYIPG0MMQ8c0xYqoJxA==}
|
||||
'@rolldown/binding-freebsd-x64@1.2.8':
|
||||
resolution: {integrity: sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rolldown/binding-linux-arm-gnueabihf@1.2.7':
|
||||
resolution: {integrity: sha512-bxrWIRvHWQvbJwi+VIie/kDJmQxcNE6xxWwZdqF/ExVAigtHkv54WTLQPb+QsZdnFy18fg7JPfWGL0RH6vwIlQ==}
|
||||
'@rolldown/binding-linux-arm-gnueabihf@1.2.8':
|
||||
resolution: {integrity: sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-arm64-gnu@1.2.7':
|
||||
resolution: {integrity: sha512-toOY2BChBZyuxU7OYX6Tn389di4IzAqPTycVcci0O7FSfBqzRB3RZn+K5Is6ANf4tmgRd/K1yZTsNTXbkXsnLg==}
|
||||
'@rolldown/binding-linux-arm64-gnu@1.2.8':
|
||||
resolution: {integrity: sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.2.7':
|
||||
resolution: {integrity: sha512-lAIXTH/aiLRLxsTgQvfhjo4K1ydWIp00+V0voOr9beb/9ZmkUFrSIb03dXNFRgMNvkE6oGsF10ioQ6UsI+vS5Q==}
|
||||
'@rolldown/binding-linux-arm64-musl@1.2.8':
|
||||
resolution: {integrity: sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-ppc64-gnu@1.2.7':
|
||||
resolution: {integrity: sha512-kdnwS28Pkenp/mZMRwjXXXwxQ7pIsm+bF919LUK93BOyhcLsrVKdP2p9fxpiPNPAbNuch8ypQt0pm2P2LYCAGg==}
|
||||
'@rolldown/binding-linux-ppc64-gnu@1.2.8':
|
||||
resolution: {integrity: sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-s390x-gnu@1.2.7':
|
||||
resolution: {integrity: sha512-516OdsyLdr5E65paF3yBF55t8mfm9+gmtCsK3xI7XKXIT7EfRlHhxL8K/NR6Hu8BWSgF5+1w74lTL0+nxcc8Qw==}
|
||||
'@rolldown/binding-linux-s390x-gnu@1.2.8':
|
||||
resolution: {integrity: sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.2.7':
|
||||
resolution: {integrity: sha512-r8/z8n7GFaYRln3xmP1Cxy0HH/HLM0uBUPkEuSVEfKGDA89M0FsZRZJRSwe/tJjRx+fpH/gjorfhB8tmEbSFLA==}
|
||||
'@rolldown/binding-linux-x64-gnu@1.2.8':
|
||||
resolution: {integrity: sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.2.7':
|
||||
resolution: {integrity: sha512-pAsE8iiDxUg1xBqdhrTfg45AVDVpirjz00sblEYClGNNcMnDb+e8beQgqIAw6LvauX/APvgxUnwrgun/YYGBhw==}
|
||||
'@rolldown/binding-linux-x64-musl@1.2.8':
|
||||
resolution: {integrity: sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.2.7':
|
||||
resolution: {integrity: sha512-lTcIYmmnQQA8Or/2DatS6oSqcdLHvendjS+zLu+FwgToynWMRSmQdpM65fTANJgIS4mjbMOo5KT2lnT9SAb96w==}
|
||||
'@rolldown/binding-openharmony-arm64@1.2.8':
|
||||
resolution: {integrity: sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@rolldown/binding-win32-arm64-msvc@1.2.7':
|
||||
resolution: {integrity: sha512-e3Gu3WxbNk/UqQhxqU7YIYO+9ZBvWNz3U+h/qRFosscMFzdRPbXYSaSWgSnklv2fz1TgzBTcti2z35c/7irsHw==}
|
||||
'@rolldown/binding-win32-arm64-msvc@1.2.8':
|
||||
resolution: {integrity: sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@rolldown/binding-win32-x64-msvc@1.2.7':
|
||||
resolution: {integrity: sha512-W/jg5qoRSqjsEv0+dZi4e687mcHqmVuU0P4fK6qS/xjetW2Gmc1W8j//z5nAeNcC8Ttm0hV46IjcYeuVwYhuiw==}
|
||||
'@rolldown/binding-win32-x64-msvc@1.2.8':
|
||||
resolution: {integrity: sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -1653,8 +1674,8 @@ packages:
|
||||
electron-publish@26.15.3:
|
||||
resolution: {integrity: sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==}
|
||||
|
||||
electron-to-chromium@1.5.423:
|
||||
resolution: {integrity: sha512-rRZfTSY8ptHYMQxa+uIycJMFKmY1T0GIApNMXJYGehguTZa56TEEl19pKPCoBqk5Gpf7QizZn/jt7xur+DYxag==}
|
||||
electron-to-chromium@1.5.425:
|
||||
resolution: {integrity: sha512-QvPtl41EUOnuT1HBvMKgxXRIaHNcagBPs50u7VULzhZXaGfqTbZyE16LQsctZ/RQHlGu+FOWeDTR4mY6YbeF1g==}
|
||||
|
||||
electron-vite@5.0.0:
|
||||
resolution: {integrity: sha512-OHp/vjdlubNlhNkPkL/+3JD34ii5ov7M0GpuXEVdQeqdQ3ulvVR7Dg/rNBLfS5XPIFwgoBLDf9sjjrL+CuDyRQ==}
|
||||
@@ -2510,8 +2531,8 @@ packages:
|
||||
node-int64@0.4.0:
|
||||
resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==}
|
||||
|
||||
node-releases@2.0.54:
|
||||
resolution: {integrity: sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==}
|
||||
node-releases@2.0.55:
|
||||
resolution: {integrity: sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
nopt@9.0.0:
|
||||
@@ -2743,8 +2764,8 @@ packages:
|
||||
resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==}
|
||||
engines: {node: '>=8.0'}
|
||||
|
||||
rolldown@1.2.7:
|
||||
resolution: {integrity: sha512-g0EtLvBjTUB7jhyV0S/TCup3v/XSVl45vUIGbOGU4QPiyjTenCe4mKuFvW9fEgYmS2Fo42AUssRmNuMziXdrig==}
|
||||
rolldown@1.2.8:
|
||||
resolution: {integrity: sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
|
||||
@@ -3946,7 +3967,7 @@ snapshots:
|
||||
|
||||
'@noble/hashes@2.4.0': {}
|
||||
|
||||
'@oxc-project/types@0.148.0': {}
|
||||
'@oxc-project/types@0.149.0': {}
|
||||
|
||||
'@peculiar/asn1-schema@2.9.4':
|
||||
dependencies:
|
||||
@@ -3990,49 +4011,49 @@ snapshots:
|
||||
|
||||
'@protobufjs/utf8@1.1.2': {}
|
||||
|
||||
'@rolldown/binding-android-arm-eabi@1.2.7':
|
||||
'@rolldown/binding-android-arm-eabi@1.2.8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-android-arm64@1.2.7':
|
||||
'@rolldown/binding-android-arm64@1.2.8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-darwin-arm64@1.2.7':
|
||||
'@rolldown/binding-darwin-arm64@1.2.8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-darwin-x64@1.2.7':
|
||||
'@rolldown/binding-darwin-x64@1.2.8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-freebsd-x64@1.2.7':
|
||||
'@rolldown/binding-freebsd-x64@1.2.8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm-gnueabihf@1.2.7':
|
||||
'@rolldown/binding-linux-arm-gnueabihf@1.2.8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm64-gnu@1.2.7':
|
||||
'@rolldown/binding-linux-arm64-gnu@1.2.8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-arm64-musl@1.2.7':
|
||||
'@rolldown/binding-linux-arm64-musl@1.2.8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-ppc64-gnu@1.2.7':
|
||||
'@rolldown/binding-linux-ppc64-gnu@1.2.8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-s390x-gnu@1.2.7':
|
||||
'@rolldown/binding-linux-s390x-gnu@1.2.8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-x64-gnu@1.2.7':
|
||||
'@rolldown/binding-linux-x64-gnu@1.2.8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-linux-x64-musl@1.2.7':
|
||||
'@rolldown/binding-linux-x64-musl@1.2.8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-openharmony-arm64@1.2.7':
|
||||
'@rolldown/binding-openharmony-arm64@1.2.8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-win32-arm64-msvc@1.2.7':
|
||||
'@rolldown/binding-win32-arm64-msvc@1.2.8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/binding-win32-x64-msvc@1.2.7':
|
||||
'@rolldown/binding-win32-x64-msvc@1.2.8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/pluginutils@1.0.1': {}
|
||||
@@ -4462,8 +4483,8 @@ snapshots:
|
||||
dependencies:
|
||||
baseline-browser-mapping: 2.11.21
|
||||
caniuse-lite: 1.0.30001810
|
||||
electron-to-chromium: 1.5.423
|
||||
node-releases: 2.0.54
|
||||
electron-to-chromium: 1.5.425
|
||||
node-releases: 2.0.55
|
||||
update-browserslist-db: 1.3.2(browserslist@4.28.9)
|
||||
|
||||
buffer-equal-constant-time@1.0.1: {}
|
||||
@@ -4719,7 +4740,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
electron-to-chromium@1.5.423: {}
|
||||
electron-to-chromium@1.5.425: {}
|
||||
|
||||
electron-vite@5.0.0(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.1)(jiti@2.7.0)(yaml@2.9.0)):
|
||||
dependencies:
|
||||
@@ -5837,7 +5858,7 @@ snapshots:
|
||||
|
||||
node-int64@0.4.0: {}
|
||||
|
||||
node-releases@2.0.54: {}
|
||||
node-releases@2.0.55: {}
|
||||
|
||||
nopt@9.0.0:
|
||||
dependencies:
|
||||
@@ -6114,26 +6135,26 @@ snapshots:
|
||||
sprintf-js: 1.1.3
|
||||
optional: true
|
||||
|
||||
rolldown@1.2.7:
|
||||
rolldown@1.2.8:
|
||||
dependencies:
|
||||
'@oxc-project/types': 0.148.0
|
||||
'@oxc-project/types': 0.149.0
|
||||
'@rolldown/pluginutils': 1.0.1
|
||||
optionalDependencies:
|
||||
'@rolldown/binding-android-arm-eabi': 1.2.7
|
||||
'@rolldown/binding-android-arm64': 1.2.7
|
||||
'@rolldown/binding-darwin-arm64': 1.2.7
|
||||
'@rolldown/binding-darwin-x64': 1.2.7
|
||||
'@rolldown/binding-freebsd-x64': 1.2.7
|
||||
'@rolldown/binding-linux-arm-gnueabihf': 1.2.7
|
||||
'@rolldown/binding-linux-arm64-gnu': 1.2.7
|
||||
'@rolldown/binding-linux-arm64-musl': 1.2.7
|
||||
'@rolldown/binding-linux-ppc64-gnu': 1.2.7
|
||||
'@rolldown/binding-linux-s390x-gnu': 1.2.7
|
||||
'@rolldown/binding-linux-x64-gnu': 1.2.7
|
||||
'@rolldown/binding-linux-x64-musl': 1.2.7
|
||||
'@rolldown/binding-openharmony-arm64': 1.2.7
|
||||
'@rolldown/binding-win32-arm64-msvc': 1.2.7
|
||||
'@rolldown/binding-win32-x64-msvc': 1.2.7
|
||||
'@rolldown/binding-android-arm-eabi': 1.2.8
|
||||
'@rolldown/binding-android-arm64': 1.2.8
|
||||
'@rolldown/binding-darwin-arm64': 1.2.8
|
||||
'@rolldown/binding-darwin-x64': 1.2.8
|
||||
'@rolldown/binding-freebsd-x64': 1.2.8
|
||||
'@rolldown/binding-linux-arm-gnueabihf': 1.2.8
|
||||
'@rolldown/binding-linux-arm64-gnu': 1.2.8
|
||||
'@rolldown/binding-linux-arm64-musl': 1.2.8
|
||||
'@rolldown/binding-linux-ppc64-gnu': 1.2.8
|
||||
'@rolldown/binding-linux-s390x-gnu': 1.2.8
|
||||
'@rolldown/binding-linux-x64-gnu': 1.2.8
|
||||
'@rolldown/binding-linux-x64-musl': 1.2.8
|
||||
'@rolldown/binding-openharmony-arm64': 1.2.8
|
||||
'@rolldown/binding-win32-arm64-msvc': 1.2.8
|
||||
'@rolldown/binding-win32-x64-msvc': 1.2.8
|
||||
|
||||
safe-buffer@5.1.2: {}
|
||||
|
||||
@@ -6408,7 +6429,7 @@ snapshots:
|
||||
lightningcss: 1.33.0
|
||||
picomatch: 4.0.7
|
||||
postcss: 8.5.28
|
||||
rolldown: 1.2.7
|
||||
rolldown: 1.2.8
|
||||
tinyglobby: 0.2.17
|
||||
optionalDependencies:
|
||||
'@types/node': 26.4.1
|
||||
@@ -1,3 +1,7 @@
|
||||
packages:
|
||||
- apps/*
|
||||
- packages/*
|
||||
|
||||
onlyBuiltDependencies:
|
||||
- '@google/genai'
|
||||
- electron
|
||||
Reference in New Issue
Block a user