From 411f4f34217383952b8103aad27182c2d5709949 Mon Sep 17 00:00:00 2001
From: DEV_DSW <562304744@qq.com>
Date: Thu, 16 Apr 2026 15:13:30 +0800
Subject: [PATCH] feat: Refactor channel management and UI components
- Removed hardcoded channel data from `channel.ts` and replaced it with a dynamic channel dictionary.
- Introduced a new Pinia store `channel.ts` to manage selected and available channels.
- Reworked `AddChannelDialog.vue` to allow users to search and select channels dynamically.
- Updated `TaskCenter.vue` to utilize the new channel store and handle empty channel selections gracefully.
- Enhanced IPC communication for loading and saving selected channels in the configuration.
- Adjusted `runTaskOperationService.ts` to ensure proper handling of channel data.
- Improved styling and structure of UI components for better user experience.
---
dist-electron/main/main.js | 2 +-
dist/index.html | 34 +-
docs/ChannelRefactorPlan.md | 238 ++++++++++++++
electron/main.ts | 4 +-
electron/process/runTaskOperationService.ts | 18 +-
electron/service/config-service/index.ts | 105 +++---
global.d.ts | 11 +
src/constant/channel.ts | 52 +--
src/lib/constants.ts | 1 +
src/lib/types.ts | 2 +
src/pages/home/TaskCenter.vue | 10 +-
.../home/components/AddChannelDialog.vue | 303 +++++++++++-------
src/pages/home/index.vue | 6 +
.../scripts/components/ScriptEditorDialog.vue | 11 +-
src/stores/channel.ts | 85 +++++
15 files changed, 668 insertions(+), 214 deletions(-)
create mode 100644 docs/ChannelRefactorPlan.md
create mode 100644 src/stores/channel.ts
diff --git a/dist-electron/main/main.js b/dist-electron/main/main.js
index 2a3e22d..cfe8d25 100644
--- a/dist-electron/main/main.js
+++ b/dist-electron/main/main.js
@@ -1,6 +1,6 @@
"use strict";
require("electron");
-require("./main-D4EDpIiu.js");
+require("./main-3jJaZPgE.js");
require("electron-squirrel-startup");
require("electron-log");
require("bytenode");
diff --git a/dist/index.html b/dist/index.html
index 6add19d..28380ff 100644
--- a/dist/index.html
+++ b/dist/index.html
@@ -1,17 +1,17 @@
-
-
-
-
- NIANXX
-
-
-
-
-
-
-
-
-
+
+
+
+
+ NIANXX
+
+
+
+
+
+
+
+
+
diff --git a/docs/ChannelRefactorPlan.md b/docs/ChannelRefactorPlan.md
new file mode 100644
index 0000000..d8052cd
--- /dev/null
+++ b/docs/ChannelRefactorPlan.md
@@ -0,0 +1,238 @@
+# 渠道硬编码移除与动态关联重构计划
+
+## 上下文与目标
+
+当前 `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 或合适位置)。
+- 运行项目(`npm 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。
diff --git a/electron/main.ts b/electron/main.ts
index ac983b2..d36f20b 100644
--- a/electron/main.ts
+++ b/electron/main.ts
@@ -152,7 +152,9 @@ if (started) {
// logManager.error('unhandledRejection', reason, promise);
// });
-app.whenReady().then(() => {
+app.whenReady().then(async () => {
+ await configManager.init();
+
gatewayManager.init();
onProviderChange(() => {
diff --git a/electron/process/runTaskOperationService.ts b/electron/process/runTaskOperationService.ts
index 8cd8a36..fb90c11 100644
--- a/electron/process/runTaskOperationService.ts
+++ b/electron/process/runTaskOperationService.ts
@@ -205,14 +205,20 @@ export function runTaskOperationService() {
openedTabIndexByChannelName.clear()
- if (Array.isArray(channels)) {
- for (let i = 0; i < channels.length; i++) {
- const name = channels[i]?.channelName
- if (name) openedTabIndexByChannelName.set(String(name), i)
- }
+ const validChannels = Array.isArray(channels)
+ ? channels.filter((c) => c && typeof c === 'object' && c.channelUrl)
+ : []
+
+ if (validChannels.length === 0) {
+ return { success: false, error: '没有可用的渠道配置' }
}
- const result = await executeScriptServiceInstance.executeScript(scriptPath, { channels })
+ for (let i = 0; i < validChannels.length; i++) {
+ const name = validChannels[i]?.channelName
+ if (name) openedTabIndexByChannelName.set(String(name), i)
+ }
+
+ const result = await executeScriptServiceInstance.executeScript(scriptPath, { channels: validChannels })
return { success: true, result }
} catch (error) {
return { success: false, error: (error as any)?.message || 'open channel failed' }
diff --git a/electron/service/config-service/index.ts b/electron/service/config-service/index.ts
index 1ca5596..1f1ed4f 100644
--- a/electron/service/config-service/index.ts
+++ b/electron/service/config-service/index.ts
@@ -1,9 +1,7 @@
-import { app, BrowserWindow, ipcMain } from 'electron'
+import { BrowserWindow, ipcMain } from 'electron'
import type { ConfigKeys, IConfig } from '@lib/types'
import { CONFIG_KEYS, IPC_EVENTS } from '@lib/constants'
-import { debounce, simpleCloneDeep } from '@lib/utils'
-import * as fs from 'fs'
-import * as path from 'path'
+import { debounce } from '@lib/utils'
import logManager from '@electron/service/logger'
@@ -15,33 +13,47 @@ const DEFAULT_CONFIG: IConfig = {
[CONFIG_KEYS.MINIMIZE_TO_TRAY]: false,
[CONFIG_KEYS.PROVIDER]: '',
[CONFIG_KEYS.DEFAULT_MODEL]: null,
+ [CONFIG_KEYS.SELECTED_CHANNELS]: [],
}
export class ConfigService {
private static _instance: ConfigService;
- private _config: IConfig;
- private _configPath: string;
+ private _store: any;
private _defaultConfig: IConfig = DEFAULT_CONFIG;
private _listeners: Array<(config: IConfig) => void> = [];
private constructor() {
- // 获取配置文件路径
- this._configPath = path.join(app.getPath('userData'), 'config.json');
- // 加载配置
- this._config = this._loadConfig();
- // 设置 IPC 事件
+ // store 通过 init() 异步初始化,避免 ESM 模块在 CJS 打包环境中同步导入的问题
+ }
+
+ public async init() {
+ if (this._store) return;
+ const { default: Store } = await import('electron-store');
+ this._store = new Store({
+ name: 'config',
+ defaults: DEFAULT_CONFIG,
+ });
this._setupIpcEvents();
logManager.info('ConfigService initialized successfully.')
}
+ private _ensureStore() {
+ if (!this._store) {
+ throw new Error('ConfigService has not been initialized. Call init() first.');
+ }
+ }
+
private _setupIpcEvents() {
const duration = 200;
const handelUpdate = debounce((val) => this.update(val), duration);
ipcMain.handle(IPC_EVENTS.GET_CONFIG, (_, key) => this.get(key));
- ipcMain.on(IPC_EVENTS.SET_CONFIG, (_, key, val) => this.set(key, val));
+ ipcMain.handle(IPC_EVENTS.SET_CONFIG, (_, key, val) => {
+ this.set(key, val);
+ return { success: true };
+ });
ipcMain.on(IPC_EVENTS.UPDATE_CONFIG, (_, updates) => handelUpdate(updates));
}
@@ -52,66 +64,51 @@ export class ConfigService {
return this._instance;
}
- private _loadConfig(): IConfig {
- try {
- if (fs.existsSync(this._configPath)) {
- const configContent = fs.readFileSync(this._configPath, 'utf-8');
- const config = { ...this._defaultConfig, ...JSON.parse(configContent) };
- logManager.info('Config loaded successfully from:', this._configPath);
- return config;
- }
- } catch (error) {
- logManager.error('Failed to load config:', error);
- }
- return { ...this._defaultConfig };
- }
-
- private _saveConfig(): void {
- try {
- // 确保目录存在
- fs.mkdirSync(path.dirname(this._configPath), { recursive: true });
- // 写入
- fs.writeFileSync(this._configPath, JSON.stringify(this._config, null, 2), 'utf-8');
- // 通知监听者
- this._notifyListeners();
-
- logManager.info('Config saved successfully to:', this._configPath);
- } catch (error) {
- logManager.error('Failed to save config:', error);
- }
- }
-
private _notifyListeners(): void {
- BrowserWindow.getAllWindows().forEach(win => win.webContents.send(IPC_EVENTS.CONFIG_UPDATED, this._config));
- this._listeners.forEach(listener => listener({ ...this._config }));
+ this._ensureStore();
+ BrowserWindow.getAllWindows().forEach(win => win.webContents.send(IPC_EVENTS.CONFIG_UPDATED, this._store.store));
+ this._listeners.forEach(listener => listener({ ...this._store.store }));
}
public getConfig(): IConfig {
- return simpleCloneDeep(this._config);
+ if (!this._store) {
+ return { ...this._defaultConfig };
+ }
+ return this._store.store;
}
public get(key: ConfigKeys): T {
- return this._config[key] as T
+ if (!this._store) {
+ return this._defaultConfig[key] as T;
+ }
+ return this._store.get(key) as T
}
public set(key: ConfigKeys, value: unknown, autoSave: boolean = true): void {
- if (!(key in this._config)) return;
- const oldValue = this._config[key];
+ this._ensureStore();
+ const oldValue = this._store.get(key);
if (oldValue === value) return;
- this._config[key] = value as never;
- logManager.debug(`Config set: ${key} = ${value}`);
- autoSave && this._saveConfig();
+ this._store.set(key, value);
+ logManager.info(`Config set: ${key} = ${JSON.stringify(value)}`);
+ logManager.info(`Config file path: ${this._store.path}`);
+ autoSave && this._notifyListeners();
}
public update(updates: Partial, autoSave: boolean = true): void {
- this._config = { ...this._config, ...updates };
- autoSave && this._saveConfig();
+ this._ensureStore();
+ (Object.keys(updates) as ConfigKeys[]).forEach((key) => {
+ this._store.set(key, updates[key]);
+ });
+ autoSave && this._notifyListeners();
}
public resetToDefault(): void {
- this._config = { ...this._defaultConfig };
+ this._ensureStore();
+ (Object.keys(this._defaultConfig) as ConfigKeys[]).forEach((key) => {
+ this._store.set(key, this._defaultConfig[key]);
+ });
logManager.info('Config reset to default.');
- this._saveConfig();
+ this._notifyListeners();
}
public onConfigChange(listener: ((config: IConfig) => void)): () => void {
diff --git a/global.d.ts b/global.d.ts
index 162dd9a..6585fce 100644
--- a/global.d.ts
+++ b/global.d.ts
@@ -1,4 +1,5 @@
import { IPC_EVENTS } from '@lib/constants'
+import type { ConfigKeys } from '@lib/types'
import type { AutomationScript, ScriptSaveInput, ScriptExecutionResult } from '@lib/script-types'
declare global {
@@ -61,6 +62,16 @@ declare global {
return: Promise
}
+ // 配置
+ [IPC_EVENTS.GET_CONFIG]: {
+ params: [key: ConfigKeys]
+ return: Promise
+ }
+ [IPC_EVENTS.SET_CONFIG]: {
+ params: [key: ConfigKeys, value: any]
+ return: Promise<{ success: boolean }>
+ }
+
// 脚本管理
[IPC_EVENTS.SCRIPT_LIST]: {
params: []
diff --git a/src/constant/channel.ts b/src/constant/channel.ts
index 89501cb..3640d3b 100644
--- a/src/constant/channel.ts
+++ b/src/constant/channel.ts
@@ -1,25 +1,41 @@
-import { v4 as uuidv4 } from 'uuid'
-
export interface Item {
id: string
channelName: string
channelUrl: string
}
-export const channels: Item[] = [
- {
- id: uuidv4(),
- channelName: 'fliggy',
- channelUrl: 'https://hotel.fliggy.com/ebooking/hotelBaseInfoUv.htm#/ebk/homeV1',
- },
- {
- id: uuidv4(),
- channelName: 'meituan',
- channelUrl: 'https://me.meituan.com/ebooking/merchant/product#/index',
- },
- {
- id: uuidv4(),
- channelName: 'douyin',
- channelUrl: 'https://life.douyin.com/p/travel-ari/hotel/price_amount_state?groupid=1816249020842116',
+export const channelDictionary: Record = {
+ fliggy: 'https://hotel.fliggy.com/ebooking/hotelBaseInfoUv.htm#/ebk/homeV1',
+ meituan: 'https://me.meituan.com/ebooking/merchant/product#/index',
+ douyin: 'https://life.douyin.com/p/travel-ari/hotel/price_amount_state?groupid=1816249020842116',
+}
+
+export function resolveChannel(value: string): { name?: string; url: string } {
+ const trimmed = value.trim()
+ if (!trimmed) {
+ return { url: '' }
}
-]
\ No newline at end of file
+
+ // 如果是已知渠道名称
+ if (channelDictionary[trimmed]) {
+ return { name: trimmed, url: channelDictionary[trimmed] }
+ }
+
+ // 如果是 URL
+ if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
+ // 尝试反向查找名称
+ const entry = Object.entries(channelDictionary).find(([, url]) => url === trimmed)
+ if (entry) {
+ return { name: entry[0], url: trimmed }
+ }
+ try {
+ const hostname = new URL(trimmed).hostname
+ return { name: hostname, url: trimmed }
+ } catch {
+ return { name: trimmed, url: trimmed }
+ }
+ }
+
+ // 其他情况:当作未知名称,URL 也用它本身(下游需自行判断)
+ return { name: trimmed, url: trimmed }
+}
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index 041a188..3268d59 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -102,6 +102,7 @@ export enum CONFIG_KEYS {
DEFAULT_MODEL = 'defaultModel',
AUTO_CHECK_UPDATE = 'autoCheckUpdate',
AUTO_DOWNLOAD_UPDATE = 'autoDownloadUpdate',
+ SELECTED_CHANNELS = 'selectedChannels',
}
export enum MENU_IDS {
diff --git a/src/lib/types.ts b/src/lib/types.ts
index c9bfa6a..9f03fcb 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -18,6 +18,8 @@ export interface IConfig {
[CONFIG_KEYS.PROVIDER]?: string;
// 默认模型
[CONFIG_KEYS.DEFAULT_MODEL]?: string | null;
+ // 选中的渠道
+ [CONFIG_KEYS.SELECTED_CHANNELS]: Array<{ id: string; channelName: string; channelUrl: string }>;
}
export interface Provider {
diff --git a/src/pages/home/TaskCenter.vue b/src/pages/home/TaskCenter.vue
index 9374e9f..abf4a10 100644
--- a/src/pages/home/TaskCenter.vue
+++ b/src/pages/home/TaskCenter.vue
@@ -34,18 +34,24 @@
-