merge: resolve conflicts with origin/main

Merge remote main branch, keeping both device verification (ws-auth-handshake)
and agent settings/profile features from main.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
yushen 2026-02-04 14:04:47 +08:00
commit c3ded8da41
15 changed files with 560 additions and 256 deletions

View file

@ -466,255 +466,26 @@ pnpm --filter @multica/desktop add -D @types/qrcode.react
---
## 八、实现步骤计划
## 八、待办事项
### Phase 1: 统一布局与路由重构
### 功能开发
**目标**: 统一页面结构,移除 /admin 子路由
#### 设备授权与白名单
#### Step 1.1: 路由重构
- [ ] Client 连接授权弹窗
- [ ] 已授权设备白名单管理
- [ ] 显示当前连接的 Client 数量
- [x] 重构 `App.tsx` 路由
- 移除 `/admin` 子路由
- 统一页面结构: / (Home) / /chat / /tools / /skills
- [x] 创建 `pages/layout.tsx` - 全局布局
- Header: Logo + 标题 + Settings 按钮
- Tabs: Home / Chat / Tools / Skills
- Content Area: 子路由出口
- [x] 移动页面文件到根级别
#### Settings 页面
#### Step 1.2: Home 页面 (三入口)
- [ ] Gateway URL 配置
- [ ] Theme 切换 (Light / Dark / System)
- [ ] 打开 credentials.json5 按钮
- [x] 重构 `pages/home.tsx`
- 左侧二维码 + 右侧 Agent 状态面板
- 底部: Open Chat 按钮 + Connect to Remote (Coming soon)
- [x] 安装 `qrcode.react` 依赖
- [x] 创建 `components/qr-code.tsx` - 分享二维码组件
- 生成二维码数据 (hubId, agentId, token, gateway, expires)
- 倒计时显示 + 自动过期刷新
- Refresh 按钮 + Copy Link 按钮
- 装饰性角落边框
### 联调测试
#### Step 1.3: Chat 页面 (双模式)
- [ ] 重构 `pages/chat.tsx`
- 顶部模式切换: Local Agent / Remote Agent
- 支持本地 Agent 直接调用
- 支持远程 Agent WebSocket 连接
- [ ] 创建 `hooks/use-local-agent.ts` - 本地 Agent 调用
- [ ] 创建 `hooks/use-remote-agent.ts` - 远程 Agent 连接
**交付物**: 统一的页面结构Home 页面三入口可用
---
### Phase 2: IPC 集成与 Hub 启动 ✅ (完成)
**目标**: 在 Main Process 中启动 Hub通过 IPC 与 Renderer 通信
#### Step 2.1: IPC 基础设施
- [x] 创建 `electron/ipc/` 目录结构
- [x] 创建 `electron/ipc/index.ts` - 统一注册 handlers
- [x] 创建 `electron/ipc/agent.ts` - Tools 相关 IPC handlers
- [x] 创建 `electron/ipc/skills.ts` - Skills 相关 IPC handlers
- [x] 更新 `electron/main.ts` - 注册 IPC handlers
#### Step 2.2: Hub 集成
- [x] 创建 `electron/ipc/hub.ts` - Hub 管理
- [x] 实现 Hub 自动启动 (App ready 时)
- [x] 实现 Agent 自动创建
- [x] 实现 Hub 状态查询 (`hub:getStatus`)
#### Step 2.3: Preload 脚本
- [x] 更新 `electron/preload.ts`
- 暴露 `window.electronAPI.hub.*`
- 暴露 `window.electronAPI.tools.*`
- 暴露 `window.electronAPI.skills.*`
#### Step 2.4: Hooks 更新
- [x] 更新 `hooks/use-tools.ts` - 调用 IPC
- [x] 更新 `hooks/use-skills.ts` - 调用 IPC
- [x] 创建 `hooks/use-hub.ts` - Hub 状态
**交付物**: Hub 在主进程运行UI 可通过 IPC 获取真实数据
---
### Phase 3: Tools 管理页面
**目标**: 查看和管理 Agent Tools
#### Step 3.1: Tools 数据获取
- [x] 创建 `hooks/use-tools.ts`
- 获取所有 tools 列表
- 获取 tool groups 和 profiles
- 管理 allow/deny 状态
#### Step 3.2: Tools UI 组件
- [x] 创建 `components/tool-list.tsx`
- 表格展示: Name / Group / Status / Toggle
- 按 Group 分组折叠
- 开关切换 (Switch 组件)
- Profile 下拉选择器 (内置)
- Reset to Default 按钮 (内置)
#### Step 3.3: Tools 页面整合
- [x] 更新 `pages/tools.tsx`
- Profile 选择器
- Tool 列表
- (状态持久化待后续实现)
#### Step 3.4: Tools 实时同步
- [x] 实现 `tools:list` 从真实 Agent 获取活跃 tools
- [x] 实现 `tools:active` 获取当前活跃工具
- [x] 实现 `tools:reload` 调用 Agent.reloadTools()
- [x] 暴露 AsyncAgent.getActiveTools() 和 reloadTools() 方法
- [x] 实现 `tools:setStatus` 持久化到 profile config.json
- [ ] 验证 Tool 开关影响 Agent 行为
**交付物**: 可查看所有 Tools切换 Profile开关单个 Tool实时影响 Agent
---
### Phase 4: Skills 管理页面
**目标**: 查看、添加、删除 Skills
#### Step 4.1: Skills 数据获取
- [x] 创建 `hooks/use-skills.ts`
- 加载所有 skills (mock data for now)
- 检查 eligibility
- 添加/删除/安装操作 (stub)
#### Step 4.2: Skills UI 组件
- [x] 创建 `components/skill-list.tsx`
- 表格展示: Name / Source / Status / Actions
- Status 徽章 (ready / missing / disabled)
- Action 按钮 (View / Install / Delete)
- Add Skill dialog (内置 skills.tsx)
- View Skill dialog (内置 skills.tsx)
#### Step 4.3: Skills 页面整合
- [x] 更新 `pages/skills.tsx`
- Skill 列表
- Add Skill 按钮 + dialog
- View Skill dialog
- Refresh 按钮
#### Step 4.4: Skills IPC 集成
- [x] 在 Agent 中添加 `getSkillsWithStatus()` 方法
- [x] 在 AsyncAgent 中暴露 `getSkillsWithStatus()` 方法
- [x] 实现 `skills:list` 从真实 Agent 获取 skills
- [x] 实现 `skills:get` 获取单个 skill 详情
- [x] 实现 `skills:toggle` 返回当前 eligibility 状态
- [x] 实现 `skills:reload` 重新加载 skills
- [x] 实现 `skills:add` 调用 `addSkill()`
- [x] 实现 `skills:remove` 调用 `removeSkill()`
**交付物**: 可查看所有 Skills查看 Skill 详情,显示 eligibility 状态
---
### Phase 5: 设置与完善
**目标**: Settings 页面 + 体验优化
#### Step 5.1: Settings 页面
- [ ] 创建 `components/settings-dialog.tsx`
- Gateway URL 配置
- Theme 切换 (Light / Dark / System)
- 打开 credentials.json5 按钮
#### Step 5.2: 连接状态管理
- [ ] 创建 `components/connection-status.tsx`
- 显示 Gateway 连接状态
- 显示已连接的 Client 信息
- 显示 Agent 状态
#### Step 5.3: 体验优化
- [ ] Toast 通知 (操作成功/失败)
- [ ] Loading 状态优化 (各页面)
- [ ] 错误边界处理 (React Error Boundary)
- [ ] 二维码自动刷新 (5 分钟过期后自动刷新)
**交付物**: 完整的管理功能,良好的用户体验
---
### Phase 6: Chat 页面与 Agent 联调
**目标**: 实现 Chat 功能,支持本地和远程 Agent
#### Step 6.1: 本地 Chat 实现
- [ ] 重构 `pages/chat.tsx`
- 消息输入框 + 发送按钮
- 消息历史展示
- 流式响应显示
- [ ] 创建 `hooks/use-local-agent.ts`
- 通过 IPC 调用 Agent.run()
- 处理流式响应
- 管理消息历史
#### Step 6.2: 远程 Chat 实现
- [ ] 创建 `hooks/use-remote-agent.ts`
- 通过 Gateway WebSocket 连接
- 处理远程消息
- [ ] Chat 页面模式切换
- Local Mode / Remote Mode 切换
**交付物**: 可与本地 Agent 对话,可连接远程 Agent
---
### Phase 7: 联调与测试
**目标**: 完整流程联调
#### Step 7.1: 本地 Agent 联调
- [ ] Tools 开关实时影响 Agent
- [ ] Tools 开关实时影响 Agent 行为
- [ ] Skills 启用/禁用影响 Agent
- [ ] Chat 流式响应正常
#### Step 7.2: 远程连接联调
- [ ] 扫码连接远程 Agent
- [ ] Token 验证流程
- [ ] 消息流转测试
#### Step 7.3: 异常处理
- [ ] 断开重连
- [ ] Token 过期处理
- [ ] Gateway 断开处理
---
## 九、当前进度摘要
| Phase | 名称 | 状态 |
| ------- | -------------- | ----------------------- |
| Phase 1 | 布局与路由 | ✅ 完成 |
| Phase 2 | IPC 集成与 Hub | ✅ 完成 |
| Phase 3 | Tools 管理 | ✅ UI + IPC 集成完成 |
| Phase 4 | Skills 管理 | ✅ UI + IPC 集成完成 |
| Phase 5 | 设置与完善 | ⏳ 待开始 |
| Phase 6 | Chat 页面 | ⏳ 待开始 (同事负责 UI) |
| Phase 7 | 联调测试 | ⏳ 待开始 |
- [ ] 断开重连、Token 过期处理

View file

@ -78,6 +78,12 @@ interface SkillAddResult {
skills?: string[]
}
interface ProfileData {
profileId: string | undefined
name: string | undefined
userContent: string | undefined
}
interface ElectronAPI {
hub: {
init: () => Promise<unknown>
@ -116,6 +122,11 @@ interface ElectronAPI {
agent: {
status: () => Promise<unknown>
}
profile: {
get: () => Promise<ProfileData>
updateName: (name: string) => Promise<unknown>
updateUser: (content: string) => Promise<unknown>
}
}
// Used in Renderer process, expose in `preload.ts`

View file

@ -125,8 +125,6 @@ export function registerAgentIpcHandlers(): void {
* Persists the change to profile config and reloads tools.
*/
ipcMain.handle('tools:toggle', async (_event, toolName: string) => {
console.log(`[IPC] tools:toggle called for: ${toolName}`)
const agent = getDefaultAgent()
if (!agent) {
return { error: 'No agent available' }
@ -145,7 +143,6 @@ export function registerAgentIpcHandlers(): void {
// Get updated status
const newActiveTools = agent.getActiveTools()
const isNowEnabled = newActiveTools.includes(toolName)
console.log(`[IPC] Tool ${toolName} toggled: ${isCurrentlyEnabled} -> ${isNowEnabled}`)
return {
name: toolName,

View file

@ -13,31 +13,43 @@ import type { AsyncAgent } from '../../../../src/agent/async-agent.js'
let hub: Hub | null = null
let defaultAgentId: string | null = null
/**
* Safe log function that catches EPIPE errors.
* Electron main process stdout can be closed unexpectedly.
*/
function safeLog(...args: unknown[]): void {
try {
console.log(...args)
} catch {
// Ignore EPIPE errors when stdout is closed
}
}
/**
* Initialize Hub on app startup.
* Creates Hub and a default Agent automatically.
*/
export async function initializeHub(): Promise<void> {
if (hub) {
console.log('[Desktop] Hub already initialized')
safeLog('[Desktop] Hub already initialized')
return
}
const gatewayUrl = process.env['GATEWAY_URL'] ?? 'http://localhost:3000'
console.log(`[Desktop] Initializing Hub, connecting to Gateway: ${gatewayUrl}`)
safeLog(`[Desktop] Initializing Hub, connecting to Gateway: ${gatewayUrl}`)
hub = new Hub(gatewayUrl)
// Create default agent if none exists
const agents = hub.listAgents()
if (agents.length === 0) {
console.log('[Desktop] Creating default agent...')
safeLog('[Desktop] Creating default agent...')
const agent = hub.createAgent()
defaultAgentId = agent.sessionId
console.log(`[Desktop] Default agent created: ${defaultAgentId}`)
safeLog(`[Desktop] Default agent created: ${defaultAgentId}`)
} else {
defaultAgentId = agents[0]
console.log(`[Desktop] Using existing agent: ${defaultAgentId}`)
safeLog(`[Desktop] Using existing agent: ${defaultAgentId}`)
}
}
@ -47,7 +59,7 @@ export async function initializeHub(): Promise<void> {
function getHub(): Hub {
if (!hub) {
const gatewayUrl = process.env['GATEWAY_URL'] ?? 'http://localhost:3000'
console.log(`[Desktop] Creating Hub, connecting to Gateway: ${gatewayUrl}`)
safeLog(`[Desktop] Creating Hub, connecting to Gateway: ${gatewayUrl}`)
hub = new Hub(gatewayUrl)
}
return hub
@ -289,7 +301,7 @@ export function setupDeviceConfirmation(mainWindow: Electron.BrowserWindow): voi
*/
export function cleanupHub(): void {
if (hub) {
console.log('[Desktop] Shutting down Hub')
safeLog('[Desktop] Shutting down Hub')
hub.shutdown()
hub = null
}

View file

@ -4,10 +4,12 @@
export { registerAgentIpcHandlers, cleanupAgent } from './agent.js'
export { registerSkillsIpcHandlers } from './skills.js'
export { registerHubIpcHandlers, cleanupHub, initializeHub, setupDeviceConfirmation } from './hub.js'
export { registerProfileIpcHandlers } from './profile.js'
import { registerAgentIpcHandlers, cleanupAgent } from './agent.js'
import { registerSkillsIpcHandlers } from './skills.js'
import { registerHubIpcHandlers, cleanupHub, initializeHub, setupDeviceConfirmation } from './hub.js'
import { registerProfileIpcHandlers } from './profile.js'
/**
* Register all IPC handlers.
@ -17,6 +19,7 @@ export function registerAllIpcHandlers(): void {
registerHubIpcHandlers()
registerAgentIpcHandlers()
registerSkillsIpcHandlers()
registerProfileIpcHandlers()
}
/**

View file

@ -0,0 +1,91 @@
/**
* Profile IPC handlers for Electron main process.
*
* Manages agent profile settings like name and user.md content.
*/
import { ipcMain } from 'electron'
import { getCurrentHub } from './hub.js'
/**
* Get the default agent from Hub.
*/
function getDefaultAgent() {
const hub = getCurrentHub()
if (!hub) return null
const agentIds = hub.listAgents()
if (agentIds.length === 0) return null
return hub.getAgent(agentIds[0]) ?? null
}
/**
* Profile data returned to renderer.
*/
export interface ProfileData {
profileId: string | undefined
name: string | undefined
userContent: string | undefined
}
/**
* Register all Profile-related IPC handlers.
*/
export function registerProfileIpcHandlers(): void {
/**
* Get profile data (name + user content).
*/
ipcMain.handle('profile:get', async (): Promise<ProfileData> => {
const agent = getDefaultAgent()
if (!agent) {
return {
profileId: undefined,
name: undefined,
userContent: undefined,
}
}
return {
profileId: agent.getProfileId(),
name: agent.getAgentName(),
userContent: agent.getUserContent(),
}
})
/**
* Update agent display name.
*/
ipcMain.handle('profile:updateName', async (_event, name: string) => {
const agent = getDefaultAgent()
if (!agent) {
return { error: 'No agent available' }
}
agent.setAgentName(name)
return { ok: true, name }
})
/**
* Update user.md content.
*/
ipcMain.handle('profile:updateUser', async (_event, content: string) => {
const agent = getDefaultAgent()
if (!agent) {
console.error('[Profile IPC] No agent available for updateUser')
return { error: 'No agent available' }
}
console.log('[Profile IPC] Updating user content:', content.substring(0, 50) + '...')
agent.setUserContent(content)
// Reload system prompt to apply changes immediately
console.log('[Profile IPC] Reloading system prompt...')
agent.reloadSystemPrompt()
// Verify the change
const newUserContent = agent.getUserContent()
console.log('[Profile IPC] New user content:', newUserContent?.substring(0, 50) + '...')
return { ok: true }
})
}

View file

@ -1,3 +1,49 @@
// Patch console methods to handle EPIPE errors in Electron main process
// This MUST be done before any other imports that might use console
// EPIPE happens when stdout/stderr pipes are closed unexpectedly
const originalConsoleLog = console.log.bind(console)
const originalConsoleError = console.error.bind(console)
const originalConsoleWarn = console.warn.bind(console)
const safeLog = (...args: unknown[]) => {
try {
originalConsoleLog(...args)
} catch {
// Ignore EPIPE errors silently
}
}
const safeError = (...args: unknown[]) => {
try {
originalConsoleError(...args)
} catch {
// Ignore EPIPE errors silently
}
}
const safeWarn = (...args: unknown[]) => {
try {
originalConsoleWarn(...args)
} catch {
// Ignore EPIPE errors silently
}
}
// Override global console
console.log = safeLog
console.error = safeError
console.warn = safeWarn
// Also handle process stdout/stderr EPIPE errors
process.stdout?.on?.('error', (err: NodeJS.ErrnoException) => {
if (err.code === 'EPIPE') return // Ignore
throw err
})
process.stderr?.on?.('error', (err: NodeJS.ErrnoException) => {
if (err.code === 'EPIPE') return // Ignore
throw err
})
import { app, BrowserWindow } from 'electron'
import { fileURLToPath } from 'node:url'
import path from 'node:path'

View file

@ -37,6 +37,12 @@ export interface SkillInfo {
triggers: string[]
}
export interface ProfileData {
profileId: string | undefined
name: string | undefined
userContent: string | undefined
}
// ============================================================================
// Expose typed API to Renderer process
// ============================================================================
@ -97,6 +103,13 @@ const electronAPI = {
agent: {
status: () => ipcRenderer.invoke('agent:status'),
},
// Profile management
profile: {
get: (): Promise<ProfileData> => ipcRenderer.invoke('profile:get'),
updateName: (name: string) => ipcRenderer.invoke('profile:updateName', name),
updateUser: (content: string) => ipcRenderer.invoke('profile:updateUser', content),
},
}
// Expose to renderer

View file

@ -0,0 +1,130 @@
import { useState, useEffect } from 'react'
import {
Dialog,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
} from '@multica/ui/components/ui/dialog'
import { Button } from '@multica/ui/components/ui/button'
import { Input } from '@multica/ui/components/ui/input'
import { Textarea } from '@multica/ui/components/ui/textarea'
import { Label } from '@multica/ui/components/ui/label'
import { HugeiconsIcon } from '@hugeicons/react'
import { Loading03Icon } from '@hugeicons/core-free-icons'
interface AgentSettingsDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
}
export function AgentSettingsDialog({ open, onOpenChange }: AgentSettingsDialogProps) {
const [loading, setLoading] = useState(false)
const [saving, setSaving] = useState(false)
const [name, setName] = useState('')
const [userContent, setUserContent] = useState('')
const [profileId, setProfileId] = useState<string | undefined>()
// Load profile data when dialog opens
useEffect(() => {
if (open) {
loadProfile()
}
}, [open])
const loadProfile = async () => {
setLoading(true)
try {
const data = await window.electronAPI.profile.get()
setProfileId(data.profileId)
setName(data.name ?? '')
setUserContent(data.userContent ?? '')
} catch (err) {
console.error('Failed to load profile:', err)
} finally {
setLoading(false)
}
}
const handleSave = async () => {
setSaving(true)
try {
// Update name if changed
await window.electronAPI.profile.updateName(name)
// Update user content
await window.electronAPI.profile.updateUser(userContent)
onOpenChange(false)
} catch (err) {
console.error('Failed to save profile:', err)
} finally {
setSaving(false)
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Edit Agent</DialogTitle>
<DialogDescription>
Customize your agent's name and personal settings.
</DialogDescription>
</DialogHeader>
{loading ? (
<div className="flex items-center justify-center py-8">
<HugeiconsIcon icon={Loading03Icon} className="size-6 animate-spin text-muted-foreground" />
</div>
) : (
<div className="space-y-4">
{/* Profile ID (read-only) */}
{profileId && (
<div className="text-xs text-muted-foreground">
Profile: {profileId}
</div>
)}
{/* Name */}
<div className="space-y-2">
<Label htmlFor="agent-name">Name</Label>
<Input
id="agent-name"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="My Assistant"
/>
</div>
{/* User Content */}
<div className="space-y-2">
<Label htmlFor="user-content">About You</Label>
<p className="text-xs text-muted-foreground">
Help the agent understand you better. Share your preferences, role, or any context.
</p>
<Textarea
id="user-content"
value={userContent}
onChange={(e) => setUserContent(e.target.value)}
placeholder="- I'm a frontend developer&#10;- I prefer TypeScript&#10;- Please respond in Chinese"
className="min-h-[160px] font-mono text-sm"
/>
</div>
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
<Button onClick={handleSave} disabled={loading || saving}>
{saving && <HugeiconsIcon icon={Loading03Icon} className="size-4 animate-spin mr-2" />}
Save
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
export default AgentSettingsDialog

View file

@ -1,3 +1,4 @@
import { useState, useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import { Button } from '@multica/ui/components/ui/button'
import { HugeiconsIcon } from '@hugeicons/react'
@ -6,14 +7,39 @@ import {
LinkSquare01Icon,
Loading03Icon,
AlertCircleIcon,
Edit02Icon,
} from '@hugeicons/core-free-icons'
import { ConnectionQRCode } from '../components/qr-code'
import { DeviceList } from '../components/device-list'
import { AgentSettingsDialog } from '../components/agent-settings-dialog'
import { useHub } from '../hooks/use-hub'
export default function HomePage() {
const navigate = useNavigate()
const { hubInfo, agents, loading, error } = useHub()
const [settingsOpen, setSettingsOpen] = useState(false)
const [agentName, setAgentName] = useState<string | undefined>()
// Load agent profile info
useEffect(() => {
loadAgentInfo()
}, [])
// Reload agent info when settings dialog closes
useEffect(() => {
if (!settingsOpen) {
loadAgentInfo()
}
}, [settingsOpen])
const loadAgentInfo = async () => {
try {
const data = await window.electronAPI.profile.get()
setAgentName(data.name)
} catch (err) {
console.error('Failed to load agent info:', err)
}
}
// Get the first agent (or create one if none exists)
const primaryAgent = agents[0]
@ -108,6 +134,23 @@ export default function HomePage() {
</p>
</div>
{/* Agent Settings */}
<div className="p-4 rounded-lg bg-muted/50 border border-border/50">
<div className="flex items-center justify-between mb-2">
<p className="text-xs text-muted-foreground uppercase tracking-wider">
Agent Settings
</p>
<Button
variant="ghost"
size="icon-sm"
onClick={() => setSettingsOpen(true)}
>
<HugeiconsIcon icon={Edit02Icon} className="size-4" />
</Button>
</div>
<p className="font-medium">{agentName || 'Unnamed Agent'}</p>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-2 gap-4">
<div className="p-4 rounded-lg bg-muted/50 border border-border/50">
@ -148,6 +191,9 @@ export default function HomePage() {
<DeviceList />
</div>
{/* Agent Settings Dialog */}
<AgentSettingsDialog open={settingsOpen} onOpenChange={setSettingsOpen} />
{/* Bottom: Actions */}
<div className="border-t p-4">
<div className="flex items-center justify-between">

View file

@ -148,4 +148,40 @@ export class AsyncAgent {
getProfileId(): string | undefined {
return this.agent.getProfileId();
}
/**
* Get agent display name from profile config.
*/
getAgentName(): string | undefined {
return this.agent.getAgentName();
}
/**
* Update agent display name in profile config.
*/
setAgentName(name: string): void {
this.agent.setAgentName(name);
}
/**
* Get user.md content from profile.
*/
getUserContent(): string | undefined {
return this.agent.getUserContent();
}
/**
* Update user.md content in profile.
*/
setUserContent(content: string): void {
this.agent.setUserContent(content);
}
/**
* Reload profile from disk and rebuild system prompt.
* Call this after updating profile files to apply changes immediately.
*/
reloadSystemPrompt(): void {
this.agent.reloadSystemPrompt();
}
}

View file

@ -13,7 +13,10 @@ import {
loadProfile,
profileExists,
saveProfile,
writeProfileConfig,
writeProfileFile,
} from "./storage.js";
import { PROFILE_FILES } from "./types.js";
export { type AgentProfile, type CreateProfileOptions, type ProfileConfig, type ProfileManagerOptions } from "./types.js";
export { DEFAULT_TEMPLATES } from "./templates.js";
@ -110,6 +113,12 @@ export class ProfileManager {
return this.profile;
}
/** 重新从磁盘加载 profile清除缓存 */
reloadProfile(): AgentProfile | undefined {
this.profile = loadAgentProfile(this.profileId, { baseDir: this.baseDir });
return this.profile;
}
/** 获取或创建 profile */
getOrCreateProfile(useTemplates = true): AgentProfile {
if (!this.profile) {
@ -129,6 +138,7 @@ export class ProfileManager {
/** 构建 system prompt */
buildSystemPrompt(): string {
const profile = this.getProfile();
console.log('[ProfileManager] buildSystemPrompt called, profile exists:', !!profile);
if (!profile) {
return "";
}
@ -136,11 +146,15 @@ export class ProfileManager {
const parts: string[] = [];
if (profile.soul) {
console.log('[ProfileManager] Adding soul, length:', profile.soul.length);
parts.push(profile.soul);
}
if (profile.user) {
console.log('[ProfileManager] Adding user, content:', profile.user.substring(0, 100));
parts.push(profile.user);
} else {
console.log('[ProfileManager] No user content in profile');
}
if (profile.workspace) {
@ -215,4 +229,72 @@ export class ProfileManager {
this.updateToolsConfig(newConfig);
return newConfig;
}
/** 获取 Agent 名称 */
getName(): string | undefined {
const profile = this.getProfile();
return profile?.config?.name;
}
/** 更新 Agent 名称 */
updateName(name: string): void {
const profile = this.getOrCreateProfile(false);
const currentConfig = profile.config ?? {};
const newConfig: ProfileConfig = {
...currentConfig,
name,
};
profile.config = newConfig;
this.profile = profile;
writeProfileConfig(this.profileId, newConfig, { baseDir: this.baseDir });
// Also update soul.md to include the agent name
this.updateSoulWithName(name);
}
/** 更新 soul.md确保包含 Agent 名称 */
private updateSoulWithName(name: string): void {
const profile = this.getOrCreateProfile(true); // 确保有默认模板
let soulContent = profile.soul ?? DEFAULT_TEMPLATES.soul;
// 替换 soul.md 中的 Name 字段
// 匹配 "- **Name:** xxx" 格式
const namePattern = /- \*\*Name:\*\* .*/;
const newNameLine = `- **Name:** ${name}`;
if (namePattern.test(soulContent)) {
soulContent = soulContent.replace(namePattern, newNameLine);
} else {
// 如果没有找到 Name 字段,在 Identity 部分后添加
const identityPattern = /## Identity\n/;
if (identityPattern.test(soulContent)) {
soulContent = soulContent.replace(identityPattern, `## Identity\n\n${newNameLine}\n`);
} else {
// 如果没有 Identity 部分,在开头添加
soulContent = `# Soul\n\n## Identity\n\n${newNameLine}\n\n${soulContent}`;
}
}
// 保存更新后的 soul.md
writeProfileFile(this.profileId, PROFILE_FILES.soul, soulContent, { baseDir: this.baseDir });
// 更新缓存
if (this.profile) {
this.profile.soul = soulContent;
}
}
/** 获取 user.md 内容 */
getUserContent(): string | undefined {
const profile = this.getProfile();
return profile?.user;
}
/** 更新 user.md 内容 */
updateUserContent(content: string): void {
writeProfileFile(this.profileId, PROFILE_FILES.user, content, { baseDir: this.baseDir });
// Update cached profile
if (this.profile) {
this.profile.user = content;
}
}
}

View file

@ -3,7 +3,7 @@
*/
export const DEFAULT_TEMPLATES = {
soul: `# Soul
soul: `# Soul
_You're not a chatbot. You're becoming someone._
@ -40,7 +40,7 @@ If you change this file, tell the user — it's your soul, and they should know.
_This file is yours to evolve. As you learn who you are, update it._
`,
user: `# User
user: `# User
_Learn about the person you're helping. Update this as you go._
@ -58,7 +58,7 @@ _(What do they care about? What projects are they working on? What annoys them?
The more you know, the better you can help. But remember you're learning about a person, not building a dossier. Respect the difference.
`,
workspace: `# Workspace
workspace: `# Workspace
This folder is home. Treat it that way.
@ -127,7 +127,7 @@ Capture what matters. Decisions, context, things to remember.
This is a starting point. Add your own conventions, style, and rules as you figure out what works.
`,
memory: `# Memory
memory: `# Memory
_(Persistent knowledge will be stored here. Update this as you learn.)_

View file

@ -15,6 +15,8 @@ export const PROFILE_FILES = {
/** Profile config.json structure */
export interface ProfileConfig {
/** Agent display name */
name?: string;
/** Tools policy configuration */
tools?: ToolsConfig;
/** Default LLM provider */

View file

@ -446,12 +446,15 @@ export class Agent {
reloadTools(): string[] {
// Re-read profile tools config to get latest changes
const profileToolsConfig = this.profile?.getToolsConfig();
console.log(`[Agent] reloadTools: profileToolsConfig =`, JSON.stringify(profileToolsConfig));
const mergedToolsConfig = mergeToolsConfig(profileToolsConfig, this.originalToolsConfig);
console.log(`[Agent] reloadTools: mergedToolsConfig =`, JSON.stringify(mergedToolsConfig));
this.toolsOptions = mergedToolsConfig
? { ...this.toolsOptions, tools: mergedToolsConfig }
: this.toolsOptions;
const tools = resolveTools(this.toolsOptions);
console.log(`[Agent] reloadTools: resolved ${tools.length} tools: ${tools.map(t => t.name).join(", ") || "(none)"}`);
this.agent.setTools(tools);
if (this.debug) {
console.error(`[debug] Reloaded ${tools.length} tools: ${tools.map(t => t.name).join(", ") || "(none)"}`);
@ -532,4 +535,65 @@ export class Agent {
getProfileId(): string | undefined {
return this.profile?.getProfile()?.id;
}
/**
* Get agent display name from profile config.
*/
getAgentName(): string | undefined {
return this.profile?.getName();
}
/**
* Update agent display name in profile config.
*/
setAgentName(name: string): void {
this.profile?.updateName(name);
}
/**
* Get user.md content from profile.
*/
getUserContent(): string | undefined {
return this.profile?.getUserContent();
}
/**
* Update user.md content in profile.
*/
setUserContent(content: string): void {
this.profile?.updateUserContent(content);
}
/**
* Reload profile from disk and rebuild system prompt.
* Call this after updating profile files to apply changes immediately.
*/
reloadSystemPrompt(): void {
if (!this.profile) {
console.log('[Agent] reloadSystemPrompt: no profile');
return;
}
// Reload profile from disk
console.log('[Agent] Reloading profile from disk...');
this.profile.reloadProfile();
// Rebuild system prompt
let systemPrompt = this.profile.buildSystemPrompt();
console.log('[Agent] Built system prompt, length:', systemPrompt?.length);
// Re-add skills prompt if skills are enabled
if (this.skillManager) {
const skillsPrompt = this.skillManager.buildModelSkillsPrompt();
if (skillsPrompt) {
systemPrompt = systemPrompt ? `${systemPrompt}\n\n${skillsPrompt}` : skillsPrompt;
}
}
// Apply new system prompt
if (systemPrompt) {
console.log('[Agent] Applying system prompt, first 200 chars:', systemPrompt.substring(0, 200));
this.agent.setSystemPrompt(systemPrompt);
}
}
}