# 渠道硬编码移除与动态关联重构计划 ## 上下文与目标 当前 `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`,仅作为"已知渠道名称→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([]); // 从脚本 store 动态聚合可用渠道(去重,按 URL 去重) const availableChannels = computed(() => { const map = new Map(); 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。