diff --git a/.changeset/witty-ideas-flow.md b/.changeset/witty-ideas-flow.md new file mode 100644 index 00000000..3bba43a5 --- /dev/null +++ b/.changeset/witty-ideas-flow.md @@ -0,0 +1,15 @@ +--- +"@promptx/desktop": patch +--- +feat: 角色/工具详情面板添加导出按钮,支持 v1 和 v2 角色导出 + +- 角色和工具详情面板右上角新增导出按钮(非 system 资源可见) +- 后端 resources:download 支持 version 参数,v2 角色正确定位 ~/.rolex/roles/ 目录 +- v2 角色导出的 ZIP 以 roleId 为顶层目录,确保导入时还原正确 ID +- 添加 i18n 键:export / exportSuccess / exportFailed(中英文) + +fix: macOS 上 AgentX 对话时子进程不再显示 Dock 图标 + +- macOS 启动时检测 Electron Helper 二进制(LSUIElement=true),用于 spawn 子进程 +- buildOptions 和 AgentXService 的 MCP server 在 macOS 上优先使用 Helper 二进制 +- 所有 spawn 调用添加 windowsHide: true diff --git a/CENSUS_PARSER_FIX.md b/CENSUS_PARSER_FIX.md new file mode 100644 index 00000000..a9effaca --- /dev/null +++ b/CENSUS_PARSER_FIX.md @@ -0,0 +1,214 @@ +# Census.list 文本解析器修复 + +## 问题描述 + +用户报告在角色页面看不到组织,尽管通过 `directory` 操作可以看到两个组织: +1. rolex (RoleX) - 系统组织 +2. 火花堆栈人工智能有限公司 - 用户创建的组织 + +## 根本原因 + +RoleX 1.1.0 的 `census.list` 返回的是**文本格式**的输出,而不是 JSON 结构化数据。原代码期望的是 JSON 格式,导致解析失败。 + +### census.list 输出格式示例 + +``` +rolex (RoleX) + nuwa (女娲, nvwa) — individual-manager, organization-manager, position-manager + +火花堆栈人工智能有限公司 + Node全栈工程师 — Node全栈工程师岗位 + 测试工程师 — 测试工程师岗位 + AI系统架构师 — AI系统架构师岗位 + UI设计师 — UI设计师岗位 + 产品经理 — 产品经理岗位 +``` + +## 解决方案 + +### 1. 添加文本解析器 + +在 `RolexBridge.js` 中添加 `_parseCensusOutput()` 方法,将文本格式转换为结构化 JSON: + +```javascript +_parseCensusOutput(text) { + const result = { + roles: [], + organizations: [] + } + + // 解析逻辑: + // 1. 识别组织行(无缩进) + // 2. 识别角色行(有缩进,描述包含 manager 等关键词) + // 3. 识别职位行(有缩进,描述是职位说明) + + return result +} +``` + +### 2. 区分角色和职位 + +**关键判断逻辑:** +- **角色**:描述包含多个逗号分隔的职位,或包含 "manager"、"individual"、"organization"、"position" 等关键词 +- **职位**:描述是职位说明文本(如"Node全栈工程师岗位") + +### 3. 输出结构 + +```json +{ + "roles": [ + { + "name": "nuwa", + "org": "rolex (RoleX)", + "position": "individual-manager" + } + ], + "organizations": [ + { + "name": "rolex (RoleX)", + "members": [ + { + "name": "nuwa", + "position": "individual-manager" + } + ], + "positions": [] + }, + { + "name": "火花堆栈人工智能有限公司", + "members": [], + "positions": [ + { + "name": "Node全栈工程师", + "description": "Node全栈工程师岗位" + }, + ... + ] + } + ] +} +``` + +## 文件改动 + +### 1. `packages/core/src/rolex/RolexBridge.js` +- 修改 `directory()` 方法,调用 `_parseCensusOutput()` 解析文本 +- 添加 `_parseCensusOutput()` 私有方法 + +### 2. `apps/desktop/src/main/windows/ResourceListWindow.ts` +- 移除 `rolex:directory` handler 中的 `JSON.parse()` +- 移除 `resources:getV2RoleData` handler 中的 `JSON.parse()` + +### 3. `apps/desktop/src/view/pages/roles-window/index.tsx` +- 移除前端的 `JSON.parse()` + +## 测试结果 + +### 解析前(错误) +``` +角色列表包含职位定义: +- nuwa +- Node全栈工程师 ❌ 这是职位,不是角色 +- 测试工程师 ❌ 这是职位,不是角色 +... +``` + +### 解析后(正确) +``` +角色列表: +- nuwa (rolex (RoleX) - individual-manager) ✅ + +组织列表: +- rolex (RoleX) + 成员: nuwa [individual-manager] ✅ + +- 火花堆栈人工智能有限公司 + 职位: Node全栈工程师, 测试工程师, ... ✅ + 成员: (空,因为还没有任命角色到职位) +``` + +## 为什么看不到"火花堆栈人工智能有限公司"组织? + +**原因:该组织没有成员!** + +虽然组织定义了 5 个职位,但还没有任命任何角色到这些职位。树状列表只显示**有成员的组织**。 + +## 如何让组织显示在列表中? + +需要任命角色到职位: + +```javascript +// 1. 创建一个新角色(或使用现有角色) +// 假设已有角色 "alice" + +// 2. 任命角色到职位 +{ + "operation": "require", + "role": "nuwa", + "orgName": "火花堆栈人工智能有限公司", + "position": "Node全栈工程师", + "individual": "alice" +} +``` + +任命后,`census.list` 输出会变成: + +``` +火花堆栈人工智能有限公司 + alice — Node全栈工程师 + Node全栈工程师 — Node全栈工程师岗位 + ... +``` + +此时解析器会识别: +- `alice` 是角色(因为描述是职位名称,不是职位说明) +- `Node全栈工程师` 是职位定义 + +## 下一步 + +1. **重启 PromptX Desktop** +2. **打开角色窗口** +3. **切换到 V2 Rolex 标签** +4. **应该能看到 "rolex (RoleX)" 组织,展开后看到 nuwa** +5. **任命角色到"火花堆栈人工智能有限公司"的职位** +6. **刷新后应该能看到该组织** + +## 调试命令 + +### 查看解析后的目录数据 +```javascript +window.electronAPI?.invoke("rolex:directory", {}).then(result => { + console.log('Directory:', result.data) + console.log('Organizations:', result.data.organizations) + console.log('Roles:', result.data.roles) +}) +``` + +### 查看原始 census.list 输出 +通过 MCP action 工具: +```json +{ + "role": "nuwa", + "operation": "directory" +} +``` + +## 注意事项 + +1. **职位 vs 角色**:census.list 中同时包含职位定义和角色任命,需要正确区分 +2. **空组织**:没有成员的组织不会显示在树状列表中(这是设计行为) +3. **组织名称**:包含括号的组织名称(如 "rolex (RoleX)")会被完整保留 +4. **多职位角色**:如果角色担任多个职位,只显示第一个职位 + +## 已知限制 + +1. **解析启发式**:使用关键词判断是否为角色,可能在某些边缘情况下误判 +2. **职位描述格式**:假设职位描述不包含逗号和 manager 等关键词 +3. **文本格式依赖**:依赖 census.list 的固定文本格式,如果 RoleX 更新格式可能需要调整解析器 + +## 改进建议 + +如果 RoleX 未来提供结构化 API(返回 JSON),应该: +1. 优先使用结构化 API +2. 保留文本解析器作为后备方案 +3. 添加版本检测逻辑 diff --git a/ORGANIZATION_API_GUIDE.md b/ORGANIZATION_API_GUIDE.md new file mode 100644 index 00000000..d1b34f71 --- /dev/null +++ b/ORGANIZATION_API_GUIDE.md @@ -0,0 +1,314 @@ +# 角色组织架构 API 使用指南 + +## 概述 + +PromptX 集成了 RoleX 1.1.0,支持完整的组织架构管理功能。本文档介绍如何获取和管理角色的组织架构信息。 + +## 1. 获取组织目录(Directory) + +### 方法:`directory()` + +获取整个社会目录,包含所有角色和组织的信息。 + +```javascript +const { RolexActionDispatcher } = require('@promptx/core').rolex +const dispatcher = new RolexActionDispatcher() + +// 获取完整的组织目录 +const directoryResult = await dispatcher.dispatch('directory', {}) + +// directory 返回的是 JSON 字符串,需要解析 +const directory = JSON.parse(directoryResult) + +console.log(directory) +// 输出结构: +// { +// roles: [ +// { name: "角色ID", org: "组织名称", position: "职位名称" }, +// ... +// ], +// organizations: [ +// { name: "组织名称", charter: "组织章程", members: [...] }, +// ... +// ] +// } +``` + +### 从目录中查找特定角色的组织信息 + +```javascript +const directory = JSON.parse(directoryResult) + +// 查找角色的组织信息 +const roleId = "nuwa" +const roleEntry = directory.roles?.find(r => r.name === roleId) + +if (roleEntry && roleEntry.org) { + const orgName = roleEntry.org + const position = roleEntry.position + + // 查找组织详情 + const org = directory.organizations?.find(o => o.name === orgName) + + console.log(`角色 ${roleId} 属于组织 ${orgName}`) + console.log(`职位:${position}`) + console.log(`组织章程:${org?.charter}`) + console.log(`组织成员:`, org?.members) +} else { + console.log(`角色 ${roleId} 未加入任何组织`) +} +``` + +## 2. 组织管理操作 + +### 2.1 创建组织(Synthesize) + +```javascript +// 创建新组织 +await dispatcher.dispatch('synthesize', { + role: 'founder-role-id', // 创建者角色ID + name: 'MyOrganization', // 组织名称 + charter: '组织章程内容' // 组织章程 +}) +``` + +### 2.2 定义组织章程(Charter) + +```javascript +// 为组织定义或更新章程 +await dispatcher.dispatch('charter', { + role: 'admin-role-id', + orgName: 'MyOrganization', + content: '更新后的组织章程内容' +}) +``` + +### 2.3 解散组织(Dissolve) + +```javascript +// 解散组织 +await dispatcher.dispatch('dissolve', { + role: 'admin-role-id', + orgName: 'MyOrganization' +}) +``` + +## 3. 职位管理操作 + +### 3.1 设立职位(Charge) + +```javascript +// 在组织中设立新职位 +await dispatcher.dispatch('charge', { + role: 'admin-role-id', + orgName: 'MyOrganization', + position: 'Engineer', // 职位名称 + procedure: '职位职责描述' // 职位流程/职责 +}) +``` + +### 3.2 任命角色到职位(Require) + +```javascript +// 任命角色到特定职位 +await dispatcher.dispatch('require', { + role: 'admin-role-id', + orgName: 'MyOrganization', + position: 'Engineer', + individual: 'target-role-id' // 被任命的角色ID +}) +``` + +### 3.3 撤销职位(Abolish) + +```javascript +// 撤销组织中的职位 +await dispatcher.dispatch('abolish', { + role: 'admin-role-id', + orgName: 'MyOrganization', + position: 'Engineer' +}) +``` + +## 4. 在前端获取组织信息 + +### 4.1 通过 IPC 调用 + +```typescript +// 在 Electron 渲染进程中 +const result = await window.electronAPI?.invoke('resources:getV2RoleData', { + roleId: 'nuwa' +}) + +if (result?.success) { + const { identity, focus, directory } = result + + // directory 已经是解析后的对象 + const roleEntry = directory?.roles?.find(r => r.name === 'nuwa') + const orgName = roleEntry?.org + const position = roleEntry?.position + const org = orgName ? directory?.organizations?.find(o => o.name === orgName) : null + + console.log('组织信息:', { + orgName, + position, + charter: org?.charter, + members: org?.members + }) +} +``` + +### 4.2 使用 React Hook + +```typescript +// 在 React 组件中使用 +function MyComponent({ roleId }: { roleId: string }) { + const [orgInfo, setOrgInfo] = useState(null) + + useEffect(() => { + const loadOrgInfo = async () => { + const result = await window.electronAPI?.invoke('resources:getV2RoleData', { + roleId + }) + + if (result?.success && result.directory) { + const roleEntry = result.directory.roles?.find(r => r.name === roleId) + if (roleEntry?.org) { + const org = result.directory.organizations?.find( + o => o.name === roleEntry.org + ) + setOrgInfo({ + orgName: roleEntry.org, + position: roleEntry.position, + org + }) + } + } + } + + loadOrgInfo() + }, [roleId]) + + return ( +
+ {orgInfo ? ( + <> +

组织:{orgInfo.orgName}

+

职位:{orgInfo.position}

+

章程:{orgInfo.org?.charter}

+ + ) : ( +

未加入任何组织

+ )} +
+ ) +} +``` + +## 5. 完整示例:组织管理流程 + +```javascript +const { RolexActionDispatcher } = require('@promptx/core').rolex +const dispatcher = new RolexActionDispatcher() + +async function organizationExample() { + // 1. 创建组织 + await dispatcher.dispatch('synthesize', { + role: 'founder', + name: 'TechCorp', + charter: '致力于技术创新的组织' + }) + + // 2. 设立职位 + await dispatcher.dispatch('charge', { + role: 'founder', + orgName: 'TechCorp', + position: 'CTO', + procedure: '负责技术战略和团队管理' + }) + + await dispatcher.dispatch('charge', { + role: 'founder', + orgName: 'TechCorp', + position: 'Engineer', + procedure: '负责产品开发和维护' + }) + + // 3. 任命角色到职位 + await dispatcher.dispatch('require', { + role: 'founder', + orgName: 'TechCorp', + position: 'CTO', + individual: 'alice' + }) + + await dispatcher.dispatch('require', { + role: 'founder', + orgName: 'TechCorp', + position: 'Engineer', + individual: 'bob' + }) + + // 4. 查看组织结构 + const directoryResult = await dispatcher.dispatch('directory', {}) + const directory = JSON.parse(directoryResult) + + const org = directory.organizations?.find(o => o.name === 'TechCorp') + console.log('TechCorp 组织结构:', org) + + // 5. 查看特定角色的组织信息 + const aliceEntry = directory.roles?.find(r => r.name === 'alice') + console.log('Alice 的组织信息:', { + org: aliceEntry?.org, + position: aliceEntry?.position + }) +} +``` + +## 6. 数据结构说明 + +### Directory 结构 + +```typescript +interface Directory { + roles: Array<{ + name: string // 角色ID + org?: string // 所属组织名称(可选) + position?: string // 在组织中的职位(可选) + }> + organizations: Array<{ + name: string // 组织名称 + charter?: string // 组织章程 + members?: Array<{ // 成员列表 + name: string // 成员角色ID + position: string // 职位 + }> + }> +} +``` + +## 7. 注意事项 + +1. **V2 角色专属**:组织架构功能仅适用于 RoleX V2 角色,V1 角色不支持 +2. **权限管理**:某些操作(如解散组织、撤销职位)可能需要特定权限 +3. **数据持久化**:组织信息存储在 RoleX SQLite 数据库中(`~/.rolex/rolex.db`) +4. **字符串格式**:`directory()` 返回的是 JSON 字符串,需要使用 `JSON.parse()` 解析 + +## 8. 相关文件 + +- **后端 API**:`packages/core/src/rolex/RolexBridge.js` +- **前端组件**:`apps/desktop/src/view/pages/roles-window/components/RoleDetailPanel.tsx` +- **IPC Handler**:`apps/desktop/src/main/windows/ResourceListWindow.ts` (line 984-1011) +- **MCP 工具**:`packages/mcp-server/src/tools/action.ts` + +## 9. 相关操作列表 + +| 操作 | 方法 | 说明 | +|------|------|------| +| 获取目录 | `directory()` | 获取所有角色和组织信息 | +| 创建组织 | `synthesize(name, charter)` | 创建新组织 | +| 定义章程 | `charter(orgName, content)` | 定义或更新组织章程 | +| 解散组织 | `dissolve(orgName)` | 解散组织 | +| 设立职位 | `charge(orgName, position, procedure)` | 在组织中设立职位 | +| 任命角色 | `require(orgName, position, individual)` | 任命角色到职位 | +| 撤销职位 | `abolish(orgName, position)` | 撤销组织中的职位 | diff --git a/ROLEX_UPGRADE_GUIDE.md b/ROLEX_UPGRADE_GUIDE.md new file mode 100644 index 00000000..bb38a4e4 --- /dev/null +++ b/ROLEX_UPGRADE_GUIDE.md @@ -0,0 +1,257 @@ +# PromptX RoleX v2 升级到 1.1.0 指南 + +## 当前状态 + +- **RoleX 版本**: 1.1.0 +- **问题**: v2 角色列表显示 0 个角色 +- **原因**: RoleX 1.1.0 使用 SQLite 数据库存储,需要重新初始化 + +## 升级步骤 + +### 1. 备份现有数据(可选) + +如果你有重要的 v2 角色数据,先备份: + +```bash +# 备份旧的 RoleX 数据 +cp -r ~/.rolex ~/.rolex.backup.$(date +%Y%m%d) +``` + +### 2. 清理旧数据 + +RoleX 1.1.0 使用全新的数据库存储,旧的文件系统数据不兼容。建议清理: + +```bash +# 删除旧的 RoleX 数据(会删除所有 v2 角色) +rm -rf ~/.rolex + +# 或者只删除数据库文件,保留 roles 目录作为参考 +rm -f ~/.rolex/rolex.db +``` + +**注意**: 这会删除所有现有的 v2 角色数据。如果需要保留,请先备份。 + +### 3. 重新初始化 RoleX + +RoleX 1.1.0 会在首次运行时自动调用 `genesis()` 创建初始世界。 + +当前代码已经包含了 genesis 调用(RolexBridge.js line 98): + +```javascript +await this.rolex.genesis() +``` + +### 4. 验证升级 + +重新启动 PromptX Desktop 应用,然后: + +1. 打开角色窗口 +2. 切换到 "RoleX" 标签 +3. 应该能看到初始角色(如 nuwa) + +### 5. 检查日志 + +查看日志确认初始化成功: + +```bash +tail -f ~/.promptx/logs/promptx-$(date +%Y-%m-%d).log | grep -i rolex +``` + +应该看到类似的日志: + +``` +[RolexBridge] Initializing RoleX... +[RolexBridge] Creating platform... +[RolexBridge] Creating Rolex instance... +[RolexBridge] Running genesis... +[RolexBridge] RoleX initialized successfully +[RolexBridge] Census result: [...] +[RolexBridge] Found X V2 roles from database +``` + +## 已知问题和解决方案 + +### 问题 1: Census 返回空数组 + +**症状**: 日志显示 `[RolexBridge] Census result:` 后面是空的 + +**原因**: +1. genesis() 可能没有正确执行 +2. 或者 census.list 返回格式不符合预期 + +**解决方案**: + +检查 census.list 的返回格式。根据 RoleX 源码分析,census.list 应该返回 `CensusEntry[]` 数组: + +```typescript +interface CensusEntry { + id?: string; + name: string; + tag?: string; +} +``` + +当前代码(RolexBridge.js line 349-366)已经处理了这个格式。 + +### 问题 2: 数据库权限问题 + +**症状**: 初始化失败,提示数据库错误 + +**解决方案**: + +```bash +# 确保目录权限正确 +chmod 755 ~/.rolex +chmod 644 ~/.rolex/rolex.db +``` + +### 问题 3: Node.js 版本不兼容 + +**症状**: SQLite 相关错误 + +**要求**: Node.js 22+ 或 Bun + +**解决方案**: 升级 Node.js 或使用 Bun + +## 代码修改总结 + +已完成的修改(在 `packages/core/src/rolex/RolexBridge.js`): + +1. ✅ **localPlatform 配置** (line 91) + ```javascript + this.platform = localPlatform({ dataDir: this.rolexRoot }) + ``` + +2. ✅ **Rolex.create()** (line 94) + ```javascript + this.rolex = await Rolex.create(this.platform) + ``` + +3. ✅ **genesis() 调用** (line 98) + ```javascript + await this.rolex.genesis() + ``` + +4. ✅ **listV2Roles() 数据库查询** (line 339-410) + - 使用 `rolex.direct('!census.list')` 查询 + - 降级方案使用 `platform.repository.runtime` + +## 测试步骤 + +### 1. 重新构建 + +```bash +cd /e/Users/DF/Desktop/11111/PromptX +pnpm build:core +pnpm build:desktop +``` + +### 2. 启动应用 + +启动 PromptX Desktop 应用 + +### 3. 检查角色列表 + +1. 打开角色窗口 +2. 切换到 "RoleX" 标签 +3. 应该看到至少 1 个角色(nuwa) + +### 4. 测试 census.list + +在应用中激活 nuwa 角色,然后执行: + +```javascript +// 通过 MCP 或直接调用 +await rolex.direct('!census.list') +``` + +应该返回类似: + +```json +[ + { + "id": "nuwa", + "name": "Nuwa", + "tag": null + } +] +``` + +## 如果仍然没有角色 + +### 调试步骤 + +1. **检查 genesis 是否执行** + +查看日志中是否有 "Running genesis" 和 "RoleX initialized successfully" + +2. **手动查询数据库** + +```bash +# 如果有 sqlite3 命令 +sqlite3 ~/.rolex/rolex.db "SELECT * FROM nodes WHERE name='individual';" +``` + +3. **检查 bootstrap 配置** + +确认 localPlatform 配置中包含 bootstrap: + +```javascript +this.platform = localPlatform({ + dataDir: this.rolexRoot, + bootstrap: ['npm:@rolexjs/genesis'] // 添加这一行 +}) +``` + +4. **强制重新初始化** + +```bash +# 删除数据库 +rm -f ~/.rolex/rolex.db + +# 重启应用,会重新运行 genesis +``` + +## 迁移现有角色(可选) + +如果你有旧的 v2 角色需要迁移到 1.1.0: + +### 方案 1: 手动重建 + +1. 导出旧角色的 Feature 内容 +2. 使用 `!individual.born` 重新创建 +3. 使用 `!individual.train` 添加技能 + +### 方案 2: 使用 ResourceX + +1. 将旧角色打包为 ResourceX 资源 +2. 使用 `!prototype.summon` 导入 + +## 提交更改 + +完成升级后,提交代码: + +```bash +cd /e/Users/DF/Desktop/11111/PromptX +git add packages/core/src/rolex/RolexBridge.js +git commit -m "feat(core): complete RoleX 1.1.0 migration + +- Use Rolex.create() instead of new Rolex() +- Add genesis() call for world initialization +- Update listV2Roles() to query from database via census.list +- Fix localPlatform configuration with dataDir object" +``` + +## 参考文档 + +- RoleX 1.1.0 源码分析: `RoleX-Analysis-Phase*.md` +- RoleX GitHub: https://github.com/Deepractice/RoleX +- Genesis 包: `@rolexjs/genesis` + +## 需要帮助? + +如果升级过程中遇到问题: + +1. 检查日志文件: `~/.promptx/logs/promptx-*.log` +2. 查看 RoleX 数据目录: `~/.rolex/` +3. 确认 Node.js 版本: `node --version` (需要 22+) diff --git a/ROLEX_UPGRADE_QUICK.md b/ROLEX_UPGRADE_QUICK.md new file mode 100644 index 00000000..39721e0d --- /dev/null +++ b/ROLEX_UPGRADE_QUICK.md @@ -0,0 +1,113 @@ +# RoleX v2 升级到 1.1.0 - 快速指南 + +## 🎯 核心问题 + +RoleX 1.1.0 使用 SQLite 数据库存储,不再使用文件系统。需要重新初始化。 + +## ⚡ 快速升级步骤 + +### 1. 清理旧数据 + +```bash +# 删除旧的 RoleX 数据库(会清空所有 v2 角色) +rm -f ~/.rolex/rolex.db + +# 或者完全重置(推荐) +rm -rf ~/.rolex +``` + +**⚠️ 警告**: 这会删除所有现有的 v2 角色。如需保留,请先备份: +```bash +cp -r ~/.rolex ~/.rolex.backup +``` + +### 2. 重启应用 + +重新启动 PromptX Desktop 应用。 + +应用会自动: +1. 创建新的 SQLite 数据库 +2. 运行 `genesis()` 初始化世界 +3. 创建 Nuwa 种子角色 + +### 3. 验证 + +打开角色窗口 → 切换到 "RoleX" 标签 → 应该看到 **nuwa** 角色 + +## 🔧 已完成的代码修改 + +所有必要的代码修改已完成: + +1. ✅ **添加 bootstrap 配置** - 注册 Genesis 原型 +2. ✅ **使用 Rolex.create()** - 替代 new Rolex() +3. ✅ **调用 genesis()** - 初始化世界 +4. ✅ **数据库查询** - 使用 census.list 查询角色 + +## 📋 检查清单 + +- [ ] 删除旧数据库: `rm -f ~/.rolex/rolex.db` +- [ ] 重启 PromptX Desktop +- [ ] 打开角色窗口 +- [ ] 切换到 "RoleX" 标签 +- [ ] 确认看到 nuwa 角色 + +## 🐛 故障排除 + +### 问题: 仍然显示 0 个角色 + +**解决方案 1**: 完全重置 +```bash +rm -rf ~/.rolex +# 重启应用 +``` + +**解决方案 2**: 检查日志 +```bash +tail -f ~/.promptx/logs/promptx-$(date +%Y-%m-%d).log | grep -i rolex +``` + +应该看到: +``` +[RolexBridge] Running genesis... +[RolexBridge] RoleX initialized successfully +[RolexBridge] Found X V2 roles from database +``` + +### 问题: Genesis 失败 + +**可能原因**: Node.js 版本过低 + +**解决方案**: 确保 Node.js 22+ 或使用 Bun +```bash +node --version # 应该 >= 22 +``` + +## 📝 提交更改 + +```bash +git add packages/core/src/rolex/RolexBridge.js +git commit -m "feat(core): complete RoleX 1.1.0 migration with bootstrap config" +``` + +## 🎓 关键变化 + +| 项目 | 0.11.0 | 1.1.0 | +|------|--------|-------| +| 存储 | 文件系统 (`~/.rolex/roles/`) | SQLite 数据库 (`~/.rolex/rolex.db`) | +| 初始化 | `bootstrap()` | `genesis()` | +| 实例化 | `new Rolex(platform)` | `await Rolex.create(platform)` | +| Platform | `localPlatform(path)` | `localPlatform({ dataDir, bootstrap })` | +| 角色查询 | 文件系统扫描 | 数据库查询 (`census.list`) | + +## ✨ 新功能 + +- **Genesis 系统**: 自动创建初始世界和 Nuwa 角色 +- **Census 查询**: 统一的实体查询接口 +- **数据库存储**: 更高效的数据管理 +- **原型系统**: 支持角色模板和复用 + +## 📚 参考 + +- 详细升级指南: `ROLEX_UPGRADE_GUIDE.md` +- RoleX 源码分析: `RoleX-Analysis-Phase*.md` +- RoleX GitHub: https://github.com/Deepractice/RoleX diff --git a/V2_ROLE_TREE_CHANGES.md b/V2_ROLE_TREE_CHANGES.md new file mode 100644 index 00000000..cdd90f45 --- /dev/null +++ b/V2_ROLE_TREE_CHANGES.md @@ -0,0 +1,213 @@ +# V2 角色树状列表和数据修复 + +## 改动概述 + +本次更新主要解决两个问题: +1. V2 角色列表改为树状结构,按组织分组显示 +2. 修复 V2 角色详细页的数据显示问题(适配 RoleX 1.1.0 数据库架构) + +## 文件改动 + +### 1. 新增文件 + +#### `apps/desktop/src/view/pages/roles-window/components/RoleTreeListPanel.tsx` +- 新的树状角色列表组件 +- 支持按组织分组显示 V2 角色 +- 显示组织章程、职位信息 +- 可展开/折叠组织节点 +- 独立角色单独分组显示 +- V1 角色保持平面列表显示 + +**主要特性:** +- 组织节点显示:组织名称、章程、成员数量 +- 角色节点显示:角色名称、职位标签、来源标签、描述 +- 树状缩进和连接线视觉效果 +- 支持搜索和筛选 + +### 2. 修改文件 + +#### `apps/desktop/src/view/pages/roles-window/index.tsx` +**改动:** +- 导入 `RoleTreeListPanel` 替代 `RoleListPanel` +- 添加 `organizations` 状态管理 +- 修改 `loadRoles` 函数,增加组织目录数据加载: + ```typescript + // 加载组织目录信息 + const directoryResult = await window.electronAPI?.invoke("rolex:directory", {}) + // 更新角色的组织信息(org, position) + // 设置组织列表 + ``` +- 将组织数据传递给 `RoleTreeListPanel` + +#### `apps/desktop/src/view/pages/roles-window/components/RoleDetailPanel.tsx` +**改动:** +- 修复 `V2GoalsTab` 组件,适配 RoleX 1.1.0 的 `focus()` 输出格式 +- RoleX 1.1.0 的 `focus()` 返回文本格式而非结构化数据 +- 改为直接显示原始文本输出(使用 `
` 标签)
+
+**修改前:**
+```typescript
+const current = data?.focus?.current  // 期望结构化数据
+const otherGoals: any[] = data?.focus?.otherGoals || []
+```
+
+**修改后:**
+```typescript
+const focusText = data?.focus  // 直接使用文本输出
+// 使用 
 标签显示格式化文本
+```
+
+#### `apps/desktop/src/main/windows/ResourceListWindow.ts`
+**改动:**
+- 添加 `rolex:directory` IPC handler
+- 调用 `RolexActionDispatcher.dispatch('directory', {})` 获取组织目录
+- 返回解析后的 JSON 数据
+
+```typescript
+ipcMain.handle('rolex:directory', async (_evt) => {
+  const dispatcher = new RolexActionDispatcher()
+  const directoryResult = await dispatcher.dispatch('directory', {})
+  const directory = typeof directoryResult === 'string'
+    ? JSON.parse(directoryResult)
+    : directoryResult
+  return { success: true, data: directory }
+})
+```
+
+#### `apps/desktop/src/i18n/locales/en.json`
+**改动:**
+- 添加 `roles.filters.independent`: "Independent Roles"
+
+#### `apps/desktop/src/i18n/locales/zh-CN.json`
+**改动:**
+- 添加 `roles.filters.independent`: "独立角色"
+
+## 数据结构
+
+### RoleItem 扩展
+```typescript
+type RoleItem = {
+  id: string
+  name: string
+  description?: string
+  type: "role"
+  source?: string
+  version?: "v1" | "v2"
+  org?: string        // 新增:所属组织
+  position?: string   // 新增:在组织中的职位
+}
+```
+
+### OrganizationNode
+```typescript
+type OrganizationNode = {
+  name: string
+  charter?: string
+  roles: RoleItem[]
+}
+```
+
+### Directory 结构(来自 RoleX)
+```typescript
+interface Directory {
+  roles: Array<{
+    name: string        // 角色ID
+    org?: string        // 所属组织
+    position?: string   // 职位
+  }>
+  organizations: Array<{
+    name: string        // 组织名称
+    charter?: string    // 组织章程
+    members?: Array<{
+      name: string      // 成员角色ID
+      position: string  // 职位
+    }>
+  }>
+}
+```
+
+## UI 效果
+
+### V2 角色列表(树状结构)
+```
+┌─ 组织A (3)
+│  ├─ 角色1 [CTO]
+│  ├─ 角色2 [Engineer]
+│  └─ 角色3 [Designer]
+├─ 组织B (2)
+│  ├─ 角色4 [Manager]
+│  └─ 角色5 [Developer]
+└─ 独立角色 (2)
+   ├─ 角色6
+   └─ 角色7
+```
+
+### V1 角色列表(平面列表)
+```
+- 角色A
+- 角色B
+- 角色C
+```
+
+## API 调用流程
+
+### 加载角色列表
+1. `window.electronAPI?.getGroupedResources()` - 获取基础角色数据
+2. `window.electronAPI?.invoke("rolex:directory", {})` - 获取组织目录
+3. 合并数据:将组织信息(org, position)添加到角色对象
+4. 构建组织节点列表
+
+### 加载角色详情
+1. `window.electronAPI?.invoke("resources:getV2RoleData", { roleId })` - 获取角色详细数据
+2. 返回:`{ identity, focus, directory }`
+3. `focus` 是文本格式的当前目标输出
+4. `directory` 包含组织结构信息
+
+## 兼容性
+
+- ✅ V1 角色:保持原有平面列表显示
+- ✅ V2 角色(无组织):显示在"独立角色"分组
+- ✅ V2 角色(有组织):显示在对应组织节点下
+- ✅ 搜索和筛选:在所有模式下正常工作
+- ✅ V2 功能禁用时:自动切换到 V1 模式
+
+## 测试建议
+
+1. **树状列表测试**
+   - 创建多个组织和角色
+   - 验证组织节点展开/折叠
+   - 验证角色的组织和职位标签显示
+   - 测试搜索功能
+
+2. **数据显示测试**
+   - 查看有目标的 V2 角色的 Goals 标签
+   - 查看有组织的 V2 角色的 Organization 标签
+   - 验证 Identity 文本正确显示
+
+3. **边界情况测试**
+   - 无组织的 V2 角色
+   - 无目标的 V2 角色
+   - 空组织
+   - V1/V2 混合场景
+
+## 后续优化建议
+
+1. **Focus 数据解析**
+   - 可以考虑在后端解析 focus 文本输出为结构化数据
+   - 提取目标名称、计划、任务列表等信息
+   - 提供更友好的 UI 展示
+
+2. **组织管理功能**
+   - 添加创建组织的 UI
+   - 添加设立职位的 UI
+   - 添加任命角色的 UI
+
+3. **性能优化**
+   - 大量角色时的虚拟滚动
+   - 组织节点的懒加载
+   - 缓存组织目录数据
+
+4. **视觉优化**
+   - 组织节点的图标和颜色主题
+   - 更丰富的职位标签样式
+   - 组织层级的视觉连接线
diff --git a/V2_ROLE_TREE_TEST_GUIDE.md b/V2_ROLE_TREE_TEST_GUIDE.md
new file mode 100644
index 00000000..cfe7911c
--- /dev/null
+++ b/V2_ROLE_TREE_TEST_GUIDE.md
@@ -0,0 +1,226 @@
+# V2 角色树状列表测试指南
+
+## 测试前准备
+
+1. 确保已启用 V2 功能(设置 → 服务器配置 → 启用 V2 功能)
+2. 确保有一些 V2 角色存在
+3. 重启 PromptX Desktop
+
+## 测试步骤
+
+### 1. 基础显示测试
+
+**测试树状列表显示:**
+1. 打开角色窗口
+2. 切换到 "V2 Rolex" 标签
+3. 验证:
+   - [ ] 如果有组织,应该看到组织节点(带 Building2 图标)
+   - [ ] 组织节点显示组织名称和成员数量
+   - [ ] 组织节点可以展开/折叠(点击查看)
+   - [ ] 展开后显示该组织下的所有角色
+   - [ ] 角色显示职位标签(紫色)
+   - [ ] 无组织的角色显示在"独立角色"分组下
+
+**测试 V1 角色显示:**
+1. 切换到 "V1 DPML" 标签
+2. 验证:
+   - [ ] V1 角色保持平面列表显示(无树状结构)
+   - [ ] 所有 V1 角色正常显示
+
+### 2. 创建测试组织和角色
+
+使用 MCP action 工具或 AgentX 对话创建测试数据:
+
+```javascript
+// 1. 创建组织
+{
+  "operation": "synthesize",
+  "role": "nuwa",
+  "name": "TechCorp",
+  "charter": "致力于技术创新的组织"
+}
+
+// 2. 设立职位
+{
+  "operation": "charge",
+  "role": "nuwa",
+  "orgName": "TechCorp",
+  "position": "CTO",
+  "procedure": "负责技术战略"
+}
+
+{
+  "operation": "charge",
+  "role": "nuwa",
+  "orgName": "TechCorp",
+  "position": "Engineer",
+  "procedure": "负责产品开发"
+}
+
+// 3. 创建新角色(如果需要)
+// 使用 nuwa 角色创建新的 V2 角色
+
+// 4. 任命角色到职位
+{
+  "operation": "require",
+  "role": "nuwa",
+  "orgName": "TechCorp",
+  "position": "CTO",
+  "individual": "alice"  // 替换为实际角色ID
+}
+```
+
+### 3. 刷新并验证
+
+1. 关闭并重新打开角色窗口(或重启应用)
+2. 验证:
+   - [ ] 看到 "TechCorp" 组织节点
+   - [ ] 组织节点显示成员数量
+   - [ ] 展开后看到被任命的角色
+   - [ ] 角色显示对应的职位标签(如 "CTO")
+
+### 4. 角色详情测试
+
+**测试组织信息显示:**
+1. 选择一个有组织的 V2 角色
+2. 点击 "Organization" 标签
+3. 验证:
+   - [ ] 显示组织名称
+   - [ ] 显示角色的职位
+   - [ ] 显示组织成员列表
+
+**测试目标信息显示:**
+1. 选择一个有目标的 V2 角色
+2. 点击 "Goals" 标签
+3. 验证:
+   - [ ] 显示当前目标的文本输出
+   - [ ] 文本格式正确(使用等宽字体)
+   - [ ] 如果没有目标,显示"暂无活跃目标"
+
+**测试身份信息显示:**
+1. 点击 "Overview" 标签
+2. 验证:
+   - [ ] 显示角色描述
+   - [ ] 显示角色身份文本(identity)
+
+**测试结构信息显示:**
+1. 点击 "Structure" 标签
+2. 验证:
+   - [ ] 显示四个层级:Persona、Knowledge、Voice、Experience
+   - [ ] 可以展开查看各层级的文件
+   - [ ] 可以点击文件查看内容
+
+### 5. 搜索和筛选测试
+
+**测试搜索功能:**
+1. 在搜索框输入角色名称
+2. 验证:
+   - [ ] 只显示匹配的角色
+   - [ ] 组织节点根据是否有匹配角色显示/隐藏
+   - [ ] 清空搜索后恢复所有角色
+
+**测试来源筛选:**
+1. 点击不同的来源筛选按钮(All/System/Plaza/User)
+2. 验证:
+   - [ ] 只显示对应来源的角色
+   - [ ] 组织节点根据筛选结果更新
+
+### 6. 边界情况测试
+
+**测试无组织角色:**
+1. 创建一个新的 V2 角色但不加入任何组织
+2. 验证:
+   - [ ] 角色显示在"独立角色"分组下
+   - [ ] 点击角色查看详情正常
+
+**测试空组织:**
+1. 创建一个组织但不任命任何角色
+2. 验证:
+   - [ ] 组织节点显示成员数量为 0
+   - [ ] 展开后显示为空
+
+**测试混合场景:**
+1. 同时有 V1 和 V2 角色
+2. 验证:
+   - [ ] 切换 V1/V2 标签正常工作
+   - [ ] 数据不会混淆
+
+### 7. 性能测试
+
+**测试大量角色:**
+1. 如果有大量角色(>50个)
+2. 验证:
+   - [ ] 列表滚动流畅
+   - [ ] 展开/折叠响应及时
+   - [ ] 搜索响应快速
+
+## 常见问题排查
+
+### 问题1:看不到组织节点
+**可能原因:**
+- 没有创建组织
+- 角色没有被任命到组织
+- 需要刷新数据
+
+**解决方法:**
+1. 使用 action 工具创建组织和任命角色
+2. 关闭并重新打开角色窗口
+3. 检查后端日志是否有错误
+
+### 问题2:角色详情显示不正确
+**可能原因:**
+- RoleX 数据库数据格式问题
+- 后端 API 返回数据格式不匹配
+
+**解决方法:**
+1. 检查浏览器控制台是否有错误
+2. 查看后端日志
+3. 验证 `resources:getV2RoleData` 返回的数据结构
+
+### 问题3:树状列表不显示
+**可能原因:**
+- 组件导入错误
+- 构建失败
+
+**解决方法:**
+1. 重新构建:`pnpm --filter @promptx/desktop build`
+2. 检查构建日志是否有错误
+3. 重启应用
+
+## 调试技巧
+
+### 查看组织目录数据
+在浏览器控制台执行:
+```javascript
+window.electronAPI?.invoke("rolex:directory", {}).then(console.log)
+```
+
+### 查看角色详细数据
+```javascript
+window.electronAPI?.invoke("resources:getV2RoleData", { roleId: "nuwa" }).then(console.log)
+```
+
+### 查看所有角色数据
+```javascript
+window.electronAPI?.getGroupedResources().then(console.log)
+```
+
+## 预期结果
+
+✅ **成功标准:**
+1. V2 角色按组织分组显示
+2. 组织节点可以展开/折叠
+3. 角色显示职位标签
+4. 角色详情页正确显示组织、目标、身份信息
+5. 搜索和筛选功能正常
+6. V1 角色保持原有显示方式
+7. 性能流畅,无明显卡顿
+
+## 反馈
+
+如果发现问题,请记录:
+1. 问题描述
+2. 复现步骤
+3. 预期行为 vs 实际行为
+4. 浏览器控制台错误信息
+5. 后端日志(如果有)
diff --git a/V2_STRUCTURE_TAB_DATABASE_MIGRATION.md b/V2_STRUCTURE_TAB_DATABASE_MIGRATION.md
new file mode 100644
index 00000000..76b3b4ff
--- /dev/null
+++ b/V2_STRUCTURE_TAB_DATABASE_MIGRATION.md
@@ -0,0 +1,124 @@
+# V2 Structure Tab Database Migration
+
+## 问题
+V2 角色详情页的"结构"标签页仍在从文件系统读取 identity 数据(`~/.rolex/roles//identity/*.feature`),但 RoleX 1.1.0 已将所有数据迁移到 SQLite 数据库。
+
+## 解决方案
+
+### 1. 后端改动
+
+**文件**: `apps/desktop/src/main/windows/ResourceListWindow.ts`
+
+添加新的 IPC handler `rolex:getIdentityNodes`:
+
+```typescript
+ipcMain.handle('rolex:getIdentityNodes', async (_evt, payload: { roleId: string }) => {
+  try {
+    const { RolexBridge } = require('@promptx/core')
+    const bridge = RolexBridge.getInstance()
+    const identityData = await bridge.identity(payload.roleId)
+    return { success: true, data: identityData }
+  } catch (error: any) {
+    return { success: false, message: error?.message }
+  }
+})
+```
+
+这个 handler 调用 `RolexBridge.identity(roleId)`,它内部会:
+1. 激活角色:`await this.rolex.activate(roleId)`
+2. 获取身份投影:`return role.project()`
+3. 返回包含 identity 节点的结构化数据
+
+### 2. 前端改动
+
+**文件**: `apps/desktop/src/view/pages/roles-window/components/RoleDetailPanel.tsx`
+
+完全重写 `V2StructureTab` 组件:
+
+#### 主要变化:
+
+1. **数据加载**:
+   - 旧:调用 `resources:listV2RoleFiles` 获取文件列表
+   - 新:调用 `rolex:getIdentityNodes` 获取数据库节点
+
+2. **数据结构**:
+   - 旧:`files: string[]` (文件名数组)
+   - 新:`nodes: any[]` (节点对象数组,包含 id, name, information)
+
+3. **分类逻辑**:
+   - 旧:根据文件名模式 (`.knowledge.`, `.voice.`, `.experience.`)
+   - 新:根据节点的 name 或 id 包含的关键词
+
+4. **内容查看**:
+   - 旧:点击文件后调用 `resources:readV2RoleFile` 读取文件内容
+   - 新:直接显示节点的 `information` 字段(Gherkin Feature 内容)
+
+5. **编辑功能**:
+   - 旧:支持编辑和保存(调用 `resources:saveV2RoleFile`)
+   - 新:只读模式(数据库节点不支持 UI 直接编辑)
+
+## 数据流
+
+```
+用户点击"结构"标签
+  ↓
+V2StructureTab 调用 rolex:getIdentityNodes
+  ↓
+IPC handler 调用 RolexBridge.identity(roleId)
+  ↓
+RolexBridge 调用 role.project()
+  ↓
+RoleX 从 SQLite 数据库查询 identity 节点
+  ↓
+返回节点数组 [{ id, name, information, ... }]
+  ↓
+前端按类别显示节点
+  ↓
+用户点击节点查看 Gherkin 内容
+```
+
+## RoleX 1.1.0 数据库架构
+
+根据 RoleX 分析文档:
+
+- **nodes 表**:存储所有节点
+  - `id`: 节点唯一标识
+  - `name`: 节点名称
+  - `information`: Gherkin Feature 格式的内容
+  - `prototype`: 原型引用
+
+- **identity 节点**:角色的身份结构
+  - persona: 人格特征
+  - knowledge: 知识库
+  - voice: 语音风格
+  - experience: 经验记录
+
+## 测试建议
+
+1. 打开一个 V2 角色的详情页
+2. 切换到"结构"标签
+3. 验证显示了 4 个层级(persona, knowledge, voice, experience)
+4. 点击展开每个层级,查看节点列表
+5. 点击节点,查看 Gherkin Feature 内容
+6. 确认内容是从数据库加载的(不是文件系统)
+
+## 注意事项
+
+1. **只读模式**:当前实现将所有节点设为只读。如果需要编辑功能,需要:
+   - 添加新的 IPC handler 调用 RoleX API 更新节点
+   - 在前端恢复编辑和保存逻辑
+
+2. **节点分类**:当前使用简单的关键词匹配。如果 RoleX 提供了更明确的节点类型标识,应该使用那个。
+
+3. **错误处理**:如果角色没有 identity 数据,会显示空列表。可以考虑添加更友好的提示。
+
+## 相关文件
+
+- `packages/core/src/rolex/RolexBridge.js` - identity() 方法
+- `apps/desktop/src/main/windows/ResourceListWindow.ts` - IPC handlers
+- `apps/desktop/src/view/pages/roles-window/components/RoleDetailPanel.tsx` - V2StructureTab
+
+## 提交
+
+Commit: d869209
+Message: "fix(core): migrate V2 role Structure tab from filesystem to database"
diff --git a/apps/desktop/src/i18n/locales/en.json b/apps/desktop/src/i18n/locales/en.json
index cadab941..1df513f0 100644
--- a/apps/desktop/src/i18n/locales/en.json
+++ b/apps/desktop/src/i18n/locales/en.json
@@ -53,7 +53,9 @@
         "activate": "Set as active",
         "activated": "Configuration activated",
         "deleted": "Configuration deleted",
-        "saved": "Configuration saved"
+        "saved": "Configuration saved",
+        "anthropicOnly": "Anthropic format only",
+        "openaiNotSupported": "OpenAI protocol endpoints are not supported"
       },
       "skills": {
         "title": "Skills Configuration",
@@ -419,7 +421,8 @@
       "all": "All",
       "system": "System",
       "plaza": "Plaza",
-      "user": "User"
+      "user": "User",
+      "independent": "Independent Roles"
     },
     "activate": "Activate",
     "empty": "No roles found",
@@ -690,6 +693,7 @@
       },
       "rename": {
         "placeholder": "Enter conversation name",
+        "cancel": "Cancel",
         "saving": "Saving...",
         "save": "Save"
       },
@@ -768,5 +772,40 @@
         "toolPrompt": "What tools are available?"
       }
     }
+  },
+  "notifications": {
+    "button": "Notifications",
+    "title": "Notifications",
+    "unreadCount": "{{count}} unread notification",
+    "unreadCount_other": "{{count}} unread notifications",
+    "allRead": "All notifications read",
+    "markAllRead": "Mark all as read",
+    "markRead": "Mark as read",
+    "empty": "No notifications",
+    "time": {
+      "justNow": "Just now",
+      "minutesAgo": "{{count}} minute ago",
+      "minutesAgo_other": "{{count}} minutes ago",
+      "hoursAgo": "{{count}} hour ago",
+      "hoursAgo_other": "{{count}} hours ago",
+      "daysAgo": "{{count}} day ago",
+      "daysAgo_other": "{{count}} days ago"
+    },
+    "welcome": {
+      "title": "Welcome to PromptX!",
+      "content": "Thank you for using PromptX. Explore roles, tools, and AgentX to enhance your AI experience."
+    },
+    "update": {
+      "title": "New Features Available",
+      "content": "Check out the latest updates: RoleX lifecycle management, improved UI, and more!"
+    },
+    "updateV220": {
+      "title": "v2.2.0 Update Released",
+      "content": "New notification center, AgentX config with preset & OpenAI protocol detection, RoleX core upgraded from v0.11.0 to v1.3.0, fixed conversation rename bug, fixed Windows Git link issue. V2 role import/export/delete features temporarily disabled."
+    },
+    "rolexUpgrade": {
+      "title": "RoleX (V2) Architecture Upgrade",
+      "content": "Existing V2 roles need to be upgraded. Please activate Nuwa and ask her to upgrade and migrate your roles to continue using them."
+    }
   }
 }
diff --git a/apps/desktop/src/i18n/locales/en.json.bak b/apps/desktop/src/i18n/locales/en.json.bak
new file mode 100644
index 00000000..8af143f1
--- /dev/null
+++ b/apps/desktop/src/i18n/locales/en.json.bak
@@ -0,0 +1,776 @@
+{
+  "settings": {
+    "title": "Settings",
+    "subtitle": "Customize your PromptX experience",
+    "tabs": {
+      "system": "System",
+      "agentx": "AgentX",
+      "remote": "Remote Access"
+    },
+    "language": {
+      "title": "Language",
+      "description": "Choose your preferred language",
+      "english": "English",
+      "chinese": "简体中文"
+    },
+    "agentx": {
+      "title": "AgentX Configuration",
+      "description": "Configure AgentX AI assistant API connection",
+      "apiKey": {
+        "label": "API Key",
+        "placeholder": "sk-ant-...",
+        "description": "Your AgentX API Key"
+      },
+      "baseUrl": {
+        "label": "API Base URL",
+        "placeholder": "https://api.anthropic.com",
+        "description": "API server address, defaults to Anthropic official endpoint"
+      },
+      "model": {
+        "label": "Model",
+        "placeholder": "claude-opus-4-5-20251101",
+        "description": "Model name to use"
+      },
+      "testConnection": "Test Connection",
+      "testing": "Testing...",
+      "testSuccess": "Connection successful!",
+      "testFailed": "Connection failed",
+      "save": "Save Configuration",
+      "saving": "Saving...",
+      "saveSuccess": "AgentX configuration saved successfully",
+      "saveFailed": "Failed to save AgentX configuration",
+      "windowsGitWarning": {
+        "text": "AgentX requires Git to be installed on Windows.",
+        "link": "Download Git for Windows"
+      },
+      "profiles": {
+        "empty": "No configurations yet. Add one to get started.",
+        "add": "Add Configuration",
+        "addTitle": "Add Configuration",
+        "editTitle": "Edit Configuration",
+        "name": "Name",
+        "active": "Active",
+        "activate": "Set as active",
+        "activated": "Configuration activated",
+        "deleted": "Configuration deleted",
+        "saved": "Configuration saved",
+        "anthropicOnly": "Anthropic format only",
+        "openaiNotSupported": "OpenAI protocol endpoints are not supported"
+      },
+      "skills": {
+        "title": "Skills Configuration",
+        "description": "Manage enabled Skills that will be used for all AgentX conversations",
+        "empty": "No available Skills",
+        "emptyHint": "Click 'Add Skill' to import your first skill",
+        "refresh": "Refresh",
+        "save": "Save",
+        "saving": "Saving...",
+        "addSkill": "Add Skill",
+        "importing": "Importing...",
+        "loadError": "Failed to load Skills",
+        "saveSuccess": "Skills configuration saved",
+        "saveError": "Failed to save Skills configuration",
+        "importSuccess": "Skill \"{{name}}\" imported successfully",
+        "importError": "Failed to import Skill",
+        "invalidStructure": "Invalid skill package: SKILL.md not found in the zip",
+        "deleteConfirm": "Are you sure you want to delete skill \"{{name}}\"?",
+        "deleteSuccess": "Skill \"{{name}}\" deleted successfully",
+        "deleteError": "Failed to delete Skill",
+        "delete": "Delete"
+      }
+    },
+    "autoStart": {
+      "title": "Auto Start",
+      "description": "Launch PromptX automatically when computer starts",
+      "enable": "Enable auto start"
+    },
+    "webAccess": {
+      "title": "Remote Access",
+      "description": "Enable LAN access to PromptX via browser, supports multiple sessions",
+      "port": {
+        "label": "HTTP Port",
+        "description": "Web service port, default 5201"
+      },
+      "toggle": "Enable Remote Access",
+      "enabled": "Remote access enabled",
+      "disabled": "Remote access disabled",
+      "enableFailed": "Failed to enable",
+      "disableFailed": "Failed to disable",
+      "copyUrl": "URL copied",
+      "qrHint": "Scan to access on phone or other devices"
+    },
+    "server": {
+      "title": "Server Configuration",
+      "description": "Configure server host, port and debug settings",
+      "host": {
+        "label": "Server Host",
+        "placeholder": "127.0.0.1",
+        "description": "Host address for the server to listen on, e.g. 127.0.0.1"
+      },
+      "port": {
+        "label": "Server Port",
+        "placeholder": "5203",
+        "description": "Port for the server to listen on, range 1-65535"
+      },
+      "debug": {
+        "label": "Debug Mode",
+        "description": "Output more log information for troubleshooting"
+      },
+      "enableV2": {
+        "label": "Enable V2 Features (RoleX)",
+        "description": "Enable RoleX role lifecycle management: role creation, goal management, and organization features"
+      },
+      "save": "Save Configuration",
+      "saving": "Saving...",
+      "reset": "Reset to Default"
+    },
+    "mcp": {
+      "title": "MCP Configuration",
+      "description": "Configure Model Context Protocol servers",
+      "add": "Add Server",
+      "edit": "Edit Server",
+      "empty": "No MCP servers configured",
+      "emptyHint": "Click 'Add Server' to add your first MCP server",
+      "dialogDescription": "Configure the MCP server with JSON format",
+      "fields": {
+        "name": "Server Name",
+        "namePlaceholder": "e.g., my-mcp-server",
+        "config": "Configuration (JSON)",
+        "configHint": "Stdio: {command, args, env} | HTTP/SSE: {type, url}",
+        "enabled": "Enable this server"
+      },
+      "save": "Save",
+      "saving": "Saving...",
+      "cancel": "Cancel",
+      "saveSuccess": "MCP configuration saved successfully",
+      "saveFailed": "Failed to save MCP configuration",
+      "loadError": "Failed to load MCP servers",
+      "validation": {
+        "nameRequired": "Server name is required",
+        "commandOrUrlRequired": "Either 'command' (stdio) or 'url' (http/sse) is required",
+        "typeRequired": "The 'type' field is required when using 'url' (http or sse)",
+        "invalidJson": "Invalid JSON format",
+        "invalidConfig": "Invalid configuration",
+        "duplicate": "A server with this name already exists"
+      },
+      "builtin": "Built-in",
+      "cannotDeleteBuiltin": "Cannot delete built-in server",
+      "cannotDisableBuiltin": "Cannot disable built-in server"
+    },
+    "skills": {
+      "title": "Skills Configuration",
+      "description": "Manage and configure AI skills"
+    }
+  },
+
+  "logs": {
+    "title": "Application Logs",
+    "subtitle": "View and manage application logs",
+    "management": "Logs Management",
+    "description": "View, filter, and manage application logs",
+    "search": {
+      "placeholder": "Search logs..."
+    },
+    "filters": {
+      "all": "All",
+      "normal": "Normal",
+      "error": "Error",
+      "clearDate": "Clear"
+    },
+    "list": {
+      "title": "Log Files",
+      "count": "Log Files ({{count}})",
+      "empty": "No logs found"
+    },
+    "viewer": {
+      "selectPrompt": "Select a log file to view its content",
+      "loading": "Loading...",
+      "empty": "Log is empty"
+    },
+    "actions": {
+      "clearAll": "Clear All",
+      "delete": "Delete"
+    },
+    "messages": {
+      "loadFailed": "Failed to load logs",
+      "readFailed": "Failed to read log",
+      "deleteFailed": "Failed to delete log",
+      "deleteSuccess": "Log deleted successfully",
+      "deleteConfirm": "Delete log file: {{name}}?",
+      "clearAllConfirm": "Are you sure you want to delete ALL log files?",
+      "clearSuccess": "Cleared {{count}} log files",
+      "clearFailed": "Failed to clear logs"
+    }
+  },
+  "resources": {
+    "comingSoon": {
+      "title": "Coming Soon",
+      "description": "Agent Shop is under construction, stay tuned",
+      "badge": "Coming Soon"
+    },
+    "banner": {
+      "title": "Share Your Creations",
+      "subtitle": "Discover and share roles & tools with the community",
+      "publishRole": "Publish Role",
+      "publishTool": "Publish Tool"
+    },
+    "search": {
+      "placeholder": "Search resources / roles / tools"
+    },
+    "import": {
+      "title": "Import Resource",
+      "description": "Import a role or tool from a file ",
+      "button": "Import Resource",
+      "methods": {
+        "file": "File Import",
+        "url": "URL Import (Coming Soon)"
+      },
+      "fields": {
+        "resourceType": "Resource Type",
+        "zipFile": "ZIP File",
+        "browse": "Browse",
+        "upload": "Upload",
+        "customId": "Custom ID (Optional)",
+        "customIdPlaceholder": "Leave empty to use original ID",
+        "customIdHint": "The ID will be used as the folder name",
+        "customName": "Custom Name (Optional)",
+        "customNamePlaceholder": "Display name for the resource",
+        "customDescription": "Custom Description (Optional)",
+        "customDescriptionPlaceholder": "Brief description of the resource"
+      },
+      "actions": {
+        "cancel": "Cancel",
+        "import": "Import",
+        "importing": "Importing..."
+      },
+      "messages": {
+        "selectFile": "Please select a file",
+        "importSuccess": "Resource imported successfully",
+        "importFailed": "Failed to import resource",
+        "invalidStructure": "Invalid resource structure",
+        "fileNotFound": "File not found",
+        "resourceExists": "Resource exists",
+        "resourceExistsMessage": "Resource \"{{id}}\" already exists. Overwrite?",
+        "cancelled": "Operation cancelled"
+      },
+      "urlComingSoon": "URL import feature is coming soon..."
+    },
+    "cards": {
+      "roles": {
+        "title": "Roles",
+        "description": "Number of activatable roles"
+      },
+      "tools": {
+        "title": "Tools", 
+        "description": "Number of executable tools"
+      },
+      "sources": {
+        "title": "Sources",
+        "description": "Resource count by source"
+      }
+    },
+    "filters": {
+      "type": "Type:",
+      "source": "Source:",
+      "all": "All",
+      "roles": "Roles",
+      "tools": "Tools",
+      "system": "System",
+      "user": "User"
+    },
+    "actions": {
+      "edit": "Edit",
+      "download": "Download",
+      "delete": "Delete",
+      "use": "Use",
+      "export": "Export",
+      "exportSuccess": "Exported successfully",
+      "exportFailed": "Export failed"
+    },
+    "messages": {
+      "loadFailed": "Failed to load resources",
+      "searchFailed": "Search failed",
+      "downloadSuccess": "Saved to: {{path}}",
+      "downloadFailed": "Download failed",
+      "deleteConfirm": "Confirm delete {{type}} \"{{name}}\"? This action cannot be undone.",
+      "deleteSuccess": "Deleted successfully",
+      "deleteFailed": "Delete failed",
+      "deleteOnlyUser": "Only user resources can be deleted (system/project cannot be deleted)",
+      "noMore": "No more items :-I",
+      "editOnlyUser": "Only user resources can be edited "
+    },
+    "types": {
+      "role": "Role",
+      "tool": "Tool"
+    },
+    "editor": {
+      "title": "Edit {{type}}: {{name}}",
+      "fields": {
+        "name": "Name",
+        "description": "Description",
+        "namePlaceholder": "Enter resource name",
+        "descriptionPlaceholder": "Enter resource description"
+      },
+      "fileList": "File List",
+      "editFile": "Edit: {{file}}",
+      "selectFile": "Please select a file",
+      "fileContent": "File content...",
+      "selectFilePrompt": "Please select a file to edit from the left",
+      "buttons": {
+        "saveResourceInfo": "Save Resource Info",
+        "saveFile": "Save File",
+        "close": "Close",
+        "saving": "Saving...",
+        "copyPrompt": "Copy Prompt",
+        "copySuccess": "Prompt copied to clipboard"
+      },
+      "messages": {
+        "loadFilesFailed": "Failed to load file list",
+        "readFileFailed": "Failed to read file",
+        "editorOpenFailed": "Failed to open editor",
+        "saveFailed": "Save failed",
+        "saveSuccess": "Saved successfully",
+        "resourceInfoSaveSuccess": "Resource info saved successfully",
+        "saveResourceInfoFailed": "Failed to save resource info",
+        "onlyUserEditable": "Only user resources can be edited (system/project are read-only)",
+        "loading": "Loading...",
+        "loadingFileContent": "Loading file content..."
+      },
+      "tabs": {
+        "editor": "Editor",
+        "preview": "Preview"
+      },
+      "preview": {
+        "loading": "Loading preview...",
+        "empty": "No preview content",
+        "failed": "Failed to load preview"
+      },
+      "readOnly": "⚠️ This resource is in read-only mode ({{source}})"
+    }
+  },
+  "tray": {
+    "tooltip": "PromptX Desktop",
+    "status": {
+      "running": "Running",
+      "stopped": "Stopped",
+      "starting": "Starting...",
+      "stopping": "Stopping...",
+      "error": "Error",
+      "unknown": "Unknown"
+    },
+    "menu": {
+      "status": "Status: {{status}}",
+      "startServer": "Start Server",
+      "stopServer": "Stop Server",
+      "toggleServer": "Toggle Server",
+      "copyAddress": "Copy Server Address",
+      "openMainWindow": "Open Main Window",
+      "manageResources": "Manage Resources",
+      "showLogs": "Show Logs",
+      "settings": "Settings...",
+      "checkUpdates": "Check for Updates...",
+      "checkingUpdates": "Checking for Updates...",
+      "downloadUpdate": "Download Update ({{version}})",
+      "downloading": "Downloading... {{percent}}%",
+      "installUpdate": "Install Update ({{version}})",
+      "retryUpdate": "Retry Update Check",
+      "about": "About PromptX",
+      "quit": "Quit PromptX"
+    },
+    "windows": {
+      "logs": "PromptX Logs",
+      "settings": "PromptX Settings"
+    }
+  },
+  "messages": {
+    "autoStartEnabled": "Auto start enabled successfully",
+    "autoStartDisabled": "Auto start disabled successfully",
+    "autoStartError": "Failed to toggle auto start",
+    "configSaved": "Configuration saved successfully",
+    "configSaveError": "Failed to save configuration",
+    "configReset": "Configuration reset to default",
+    "configResetError": "Failed to reset configuration",
+    "loadError": "Failed to load settings, please try again",
+    "languageChanged": "Language changed successfully",
+    "restartRequired": "Restart Required",
+    "restartDescription": "Server configuration has been saved. The application needs to restart for changes to take effect.",
+    "restartNow": "Restart Now",
+    "restartLater": "Later",
+    "restartError": "Failed to restart application"
+  },
+  "sidebar": {
+    "agentx": "AgentX Chat",
+    "resources": "Agent Shop",
+    "roles":"Roles",
+    "tools":"Tools",
+    "logs": "Logs",
+    "settings": "Settings",
+    "update": "Check for Updates"
+  },
+  "roles": {
+    "search": {
+      "placeholder": "Search roles..."
+    },
+    "stats": {
+      "total": "Total Roles",
+      "system": "System Roles",
+      "user": "User Roles"
+    },
+    "filters": {
+      "source": "Source:",
+      "all": "All",
+      "system": "System",
+      "plaza": "Plaza",
+      "user": "User",
+      "independent": "Independent Roles"
+    },
+    "activate": "Activate",
+    "empty": "No roles found",
+    "noDescription": "No description available",
+    "detail": {
+      "description": "Description",
+      "createRole":"Create Role",
+      "close": "Close",
+      "editRole": "Edit Role",
+      "editRoleName": "Name",
+      "uploadAvatar": "Upload Avatar",
+      "structure": "Structure",
+      "memory": "Memory",
+      "architecture": "DPML Layer Architecture (Three-Layer Architecture, On-Demand Loading)",
+      "memoryEmpty": "No memory data available",
+      "selectRole": "Select a role to view details",
+      "overview": "Overview",
+      "structure": "Structure",
+      "prompt": "Role Prompt",
+      "rolePrompt": "Role Prompt",
+      "promptLoading": "Loading prompt...",
+      "promptError": "Failed to load prompt",
+      "promptLoadError": "Failed to load prompt",
+      "roleFile": "Role Definition",
+      "personalityLayer": "Personality Layer",
+      "personalityDesc": "Defines thinking patterns and cognitive traits",
+      "principleLayer": "Principle Layer",
+      "principleDesc": "Defines workflows and execution standards",
+      "knowledgeLayer": "Knowledge Layer",
+      "knowledgeDesc": "Defines domain expertise and professional knowledge",
+      "noFiles": "No files",
+      "loadingFiles": "Loading files...",
+      "loadFilesFailed": "Failed to load file list",
+      "systemRoleReadOnly": "Built-in system roles cannot be viewed",
+      "fileContent": "File Content",
+      "loadingContent": "Loading content...",
+      "loadContentFailed": "Failed to load content",
+      "readOnly": "Read-only",
+      "save": "Save",
+      "saving": "Saving...",
+      "saveSuccess": "Saved successfully",
+      "saveFailed": "Failed to save",
+      "close": "Close",
+      "goals": "Goals",
+      "organization": "Organization",
+      "identity": "Identity",
+      "currentGoal": "Current Goal",
+      "noGoals": "No active goals",
+      "goalPlan": "Execution Plan",
+      "goalTasks": "Tasks",
+      "noTasks": "No tasks",
+      "noOrganization": "Not in any organization",
+      "orgPosition": "Position",
+      "orgMembers": "Members",
+      "taskPending": "Pending",
+      "taskInProgress": "In Progress",
+      "taskCompleted": "Completed",
+      "loadingData": "Loading...",
+      "v2PersonaLayer": "Persona",
+      "v2PersonaDesc": "Core personality and behavioral traits",
+      "v2KnowledgeLayer": "Knowledge",
+      "v2KnowledgeDesc": "Domain knowledge and professional expertise",
+      "v2VoiceLayer": "Voice",
+      "v2VoiceDesc": "Language style and communication patterns",
+      "v2ExperienceLayer": "Experience",
+      "v2ExperienceDesc": "Accumulated experience from practice"
+    },
+    "memory": {
+      "overview": "Overview",
+      "engrams": "Engrams",
+      "network": "Network",
+      "cues": "Cues",
+      "engramCount": "Engrams",
+      "cueCount": "Cues",
+      "connections": "Connections",
+      "lastActive": "Last Active",
+      "topCues": "Top Cues",
+      "noMemoryData": "No memory data available",
+      "loading": "Loading...",
+      "type": "Type",
+      "strength": "Strength",
+      "schema": "Schema",
+      "search": "Search...",
+      "page": "Page",
+      "clickToExplore": "Click to explore",
+      "recallFrequency": "Recall Frequency",
+      "weight": "Weight",
+      "edit": "Edit",
+      "delete": "Delete",
+      "save": "Save",
+      "cancel": "Cancel",
+      "confirmDelete": "Are you sure?",
+      "deleteCue": "Delete Cue",
+      "typeAtomic": "Atomic",
+      "typeLink": "Link",
+      "typePattern": "Pattern",
+      "allTypes": "All"
+    },
+    "messages": {
+      "loadFailed": "Failed to load roles",
+      "activateSuccess": "Role \"{{name}}\" activated successfully",
+      "activateFailed": "Failed to activate role",
+      "activatePrompt": "Activate role {{name}}",
+      "deleteConfirm": "Delete role \"{{name}}\"? This cannot be undone.",
+      "deleteSuccess": "Role \"{{name}}\" deleted successfully",
+      "deleteFailed": "Failed to delete role",
+      "deleteOnlyUser": "Only user-created roles can be deleted"
+    }
+  },
+  "tools": {
+    "search": {
+      "placeholder": "Search tools..."
+    },
+    "stats": {
+      "total": "Total Tools",
+      "system": "System Tools",
+      "user": "User Tools"
+    },
+    "filters": {
+      "source": "Source:",
+      "all": "All",
+      "system": "System",
+      "plaza": "Plaza",
+      "user": "User"
+    },
+    "execute": "Execute",
+    "empty": "No tools found",
+    "noDescription": "No description available",
+    "detail": {
+      "description": "Description",
+      "parameters": "Parameters",
+      "parametersPlaceholder": "{\"key\": \"value\"}",
+      "parametersHint": "Optional. Enter JSON parameters for tool execution.",
+      "result": "Execution Result",
+      "close": "Close",
+      "overview": "Overview",
+      "test": "Test",
+      "configure": "Configure",
+      "logs": "Logs",
+      "install": "Install Tool",
+      "output": "Output",
+      "outputEmpty": "Run a test to see output here...",
+      "runTest": "Run Test",
+      "envVars": "Environment Variables",
+      "noEnvVars": "No environment variables configured",
+      "noLogs": "No execution logs available",
+      "selectTool": "Select a tool to view details",
+      "deleteTool": "Delete Tool",
+      "tags": "Tags",
+      "manual": "Documentation",
+      "info": "Tool Info",
+      "source": "Source",
+      "execSuccess": "Execution succeeded",
+      "execFailed": "Execution failed",
+      "paramsHint": "Enter JSON parameters for tool execution. Leave {} for no parameters.",
+      "executing": "Executing...",
+      "execNoOutput": "Execution completed with no output",
+      "toolFiles": "Tool Files",
+      "noFiles": "No tool files found",
+      "readOnly": "Read-only",
+      "loadingFile": "Loading file...",
+      "paramSchema": "Parameter Schema",
+      "execHistory": "Execution History",
+      "clearLogs": "Clear Logs",
+      "formMode": "Form",
+      "edit": "Edit",
+      "save": "Save",
+      "cancel": "Cancel",
+      "editInfo": "Edit Info",
+      "editName": "Tool Name",
+      "editDescription": "Tool Description",
+      "namePlaceholder": "Enter tool name",
+      "descriptionPlaceholder": "Enter tool description",
+      "createTool": "Create Tool"
+    },
+    "messages": {
+      "loadFailed": "Failed to load tools",
+      "executeSuccess": "Tool \"{{name}}\" executed successfully",
+      "executeFailed": "Failed to execute tool",
+      "invalidParams": "Invalid JSON parameters",
+      "deleteOnlyUser": "Only user-created tools can be deleted",
+      "deleteConfirm": "Are you sure you want to delete tool \"{{name}}\"? This cannot be undone.",
+      "deleteSuccess": "Tool \"{{name}}\" deleted successfully",
+      "deleteFailed": "Failed to delete tool",
+      "saveSuccess": "File saved successfully",
+      "saveFailed": "Failed to save file",
+      "updateSuccess": "Tool info updated successfully",
+      "updateFailed": "Failed to update tool info",
+      "updateOnlyUser": "Only user-created tools can be edited"
+    }
+  },
+  "update": {
+    "title": "Software Update",
+    "description": "Check for and install software updates",
+    "currentVersion": "Current Version",
+    "checkNow": "Check Now",
+    "checking": "Checking...",
+    "upToDate": "You're up to date!",
+    "upToDateMessage": "You're running the latest version",
+    "newVersion": "New Version Available",
+    "download": "Download",
+    "downloading": "Downloading...",
+    "downloadComplete": "Download complete",
+    "readyToInstall": "Update ready to install",
+    "installNow": "Install Now",
+    "releaseNotes": "What's New",
+    "checkFailed": "Failed to check for updates",
+    "downloadFailed": "Failed to download update",
+    "installFailed": "Failed to install update",
+    "errorMessage": "An error occurred while checking for updates"
+  },
+  "datePicker": {
+    "placeholder": "Select date",
+    "selectDate": "Select Date",
+    "clear": "Clear"
+  },
+  "agentxUI": {
+    "chat": {
+      "placeholder": "Type a message...",
+      "toolbar": {
+        "emoji": "Emoji",
+        "file": "File",
+        "save": "Save conversation"
+      },
+      "status": {
+        "thinking": "Thinking",
+        "responding": "Responding",
+        "planning": "Planning",
+        "executing": "Executing",
+        "error": "Error",
+        "idle": "Idle",
+        "queued": "Queued",
+        "processing": "Processing"
+      },
+      "messageCount": "{{count}} message",
+      "messageCount_other": "{{count}} messages",
+      "actions": {
+        "stop": "Stop",
+        "send": "Send (Enter)",
+        "remove": "Remove",
+        "download": "Download",
+        "removeImage": "Remove image",
+        "escToStop": "esc to stop",
+        "copy": "Copy",
+        "regenerate": "Regenerate",
+        "like": "Like",
+        "dislike": "Dislike"
+      },
+      "empty": {
+        "title": "No conversation selected",
+        "description": "Select a conversation or start a new one"
+      },
+      "dropToSend": "Drop to send",
+      "errors": {
+        "maxAttachments": "Maximum {{max}} attachments allowed",
+        "fileTooLarge": "{{name}} exceeds the {{max}} size limit",
+        "fileTypeNotAccepted": "{{name}} is not a supported file type"
+      }
+    },
+    "conversations": {
+      "title": "Conversations",
+      "new": "New conversation",
+      "search": "Search conversations...",
+      "empty": {
+        "title": "No conversations yet",
+        "description": "Start a new conversation to begin",
+        "action": "New conversation"
+      },
+      "rename": {
+        "placeholder": "Enter conversation name",
+        "cancel": "Cancel",
+        "saving": "Saving...",
+        "save": "Save"
+      },
+      "delete": {
+        "confirm": "Are you sure you want to delete \"{{name}}\"?",
+        "cancel": "Cancel",
+        "confirm_button": "Delete",
+        "deleting": "Deleting..."
+      },
+      "actions": {
+        "rename": "Rename",
+        "delete": "Delete"
+      },
+      "status": {
+        "online": "Online",
+        "offline": "Offline"
+      },
+      "untitled": "Untitled"
+    },
+    "messages": {
+      "empty": {
+        "title": "No messages",
+        "description": "Start the conversation by sending a message"
+      }
+    },
+    "tool": {
+      "status": {
+        "planning": "Planning...",
+        "executing": "Executing...",
+        "completed": "Completed",
+        "error": "Error"
+      }
+    },
+    "sidebar": {
+      "expand": "Expand sidebar",
+      "collapse": "Collapse sidebar"
+    },
+    "list": {
+      "empty": {
+        "description": "Get started by creating a new item"
+      }
+    },
+    "time": {
+      "justNow": "Just now",
+      "unknown": "Unknown",
+      "minAgo": "1 min ago",
+      "minsAgo": "{{count}} mins ago",
+      "hourAgo": "1 hour ago",
+      "hoursAgo": "{{count}} hours ago",
+      "dayAgo": "1 day ago",
+      "daysAgo": "{{count}} days ago"
+    },
+    "mobile": {
+      "search": "Search..."
+    },
+    "page": {
+      "notConfigured": {
+        "title": "AgentX Not Configured",
+        "description": "Please configure your AgentX API Key in Settings to use AgentX.",
+        "hint": "Settings → AgentX Configuration → Enter API Key → Save"
+      },
+      "connectionError": "Connection Error",
+      "connecting": "Connecting to AgentX..."
+    },
+    "welcome": {
+      "tagline": "PromptX, Making AI Accessible",
+      "inputPlaceholder": "Ask me anything...",
+      "presets": {
+        "luban": "Activate Luban to build tools",
+        "lubanPrompt": "Activate Luban, I want to build a tool",
+        "nuwa": "Activate Nuwa to create roles",
+        "nuwaPrompt": "Activate Nuwa, I want to create a role",
+        "role": "Explore available roles",
+        "rolePrompt": "What roles are available?",
+        "tool": "Explore available tools",
+        "toolPrompt": "What tools are available?"
+      }
+    }
+  }
+}
diff --git a/apps/desktop/src/i18n/locales/zh-CN.json b/apps/desktop/src/i18n/locales/zh-CN.json
index fee51d7a..7bc628af 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.json
+++ b/apps/desktop/src/i18n/locales/zh-CN.json
@@ -53,7 +53,9 @@
         "activate": "设为当前",
         "activated": "已切换配置",
         "deleted": "配置已删除",
-        "saved": "配置已保存"
+        "saved": "配置已保存",
+        "anthropicOnly": "仅支持 Anthropic 格式",
+        "openaiNotSupported": "暂不支持 OpenAI 协议的端点"
       },
       "skills": {
         "title": "Skills 配置",
@@ -418,7 +420,8 @@
       "all": "全部",
       "system": "系统",
       "plaza": "广场",
-      "user": "用户"
+      "user": "用户",
+      "independent": "独立角色"
     },
     "activate": "激活",
     "empty": "暂无角色",
@@ -687,6 +690,7 @@
       },
       "rename": {
         "placeholder": "输入对话名称",
+        "cancel": "取消",
         "saving": "保存中...",
         "save": "保存"
       },
@@ -765,5 +769,40 @@
         "toolPrompt": "有哪些可用的工具?"
       }
     }
+  },
+  "notifications": {
+    "button": "通知",
+    "title": "通知中心",
+    "unreadCount": "{{count}} 条未读通知",
+    "unreadCount_other": "{{count}} 条未读通知",
+    "allRead": "所有通知已读",
+    "markAllRead": "全部标为已读",
+    "markRead": "标为已读",
+    "empty": "暂无通知",
+    "time": {
+      "justNow": "刚刚",
+      "minutesAgo": "{{count}} 分钟前",
+      "minutesAgo_other": "{{count}} 分钟前",
+      "hoursAgo": "{{count}} 小时前",
+      "hoursAgo_other": "{{count}} 小时前",
+      "daysAgo": "{{count}} 天前",
+      "daysAgo_other": "{{count}} 天前"
+    },
+    "welcome": {
+      "title": "欢迎使用 PromptX!",
+      "content": "感谢使用 PromptX。探索角色、工具和 AgentX,提升您的 AI 体验。"
+    },
+    "update": {
+      "title": "新功能上线",
+      "content": "查看最新更新:RoleX 生命周期管理、界面优化等更多功能!"
+    },
+    "updateV220": {
+      "title": "v2.2.0 版本更新",
+      "content": "新增通知中心、AgentX 配置增加预设与 OpenAI 协议识别功能、V2 RoleX 内核从 v0.11.0 更新到 v1.3.0、修复对话重命名后无效的 bug、修复 Windows 平台点击 Git 链接无响应的 bug。V2 的角色导入导出与删除功能暂时下架。"
+    },
+    "rolexUpgrade": {
+      "title": "RoleX(V2)架构升级",
+      "content": "原有 V2 角色需要升级。请激活女娲,让女娲进行升级与迁移才能继续使用。"
+    }
   }
 }
diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts
index 4b97f6a4..0bbd365c 100644
--- a/apps/desktop/src/main/index.ts
+++ b/apps/desktop/src/main/index.ts
@@ -69,6 +69,7 @@ class PromptXDesktopApp {
     this.setupLanguageIPC()
     this.setupLogsIPC()
     this.setupDialogIPC()
+    this.setupShellIPC()
     this.setupAgentXIPC()
     this.setupWebAccessIPC()
 
@@ -566,6 +567,34 @@ class PromptXDesktopApp {
     })
   }
 
+  private setupShellIPC(): void {
+    // 打开外部链接 - 在新的 Electron 窗口中打开
+    ipcMain.handle('shell:openExternal', async (_event, url: string) => {
+      try {
+        const { BrowserWindow } = await import('electron')
+
+        // 创建新的浏览器窗口
+        const browserWindow = new BrowserWindow({
+          width: 1200,
+          height: 800,
+          webPreferences: {
+            nodeIntegration: false,
+            contextIsolation: true,
+          },
+          title: 'Browser',
+        })
+
+        // 加载 URL
+        await browserWindow.loadURL(url)
+
+        logger.info('Opened URL in Electron browser window:', url)
+      } catch (error) {
+        logger.error('Failed to open URL in browser window:', String(error))
+        throw error
+      }
+    })
+  }
+
   private setupAgentXIPC(): void {
     // 获取 AgentX 服务器 URL
     ipcMain.handle('agentx:getServerUrl', () => {
diff --git a/apps/desktop/src/main/windows/ResourceListWindow.ts b/apps/desktop/src/main/windows/ResourceListWindow.ts
index 71006644..dccc9dec 100644
--- a/apps/desktop/src/main/windows/ResourceListWindow.ts
+++ b/apps/desktop/src/main/windows/ResourceListWindow.ts
@@ -1000,8 +1000,7 @@ export class ResourceListWindow {
         // 获取组织目录
         let directory = null
         try {
-          const dirResult = await dispatcher.dispatch('directory', { role: payload.roleId })
-          directory = typeof dirResult === 'string' ? JSON.parse(dirResult) : dirResult
+          directory = await dispatcher.dispatch('directory', { role: payload.roleId })
         } catch { /* no organizations */ }
 
         return { success: true, identity, focus, directory }
@@ -1010,6 +1009,24 @@ export class ResourceListWindow {
       }
     })
 
+    // 获取 RoleX 组织目录
+    ipcMain.handle('rolex:directory', async (_evt) => {
+      try {
+        const core = await import('@promptx/core')
+        const coreExports = (core as any).default || core
+        const { RolexActionDispatcher } = (coreExports as any).rolex
+        const dispatcher = new RolexActionDispatcher()
+
+        // directory() 现在返回结构化的 JSON 数据
+        const directory = await dispatcher.dispatch('directory', {})
+
+        return { success: true, data: directory }
+      } catch (error: any) {
+        console.error('Failed to get directory:', error)
+        return { success: false, message: error?.message }
+      }
+    })
+
     // V2 角色文件列表(~/.rolex/roles//identity/)
     ipcMain.handle('resources:listV2RoleFiles', async (_evt, payload: { roleId: string }) => {
       try {
@@ -1056,6 +1073,49 @@ export class ResourceListWindow {
       }
     })
 
+    // V2 角色身份结构(从数据库读取)
+    ipcMain.handle('rolex:getIdentityNodes', async (_evt, payload: { roleId: string }) => {
+      try {
+        const core = require('@promptx/core')
+        const bridge = core.rolex.getRolexBridge()
+        const identityText = await bridge.identity(payload.roleId)
+        console.log('[rolex:getIdentityNodes] roleId:', payload.roleId)
+        console.log('[rolex:getIdentityNodes] identityText type:', typeof identityText)
+
+        // Parse the text into structured nodes
+        const nodes: any[] = []
+        if (typeof identityText === 'string') {
+          // Split by ## markers to find top-level nodes
+          const sections = identityText.split(/\n## \[/)
+          for (let i = 1; i < sections.length; i++) {
+            const section = sections[i]
+            // Extract node type and id from [type] (id)
+            const match = section.match(/^([^\]]+)\]\s*\(([^)]+)\)/)
+            if (match) {
+              const nodeType = match[1].trim()
+              const nodeId = match[2].trim()
+              // Extract the content (everything after the first line until next ## or end)
+              const contentMatch = section.match(/\n([\s\S]*?)(?=\n## \[|$)/)
+              const content = contentMatch ? contentMatch[1].trim() : ''
+
+              nodes.push({
+                id: nodeId,
+                name: nodeId,
+                type: nodeType,
+                information: content
+              })
+            }
+          }
+        }
+
+        console.log('[rolex:getIdentityNodes] Parsed nodes:', nodes.length)
+        return { success: true, data: nodes }
+      } catch (error: any) {
+        console.error('[rolex:getIdentityNodes] Error:', error)
+        return { success: false, message: error?.message }
+      }
+    })
+
     // 获取角色头像(profile.png/jpg/jpeg/webp)→ base64 data URL
     ipcMain.handle('resources:getRoleAvatar', async (_evt, payload: { id: string; source?: string }) => {
       try {
diff --git a/apps/desktop/src/preload/index.ts b/apps/desktop/src/preload/index.ts
index c05a3a42..8a6d004d 100644
--- a/apps/desktop/src/preload/index.ts
+++ b/apps/desktop/src/preload/index.ts
@@ -174,7 +174,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
   },
   // Shell API
   shell: {
-    openExternal: (url: string) => shell.openExternal(url),
+    openExternal: (url: string) => ipcRenderer.invoke('shell:openExternal', url),
   },
   // System info
   platform: process.platform,
diff --git a/apps/desktop/src/view/components/agentx-ui/components/container/AgentList.tsx b/apps/desktop/src/view/components/agentx-ui/components/container/AgentList.tsx
index d8671a80..cd4e931c 100644
--- a/apps/desktop/src/view/components/agentx-ui/components/container/AgentList.tsx
+++ b/apps/desktop/src/view/components/agentx-ui/components/container/AgentList.tsx
@@ -194,7 +194,7 @@ export function AgentList({
   const items: ListPaneItem[] = React.useMemo(() => {
     return images.map((img) => ({
       id: img.imageId,
-      title: firstMessages[img.imageId] || img.name || t("agentxUI.conversations.untitled"),
+      title: img.name || firstMessages[img.imageId] || t("agentxUI.conversations.untitled"),
       trailing: (
          handleDialogClose(false)}
               disabled={isRenaming}
             >
-              Cancel
+              {t("agentxUI.conversations.rename.cancel")}
             
             
+            )}
+          
+        
+
+        
+          {notifications.length === 0 ? (
+            
+ +

{t("notifications.empty")}

+
+ ) : ( +
+ {notifications.map((notification) => ( +
+
+
+ {getNotificationIcon(notification.type)} +
+
+
+
+

+ {t(notification.title)} +

+

+ {t(notification.content)} +

+
+ +
+
+ + {formatTime(notification.timestamp)} + + {!notification.read && ( + + )} +
+
+
+
+ ))} +
+ )} +
+ + + ) +} diff --git a/apps/desktop/src/view/components/notifications/examples.ts b/apps/desktop/src/view/components/notifications/examples.ts new file mode 100644 index 00000000..c1131e6a --- /dev/null +++ b/apps/desktop/src/view/components/notifications/examples.ts @@ -0,0 +1,70 @@ +// 通知系统使用示例 +// Example usage of the notification system + +import { notificationService } from "@/components/notifications" + +// 示例 1: 添加一条信息通知 +// Example 1: Add an info notification +export function addInfoNotification() { + notificationService.addNotification({ + title: "notifications.info.title", + content: "notifications.info.content", + type: "info", + read: false, + }) +} + +// 示例 2: 添加一条成功通知 +// Example 2: Add a success notification +export function addSuccessNotification() { + notificationService.addNotification({ + title: "notifications.success.title", + content: "notifications.success.content", + type: "success", + read: false, + }) +} + +// 示例 3: 添加一条警告通知 +// Example 3: Add a warning notification +export function addWarningNotification() { + notificationService.addNotification({ + title: "notifications.warning.title", + content: "notifications.warning.content", + type: "warning", + read: false, + }) +} + +// 示例 4: 添加一条错误通知 +// Example 4: Add an error notification +export function addErrorNotification() { + notificationService.addNotification({ + title: "notifications.error.title", + content: "notifications.error.content", + type: "error", + read: false, + }) +} + +// 示例 5: 获取未读通知数量 +// Example 5: Get unread notification count +export function getUnreadCount() { + const count = notificationService.getUnreadCount() + console.log(`Unread notifications: ${count}`) + return count +} + +// 示例 6: 标记所有通知为已读 +// Example 6: Mark all notifications as read +export function markAllAsRead() { + notificationService.markAllAsRead() + console.log("All notifications marked as read") +} + +// 示例 7: 重置"已显示"状态(用于测试) +// Example 7: Reset shown status (for testing) +export function resetShownStatus() { + notificationService.resetShownStatus() + console.log("Notification shown status reset - will auto-show on next app launch") +} diff --git a/apps/desktop/src/view/components/notifications/index.ts b/apps/desktop/src/view/components/notifications/index.ts new file mode 100644 index 00000000..245951e5 --- /dev/null +++ b/apps/desktop/src/view/components/notifications/index.ts @@ -0,0 +1,3 @@ +export { NotificationList } from "./NotificationList" +export { notificationService } from "./notificationService" +export type { Notification, NotificationStore } from "./types" diff --git a/apps/desktop/src/view/components/notifications/notificationService.ts b/apps/desktop/src/view/components/notifications/notificationService.ts new file mode 100644 index 00000000..ed35a334 --- /dev/null +++ b/apps/desktop/src/view/components/notifications/notificationService.ts @@ -0,0 +1,97 @@ +import { Notification, NotificationStore } from "./types" + +const STORAGE_KEY = "promptx_notifications" +const SHOWN_KEY = "promptx_notifications_shown" + +// 默认通知数据 +const defaultNotifications: Notification[] = [ + { + id: "update-v2.2.0", + title: "notifications.updateV220.title", + content: "notifications.updateV220.content", + type: "success", + timestamp: Date.now(), + read: false, + }, + { + id: "rolex-upgrade", + title: "notifications.rolexUpgrade.title", + content: "notifications.rolexUpgrade.content", + type: "warning", + timestamp: Date.now(), + read: false, + }, +] + +export const notificationService = { + // 获取所有通知 + getNotifications(): Notification[] { + const stored = localStorage.getItem(STORAGE_KEY) + if (stored) { + return JSON.parse(stored) + } + // 首次使用,初始化默认通知 + this.saveNotifications(defaultNotifications) + return defaultNotifications + }, + + // 保存通知 + saveNotifications(notifications: Notification[]): void { + localStorage.setItem(STORAGE_KEY, JSON.stringify(notifications)) + }, + + // 标记通知为已读 + markAsRead(id: string): void { + const notifications = this.getNotifications() + const updated = notifications.map(n => + n.id === id ? { ...n, read: true } : n + ) + this.saveNotifications(updated) + }, + + // 标记所有通知为已读 + markAllAsRead(): void { + const notifications = this.getNotifications() + const updated = notifications.map(n => ({ ...n, read: true })) + this.saveNotifications(updated) + }, + + // 获取未读数量 + getUnreadCount(): number { + const notifications = this.getNotifications() + return notifications.filter(n => !n.read).length + }, + + // 添加新通知 + addNotification(notification: Omit): void { + const notifications = this.getNotifications() + const newNotification: Notification = { + ...notification, + id: `notification-${Date.now()}`, + timestamp: Date.now(), + } + this.saveNotifications([newNotification, ...notifications]) + }, + + // 删除通知 + deleteNotification(id: string): void { + const notifications = this.getNotifications() + const updated = notifications.filter(n => n.id !== id) + this.saveNotifications(updated) + }, + + // 检查是否已显示过通知弹窗 + hasShownNotifications(): boolean { + return localStorage.getItem(SHOWN_KEY) === "true" + }, + + // 标记已显示通知弹窗 + markAsShown(): void { + localStorage.setItem(SHOWN_KEY, "true") + }, + + // 重置显示状态(用于测试) + resetShownStatus(): void { + localStorage.removeItem(SHOWN_KEY) + }, +} diff --git a/apps/desktop/src/view/components/notifications/types.ts b/apps/desktop/src/view/components/notifications/types.ts new file mode 100644 index 00000000..6d2eda09 --- /dev/null +++ b/apps/desktop/src/view/components/notifications/types.ts @@ -0,0 +1,14 @@ +export interface Notification { + id: string + title: string + content: string + type: "info" | "success" | "warning" | "error" + timestamp: number + read: boolean + link?: string +} + +export interface NotificationStore { + notifications: Notification[] + unreadCount: number +} diff --git a/apps/desktop/src/view/components/ui/badge.tsx b/apps/desktop/src/view/components/ui/badge.tsx new file mode 100644 index 00000000..f000e3ef --- /dev/null +++ b/apps/desktop/src/view/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/apps/desktop/src/view/pages/main-window/index.tsx b/apps/desktop/src/view/pages/main-window/index.tsx index 005f5c99..34a6f86e 100644 --- a/apps/desktop/src/view/pages/main-window/index.tsx +++ b/apps/desktop/src/view/pages/main-window/index.tsx @@ -12,10 +12,12 @@ import { SidebarProvider, SidebarTrigger, SidebarHeader, + SidebarFooter, useSidebar, } from "@/components/ui/sidebar" -import { Store, FileText, Settings, Pickaxe, MessageSquare, UsersRound, Plus, Upload } from "lucide-react" +import { Store, FileText, Settings, Pickaxe, MessageSquare, UsersRound, Plus, Upload, Bell } from "lucide-react" import { Button } from "@/components/ui/button" +import { Badge } from "@/components/ui/badge" import ResourcesPage from "../resources-window" import LogsPage from "../logs-window" import SettingsPage from "../settings-window" @@ -23,6 +25,8 @@ import ToolsPage from "../tools-window" import RolesPage from "../roles-window" import AgentXPage from "../agentx-window" import { ResourceImporter } from "../resources-window/components/ResourceImporter" +import { NotificationList } from "@/components/notifications/NotificationList" +import { notificationService } from "@/components/notifications/notificationService" import { goToSendMessage } from "../../../utils/goToSendMessage" import logo from "../../../../assets/icons/icon-64x64.png" type PageType = "resources" | "logs" | "settings" | "update" |"agentx"|"roles"|"tools" @@ -32,6 +36,8 @@ function MainContent() { const [currentPage, setCurrentPage] = useState("agentx") const [pageKey, setPageKey] = useState(0) const { open } = useSidebar() + const [notificationOpen, setNotificationOpen] = useState(false) + const [unreadCount, setUnreadCount] = useState(0) const navigateTo = (page: PageType) => { setCurrentPage(page) @@ -48,6 +54,31 @@ function MainContent() { }).catch(() => {}) }, []) + // 自动打开通知弹窗 + useEffect(() => { + const hasShown = notificationService.hasShownNotifications() + if (!hasShown) { + // 延迟500ms打开,让应用先完全加载 + const timer = setTimeout(() => { + setNotificationOpen(true) + notificationService.markAsShown() + }, 500) + return () => clearTimeout(timer) + } + }, []) + + // 更新未读数量 + useEffect(() => { + const updateUnreadCount = () => { + setUnreadCount(notificationService.getUnreadCount()) + } + updateUnreadCount() + + // 监听通知变化 + const interval = setInterval(updateUnreadCount, 1000) + return () => clearInterval(interval) + }, [notificationOpen]) + useEffect(() => { const handler = (e: Event) => { const page = (e as CustomEvent).detail?.page @@ -173,6 +204,24 @@ function MainContent() { + + +
@@ -197,6 +246,13 @@ function MainContent() { enableV2={enableV2} onImportSuccess={() => navigateTo(currentPage)} /> + { + setNotificationOpen(false) + setUnreadCount(notificationService.getUnreadCount()) + }} + />
) } diff --git a/apps/desktop/src/view/pages/roles-window/components/RoleDetailPanel.tsx b/apps/desktop/src/view/pages/roles-window/components/RoleDetailPanel.tsx index 5d714c89..2c307719 100644 --- a/apps/desktop/src/view/pages/roles-window/components/RoleDetailPanel.tsx +++ b/apps/desktop/src/view/pages/roles-window/components/RoleDetailPanel.tsx @@ -495,10 +495,10 @@ function V2GoalsTab({ data, loading }: { data: any; loading: boolean }) { ) } - const current = data?.focus?.current - const otherGoals: any[] = data?.focus?.otherGoals || [] + // RoleX 1.1.0 focus() 返回的是文本输出,需要解析 + const focusText = data?.focus - if (!current && otherGoals.length === 0) { + if (!focusText || typeof focusText !== 'string') { return (
@@ -507,62 +507,20 @@ function V2GoalsTab({ data, loading }: { data: any; loading: boolean }) { ) } - const taskStatusColor = (s: string) => - s === "completed" ? "bg-green-500" : s === "in_progress" ? "bg-blue-500" : "bg-muted-foreground/30" - const taskStatusLabel = (s: string) => - s === "completed" ? t("roles.detail.taskCompleted") - : s === "in_progress" ? t("roles.detail.taskInProgress") - : t("roles.detail.taskPending") - + // 显示原始的 focus 输出 return (
- {current && ( -
-
-
- {t("roles.detail.currentGoal")} -
-

{current.name}

- {current.description && ( -

{current.description}

- )} - {current.plan && ( -
-

{t("roles.detail.goalPlan")}

-

{current.plan.name}

-
- )} - {current.tasks && current.tasks.length > 0 && ( -
-

{t("roles.detail.goalTasks")}

-
- {current.tasks.map((task: any, i: number) => ( -
-
- {task.name} - {taskStatusLabel(task.status)} -
- ))} -
-
- )} +
+
+ +

{t("roles.detail.currentGoal")}

- )} - {otherGoals.length > 0 && ( -
-

- {t("roles.detail.goals")} ({otherGoals.length}) -

-
- {otherGoals.map((goal: any, i: number) => ( -
-
- {goal.name} -
- ))} -
+
+
+            {focusText}
+          
- )} +
) } @@ -637,17 +595,13 @@ function V2OrganizationTab({ role, data, loading }: { role: RoleItem; data: any; // V2 Structure Tab: identity .feature files with viewer/editor function V2StructureTab({ role }: { role: RoleItem }) { const { t } = useTranslation() - const [files, setFiles] = useState([]) + const [nodes, setNodes] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState("") const [viewerOpen, setViewerOpen] = useState(false) - const [viewerFile, setViewerFile] = useState("") + const [viewerNode, setViewerNode] = useState(null) const [viewerContent, setViewerContent] = useState("") - const [viewerLoading, setViewerLoading] = useState(false) - const [editedContent, setEditedContent] = useState("") - const [saving, setSaving] = useState(false) - const [isEditing, setIsEditing] = useState(false) const [expanded, setExpanded] = useState>({ persona: true, knowledge: true, voice: true, experience: true, @@ -660,53 +614,66 @@ function V2StructureTab({ role }: { role: RoleItem }) { setLoading(true) setError("") try { - const res = await window.electronAPI?.invoke("resources:listV2RoleFiles", { roleId: role.id }) - if (res?.success) setFiles(res.files || []) - else setError(res?.message || t("roles.detail.loadFilesFailed")) - } catch { setError(t("roles.detail.loadFilesFailed")) } - finally { setLoading(false) } + const res = await window.electronAPI?.invoke("rolex:getIdentityNodes", { roleId: role.id }) + console.log('[V2StructureTab] Response:', res) + console.log('[V2StructureTab] res.data type:', typeof res?.data) + console.log('[V2StructureTab] res.data:', res?.data) + if (res?.success && res.data) { + // res.data is the identity projection from role.project() + // It should contain nodes with their information (Gherkin content) + const identityNodes = Array.isArray(res.data) ? res.data : (res.data.nodes || []) + console.log('[V2StructureTab] identityNodes:', identityNodes) + console.log('[V2StructureTab] identityNodes length:', identityNodes.length) + setNodes(identityNodes) + } else { + setError(res?.message || t("roles.detail.loadFilesFailed")) + } + } catch (err: any) { + console.error('[V2StructureTab] Error:', err) + setError(err?.message || t("roles.detail.loadFilesFailed")) + } finally { + setLoading(false) + } } load() }, [role, t]) const categorize = useCallback(() => { - const persona: string[] = [], knowledge: string[] = [], voice: string[] = [], experience: string[] = [] - for (const f of files) { - if (f.includes(".knowledge.")) knowledge.push(f) - else if (f.includes(".voice.")) voice.push(f) - else if (f.includes(".experience.")) experience.push(f) - else persona.push(f) + const persona: any[] = [], knowledge: any[] = [], voice: any[] = [], experience: any[] = [] + for (const node of nodes) { + const nodeType = node.type || "" + const id = node.id || "" + const name = node.name || id + + // Categorize by node type + if (nodeType === "identity" || nodeType === "individual") { + persona.push(node) + } else if (nodeType === "procedure" || nodeType === "principle" || name.includes("knowledge") || id.includes("knowledge")) { + knowledge.push(node) + } else if (name.includes("voice") || id.includes("voice")) { + voice.push(node) + } else if (name.includes("experience") || id.includes("experience") || nodeType === "experience") { + experience.push(node) + } else if (nodeType === "position" || nodeType === "organization" || nodeType === "duty" || nodeType === "requirement" || nodeType === "charter") { + // These are structural/organizational nodes, put in persona + persona.push(node) + } else { + // Default to persona + persona.push(node) + } } return { persona, knowledge, voice, experience } - }, [files]) + }, [nodes]) const { persona, knowledge, voice, experience } = categorize() - const openFile = async (fileName: string) => { - setViewerFile(fileName) + const openNode = (node: any) => { + setViewerNode(node) + setViewerContent(node.information || "") setViewerOpen(true) - setViewerLoading(true) - setIsEditing(false) - try { - const res = await window.electronAPI?.invoke("resources:readV2RoleFile", { roleId: role.id, fileName }) - if (res?.success) { setViewerContent(res.content || ""); setEditedContent(res.content || "") } - else setViewerContent(t("roles.detail.loadContentFailed")) - } catch { setViewerContent(t("roles.detail.loadContentFailed")) } - finally { setViewerLoading(false) } - } - - const handleSave = async () => { - setSaving(true) - try { - const res = await window.electronAPI?.invoke("resources:saveV2RoleFile", { - roleId: role.id, fileName: viewerFile, content: editedContent, - }) - if (res?.success) { setViewerContent(editedContent); setIsEditing(false) } - } catch { /* ignore */ } - finally { setSaving(false) } } - const renderLayer = (key: string, label: string, desc: string, layerFiles: string[], color: string) => ( + const renderLayer = (key: string, label: string, desc: string, layerNodes: any[], color: string) => (
{expanded[key] && (
- {layerFiles.length === 0 + {layerNodes.length === 0 ? {t("roles.detail.noFiles")} - : layerFiles.map(f => ( - )) } @@ -766,40 +736,20 @@ function V2StructureTab({ role }: { role: RoleItem }) { - {viewerFile} + {viewerNode?.name || viewerNode?.id} - {role.id}/identity/{viewerFile} - {isReadOnly && {t("roles.detail.readOnly")}} + {role.id} / {viewerNode?.id} + {t("roles.detail.readOnly")}
- {viewerLoading ? ( -
- -
- ) : isEditing && !isReadOnly ? ( -