- Updated `generateUUID` function for improved readability and performance. - Deleted `logger.ts`, `other.ts`, `request.ts`, `storage.ts`, `tansParams.ts`, and `validate.ts` as they were no longer needed. - Simplified TypeScript configuration by removing unnecessary paths and aliases. - Enhanced Vite configuration for better project structure and maintainability.
239 lines
11 KiB
Markdown
239 lines
11 KiB
Markdown
# 渠道硬编码移除与动态关联重构计划
|
||
|
||
## 上下文与目标
|
||
|
||
当前 `zn-ai` 的"一键打开各渠道"功能存在以下问题:
|
||
1. **渠道数据硬编码**在 `src/constant/channel.ts` 中,仅包含 fliggy/meituan/douyin 三个固定渠道;
|
||
2. **`AddChannelDialog.vue` 是纯 UI 空壳**,收集表单后不做任何持久化或事件抛出;
|
||
3. **`TaskCenter.vue` 存在 bug**:将普通数组 `channels` 当作 ref 使用(`channels.value`),实际传递 `undefined` 给主进程;
|
||
4. **脚本管理功能**中,每个 `AutomationScript` 已有 `channel` 字段(存储渠道名称或 URL),但未被"一键打开"功能复用。
|
||
|
||
**目标**:
|
||
- 移除 `channels.ts` 中的硬编码主数据源,改为从脚本管理中动态聚合可用渠道;
|
||
- 重塑 `AddChannelDialog` 的交互:快捷搜索选择渠道 → 展示已选列表 → 保存;
|
||
- 修复 `TaskCenter.vue` 的渠道传递逻辑,使用用户动态配置的列表;
|
||
- 确保 `open_all_channel.js` 接收的数据格式正确,按需同步调整。
|
||
|
||
---
|
||
|
||
## 关键文件清单
|
||
|
||
| 路径 | 角色 | 改动类型 |
|
||
|------|------|----------|
|
||
| `zn-ai/src/constant/channel.ts` | 硬编码渠道常量 | 移除数组,保留字典+解析工具 |
|
||
| `zn-ai/src/stores/channel.ts` | **新建**渠道状态 store | 新增 |
|
||
| `zn-ai/src/stores/script.ts` | 脚本 Pinia store | 只读依赖(聚合 channel) |
|
||
| `zn-ai/src/pages/home/components/AddChannelDialog.vue` | 渠道关联弹窗 | **重写**模板与逻辑 |
|
||
| `zn-ai/src/pages/home/TaskCenter.vue` | 任务中心卡片 | 修复+替换数据源 |
|
||
| `zn-ai/src/pages/home/index.vue` | 首页容器 | 可能需补充 store 初始化 |
|
||
| `zn-ai/src/pages/scripts/components/ScriptEditorDialog.vue` | 脚本编辑弹窗 | 替换 `channels` 引用为字典 |
|
||
| `zn-ai/electron/scripts/open_all_channel.js` | 打开渠道的 Playwright 脚本 | 按需调整字段读取 |
|
||
| `zn-ai/electron/process/runTaskOperationService.ts` | IPC 处理(OPEN_CHANNEL) | 按需做数据格式适配 |
|
||
|
||
---
|
||
|
||
## 方案设计
|
||
|
||
### 1. 数据层:`constant/channel.ts` + 新建 `stores/channel.ts`
|
||
|
||
#### `constant/channel.ts` 改造
|
||
- **移除** `export const channels: Item[]` 硬编码数组。
|
||
- **保留** `Item` 接口(`{id, channelName, channelUrl}`)。
|
||
- **新增** `channelDictionary: Record<string, string>`,仅作为"已知渠道名称→URL"的参考字典(从原数组迁移)。
|
||
- **新增** `resolveChannel(value: string): { name?: string; url: string }`:
|
||
- 若 `value` 匹配 `channelDictionary` 的 key,返回 `{name: value, url: dictionary[value]}`;
|
||
- 若 `value` 本身像 URL(以 http 开头),尝试从字典反向查找 name,找不到则 name 取 hostname 或原值;
|
||
- 其他情况返回 `{name: value, url: value}`。
|
||
|
||
#### 新建 `stores/channel.ts`(Pinia)
|
||
使用 Composition API 风格,与 `script.ts`/`cron.ts` 保持一致。
|
||
|
||
```ts
|
||
export interface ChannelItem {
|
||
id: string;
|
||
channelName: string;
|
||
channelUrl: string;
|
||
}
|
||
|
||
const STORAGE_KEY = 'zn-ai:selected-channels';
|
||
|
||
export const useChannelStore = defineStore('channel', () => {
|
||
const scriptStore = useScriptStore();
|
||
|
||
// 用户选中的"一键打开"渠道(持久化到 localStorage)
|
||
const selectedChannels = ref<ChannelItem[]>([]);
|
||
|
||
// 从脚本 store 动态聚合可用渠道(去重,按 URL 去重)
|
||
const availableChannels = computed<ChannelItem[]>(() => {
|
||
const map = new Map<string, ChannelItem>();
|
||
for (const script of scriptStore.safeScripts) {
|
||
if (!script.channel) continue;
|
||
const resolved = resolveChannel(script.channel);
|
||
const url = resolved.url;
|
||
if (!map.has(url)) {
|
||
map.set(url, {
|
||
id: `channel-${url}`,
|
||
channelName: resolved.name || url,
|
||
channelUrl: url,
|
||
});
|
||
}
|
||
}
|
||
return Array.from(map.values());
|
||
});
|
||
|
||
const loadSelectedChannels = () => { ... };
|
||
const saveSelectedChannels = () => { ... };
|
||
const addSelectedChannel = (item: ChannelItem) => { ... };
|
||
const removeSelectedChannel = (id: string) => { ... };
|
||
const setSelectedChannels = (items: ChannelItem[]) => { ... };
|
||
|
||
return {
|
||
selectedChannels,
|
||
availableChannels,
|
||
loadSelectedChannels,
|
||
saveSelectedChannels,
|
||
addSelectedChannel,
|
||
removeSelectedChannel,
|
||
setSelectedChannels,
|
||
};
|
||
});
|
||
```
|
||
|
||
**注意**:`AddChannelDialog` 打开时应基于 `availableChannels` 做搜索,确认保存时调用 `setSelectedChannels` + `saveSelectedChannels`。
|
||
|
||
---
|
||
|
||
### 2. UI 层:`AddChannelDialog.vue` 重塑
|
||
|
||
当前弹窗是两个空白输入框(名称+URL),需要改为**搜索选择 + 已选列表**。
|
||
|
||
#### 新交互流程
|
||
1. **顶部搜索区**:`el-autocomplete`(或 `el-input` + 自定义下拉列表)绑定 `availableChannels` 过滤结果;
|
||
2. **选择后**:将渠道添加到下方的"已选渠道"列表(去重检查);
|
||
3. **已选列表**:使用卡片/表格形式展示,每行包含:
|
||
- 渠道名称
|
||
- 渠道 URL(截断显示,tooltip 展示完整)
|
||
- 删除按钮(`el-icon Delete`)
|
||
4. **底部操作**:取消 / 确认。确认时持久化到 store。
|
||
|
||
#### 状态隔离
|
||
弹窗打开时,先 `loadSelectedChannels()` 到本地临时数组;编辑过程中操作临时数组;确认时写入 store 并持久化;取消时直接关闭,不污染 store。
|
||
|
||
#### 样式适配
|
||
复用项目已有的 `custom-script-dialog` 圆角/深色模式变量,保持视觉一致性。
|
||
|
||
---
|
||
|
||
### 3. 修复 `TaskCenter.vue`
|
||
|
||
- **移除** `import { channels } from '@constant/channel'`。
|
||
- **注入** `useChannelStore`。
|
||
- 点击"一键打开各渠道"时:
|
||
```ts
|
||
const channelStore = useChannelStore();
|
||
window.api.openChannel(channelStore.selectedChannels)
|
||
```
|
||
- 如果 `selectedChannels` 为空,可给出提示(`ElMessage.warning`)或仍然允许空数组由主进程处理。
|
||
|
||
---
|
||
|
||
### 4. `ScriptEditorDialog.vue` 适配
|
||
|
||
- 将 `import { channels } from '@constant/channel'` 改为 `import { channelDictionary, resolveChannel } from '@constant/channel'`。
|
||
- `getChannelUrl(channel)` 改为在 `channelDictionary` 中查找。
|
||
- channel URL 替换 watcher 逻辑同步使用 `channelDictionary` 的 values。
|
||
|
||
---
|
||
|
||
### 5. `open_all_channel.js` 与主进程适配
|
||
|
||
当前 `open_all_channel.js` 读取环境变量 `CHANNELS`,期望元素结构为 `{ channelUrl: string }`(第 81 行 `channels[i]?.channelUrl`)。
|
||
|
||
`channelStore.selectedChannels` 的结构保持为 `Item`(`{ id, channelName, channelUrl }`),因此**主进程接收到的数组本身已包含 `channelUrl` 字段**。
|
||
|
||
需要确认 `runTaskOperationService.ts` 中 `IPC_EVENTS.OPEN_CHANNEL` handler:
|
||
```ts
|
||
ipcMain.handle(IPC_EVENTS.OPEN_CHANNEL, async (_event, channels: any) => { ... })
|
||
```
|
||
它直接将 `channels` 传给 `executeScriptServiceInstance.executeScript(scriptPath, { channels })`,后者 JSON.stringify 后写入 `CHANNELS` 环境变量。
|
||
|
||
**结论**:只要 `TaskCenter.vue` 传递的数组元素包含 `channelUrl`,`open_all_channel.js` **无需修改**即可正常工作。
|
||
|
||
但用户要求"同步更新 open_all_channel.js",为确保严谨,计划内包含**兼容性检查**:若发现字段名不一致,统一调整为 `channelUrl`;若需要增强(如按名称去重、空值过滤),则在 `runTaskOperationService.ts` 的 handler 中做适配,保持 `open_all_channel.js` 的输入契约稳定。
|
||
|
||
---
|
||
|
||
## Sub-agent 分工(共 3 个)
|
||
|
||
### Agent A — 数据层与字典迁移
|
||
**负责范围**:
|
||
- 改造 `zn-ai/src/constant/channel.ts`(移除数组,保留字典+`resolveChannel`)。
|
||
- 新建 `zn-ai/src/stores/channel.ts`(Pinia store,含 `availableChannels` / `selectedChannels` / localStorage 持久化)。
|
||
- 修改 `ScriptEditorDialog.vue` 中的引用,从原 `channels` 数组切换到 `channelDictionary`。
|
||
|
||
**输入依赖**:已确认的 `script.ts` 结构、`channel.ts` 原数组。
|
||
**输出产物**:可运行的数据层代码。
|
||
|
||
---
|
||
|
||
### Agent B — AddChannelDialog UI 重构
|
||
**负责范围**:
|
||
- 重写 `zn-ai/src/pages/home/components/AddChannelDialog.vue`。
|
||
- 实现搜索选择(`el-autocomplete` 或自定义搜索+下拉)与已选列表展示。
|
||
- 对接 `useChannelStore`:读取 `availableChannels`,保存时写入 `selectedChannels`。
|
||
- 保持与项目一致的 dark/light 样式。
|
||
|
||
**输入依赖**:Agent A 完成的 `stores/channel.ts`。
|
||
**工作方式**:可以并行启动,但代码编辑需等待 Agent A 的 store 文件就位(或先在本地 mock 接口)。
|
||
|
||
---
|
||
|
||
### Agent C — TaskCenter 修复与端到端验证
|
||
**负责范围**:
|
||
- 修改 `zn-ai/src/pages/home/TaskCenter.vue`:移除硬编码导入,注入 `useChannelStore`,修复 `.value` bug。
|
||
- 检查 `zn-ai/electron/process/runTaskOperationService.ts` 与 `open_all_channel.js` 的数据格式兼容性,必要时做适配。
|
||
- 在 `zn-ai/src/pages/home/index.vue` 中添加 `channelStore.loadSelectedChannels()` 初始化(onMounted 或合适位置)。
|
||
- 运行项目(`pnpm run dev` 或等效命令),验证:
|
||
1. 脚本管理中有渠道数据时,`AddChannelDialog` 能搜索到;
|
||
2. 选择并保存后,`TaskCenter` 能正确传递数组;
|
||
3. `open_all_channel.js` 能正常解析并打开页面。
|
||
|
||
**输入依赖**:Agent A 与 Agent B 的产物。
|
||
**工作方式**:必须在 A、B 完成后启动。
|
||
|
||
---
|
||
|
||
## 执行顺序
|
||
|
||
```
|
||
Agent A (数据层) ─┬─► Agent B (UI 弹窗)
|
||
│
|
||
└─► Agent C (流程修复与验证)
|
||
```
|
||
|
||
- **A 与 B 可并行或准并行**:B 可以在 A 提交 store 初版后立即开始对接。
|
||
- **C 必须在 A、B 都完成后启动**,负责联调与验证。
|
||
|
||
---
|
||
|
||
## 风险与回退策略
|
||
|
||
| 风险 | 缓解措施 |
|
||
|------|----------|
|
||
| `availableChannels` 为空(脚本管理无渠道数据) | `AddChannelDialog` 搜索下拉显示"暂无可用渠道",已选列表允许空状态;`TaskCenter` 点击时给出提示 |
|
||
| 脚本 `channel` 字段格式混乱(既有 URL 又有名称) | `resolveChannel()` 统一做归一化处理,以 URL 为唯一键 |
|
||
| `open_all_channel.js` 字段契约被打破 | 在 `runTaskOperationService.ts` 的 handler 中做字段映射/过滤,保证脚本侧输入不变 |
|
||
| localStorage 中旧数据格式不兼容 | `loadSelectedChannels` 读取时做 schema 校验,不兼容则清空并回退到空数组 |
|
||
|
||
---
|
||
|
||
## 验证清单(Agent C 负责)
|
||
|
||
- [ ] `channels.ts` 中不再导出硬编码数组。
|
||
- [ ] `AddChannelDialog` 打开后,能从脚本中搜索出渠道(如 fliggy / meituan / douyin)。
|
||
- [ ] 选择多个渠道后列表正确显示,删除单个后状态正确。
|
||
- [ ] 点击"确认"关闭弹窗,再次打开能恢复上次保存的选项。
|
||
- [ ] `TaskCenter` 中点击"一键打开各渠道",`openChannel` 参数为正确的 `ChannelItem[]`(非 undefined)。
|
||
- [ ] `open_all_channel.js` 成功解析 `CHANNELS` 并依次打开对应页面。
|
||
- [ ] 脚本编辑弹窗中 `getChannelUrl` 仍能正确将 `fliggy` 解析为对应 URL。
|