diff --git a/dist-electron/main/main.js b/dist-electron/main/main.js index 5778cd7..4fe753f 100644 --- a/dist-electron/main/main.js +++ b/dist-electron/main/main.js @@ -1,6 +1,6 @@ "use strict"; require("electron"); -require("./main-Bp9J8VEe.js"); +require("./main-CK4u0iKH.js"); require("electron-squirrel-startup"); require("electron-log"); require("bytenode"); diff --git a/dist/index.html b/dist/index.html index d53804b..5c1a562 100644 --- a/dist/index.html +++ b/dist/index.html @@ -8,8 +8,8 @@ http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: http://8.138.234.141 https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com; connect-src 'self' http://8.138.234.141 https://api.iconify.design wss://onefeel.brother7.cn" /> - - + +
diff --git a/docs/ClawX-Agents-Migration-Plan.md b/docs/ClawX-Agents-Migration-Plan.md new file mode 100644 index 0000000..9310e81 --- /dev/null +++ b/docs/ClawX-Agents-Migration-Plan.md @@ -0,0 +1,546 @@ +# ClawX Agents 功能迁移到 zn-ai 的更新版计划 + +## 1. 背景与本次修正 + +本版文档基于两条新的约束重新整理: + +- 最新 Agents 参考图已经明确,页面信息架构必须回到 `ClawX` 的列表主视图。 +- 开发需求已经明显变化,后续实现必须严格对齐 `ClawX` Agents 功能,而不是继续延展一版“更强的 Models/Agent 控制台”。 + +这意味着上一版计划里最重要的一条判断需要作废: + +- 上一版曾把 `zn-ai` 的 `/agents` 页面导向“列表 + 固定右侧详情”的控制台详情页。 +- 这个方向与最新参考图和 `ClawX` 真实交互不一致,后续不应继续推进。 + +本轮统一后的基线结论如下: + +1. `/agents` 必须保持独立路由与侧边栏入口,不能再并回 `/models`。 +2. 页面 IA 必须切回 `ClawX` 的“列表主视图”。 +3. Agents 页主内容只保留卡片摘要、刷新、新增和设置入口,不把设置详情常驻在主布局中。 +4. `Add Agent` 必须是独立弹窗,且字段极简,只收: + - `name` + - `inheritWorkspace` +5. Settings 详情建议改为 `modal` 或 `sheet`,而不是主布局双栏右侧 inspector。 +6. Channels 继续作为 channel/account 归属绑定的唯一写入口。 +7. Models 继续负责 Provider、默认模型、Usage,不承载 Agent 实体管理。 +8. Home/Chat/Cron 继续作为 Agents 域的消费页面,不反向定义 Agents 页形态。 + +一句话总结: + +- 当前 `zn-ai` 已经补上了不少 Agents 基础设施。 +- 下一轮的重点不再是“有没有 `/agents`”,而是“把 `/agents` 的页面 IA、交互层级和职责边界重新拉回 `ClawX`”。 + +## 2. ClawX 基线:严格对齐什么 + +### 2.1 路由入口与页面装配 + +`ClawX` 的 Agents 是独立域,不是 Models 的子视图。 + +基线文件主要集中在: + +- `ClawX/src/pages/Agents/index.tsx` +- `ClawX/src/pages/Channels/index.tsx` +- `ClawX/src/stores/agents.ts` +- `ClawX/electron/api/routes/agents.ts` + +它的装配方式很明确: + +- 侧边栏有独立 `Agents` 入口。 +- `/agents` 是独立页面。 +- `/models` 与 `/agents` 是并列关系,不混用页面职责。 + +### 2.2 页面 IA:列表主视图,而不是控制台详情页 + +`ClawX` 的 Agents 首页首先是一个列表工作台,不是一个主布局双栏的详情编辑器。 + +页面骨架应理解为: + +1. 顶部标题区 + - Title + - Subtitle + - `Refresh` + - `Add Agent` +2. Warning / Error banner +3. Agent card 列表 +4. 卡片上的 `Settings` / `Delete` 入口 +5. 二级弹层 + - `AddAgentDialog` + - `AgentSettingsModal` + - `AgentModelModal` + +明确不要做的事: + +- 不要在页面右侧常驻一个大号设置面板。 +- 不要把“选中某个 agent 后右边出现详情”的双栏结构作为主交互。 +- 不要把创建、命名、模型、绑定全部平铺到首页主布局。 +- 不要让卡片列表承担复杂表单职责。 + +这也是本轮文档更新里最核心的 IA 修正。 + +### 2.3 Add Agent 弹窗:字段与交互 + +`ClawX` 的 `AddAgentDialog` 很克制,创建阶段只做最小信息采集。 + +建议严格对齐以下字段与交互: + +- 标题与简短说明 +- `name` 输入框 + - 必填 + - 非空后才能提交 +- `inheritWorkspace` 开关 + - 含义是“是否继承主 Agent 工作区” + - 默认保持轻量,不在创建弹窗里叠加更多高级参数 +- `Cancel` +- `Save / Create` + +创建阶段明确不应该出现的字段: + +- Provider 选择 +- Model override +- Channel binding +- Workspace 路径高级配置 + +创建成功后的回流建议也对齐 `ClawX`: + +- 关闭弹窗 +- 回到列表主视图 +- 列表出现新卡片 +- 可给 toast,但不要自动把用户带进重型详情页 + +### 2.4 Agents 首页卡片:只保留摘要与设置入口 + +`ClawX` 的卡片不是“迷你详情页”,而是摘要卡片。 + +建议卡片层面保留这些信息: + +- 头像 / icon +- `name` +- `default` badge +- 模型摘要 + - 当前模型 + - 是否继承默认模型 +- 频道归属摘要 + - 只展示结果,不提供写操作 + +建议卡片层面只保留这些动作: + +- `Settings` +- `Delete` + +不建议放在卡片正面的内容: + +- 大段 workspace / agentDir / sessionKey 技术细节 +- channel/account 绑定下拉 +- provider/model 大表单 +- 右侧伴随卡片选中状态的持久编辑区 + +如果需要展示更多运行时信息,优先放进设置弹层,而不是挤进首页卡片。 + +### 2.5 设置详情:建议 modal / sheet,而不是主布局双栏 + +`ClawX` 的设置入口是二级层级,不是首页主布局的一部分。 + +因此在 `zn-ai` 中更推荐: + +- 桌面端使用 `modal` 或 `side sheet` +- 移动端使用全屏 `sheet` / `dialog` + +不推荐继续保留当前这类形态: + +- 左侧列表 +- 右侧常驻详情 inspector +- 页面刷新或切换 agent 时整块右栏一起抖动 + +原因有三点: + +1. 不符合 `ClawX` 的交互层级。 +2. 会把“列表主视图”错误拉成“详情驱动”页面。 +3. 不利于后续继续保持 Cards 摘要化。 + +Settings 弹层建议承载的内容: + +- Identity + - 名称 + - Agent ID +- Model + - provider account + - model ref + - inherit default model + - 预览当前 effective provider / model +- Channel binding summary + - 只读展示当前 channel/account 归属 + - 提供“去 Channels 管理”的入口 + +如果仍需要二级模型编辑体验,可以继续保留嵌套的 `AgentModelModal`,但它依然属于弹层体系,而不是首页主布局。 + +### 2.6 与 Channels / Models 的职责边界 + +这是本轮必须写死的边界。 + +#### Agents 页负责什么 + +- Agent 实体列表 +- 新建 / 删除 / 重命名 +- per-agent model override +- 归属摘要展示 +- 设置入口 + +#### Agents 页不负责什么 + +- channel/account 绑定写操作 +- provider account CRUD +- 默认 provider 设定 +- usage 历史查询主视图 + +#### Channels 页负责什么 + +- channel 级归属 +- account 级归属 +- binding 写操作唯一入口 +- routing fallback 关系说明 + +#### Models 页负责什么 + +- provider account CRUD +- default provider / default model +- provider catalog / vendors +- usage history +- provider 视角的 models snapshot + +边界落地后的页面关系应该是: + +- `Agents` 看 Agent 和摘要 +- `Channels` 改归属 +- `Models` 管 Provider + +### 2.7 与 Home / Chat / Cron 的联动 + +`ClawX` 的 Agents 不孤立存在,它是多个页面共享的域。 + +最关键的消费关系如下: + +- Home / Chat + - 当前 Agent 名称 + - 当前 Agent 模型 + - 会话切换与发送路由 +- Cron + - 任务归属 `agentId` + - delivery channel / account / target + - 编辑时展示 Agent 与 delivery 摘要 + +因此后续实现时要记住: + +- Home/Chat/Cron 是 Agents 域的消费者。 +- 它们的联动验证很重要。 +- 但它们不应该反过来决定 `/agents` 首页长什么样。 + +## 3. zn-ai 当前现状:已经落地的部分与剩余偏差 + +### 3.1 已经落地的部分 + +与上一版文档相比,`zn-ai` 当前已经不是“Agents 完全缺失”的状态。 + +已经具备的基础包括: + +1. 独立 `/agents` 路由与侧边栏入口 +2. `agentsStore` +3. 本地 `/api/agents` +4. 本地 `/api/channels/accounts` +5. 本地 `/api/channels/targets` +6. Channels 页的 channel/account 归属写操作 +7. Cron 页的: + - `agent` + - `channel` + - `account` + - `target` +8. provider runtime sync 与 gateway reload/restart 链路 +9. Home 聊天顶部 Agent 切换与发送前模型校验 + +换句话说: + +- “Agents 域基础设施缺失”已经不再是主问题。 +- “UI/IA 没有回到 ClawX 基线”才是当前最主要的剩余问题。 + +### 3.2 仍然存在的关键偏差 + +尽管能力已经补了不少,当前 `zn-ai` 仍然和 `ClawX` 有几个显著偏差。 + +#### 偏差 1:页面仍然太像“详情页” + +当前实现已经是“列表 + 右侧设置区”的形态,这比最早的 `/agents -> /models` 已经前进很多,但仍然不是 `ClawX` 的列表主视图。 + +需要明确回退: + +- 去掉“固定右侧详情”作为主布局 +- 恢复“卡片列表 + 设置弹层” + +#### 偏差 2:Add Agent 还是 prompt / confirm 式流转 + +当前实现仍然偏命令式: + +- `prompt` 输名字 +- `confirm` 选 inherit workspace + +这不符合 `ClawX` 的统一弹层体验,也不利于后续继续扩展创建态文案和校验。 + +#### 偏差 3:Agents 页承担了过多常驻编辑语义 + +当前页面把: + +- identity +- model +- binding summary + +长期固定在主布局中展示,导致首页不再是纯列表。 + +这会让视觉重心从“列表工作台”滑向“控制台详情页”。 + +#### 偏差 4:需要继续守住 Channels / Models 边界 + +`zn-ai` 当前的方向整体是对的: + +- Channels 已经承担 binding 写操作 +- Agents 已经开始只读展示 binding summary + +但文档和实现都要继续避免回摆: + +- 不要因为 Agents settings 看起来“顺手”,就再把绑定下拉搬回去 +- 不要因为 Agents 能改 model,就把 provider CRUD 一并塞进 Agents + +#### 偏差 5:Models Snapshot 仍然不能替代 Agents 页 + +`/api/models` 当前仍然更接近“provider account 视角的合成 snapshot”,不是 Agent 域真相。 + +因此后续文档、QA 和页面说明都应避免混淆: + +- Models Snapshot 只能验证 provider 视角 +- Agent override 与 Agent 路由应以 `/api/agents`、Agents 页和 Home/Chat 行为为准 + +## 4. 新一轮 sub-agent 数量估算与分工 + +### 4.1 推荐并行规模 + +按当前代码基础判断,下一轮如果目标是“严格对齐 ClawX Agents UI 与职责边界”,推荐按 `6` 个 sub-agent 并行推进。 + +这不是为了把任务拆得更碎,而是为了减少 UI / store / local route / cross-page 联动之间的冲突。 + +但结合 `zn-ai` 当前已经具备的 `/agents` 页面骨架、`agentsStore`、本地 `/api/agents`、Channels 绑定链路,以及这轮需求已经明显收敛到“参考图驱动的 UI / IA 重构”,本轮实际执行建议按 `4` 个 sub-agent 推进更合适: + +- `1` 个负责 `Agents` 首页 IA 与视觉重构 +- `1` 个负责 `Add Agent / Settings` 弹层与文案收口 +- `1` 个负责 store / backend 能力缺口与回归风险核对 +- `1` 个负责迁移计划、验收口径和回归说明同步 + +### 4.2 推荐分工 + +| sub-agent | 写入范围 | 目标 | +| --- | --- | --- | +| 1 | `src/pages/Agents/**`, `src/router/**`, `src/components/layout/**` | 把 `/agents` 页面 IA 调回列表主视图,保证入口、标题区、刷新、新增和卡片层对齐 ClawX | +| 2 | `src/pages/Agents/**` | 重做 `Add Agent` 弹窗,以及 `Settings modal/sheet`、`Agent model modal` 的交互层级 | +| 3 | `src/stores/agents.ts`, shared agent types, 页面数据拼装逻辑 | 收口 Agents 页面所需字段,确保卡片摘要、设置详情、只读 binding summary 的数据来源稳定 | +| 4 | `electron/api/routes/agents.ts`, runtime sync 相关实现, `agent-config` | 校验 create / rename / model override / delete 的 runtime sync、reload、restart 语义是否与 UI 新交互一致 | +| 5 | `src/pages/Channels/**`, `src/pages/Models/**`, `src/pages/Home/**`, `src/pages/Cron/**` | 守住 Channels/Models/Home/Cron 的职责边界,避免 Agents IA 回正后引入新耦合 | +| 6 | `docs/**`, smoke tests, regression notes | 更新迁移文档、手工冒烟路径、验收标准和回归清单 | + +### 4.3 资源不足时的降配方案 + +如果并行资源不足,可以降到 `4` 个 sub-agent,但不要把下面两类工作混在一个人身上: + +- 首页 IA 重构 +- runtime / route / sync 硬逻辑 + +推荐合并方式: + +- `1 + 2` +- `5 + 6` + +### 4.4 本轮实际执行分工 + +结合当前会话的资源与改动范围,本轮按 `4` 个 sub-agent 推进: + +| sub-agent | 本轮职责 | 产出 | +| --- | --- | --- | +| A | 参考图与 `ClawX` IA 差异分析 | 列表主视图、卡片字段、弹层层级修正建议 | +| B | backend / store 能力核对 | 确认 `createAgent(inheritWorkspace)`、rename、model override、delete 是否足够支撑新 UI | +| C | 迁移计划文档更新 | 把“控制台详情页”基线修正为“ClawX 列表主视图” | +| D | i18n 与页面文案收口 | 让标题、副标题、添加弹窗和设置语义贴近参考图与 ClawX 心智 | + +## 5. 分阶段实施建议 + +本轮建议不再按“从零搭 Agents 域”的顺序推进,而改成“在已落地能力上收口 UI 和边界”。 + +### 5.1 阶段 0:冻结新基线 + +目标: + +- 明确上一版“控制台详情页 / 双栏布局”判断失效 +- 团队统一采用 `ClawX` 列表主视图基线 + +产出: + +- 更新后的本计划文档 +- 统一的页面 IA 草图 +- 明确的职责边界 + +验收标准: + +- 团队确认 `/agents` 首页不做固定右侧详情栏 +- 团队确认 `Add Agent` 只收最小字段 +- 团队确认 Channels / Models / Agents 三页边界 + +预计工作量: + +- `0.5` 天 + +### 5.2 阶段 1:把首页 IA 从双栏拉回列表主视图 + +目标: + +- 先把首页结构改对,再谈细节 + +建议改动: + +- 首页只保留: + - title + - subtitle + - refresh + - add + - banner + - card list +- 去掉常驻右侧 inspector + +验收标准: + +- `/agents` 首屏看起来就是列表主视图 +- 即使不打开任何设置,也能完整浏览所有 Agent 卡片 + +预计工作量: + +- `0.5 ~ 1` 天 + +### 5.3 阶段 2:补齐 Add Agent 弹窗与设置弹层 + +目标: + +- 把所有二级编辑交互收进 overlay 层 + +建议改动: + +- `Add Agent dialog` + - `name` + - `inheritWorkspace` +- `Agent settings modal / sheet` + - identity + - model + - binding summary +- `Agent model modal` + - provider + model ref + +验收标准: + +- 创建、重命名、模型覆盖都不再依赖首页双栏 +- 桌面和窄屏下的弹层层级都清晰 + +预计工作量: + +- `1 ~ 1.5` 天 + +### 5.4 阶段 3:收口首页卡片摘要 + +目标: + +- 让首页卡片真正回到摘要视角 + +建议改动: + +- 卡片只保留必要摘要 +- workspace / agentDir / sessionKey 等技术细节尽量移入设置层 +- hover 动作保持轻量 + +验收标准: + +- 卡片读起来像 `ClawX` +- 首页不出现重型编辑表单 + +预计工作量: + +- `0.5` 天 + +### 5.5 阶段 4:守住 Channels / Models / Agents 边界 + +目标: + +- 在 UI 回正后,避免功能重新耦合 + +建议改动: + +- Agents settings 里的绑定区继续只读 +- “Manage bindings” 统一跳转 `Channels` +- Models 保持 provider / default / usage,不接 Agent CRUD +- 默认模型语义留在 Models,Agent override 语义留在 Agents + +验收标准: + +- 团队对“去哪改什么”没有歧义 +- QA 能明确知道每类问题该在哪页验证 + +预计工作量: + +- `0.5` 天 + +### 5.6 阶段 5:联动与回归验证 + +目标: + +- 验证 UI 改造没有破坏现有 Agents 域能力 + +重点验证链路: + +1. Agents 创建后,Home/Chat 能看到新 Agent +2. Agent model override 更新后,Home 发送仍可走通 +3. Channels 绑定变更后,Agents settings 摘要能反映 +4. Cron 里 agent/channel/account/target 仍然可用 +5. `/api/channels/targets` 与手工输入兜底都正常 +6. 删除 Agent 后 reload/restart 行为仍稳定 + +预计工作量: + +- `1` 天 + +## 6. 风险清单 + +| 风险 | 描述 | 应对 | +| --- | --- | --- | +| 旧判断残留 | 团队继续沿用“右侧详情栏”思路实现后续 UI | 在阶段 0 明确作废旧 IA 结论,并把弹层方案写死 | +| Add Agent 范围膨胀 | 创建弹窗被不断塞入 provider/model/channel 字段 | 规定创建阶段只收 `name + inheritWorkspace` | +| 绑定边界回摆 | 因为 settings 层已有 binding summary,顺手加下拉写操作 | Agents 只读展示,Channels 保持唯一写入口 | +| Models 语义漂移 | Provider CRUD 再次被拉进 Agents | Models 专注 provider/default/usage,不承载 Agent 实体管理 | +| runtime sync 假通过 | UI 保存成功,但真实 runtime 文件或 gateway 状态没同步 | 回归必须覆盖 create / update model / delete 三类动作 | +| Cron targets 体验退化 | 因为 UI 改造误伤 target 自动加载与手输兜底 | 把 `/api/channels/targets` 与手工输入都纳入冒烟清单 | + +## 7. 验收重点 + +严格对齐 `ClawX` 时,验收不要只看“功能能不能点”,还要看“页面是不是回到了正确的形态”。 + +本轮建议把下面几条列为强验收项: + +1. `/agents` 首页是否是列表主视图,而不是双栏详情页 +2. `Add Agent` 是否是正式弹窗,而不是 prompt / confirm +3. 首页卡片是否只保留摘要与设置入口 +4. Settings 是否是 modal / sheet,而不是主布局常驻 +5. Agents 是否只读展示 binding summary,并把写操作引导到 Channels +6. Models 是否仍然只负责 provider/default/usage +7. Home / Cron 是否继续消费同一份 Agents 域数据 + +## 8. 最终建议 + +当前 `zn-ai` 已经把独立 `/agents`、`agentsStore`、本地 routes、runtime sync、Channels/Cron 联动这些“底座”补得差不多了。 + +所以下一轮不要再把重心放在“有没有 Agents 能力”,而要放在下面四件事: + +1. 把 `/agents` 的页面 IA 严格拉回 `ClawX` +2. 把 `Add Agent` 与 Settings 改成标准弹层体系 +3. 把 Agents / Channels / Models 的职责边界写死并落实到 UI +4. 用 Home / Cron / targets / runtime sync 做真实闭环回归 + +如果这四件事做对,`zn-ai` 的 Agents 才会从“功能拼装完成”真正进入“体验与职责都对齐 ClawX”的阶段。 diff --git a/docs/prompt-history.md b/docs/prompt-history.md index 40a3e25..086a47b 100644 --- a/docs/prompt-history.md +++ b/docs/prompt-history.md @@ -4,4 +4,12 @@ - 本地安装了@vitejs/plugin-react,可以直接构建,下一波直接做Knowledge / Scripts / Login,然后再收 vite.config.ts 和 Vue-only 启动链。估算sub-agent数量,安排sub-agent分工开始工作。 -- 继续做Knowledge / Scripts / Login,构建工作,检查React平替Vue技术栈是否全部完成,估算sub-agent数量,安排sub-agent分工开始工作。 \ No newline at end of file +- 继续做Knowledge / Scripts / Login,构建工作,检查React平替Vue技术栈是否全部完成,估算sub-agent数量,安排sub-agent分工开始工作。 + +- 在ClwaX项目中深度分析Agents功能包括渲染层视觉UI、主进程等实现思路,用于迁移到zn-ai项目,输出迁移开发计划到zn-ai/docs目录下,估算sub-agent数量,安排sub-agent分工分析Agents功能实现思路,期望迁移功能对齐ClawX的Agents功能。 + +- 按迁移计划继续做 Channels/Cron 接线和 agent model/provider 的可视化编辑。估算sub-agent数量,安排sub-agent分工推进开发工作,期望完全对齐ClawX + +- 继续推进,下一步最值的是补 /api/channels/targets 和 provider runtime sync;这两段补上后,zn-ai 会更接近 ClawX 的完整闭环。估算sub-agent数量,安排sub-agent分工推进开发工作,期望完全对齐ClawX + +- 替我确认真实 UI 操作流和 gateway 行为已经全部跑通。离“完全对齐 ClawX”还差两块更值钱的尾项:真实远端 target discovery,不只是本地候选合成;以及 provider/runtime 变更后的显式前端刷新事件,而不只是主进程侧同步。估算sub-agent数量,安排sub-agent分工推进开发工作,期望完全对齐ClawX。 \ No newline at end of file diff --git a/electron/api/router.ts b/electron/api/router.ts index e8df467..3e7f1d4 100644 --- a/electron/api/router.ts +++ b/electron/api/router.ts @@ -4,6 +4,9 @@ import { providerApiService } from '@electron/service/provider-api-service'; import type { HostApiContext } from './context'; import type { HostApiRequest } from './route-utils'; import { normalizeRequest } from './route-utils'; +import { handleAgentRoutes } from './routes/agents'; +import { handleChannelRoutes } from './routes/channels'; +import { handleCronRoutes } from './routes/cron'; import { handleFileRoutes } from './routes/files'; import { handleGatewayRoutes } from './routes/gateway'; import { handleModelRoutes } from './routes/models'; @@ -17,7 +20,10 @@ type RouteHandler = ( const routeHandlers: RouteHandler[] = [ handleProviderRoutes, + handleChannelRoutes, + handleAgentRoutes, handleModelRoutes, + handleCronRoutes, handleGatewayRoutes, handleFileRoutes, handleSessionRoutes, diff --git a/electron/api/routes/agents.ts b/electron/api/routes/agents.ts new file mode 100644 index 0000000..17dbf03 --- /dev/null +++ b/electron/api/routes/agents.ts @@ -0,0 +1,189 @@ +import type { HostApiContext } from '../context'; +import type { NormalizedHostApiRequest } from '../route-utils'; +import { fail, ok, parseJsonBody } from '../route-utils'; +import { syncProviderRuntimeSnapshot } from '@electron/service/provider-runtime-sync'; +import { + assignChannelToAgent, + clearChannelBinding, + createAgentConfig, + deleteAgentConfig, + listAgentsSnapshot, + updateAgentModelConfig, + updateAgentName, +} from '../../utils/agent-config'; + +function getProviderSnapshot(ctx: HostApiContext) { + const accounts = ctx.providerApiService + .getAccounts() + .filter((account) => account.enabled !== false); + const defaultAccountId = ctx.providerApiService.getDefault().accountId; + return { accounts, defaultAccountId }; +} + +function formatRuntimeWarning(warnings: string[]): string | null { + if (warnings.length === 0) return null; + return `Runtime sync warnings: ${warnings.join('; ')}`; +} + +export async function handleAgentRoutes( + request: NormalizedHostApiRequest, + ctx: HostApiContext, +) { + const { pathname, method } = request; + const { accounts, defaultAccountId } = getProviderSnapshot(ctx); + + if (pathname === '/api/agents' && method === 'GET') { + return ok({ + success: true, + ...listAgentsSnapshot(accounts, defaultAccountId), + }); + } + + if (pathname === '/api/agents' && method === 'POST') { + try { + const body = parseJsonBody<{ name?: string; inheritWorkspace?: boolean }>(request.body); + if (!body?.name || !String(body.name).trim()) { + return fail(400, 'name is required'); + } + + const snapshot = createAgentConfig(body.name, { inheritWorkspace: body.inheritWorkspace }, accounts, defaultAccountId); + const runtimeSync = syncProviderRuntimeSnapshot({ accounts, defaultAccountId, snapshot }); + ctx.gatewayManager.reloadProviders({ + topics: ['agents', 'providers', 'models'], + reason: 'agents:created', + warnings: runtimeSync.warnings, + }); + + return ok({ + success: true, + warning: formatRuntimeWarning(runtimeSync.warnings), + ...snapshot, + }); + } catch (error) { + return fail(500, error instanceof Error ? error.message : String(error)); + } + } + + if (!pathname.startsWith('/api/agents/')) { + return null; + } + + const suffix = pathname.slice('/api/agents/'.length); + const parts = suffix.split('/').filter(Boolean).map((part) => decodeURIComponent(part)); + + if (parts.length === 0) { + return null; + } + + if (method === 'PUT' && parts.length === 1) { + try { + const body = parseJsonBody<{ name?: string }>(request.body); + if (!body?.name || !String(body.name).trim()) { + return fail(400, 'name is required'); + } + + const snapshot = updateAgentName(parts[0], body.name, accounts, defaultAccountId); + const runtimeSync = syncProviderRuntimeSnapshot({ accounts, defaultAccountId, snapshot }); + ctx.gatewayManager.notifyRuntimeChanged({ + topics: ['agents'], + reason: 'agents:renamed', + warnings: runtimeSync.warnings, + }); + + return ok({ + success: true, + warning: formatRuntimeWarning(runtimeSync.warnings), + ...snapshot, + }); + } catch (error) { + return fail(500, error instanceof Error ? error.message : String(error)); + } + } + + if (method === 'PUT' && parts.length === 2 && parts[1] === 'model') { + try { + const body = parseJsonBody<{ modelRef?: string | null; providerAccountId?: string | null }>(request.body); + const snapshot = updateAgentModelConfig( + parts[0], + body?.modelRef ?? null, + body?.providerAccountId ?? null, + accounts, + defaultAccountId, + ); + const runtimeSync = syncProviderRuntimeSnapshot({ accounts, defaultAccountId, snapshot }); + ctx.gatewayManager.reloadProviders({ + topics: ['agents', 'providers', 'models'], + reason: 'agents:model-updated', + warnings: runtimeSync.warnings, + }); + + return ok({ + success: true, + warning: formatRuntimeWarning(runtimeSync.warnings), + ...snapshot, + }); + } catch (error) { + return fail(500, error instanceof Error ? error.message : String(error)); + } + } + + if (method === 'PUT' && parts.length === 3 && parts[1] === 'channels') { + try { + const body = parseJsonBody<{ accountId?: string | null }>(request.body); + const snapshot = assignChannelToAgent(parts[0], parts[2], body?.accountId ?? null, accounts, defaultAccountId); + ctx.gatewayManager.notifyRuntimeChanged({ + topics: ['agents', 'channels', 'channel-targets'], + reason: 'agents:channel-assigned', + channelType: parts[2], + accountId: body?.accountId ?? undefined, + }); + return ok({ + success: true, + ...snapshot, + }); + } catch (error) { + return fail(500, error instanceof Error ? error.message : String(error)); + } + } + + if (method === 'DELETE' && parts.length === 1) { + try { + const snapshot = deleteAgentConfig(parts[0], accounts, defaultAccountId); + const runtimeSync = syncProviderRuntimeSnapshot({ accounts, defaultAccountId, snapshot }); + await ctx.gatewayManager.restart({ + topics: ['agents', 'providers', 'models', 'channels', 'channel-targets'], + reason: 'agents:deleted', + warnings: runtimeSync.warnings, + }); + + return ok({ + success: true, + warning: formatRuntimeWarning(runtimeSync.warnings), + ...snapshot, + }); + } catch (error) { + return fail(500, error instanceof Error ? error.message : String(error)); + } + } + + if (method === 'DELETE' && parts.length === 3 && parts[1] === 'channels') { + try { + const accountId = request.url.searchParams.get('accountId')?.trim() || null; + const snapshot = clearChannelBinding(parts[2], accountId, accounts, defaultAccountId); + ctx.gatewayManager.notifyRuntimeChanged({ + topics: ['agents', 'channels', 'channel-targets'], + reason: 'agents:channel-cleared', + channelType: parts[2], + accountId: accountId ?? undefined, + }); + return ok({ + success: true, + ...snapshot, + }); + } catch (error) { + return fail(500, error instanceof Error ? error.message : String(error)); + } + } + + return null; +} diff --git a/electron/api/routes/channels.ts b/electron/api/routes/channels.ts new file mode 100644 index 0000000..95db3dc --- /dev/null +++ b/electron/api/routes/channels.ts @@ -0,0 +1,133 @@ +import type { HostApiContext } from '../context'; +import type { NormalizedHostApiRequest } from '../route-utils'; +import { fail, ok, parseJsonBody } from '../route-utils'; +import { + assignChannelToAgent, + clearChannelBinding, + listAgentsSnapshot, +} from '../../utils/agent-config'; +import { + listSelectedChannelAccountGroups, + listSelectedChannelTargets, +} from '../../utils/channels'; + +function getProviderSnapshot(ctx: HostApiContext) { + const accounts = ctx.providerApiService + .getAccounts() + .filter((account) => account.enabled !== false); + const defaultAccountId = ctx.providerApiService.getDefault().accountId; + return { accounts, defaultAccountId }; +} + +export async function handleChannelRoutes( + request: NormalizedHostApiRequest, + ctx: HostApiContext, +) { + const { pathname, method } = request; + const { accounts, defaultAccountId } = getProviderSnapshot(ctx); + const snapshot = listAgentsSnapshot(accounts, defaultAccountId); + + if (pathname === '/api/channels/accounts' && method === 'GET') { + return ok({ + success: true, + channels: listSelectedChannelAccountGroups(snapshot), + }); + } + + if (pathname === '/api/channels/targets' && method === 'GET') { + const channelType = request.url.searchParams.get('channelType')?.trim() || ''; + const accountId = request.url.searchParams.get('accountId')?.trim() || null; + const query = request.url.searchParams.get('query')?.trim() || null; + + if (!channelType) { + return fail(400, 'channelType is required'); + } + + return ok({ + success: true, + targets: listSelectedChannelTargets(channelType, accountId, query), + }); + } + + if (pathname === '/api/channels/binding' && method === 'PUT') { + try { + const body = parseJsonBody<{ + channelType?: string; + accountId?: string | null; + agentId?: string; + }>(request.body); + const channelType = String(body?.channelType ?? '').trim(); + const accountId = String(body?.accountId ?? '').trim(); + const agentId = String(body?.agentId ?? '').trim(); + + if (!channelType) { + return fail(400, 'channelType is required'); + } + if (!accountId) { + return fail(400, 'accountId is required'); + } + if (!agentId) { + return fail(400, 'agentId is required'); + } + if (!snapshot.agents.some((agent) => agent.id === agentId)) { + return fail(404, `Agent "${agentId}" not found`); + } + + const groupedChannels = listSelectedChannelAccountGroups(snapshot); + const group = groupedChannels.find((entry) => entry.channelType === channelType); + if (!group || !group.accounts.some((entry) => entry.accountId === accountId)) { + return fail(404, `Channel account "${channelType}:${accountId}" not found`); + } + + const result = assignChannelToAgent(agentId, channelType, accountId, accounts, defaultAccountId); + ctx.gatewayManager.notifyRuntimeChanged({ + topics: ['agents', 'channels', 'channel-targets'], + reason: 'channels:binding-updated', + channelType, + accountId, + }); + + return ok({ + success: true, + ...result, + }); + } catch (error) { + return fail(500, error instanceof Error ? error.message : String(error)); + } + } + + if (pathname === '/api/channels/binding' && method === 'DELETE') { + try { + const body = parseJsonBody<{ + channelType?: string; + accountId?: string | null; + }>(request.body); + const channelType = String(body?.channelType ?? '').trim(); + const accountId = String(body?.accountId ?? '').trim(); + + if (!channelType) { + return fail(400, 'channelType is required'); + } + if (!accountId) { + return fail(400, 'accountId is required'); + } + + const result = clearChannelBinding(channelType, accountId, accounts, defaultAccountId); + ctx.gatewayManager.notifyRuntimeChanged({ + topics: ['agents', 'channels', 'channel-targets'], + reason: 'channels:binding-cleared', + channelType, + accountId, + }); + + return ok({ + success: true, + ...result, + }); + } catch (error) { + return fail(500, error instanceof Error ? error.message : String(error)); + } + } + + return null; +} diff --git a/electron/api/routes/cron.ts b/electron/api/routes/cron.ts new file mode 100644 index 0000000..679866a --- /dev/null +++ b/electron/api/routes/cron.ts @@ -0,0 +1,86 @@ +import type { HostApiContext } from '../context'; +import type { NormalizedHostApiRequest } from '../route-utils'; +import { fail, ok, parseJsonBody } from '../route-utils'; +import { + createCronJob, + deleteCronJob, + listCronJobs, + toggleCronJob, + triggerCronJob, + updateCronJob, +} from '../../utils/cron-store'; +import type { CronJobCreateInput, CronJobUpdateInput } from '@src/lib/cron-types'; + +export async function handleCronRoutes( + request: NormalizedHostApiRequest, + _ctx: HostApiContext, +) { + const { pathname, method } = request; + + if (pathname === '/api/cron/jobs' && method === 'GET') { + return ok(listCronJobs()); + } + + if (pathname === '/api/cron/jobs' && method === 'POST') { + try { + const body = parseJsonBody+ {copy.inheritWorkspaceDescription} +
++ {subtitle} +
+ ) : null} ++ {pageCopy.subtitle} +
+