Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/artisancloud_powerx.json
T

1 line
118 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "artisancloud/powerx", "files": {"docs/guides/agent/ai_setting.md": {"8b845aee0fb1ad928bde2b990b47a885e859f42c": {"t": 1785746494.8433843, "n": 2460, "h": "0ced479adb8c68ad", "c": "# AI 设置与主模型生效流程\n\nPowerX 的「AI 设置」页面提供了统一的 Provider/模型管理;理解其落库与生效顺序,能帮你快速定位“为什么切换 provider 没有真正生效”的问题。\n\n## 默认配置\n\n1. **开发期种子默认值**:`backend/etc/config_example.yaml` 的 `ai.defaults` 主要用于“本地开发/初次启动”的演示默认(例如默认选中哪个 provider/model)。在生产环境里,如果数据库里没有保存任何 AI Settings(含凭据),系统应提示用户先完成安装/配置,而不是静默使用 config.yaml 的值继续执行。\n2. **环境隔离**:页面左侧的环境切换(默认 `default`)会把不同环境的数据保存在各自的 scope 下,后端通过 `env + tenant` 二元索引区分,同一个 provider 可以在不同环境配置不同的 key。\n\n## 聊天时的 LLM 生效优先级(人话版)\n\n一次“发送消息”真正用哪个 provider/model/参数,会按从高到低依次覆盖:\n\n1. **Flow 节点级配置(node params)**:某个 flow 的某个 LLM 节点写死的参数(例如强制 `model=xxx`)。这是最高优先级。\n2. **本次请求临时配置(ChatConfig)**:前端在本次发送里临时指定(例如用户临时切模型/温度),只影响这一次或这一会话。\n3. **智能体自身配置(AgentSetting,DB)**:在「智能体配置」里给某个智能体单独设置的 provider/model(以及可选参数),只影响这个智能体。\n4. **AI Settings 默认(Active Profile,DB)**:在「AI 设置」里选中的默认 provider/model(按 env + tenant 隔离),作为系统默认。\n5. **开发期种子默认(config.yaml)**:仅用于没有任何 DB 配置时的“演示默认”。如果缺少凭据/不可连通,运行时应返回明确错误并引导去 AI Settings 配置。\n\n## 保存一次 = 落两个实体\n\n点击页面的「保存」会触发 `AgentSettingService.SaveCredentialAndProfile`,落库逻辑如下:\n\n| 实体 | 说明 |\n| --- | --- |\n| `ai_provider_credentials` | 记录 provider 的 `base_url`、`api_key` 等敏感字段。重复保存会自动加密并“保留旧的 __sealed”。 |\n| `ai_model_profiles` | 记录当前模态的 provider/model 组合以及默认参数(温度、max_tokens 等)。 |\n\n换句话说,你可以先把不同 provider 的凭据都保存好,作为候选。此时数据库里会有多条 profile/credential,但 Agent 仍然会沿用「当前激活配置」。\n\n## 选择谁当主模型?\n\n真正决定“系统主智能体”使用哪个 provider/model 的,是 `ai_route_policies` 中的默认路由(`SetActiveProfile` 会写入 Name=`__default` 的记录):\n\n1. 在页面切换到目标 provider/model。\n2. (可选)点击「测试连接」保证连通性通过。\n3. 点击「保存」。这一步不仅更新 profile/credential,还会把该 provider/model 写入默认路由,后端随即改用新的组合。\n\n> ⚠️ **仅切换下拉框不生效**:如果你仅切换 provider 但没有点击「保存」,前端状态会改变,但数据库里的默认路由不会更新,Agent 仍然调用旧模型。\n\n## 智能体级覆盖(AgentSetting)\n\n如果你希望某个智能体使用不同于系统默认的模型(例如“营销智能体用 deepseek-reasoner、客服智能体用 gpt-4o-mini”),请在智能体的配置面板里设置 provider/model(并保存)。\n\n- 这会写入 `agent_settings`(按 env + tenant + agent 作用域)\n- 运行时会先读取 AI Settings 的默认,再读取该智能体的覆盖;如果智能体选择“系统默认”,则会删除该覆盖记录并回退到 AI Settings 默认\n\n## 多 provider 场景\n\n- 你可以为多个 provider 各自保存凭据、参数,但同一模态下始终只有一个“激活”组合。需要切换时,选中后再次点击「保存」,系统会覆盖默认路由。\n- 若想恢复默认(或某个候选)配置,只需切到对应 provider,检查参数无误后点击「保存」即可。\n- 删除/禁用某 provider 之前,建议先把默认路由切到其他 provider,避免出现“找不到激活 profile”。\n\n## 常见问题排查\n\n| 现象 | 可能原因 | 处理方式 |\n| --- | --- | --- |\n| 后端日志仍显示旧 provider | 切换后未点击「保存」;或保存失败 | 查看页面提示/浏览器 Network,确认 `…/settings/save` 返回成功 |\n| Base URL 不随 provider 清空 | 老版前端会缓存,记得刷新页面或在「保存」前手动覆盖 | 当前版本已在 provider 切换时同步;若仍遇到,重载页面确认 |\n| 环境切换后配置不对 | 环境 store 未刷新 | 先切换环境,下方状态加载完成后再进行操作 |\n\n掌握上述流程后,可以放心地把多个 provider 的信息一次性录入,只需记得“切换谁 → 就立即保存”来更新默认主模型。 ***\n"}, "bb25e7e153153ac449acc8b957cddf76f565bafa": {"t": 1785764947.6109354, "n": 2460, "h": "0ced479adb8c68ad", "c": "# AI 设置与主模型生效流程\n\nPowerX 的「AI 设置」页面提供了统一的 Provider/模型管理;理解其落库与生效顺序,能帮你快速定位“为什么切换 provider 没有真正生效”的问题。\n\n## 默认配置\n\n1. **开发期种子默认值**:`backend/etc/config_example.yaml` 的 `ai.defaults` 主要用于“本地开发/初次启动”的演示默认(例如默认选中哪个 provider/model)。在生产环境里,如果数据库里没有保存任何 AI Settings(含凭据),系统应提示用户先完成安装/配置,而不是静默使用 config.yaml 的值继续执行。\n2. **环境隔离**:页面左侧的环境切换(默认 `default`)会把不同环境的数据保存在各自的 scope 下,后端通过 `env + tenant` 二元索引区分,同一个 provider 可以在不同环境配置不同的 key。\n\n## 聊天时的 LLM 生效优先级(人话版)\n\n一次“发送消息”真正用哪个 provider/model/参数,会按从高到低依次覆盖:\n\n1. **Flow 节点级配置(node params)**:某个 flow 的某个 LLM 节点写死的参数(例如强制 `model=xxx`)。这是最高优先级。\n2. **本次请求临时配置(ChatConfig)**:前端在本次发送里临时指定(例如用户临时切模型/温度),只影响这一次或这一会话。\n3. **智能体自身配置(AgentSetting,DB)**:在「智能体配置」里给某个智能体单独设置的 provider/model(以及可选参数),只影响这个智能体。\n4. **AI Settings 默认(Active Profile,DB)**:在「AI 设置」里选中的默认 provider/model(按 env + tenant 隔离),作为系统默认。\n5. **开发期种子默认(config.yaml)**:仅用于没有任何 DB 配置时的“演示默认”。如果缺少凭据/不可连通,运行时应返回明确错误并引导去 AI Settings 配置。\n\n## 保存一次 = 落两个实体\n\n点击页面的「保存」会触发 `AgentSettingService.SaveCredentialAndProfile`,落库逻辑如下:\n\n| 实体 | 说明 |\n| --- | --- |\n| `ai_provider_credentials` | 记录 provider 的 `base_url`、`api_key` 等敏感字段。重复保存会自动加密并“保留旧的 __sealed”。 |\n| `ai_model_profiles` | 记录当前模态的 provider/model 组合以及默认参数(温度、max_tokens 等)。 |\n\n换句话说,你可以先把不同 provider 的凭据都保存好,作为候选。此时数据库里会有多条 profile/credential,但 Agent 仍然会沿用「当前激活配置」。\n\n## 选择谁当主模型?\n\n真正决定“系统主智能体”使用哪个 provider/model 的,是 `ai_route_policies` 中的默认路由(`SetActiveProfile` 会写入 Name=`__default` 的记录):\n\n1. 在页面切换到目标 provider/model。\n2. (可选)点击「测试连接」保证连通性通过。\n3. 点击「保存」。这一步不仅更新 profile/credential,还会把该 provider/model 写入默认路由,后端随即改用新的组合。\n\n> ⚠️ **仅切换下拉框不生效**:如果你仅切换 provider 但没有点击「保存」,前端状态会改变,但数据库里的默认路由不会更新,Agent 仍然调用旧模型。\n\n## 智能体级覆盖(AgentSetting)\n\n如果你希望某个智能体使用不同于系统默认的模型(例如“营销智能体用 deepseek-reasoner、客服智能体用 gpt-4o-mini”),请在智能体的配置面板里设置 provider/model(并保存)。\n\n- 这会写入 `agent_settings`(按 env + tenant + agent 作用域)\n- 运行时会先读取 AI Settings 的默认,再读取该智能体的覆盖;如果智能体选择“系统默认”,则会删除该覆盖记录并回退到 AI Settings 默认\n\n## 多 provider 场景\n\n- 你可以为多个 provider 各自保存凭据、参数,但同一模态下始终只有一个“激活”组合。需要切换时,选中后再次点击「保存」,系统会覆盖默认路由。\n- 若想恢复默认(或某个候选)配置,只需切到对应 provider,检查参数无误后点击「保存」即可。\n- 删除/禁用某 provider 之前,建议先把默认路由切到其他 provider,避免出现“找不到激活 profile”。\n\n## 常见问题排查\n\n| 现象 | 可能原因 | 处理方式 |\n| --- | --- | --- |\n| 后端日志仍显示旧 provider | 切换后未点击「保存」;或保存失败 | 查看页面提示/浏览器 Network,确认 `…/settings/save` 返回成功 |\n| Base URL 不随 provider 清空 | 老版前端会缓存,记得刷新页面或在「保存」前手动覆盖 | 当前版本已在 provider 切换时同步;若仍遇到,重载页面确认 |\n| 环境切换后配置不对 | 环境 store 未刷新 | 先切换环境,下方状态加载完成后再进行操作 |\n\n掌握上述流程后,可以放心地把多个 provider 的信息一次性录入,只需记得“切换谁 → 就立即保存”来更新默认主模型。 ***\n"}, "d6561e4c7f788d25d5ac398d31f6f788dcab7df3": {"t": 1785764949.7909915, "n": 1642, "h": "7a2e3d17bd8d6f85", "c": "# AI 设置与主模型生效流程\n\nPowerX 的「AI 设置」页面提供了统一的 Provider/模型管理;理解其落库与生效顺序,能帮你快速定位“为什么切换 provider 没有真正生效”的问题。\n\n## 默认配置\n\n1. **后端默认值**:若从未在后台保存任何配置,Agent 将使用 `backend/etc/config_example.yaml` 中 `ai.defaults` 段配置的 provider / model / endpoint。这是编译期的静态兜底,适合本地开发。\n2. **环境隔离**:页面左侧的环境切换(默认 `default`)会把不同环境的数据保存在各自的 scope 下,后端通过 `env + tenant` 二元索引区分,同一个 provider 可以在不同环境配置不同的 key。\n\n## 保存一次 = 落两个实体\n\n点击页面的「保存」会触发 `AgentSettingService.SaveCredentialAndProfile`,落库逻辑如下:\n\n| 实体 | 说明 |\n| --- | --- |\n| `ai_provider_credentials` | 记录 provider 的 `base_url`、`api_key` 等敏感字段。重复保存会自动加密并“保留旧的 __sealed”。 |\n| `ai_model_profiles` | 记录当前模态的 provider/model 组合以及默认参数(温度、max_tokens 等)。 |\n\n换句话说,你可以先把不同 provider 的凭据都保存好,作为候选。此时数据库里会有多条 profile/credential,但 Agent 仍然会沿用「当前激活配置」。\n\n## 选择谁当主模型?\n\n真正决定“系统主智能体”使用哪个 provider/model 的,是 `ai_route_policies` 中的默认路由(`SetActiveProfile` 会写入 Name=`__default` 的记录):\n\n1. 在页面切换到目标 provider/model。\n2. (可选)点击「测试连接」保证连通性通过。\n3. 点击「保存」。这一步不仅更新 profile/credential,还会把该 provider/model 写入默认路由,后端随即改用新的组合。\n\n> ⚠️ **仅切换下拉框不生效**:如果你仅切换 provider 但没有点击「保存」,前端状态会改变,但数据库里的默认路由不会更新,Agent 仍然调用旧模型。\n\n## 多 provider 场景\n\n- 你可以为多个 provider 各自保存凭据、参数,但同一模态下始终只有一个“激活”组合。需要切换时,选中后再次点击「保存」,系统会覆盖默认路由。\n- 若想恢复默认(或某个候选)配置,只需切到对应 provider,检查参数无误后点击「保存」即可。\n- 删除/禁用某 provider 之前,建议先把默认路由切到其他 provider,避免出现“找不到激活 profile”。\n\n## 常见问题排查\n\n| 现象 | 可能原因 | 处理方式 |\n| --- | --- | --- |\n| 后端日志仍显示旧 provider | 切换后未点击「保存」;或保存失败 | 查看页面提示/浏览器 Network,确认 `…/settings/save` 返回成功 |\n| Base URL 不随 provider 清空 | 老版前端会缓存,记得刷新页面或在「保存」前手动覆盖 | 当前版本已在 provider 切换时同步;若仍遇到,重载页面确认 |\n| 环境切换后配置不对 | 环境 store 未刷新 | 先切换环境,下方状态加载完成后再进行操作 |\n\n掌握上述流程后,可以放心地把多个 provider 的信息一次性录入,只需记得“切换谁 → 就立即保存”来更新默认主模型。 ***\n"}}, "backend/config/agents/providers.d/hunyuan.yaml": {"8b845aee0fb1ad928bde2b990b47a885e859f42c": {"t": 1785809844.3322086, "n": 3804, "h": "ab9488a2813c7892", "c": "id: hunyuan\nname: 腾讯混元 (Hunyuan)\naliases:\n # “元宝”是产品侧名称,这里作为别名兼容历史配置/输入\n - \"Hunyuan\"\n - \"hunyuan\"\n - \"腾讯混元\"\n - \"Tencent Hunyuan\"\n - \"Yuanbao\"\n - \"yuanbao\"\n - \"腾讯元宝\"\n - \"Tencent Yuanbao\"\n\ndrivers:\n llm: hunyuan\n image: external\n video: external\n\nauth:\n # 同时支持两种接入方式:\n # 1) OpenAI SDK 兼容:API KEY(优先推荐,前端默认)\n # 2) 腾讯云 SDK:SecretId/SecretKey(TC3 签名)\n modes:\n - id: openai\n label: \"OpenAI SDK(API Key)\"\n scheme: bearer\n fields: [\"api_key\", \"base_url\"]\n defaults:\n # 腾讯云大模型 API(OpenAI SDK 兼容)\n base_url: \"https://api.hunyuan.cloud.tencent.com/v1\"\n - id: tc3\n label: \"腾讯云 SDK(SecretId/SecretKey)\"\n scheme: tc3\n fields: [\"secret_id\", \"secret_key\", \"region\", \"base_url\"]\n defaults:\n base_url: \"https://hunyuan.tencentcloudapi.com\"\n region: \"ap-guangzhou\"\n\nmodalities:\n llm:\n note: \"目录占位:用于在 UI 中预置混元模型列表;实际调用需要补齐 llm driver(或接入 OpenAI-compatible 网关)。\"\n models:\n - id: hunyuan-lite\n label: \"混元 Lite (placeholder)\"\n tags: [\"llm\",\"tencent\",\"placeholder\"]\n defaults:\n temperature: 0.7\n max_tokens: 1024\n # OpenAI SDK 模式的 base_url 默认带 /v1,因此这里用相对路径\n api_path: \"/chat/completions\"\n - id: hunyuan-standard\n label: \"混元 Standard (placeholder)\"\n tags: [\"llm\",\"tencent\",\"placeholder\"]\n defaults:\n temperature: 0.7\n max_tokens: 2048\n api_path: \"/chat/completions\"\n - id: hunyuan-standard-256K\n label: \"混元 Standard 256K (placeholder)\"\n tags: [\"llm\",\"tencent\",\"placeholder\",\"long-context\"]\n defaults:\n temperature: 0.6\n max_tokens: 4096\n api_path: \"/chat/completions\"\n - id: hunyuan-turbo\n label: \"混元 Turbo (placeholder)\"\n tags: [\"llm\",\"tencent\",\"placeholder\"]\n defaults:\n temperature: 0.7\n max_tokens: 1024\n api_path: \"/chat/completions\"\n - id: hunyuan-turbo-latest\n label: \"混元 Turbo Latest (placeholder)\"\n tags: [\"llm\",\"tencent\",\"placeholder\"]\n defaults:\n temperature: 0.7\n max_tokens: 1024\n api_path: \"/chat/completions\"\n - id: hunyuan-pro\n label: \"混元 Pro (placeholder)\"\n tags: [\"llm\",\"tencent\",\"placeholder\"]\n defaults:\n temperature: 0.6\n max_tokens: 2048\n api_path: \"/chat/completions\"\n - id: hunyuan-code\n label: \"混元 Code (placeholder)\"\n tags: [\"llm\",\"tencent\",\"placeholder\",\"code\"]\n defaults:\n temperature: 0.3\n max_tokens: 2048\n api_path: \"/chat/completions\"\n\n model3d:\n note: \"腾讯混元 3D 生成模型(UI 目录)。\"\n models:\n - id: HY-3D-Express\n label: \"HY-3D-Express\"\n tags: [\"3d\",\"tencent\",\"hunyuan\"]\n - id: HY-3D-UVWrap-1.5\n label: \"HY-3D-UVWrap-1.5\"\n tags: [\"3d\",\"tencent\",\"hunyuan\"]\n - id: HY 3D Texture Generation\n label: \"HY 3D Texture Generation\"\n tags: [\"3d\",\"tencent\",\"hunyuan\",\"texture\"]\n - id: HY 3D Smart Topology\n label: \"HY 3D Smart Topology\"\n tags: [\"3d\",\"tencent\",\"hunyuan\",\"topology\"]\n - id: HY 3D Part Generation\n label: \"HY 3D Part Generation\"\n tags: [\"3d\",\"tencent\",\"hunyuan\",\"part\"]\n - id: HY-3D-Profession\n label: \"HY-3D-Profession\"\n tags: [\"3d\",\"tencent\",\"hunyuan\"]\n\n image:\n note: \"目录占位:用于在 UI 中预置混元图像生成入口;实际调用需要补齐 image driver。\"\n models:\n - id: hunyuan-image\n label: \"混元 图像生成 (placeholder)\"\n tags: [\"image\",\"tencent\",\"placeholder\"]\n\n video:\n note: \"目录占位:用于在 UI 中预置混元视频生成入口;实际调用需要补齐 video driver。\"\n models:\n - id: hunyuan-video\n label: \"混元 视频生成 (placeholder)\"\n tags: [\"video\",\"tencent\",\"placeholder\"]\n\n audio_tts:\n models: []\n\n audio_asr:\n models: []\n"}}, "web-admin/app/pages/setup/index.vue": {"8b845aee0fb1ad928bde2b990b47a885e859f42c": {"t": 1785809848.2178767, "n": 92937, "h": "d95a9f03385b26cd", "c": "<script setup lang=\"ts\">\nimport type { StepperItem } from \"@nuxt/ui\";\nimport {\n useSettingsService,\n type SetupStatus,\n} from \"~/composables/api/services/settingsService\";\nimport { AISettingService } from \"~/composables/api/services/aiSettingService\";\nimport TestPanel from \"~/components/settings/ai/TestPanel.vue\";\n\ndefinePageMeta({\n layout: false,\n});\n\n// Stepper 引用和当前步骤\nconst stepper = ref();\nconst currentStep = ref(0);\nconst isLoading = ref(false);\nconst completing = ref(false);\nconst restarting = ref(false);\nconst restartProgressText = ref(\"\");\nconst manualCheckLoading = ref(false);\nconst setupStatus = ref<SetupStatus | null>(null);\nconst settingsService = useSettingsService();\nconst toast = useToast();\nconst { locale } = useI18n();\nconst showLicenseModal = ref(false);\nconst showTermsModal = ref(false);\nconst showPrivacyModal = ref(false);\nconst isZh = computed(() => String(locale.value || \"\").startsWith(\"zh\"));\nconst uiCopy = computed(() =>\n isZh.value\n ? {\n pageTitle: \"PowerX 系统初始化\",\n pageDesc: \"欢迎使用 PowerX!请按照以下步骤完成系统初始化设置\",\n step1Title: \"系统检查 & 许可\",\n versionLabel: \"当前系统版本:\",\n envCheckTitle: \"系统环境检测\",\n checkLabelMap: {\n database: \"数据库连通性\",\n storage: \"对象存储\",\n cache: \"缓存服务\",\n email: \"邮件服务\",\n ai: \"AI 模型配置\",\n } as Record<string, string>,\n checking: \"检测中...\",\n msgTenantInited: \"租户数据已初始化\",\n msgTenantNotInited: \"租户数据尚未初始化\",\n msgStoragePending: \"请在部署配置中确认 storage 配置\",\n msgCachePending: \"请在部署配置中确认 redis 配置\",\n msgEmailPending: \"可选项,可稍后配置\",\n msgAIOk: \"已存在 AI 模型配置\",\n msgAINone: \"尚未配置 AI 模型\",\n msgCheckFailed: \"检测失败,请检查后端服务与数据库连接\",\n }\n : {\n pageTitle: \"PowerX Setup\",\n pageDesc: \"Welcome to PowerX. Complete the steps below to initialize the system.\",\n step1Title: \"System Check & License\",\n versionLabel: \"Current version: \",\n envCheckTitle: \"Environment Checks\",\n checkLabelMap: {\n database: \"Database Connectivity\",\n storage: \"Object Storage\",\n cache: \"Cache Service\",\n email: \"Email Service\",\n ai: \"AI Model Setup\",\n } as Record<string, string>,\n checking: \"Checking...\",\n msgTenantInited: \"Tenant data initialized\",\n msgTenantNotInited: \"Tenant data not initialized\",\n msgStoragePending: \"Confirm storage config in deployment settings\",\n msgCachePending: \"Confirm redis config in deployment settings\",\n msgEmailPending: \"Optional, can be configured later\",\n msgAIOk: \"AI model configuration detected\",\n msgAINone: \"AI model is not configured\",\n msgCheckFailed: \"Check failed. Verify backend service and database connection.\",\n }\n);\nconst stepperItems = computed<StepperItem[]>(() => [\n {\n slot: \"system-check\" as const,\n title: isZh.value ? \"系统检查 & 许可\" : \"System Check & License\",\n description: isZh.value ? \"检测系统环境和确认使用条款\" : \"Validate environment and accept terms\",\n icon: \"i-lucide-shield-check\",\n },\n {\n slot: \"database-config\" as const,\n title: isZh.value ? \"数据库 & 基础配置\" : \"Database & Basics\",\n description: isZh.value ? \"配置数据库、存储、缓存等基础服务\" : \"Configure database, storage, and cache\",\n icon: \"i-lucide-database\",\n },\n {\n slot: \"domain-https\" as const,\n title: isZh.value ? \"域名与 HTTPS\" : \"Domain & HTTPS\",\n description: isZh.value ? \"配置外部访问域名和 TLS 设置\" : \"Configure domain and TLS options\",\n icon: \"i-lucide-globe\",\n },\n {\n slot: \"admin-tenant\" as const,\n title: isZh.value ? \"超级管理员 & 租户初始化\" : \"Admin & Tenant Init\",\n description: isZh.value ? \"创建管理员账户和初始化租户\" : \"Create admin account and initialize tenant\",\n icon: \"i-lucide-user-cog\",\n },\n {\n slot: \"plugins-install\" as const,\n title: isZh.value ? \"插件与智能体安装\" : \"Plugins & Agents\",\n description: isZh.value ? \"安装基础插件和智能体组件\" : \"Install basic plugins and agent components\",\n icon: \"i-lucide-puzzle\",\n },\n {\n slot: \"llm-config\" as const,\n title: isZh.value ? \"LLM 模型配置\" : \"LLM Configuration\",\n description: isZh.value ? \"可选:配置文本大模型接入参数\" : \"Optional: configure text LLM access\",\n icon: \"i-lucide-bot\",\n },\n]);\nconst legalCopy = computed(() =>\n String(locale.value || \"\").startsWith(\"zh\")\n ? {\n sectionTitle: \"许可与条款\",\n agreePrefix: \"我已阅读并同意\",\n license: \"软件许可协议\",\n terms: \"服务条款\",\n privacy: \"隐私政策\",\n and: \"和\",\n telemetryTitle: \"启用匿名遥测数据收集(可选,默认关闭)\",\n telemetryDesc: \"帮助我们改进产品,不会收集敏感信息\",\n licenseModalTitle: \"软件许可协议\",\n licenseLines: [\n \"本软件按“现状”提供,仅用于合法业务场景。\",\n \"未经授权不得复制、反编译、转售或用于违法用途。\",\n \"生产环境请自行评估并承担部署、运维与数据合规责任。\",\n \"如需正式法务版本,请替换为贵司最终《软件许可协议》文本。\",\n ],\n }\n : {\n sectionTitle: \"License and Terms\",\n agreePrefix: \"I have read and agree to\",\n license: \"Software License Agreement\",\n terms: \"Terms of Service\",\n privacy: \"Privacy Policy\",\n and: \"and\",\n telemetryTitle: \"Enable anonymous telemetry (optional, disabled by default)\",\n telemetryDesc: \"Helps improve the product without collecting sensitive information.\",\n licenseModalTitle: \"Software License Agreement\",\n licenseLines: [\n \"This software is provided \\\"as is\\\" and must be used for lawful purposes only.\",\n \"Unauthorized copying, reverse engineering, resale, or illegal use is prohibited.\",\n \"In production, you are responsible for deployment, operations, and compliance.\",\n \"Replace this text with your organization's final legal license agreement if needed.\",\n ],\n }\n);\n\n// 步骤数据\nconst step1Data = reactive({\n checks: {\n database: { status: \"pending\", message: \"检测中...\" },\n storage: { status: \"pending\", message: \"检测中...\" },\n cache: { status: \"pending\", message: \"检测中...\" },\n email: { status: \"pending\", message: \"检测中...\" },\n ai: { status: \"pending\", message: \"检测中...\" },\n },\n deploymentMode: \"multi-tenant\",\n authMode: \"builtin\",\n licenseAccepted: false,\n termsAccepted: false,\n telemetryEnabled: false,\n});\n\nconst step2Data = reactive({\n domain: \"\",\n apiSubdomain: \"\",\n httpsMode: \"auto\",\n certEmail: \"\",\n certContent: \"\",\n keyContent: \"\",\n enableCdn: false,\n cdnDomain: \"\",\n});\n\nconst inferDefaultDomainFromLocation = (): string => {\n if (typeof window === \"undefined\") return \"\";\n const host = String(window.location.hostname || \"\").trim().toLowerCase();\n if (!host) return \"\";\n if (host === \"localhost\" || host === \"127.0.0.1\" || host === \"::1\") return \"\";\n return host;\n};\n\nconst inferDomainFromLocation = (): string => {\n const host = inferDefaultDomainFromLocation();\n if (!host) return \"\";\n return host;\n};\n\nconst applyStep2SmartDefaults = () => {\n const inferredDomain = inferDomainFromLocation();\n if (!step2Data.domain.trim() && inferredDomain) {\n step2Data.domain = inferredDomain;\n }\n if (!step2Data.certEmail.trim() && step4Data.adminEmail.trim()) {\n step2Data.certEmail = step4Data.adminEmail.trim();\n }\n};\n\nconst step3Data = reactive({\n backendPort: 8080,\n webAdminPort: 3000,\n dbType: \"postgresql\",\n dbVersion: \"\",\n dbHost: \"localhost\",\n dbPort: 5432,\n dbName: \"powerx\",\n dbCharset: \"utf8mb4\",\n dbUsername: \"root\",\n dbPassword: \"\",\n sqlitePath: \"/data/powerx.db\",\n cacheType: \"redis\",\n redisHost: \"localhost\",\n redisPort: 6379,\n redisPassword: \"\",\n redisDb: 0,\n storageType: \"local\",\n localStoragePath: \"/data/uploads\",\n storageAccessKey: \"\",\n storageSecretKey: \"\",\n storageBucket: \"\",\n storageRegion: \"\",\n emailEnabled: false,\n emailSmtpHost: \"\",\n emailSmtpPort: 587,\n emailFromName: \"\",\n emailFromAddress: \"\",\n});\n\nconst step4Data = reactive({\n adminUsername: \"admin\",\n adminEmail: \"\",\n adminPassword: \"\",\n adminPasswordConfirm: \"\",\n adminName: \"\",\n adminPhone: \"\",\n tenantName: \"默认租户\",\n tenantCode: \"default\",\n tenantDescription: \"\",\n createDefaultDepartments: true,\n companyName: \"我的公司\",\n});\n\nconst step5Data = reactive({\n availablePlugins: [\n {\n id: \"workflow\",\n name: \"审批流程\",\n description: \"提供工作流和审批功能\",\n category: \"workflow\",\n recommended: true,\n },\n {\n id: \"notification\",\n name: \"消息通知\",\n description: \"邮件、短信、站内信通知\",\n category: \"communication\",\n recommended: true,\n },\n {\n id: \"file-manager\",\n name: \"文件管理\",\n description: \"文件上传、管理和分享\",\n category: \"storage\",\n recommended: true,\n },\n {\n id: \"ai-assistant\",\n name: \"AI 助手\",\n description: \"智能对话和任务处理\",\n category: \"ai\",\n recommended: false,\n },\n ],\n});\n\nconst step6Data = reactive({\n skipLLMSetup: false,\n provider: \"openai\",\n baseUrl: \"\",\n model: \"gpt-4.1-mini\",\n apiKey: \"\",\n temperature: 0.7,\n topP: 1,\n maxTokens: 4096,\n stream: true,\n});\nconst llmLastTestMessage = ref(\"\");\nconst llmLastTestDetail = ref(\"\");\nconst llmProviderOptions = ref<Array<{ label: string; value: string }>>([]);\nconst qwenFallbackModels = [\n \"qwen-max-latest\",\n \"qwen-plus-latest\",\n \"qwen-turbo-latest\",\n \"qwen-flash-latest\",\n \"qwen-plus\",\n \"qwen-turbo\",\n \"qwen-max\",\n \"qwen3.5-plus\",\n \"qwen3.5-flash\",\n \"qwen3.5-plus-2026-02-15\",\n \"qwen3.5-flash-2026-02-23\",\n \"qwen3.7-plus\",\n \"deepseek-v4-flash\",\n \"qwen3-max-latest\",\n \"qwen3-plus-latest\",\n \"qwen3-turbo-latest\",\n \"qwen3-flash-latest\",\n \"qwen3-max\",\n \"qwen3-plus\",\n \"qwen3-turbo\",\n \"qwen3-flash\",\n \"qwen3-coder-plus\",\n \"qwen3-coder-flash\",\n \"qwen2.5-72b-instruct\",\n \"qwen2.5-32b-instruct\",\n \"qwen2.5-14b-instruct\",\n \"qwen2.5-7b-instruct\",\n \"qwq-plus\",\n \"qwq-32b\",\n].map((m) => ({ label: m, value: m }));\nconst llmModelOptionsMap = ref<Record<string, Array<{ label: string; value: string }>>>({\n openai: [\n { label: \"gpt-4.1-mini\", value: \"gpt-4.1-mini\" },\n { label: \"gpt-4o-mini\", value: \"gpt-4o-mini\" },\n { label: \"gpt-4.1\", value: \"gpt-4.1\" },\n ],\n ollama: [\n { label: \"llama3\", value: \"llama3\" },\n { label: \"qwen2.5:7b\", value: \"qwen2.5:7b\" },\n { label: \"deepseek-r1:7b\", value: \"deepseek-r1:7b\" },\n ],\n openrouter: [\n { label: \"openai/gpt-4.1-mini\", value: \"openai/gpt-4.1-mini\" },\n { label: \"anthropic/claude-3.5-sonnet\", value: \"anthropic/claude-3.5-sonnet\" },\n ],\n huggingface: [\n { label: \"Qwen/Qwen2.5-72B-Instruct\", value: \"Qwen/Qwen2.5-72B-Instruct\" },\n { label: \"meta-llama/Meta-Llama-3.1-8B-Instruct\", value: \"meta-llama/Meta-Llama-3.1-8B-Instruct\" },\n ],\n moonshot: [\n { label: \"moonshot-v1-8k\", value: \"moonshot-v1-8k\" },\n { label: \"moonshot-v1-32k\", value: \"moonshot-v1-32k\" },\n ],\n volcengine: [\n { label: \"doubao-seed-1-6\", value: \"doubao-seed-1-6\" },\n { label: \"doubao-1.5-pro-32k\", value: \"doubao-1.5-pro-32k\" },\n ],\n hunyuan: [\n { label: \"hunyuan-turbo\", value: \"hunyuan-turbo\" },\n { label: \"hunyuan-pro\", value: \"hunyuan-pro\" },\n ],\n \"qwen-intl\": qwenFallbackModels,\n \"qwen-cn\": qwenFallbackModels,\n openai_compatible: [\n { label: \"gpt-4o-mini\", value: \"gpt-4o-mini\" },\n { label: \"qwen-plus\", value: \"qwen-plus\" },\n ],\n custom: [\n { label: \"custom-model\", value: \"custom-model\" },\n ],\n});\nconst llmModelOptions = computed(() => {\n const p = String(step6Data.provider || \"\").trim();\n return llmModelOptionsMap.value[p] || llmModelOptionsMap.value.openai || [];\n});\n\nconst buildFallbackLLMProviderOptions = () => [\n { label: \"OpenAI\", value: \"openai\" },\n { label: \"Ollama (Local)\", value: \"ollama\" },\n { label: \"OpenRouter\", value: \"openrouter\" },\n { label: \"Hugging Face Inference\", value: \"huggingface\" },\n { label: \"Moonshot AI (Kimi)\", value: \"moonshot\" },\n { label: \"Volcengine (ByteDance)\", value: \"volcengine\" },\n { label: \"Tencent Hunyuan\", value: \"hunyuan\" },\n { label: isZh.value ? \"Qwen(官方国际)\" : \"Qwen (Intl)\", value: \"qwen-intl\" },\n { label: isZh.value ? \"Qwen(官方国内)\" : \"Qwen (CN)\", value: \"qwen-cn\" },\n { label: \"OpenAI Compatible\", value: \"openai_compatible\" },\n { label: isZh.value ? \"自定义\" : \"Custom\", value: \"custom\" },\n];\n\nconst normalizeProviderValue = (raw: unknown): string =>\n String(raw || \"\").trim().toLowerCase();\n\nconst isSetupInstalled = computed(() => {\n const status = String(\n setupStatus.value?.install?.status ??\n setupStatus.value?.install_status ??\n setupStatus.value?.status ??\n \"\"\n )\n .trim()\n .toLowerCase();\n return status === \"installed\";\n});\n\nconst desiredBackendPort = computed(() =>\n Number(setupStatus.value?.desired_ports?.backend_port || step3Data.backendPort || 0)\n);\nconst desiredWebAdminPort = computed(() =>\n Number(setupStatus.value?.desired_ports?.web_admin_port || step3Data.webAdminPort || 0)\n);\nconst effectiveBackendPort = computed(() =>\n Number(setupStatus.value?.effective_ports?.backend_port || 0)\n);\nconst effectiveWebAdminPort = computed(() =>\n Number(setupStatus.value?.effective_ports?.web_admin_port || 0)\n);\nconst targetBackendPort = computed(() =>\n Number(setupStatus.value?.desired_ports?.backend_port || step3Data.backendPort || 0)\n);\nconst restartRequired = computed(() => Boolean(setupStatus.value?.restart_required));\nconst desiredSource = computed(() =>\n String(setupStatus.value?.config_source?.desired_ports || \"\")\n);\nconst effectiveSource = computed(() =>\n String(setupStatus.value?.config_source?.effective_ports || \"\")\n);\n\nconst syncProviderAndModelDefaults = () => {\n const firstProvider = llmProviderOptions.value[0]?.value || \"openai\";\n if (!llmProviderOptions.value.some((p) => p.value === step6Data.provider)) {\n step6Data.provider = firstProvider;\n }\n const firstModel = llmModelOptions.value[0]?.value || \"\";\n if (!llmModelOptions.value.some((m) => m.value === step6Data.model)) {\n step6Data.model = firstModel;\n }\n};\n\nconst isValidHttpBaseURL = (raw: string): boolean => {\n const val = String(raw || \"\").trim();\n return /^https?:\\/\\//i.test(val);\n};\n\nconst isLikelyHTMLPayload = (payload: unknown): boolean => {\n if (typeof payload !== \"string\") return false;\n const raw = payload.trim().toLowerCase();\n return raw.startsWith(\"<!doctype html\") || raw.startsWith(\"<html\");\n};\n\nconst loadLLMProviderOptions = async () => {\n if (!isSetupInstalled.value) {\n llmProviderOptions.value = buildFallbackLLMProviderOptions();\n syncProviderAndModelDefaults();\n return;\n }\n try {\n const providers = await AISettingService.getProviders(\"llm\", \"dev\");\n const items = (providers || [])\n .map((p: any) => ({\n label: String(p?.Name || p?.ID || \"\").trim(),\n value: normalizeProviderValue(p?.ID),\n }))\n .filter((p) => p.label && p.value);\n llmProviderOptions.value = items.length ? items : buildFallbackLLMProviderOptions();\n } catch {\n llmProviderOptions.value = buildFallbackLLMProviderOptions();\n }\n syncProviderAndModelDefaults();\n};\n\nconst loadLLMModelsByProvider = async (provider: string) => {\n if (!isSetupInstalled.value) {\n return;\n }\n const p = normalizeProviderValue(provider);\n if (!p) return;\n try {\n const models = await AISettingService.getModels(p, \"llm\", \"dev\");\n const items = (models || [])\n .map((m) => String(m || \"\").trim())\n .filter((m) => m.length > 0)\n .map((m) => ({ label: m, value: m }));\n if (items.length) {\n llmModelOptionsMap.value = {\n ...llmModelOptionsMap.value,\n [p]: items,\n };\n }\n } catch {\n // setup 阶段容错:失败时保持静态兜底模型\n }\n syncProviderAndModelDefaults();\n};\n\nwatch(\n () => step6Data.provider,\n async (provider, prevProvider) => {\n await loadLLMModelsByProvider(provider);\n // provider 切换后,模型选中值必须联动为该 provider 的首个可用模型\n if (prevProvider !== undefined && provider !== prevProvider) {\n step6Data.model = llmModelOptions.value[0]?.value || \"\";\n }\n },\n { immediate: true }\n);\n\nconst setupTestConnection = async () => {\n llmLastTestMessage.value = \"\";\n llmLastTestDetail.value = \"\";\n const rawBaseURL = String(step6Data.baseUrl || \"\").trim();\n if (rawBaseURL && !isValidHttpBaseURL(rawBaseURL)) {\n llmLastTestMessage.value = isZh.value ? \"连接测试失败。\" : \"Connection test failed.\";\n llmLastTestDetail.value = isZh.value\n ? \"Base URL 必须以 http:// 或 https:// 开头。\"\n : \"Base URL must start with http:// or https://.\";\n return;\n }\n try {\n const resp: any = await $fetch(\"/api/v1/admin/setup/llm/test-connection\", {\n method: \"POST\",\n body: {\n provider: step6Data.provider,\n model: step6Data.model,\n baseURL: step6Data.baseUrl,\n apiKey: step6Data.apiKey,\n },\n });\n if (isLikelyHTMLPayload(resp)) {\n throw new Error(\n isZh.value\n ? \"连接测试返回了 HTML 页面而非 JSON,请检查前端 API 代理是否正确。\"\n : \"Connection test returned HTML instead of JSON. Please check frontend API proxy settings.\"\n );\n }\n const data = resp?.data ?? resp;\n if (isLikelyHTMLPayload(data)) {\n throw new Error(\n isZh.value\n ? \"连接测试返回了 HTML 页面而非 JSON,请检查前端 API 代理是否正确。\"\n : \"Connection test returned HTML instead of JSON. Please check frontend API proxy settings.\"\n );\n }\n if (!data || typeof data !== \"object\" || (Object.prototype.hasOwnProperty.call(data, \"ok\") && data.ok !== true)) {\n throw new Error(\n isZh.value\n ? \"连接测试响应格式异常(期望 JSON 对象)。\"\n : \"Unexpected connection test response format (expected JSON object).\"\n );\n }\n llmLastTestMessage.value = isZh.value\n ? \"连接测试通过。\"\n : \"Connection test passed.\";\n llmLastTestDetail.value = JSON.stringify(data, null, 2);\n } catch (error: any) {\n llmLastTestMessage.value = isZh.value\n ? \"连接测试失败。\"\n : \"Connection test failed.\";\n llmLastTestDetail.value = String(error?.data?.error || error?.data?.message || error?.message || \"unknown error\");\n }\n};\n\nconst setupTestQuickCall = async () => {\n llmLastTestMessage.value = \"\";\n llmLastTestDetail.value = \"\";\n const rawBaseURL = String(step6Data.baseUrl || \"\").trim();\n if (rawBaseURL && !isValidHttpBaseURL(rawBaseURL)) {\n llmLastTestMessage.value = isZh.value ? \"试跑失败。\" : \"Quick run failed.\";\n llmLastTestDetail.value = isZh.value\n ? \"Base URL 必须以 http:// 或 https:// 开头。\"\n : \"Base URL must start with http:// or https://.\";\n return;\n }\n try {\n const resp: any = await $fetch(\"/api/v1/admin/setup/llm/test-call\", {\n method: \"POST\",\n body: {\n provider: step6Data.provider,\n model: step6Data.model,\n baseURL: step6Data.baseUrl,\n apiKey: step6Data.apiKey,\n temperature: Number(step6Data.temperature || 0),\n maxTokens: Number(step6Data.maxTokens || 0),\n prompt: \"hello\",\n },\n });\n if (isLikelyHTMLPayload(resp)) {\n throw new Error(\n isZh.value\n ? \"试跑返回了 HTML 页面而非 JSON,请检查前端 API 代理是否正确。\"\n : \"Quick run returned HTML instead of JSON. Please check frontend API proxy settings.\"\n );\n }\n const data = resp?.data ?? resp;\n if (isLikelyHTMLPayload(data) || !data || typeof data !== \"object\") {\n throw new Error(\n isZh.value\n ? \"试跑响应格式异常(期望 JSON 对象)。\"\n : \"Unexpected quick run response format (expected JSON object).\"\n );\n }\n llmLastTestMessage.value = String(data?.text || data?.output || (isZh.value ? \"试跑成功。\" : \"Quick run succeeded.\"));\n llmLastTestDetail.value = JSON.stringify(data, null, 2);\n } catch (error: any) {\n llmLastTestMessage.value = isZh.value\n ? \"试跑失败。\"\n : \"Quick run failed.\";\n llmLastTestDetail.value = String(error?.data?.error || error?.data?.message || error?.message || \"unknown error\");\n }\n};\n\nconst dbTestState = reactive({\n database: {\n testing: false,\n ok: false,\n message: \"\",\n },\n cache: {\n testing: false,\n ok: false,\n message: \"\",\n },\n provisioning: false,\n provisioned: false,\n});\n\nconst setupPayload = computed(() => ({\n domain: {\n domain: step2Data.domain,\n api_subdomain: \"\",\n enable_cdn: step2Data.enableCdn,\n cdn_domain: step2Data.cdnDomain,\n },\n https: {\n mode: step2Data.httpsMode as \"auto\" | \"manual\" | \"disable\",\n cert_email: step2Data.certEmail,\n cert_content: step2Data.certContent,\n key_content: step2Data.keyContent,\n },\n storage: {\n type: normalizeStorageType(step3Data.storageType),\n local_path: step3Data.localStoragePath,\n access_key: step3Data.storageAccessKey,\n secret_key: step3Data.storageSecretKey,\n bucket: step3Data.storageBucket,\n region: step3Data.storageRegion,\n },\n cache: {\n type: step3Data.cacheType,\n redis_host: step3Data.redisHost,\n redis_port: Number(step3Data.redisPort || 0),\n redis_db: Number(step3Data.redisDb || 0),\n redis_password: step3Data.redisPassword,\n },\n email: {\n enabled: step3Data.emailEnabled,\n smtp_host: step3Data.emailSmtpHost,\n smtp_port: Number(step3Data.emailSmtpPort || 0),\n from_name: step3Data.emailFromName,\n from_address: step3Data.emailFromAddress,\n },\n admin: {\n username: step4Data.adminUsername,\n email: step4Data.adminEmail,\n password: step4Data.adminPassword,\n display_name: step4Data.adminName,\n phone: step4Data.adminPhone,\n tenant_name: step4Data.tenantName,\n tenant_code: step4Data.tenantCode,\n tenant_description: step4Data.tenantDescription,\n create_default_departments: !!step4Data.createDefaultDepartments,\n company_name: step4Data.companyName,\n },\n llm: {\n enabled: !step6Data.skipLLMSetup,\n provider: step6Data.provider,\n model: step6Data.model,\n base_url: step6Data.baseUrl,\n api_key: step6Data.apiKey,\n temperature: Number(step6Data.temperature || 0),\n top_p: Number(step6Data.topP || 0),\n max_tokens: Number(step6Data.maxTokens || 0),\n stream: !!step6Data.stream,\n },\n database: {\n type: step3Data.dbType,\n host: step3Data.dbHost,\n port: Number(step3Data.dbPort || 0),\n name: step3Data.dbName,\n username: step3Data.dbUsername,\n password: step3Data.dbPassword,\n charset: step3Data.dbCharset,\n ssl_mode: \"disable\",\n sqlite_path: step3Data.sqlitePath,\n },\n ports: {\n backend_port: Number(step3Data.backendPort || 0),\n web_admin_port: Number(step3Data.webAdminPort || 0),\n },\n}));\n\nconst setupTestDatabaseConnection = async () => {\n dbTestState.database.testing = true;\n dbTestState.database.message = \"\";\n try {\n const resp: any = await $fetch(\"/api/v1/admin/setup/test/database\", {\n method: \"POST\",\n body: {\n database: setupPayload.value.database,\n },\n });\n const data = resp?.data ?? resp;\n if (!data || data.ok !== true) {\n throw new Error(isZh.value ? \"数据库连接测试返回异常\" : \"Database test response is invalid\");\n }\n dbTestState.database.ok = true;\n dbTestState.database.message = isZh.value ? \"数据库连接成功\" : \"Database connection is healthy\";\n } catch (error: any) {\n dbTestState.database.ok = false;\n dbTestState.database.message = String(error?.data?.error || error?.data?.message || error?.message || \"unknown error\");\n } finally {\n dbTestState.database.testing = false;\n }\n};\n\nconst setupTestCacheConnection = async () => {\n dbTestState.cache.testing = true;\n dbTestState.cache.message = \"\";\n try {\n const resp: any = await $fetch(\"/api/v1/admin/setup/test/cache\", {\n method: \"POST\",\n body: {\n cache: setupPayload.value.cache,\n },\n });\n const data = resp?.data ?? resp;\n if (!data || data.ok !== true) {\n throw new Error(isZh.value ? \"缓存连接测试返回异常\" : \"Cache test response is invalid\");\n }\n dbTestState.cache.ok = true;\n dbTestState.cache.message = String(data?.message || (isZh.value ? \"缓存连接成功\" : \"Cache connection is healthy\"));\n } catch (error: any) {\n dbTestState.cache.ok = false;\n dbTestState.cache.message = String(error?.data?.error || error?.data?.message || error?.message || \"unknown error\");\n } finally {\n dbTestState.cache.testing = false;\n }\n};\n\nconst provisionSetupAtDatabaseStep = async (): Promise<boolean> => {\n if (dbTestState.provisioned) {\n return true;\n }\n dbTestState.provisioning = true;\n try {\n await settingsService.saveSetupConfig(setupPayload.value as any);\n const resp: any = await $fetch(\"/api/v1/admin/setup/provision\", {\n method: \"POST\",\n });\n const data = resp?.data ?? resp;\n if (!data || data.ok !== true) {\n throw new Error(isZh.value ? \"数据库初始化响应异常\" : \"Provision response is invalid\");\n }\n dbTestState.provisioned = true;\n toast.add({\n title: isZh.value ? \"数据库初始化完成\" : \"Database provision completed\",\n description: isZh.value ? \"已执行 migrate/seed,可继续下一步。\" : \"migrate/seed finished. You can continue.\",\n color: \"success\",\n });\n return true;\n } catch (error: any) {\n dbTestState.provisioned = false;\n toast.add({\n title: isZh.value ? \"数据库初始化失败\" : \"Database provision failed\",\n description: String(error?.data?.error || error?.data?.message || error?.message || \"unknown error\"),\n color: \"error\",\n });\n return false;\n } finally {\n dbTestState.provisioning = false;\n }\n};\n\nwatch(\n () => [\n step3Data.dbType,\n step3Data.dbHost,\n step3Data.dbPort,\n step3Data.dbName,\n step3Data.dbUsername,\n step3Data.dbPassword,\n step3Data.sqlitePath,\n step3Data.cacheType,\n step3Data.redisHost,\n step3Data.redisPort,\n step3Data.redisDb,\n step3Data.redisPassword,\n ],\n () => {\n dbTestState.provisioned = false;\n }\n);\n\n// 验证当前步骤\nconst validateCurrentStep = () => {\n switch (currentStep.value) {\n case 0:\n return step1Data.licenseAccepted && step1Data.termsAccepted;\n case 1:\n return step3Data.dbType === \"sqlite\" || step3Data.dbHost.trim() !== \"\";\n case 2:\n return (\n step2Data.domain.trim() !== \"\" &&\n Number(step3Data.backendPort) > 0 &&\n Number(step3Data.backendPort) <= 65535 &&\n Number(step3Data.webAdminPort) > 0 &&\n Number(step3Data.webAdminPort) <= 65535 &&\n Number(step3Data.backendPort) !== Number(step3Data.webAdminPort)\n );\n case 3:\n if (Number(setupStatus.value?.checks?.users || 0) > 0) {\n return true;\n }\n return (\n step4Data.adminUsername &&\n step4Data.adminEmail &&\n step4Data.adminPassword &&\n step4Data.adminPassword === step4Data.adminPasswordConfirm\n );\n case 4:\n return true;\n case 5:\n if (step6Data.skipLLMSetup) return true;\n return (\n String(step6Data.provider || \"\").trim() !== \"\" &&\n String(step6Data.model || \"\").trim() !== \"\" &&\n String(step6Data.apiKey || \"\").trim() !== \"\"\n );\n default:\n return false;\n }\n};\n\n// 下一步\nconst nextStep = async () => {\n if (!validateCurrentStep()) return;\n\n isLoading.value = true;\n try {\n if (currentStep.value === 1) {\n const ok = await provisionSetupAtDatabaseStep();\n if (!ok) return;\n }\n if (currentStep.value < stepperItems.value.length - 1) {\n currentStep.value++;\n } else {\n await completeSetup();\n }\n } finally {\n isLoading.value = false;\n }\n};\n\n// 上一步\nconst prevStep = () => {\n if (currentStep.value > 0) {\n currentStep.value--;\n }\n};\n\nconst getOriginByPort = (port: number): string => {\n if (!Number.isFinite(port) || port <= 0) {\n return window.location.origin;\n }\n return `${window.location.protocol}//${window.location.hostname}:${port}`;\n};\n\nconst sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));\n\nconst probeURLReachable = async (url: string): Promise<boolean> => {\n try {\n // no-cors 仅用于探测端口是否可连接;连接失败会抛异常。\n await fetch(url, { method: \"GET\", mode: \"no-cors\", cache: \"no-store\" });\n return true;\n } catch {\n return false;\n }\n};\n\nconst checkBackendReadyOnce = async (): Promise<boolean> => {\n try {\n const resp: any = await settingsService.getSetupStatus();\n const data = resp?.data ?? resp;\n const installStatus = String(data?.install_status || \"\").trim().toLowerCase();\n const needRestart = Boolean(data?.restart_required);\n if (installStatus === \"installed\" && !needRestart) {\n setupStatus.value = data;\n return true;\n }\n } catch {\n // setup/status 在手动重启窗口期可能会短暂失败,继续走端口探测兜底\n }\n\n const port = Number(setupStatus.value?.desired_ports?.backend_port || step3Data.backendPort || 0);\n if (!Number.isFinite(port) || port <= 0) {\n return false;\n }\n // 当 backend 端口发生变更时,旧代理无法可靠获取 setup/status,使用目标端口可达性做兜底。\n if (effectiveBackendPort.value > 0 && effectiveBackendPort.value !== port) {\n return await probeURLReachable(`${getOriginByPort(port)}/api/v1/admin/setup/status?ts=${Date.now()}`);\n }\n return false;\n};\n\nconst waitForWebAdminReachable = async (port: number, maxWaitMs = 120000, intervalMs = 2000): Promise<boolean> => {\n if (!Number.isFinite(port) || port <= 0) {\n return true;\n }\n const origin = getOriginByPort(port);\n const startedAt = Date.now();\n while (Date.now() - startedAt < maxWaitMs) {\n const ok = await probeURLReachable(`${origin}/api/v1/admin/setup/status`);\n if (ok) return true;\n await sleep(intervalMs);\n }\n return false;\n};\n\nconst redirectToReadyHome = async () => {\n // 反向代理场景(如 443 -> 3000)下,浏览器端不应强制探测 :3000。\n // 只要当前 origin 的 setup/status 可达,直接跳当前站点 /home。\n const currentOriginReady = await probeURLReachable(`${window.location.origin}/api/v1/admin/setup/status?ts=${Date.now()}`);\n if (currentOriginReady) {\n window.location.replace(\"/home\");\n return;\n }\n\n const targetWebPort =\n Number(setupStatus.value?.desired_ports?.web_admin_port || step3Data.webAdminPort || 0) ||\n Number(setupStatus.value?.effective_ports?.web_admin_port || 0);\n const currentPort = Number(window.location.port || 0);\n const samePort = currentPort > 0 && targetWebPort > 0 && currentPort === targetWebPort;\n\n if (samePort || targetWebPort <= 0) {\n window.location.replace(\"/home\");\n return;\n }\n\n restartProgressText.value = isZh.value\n ? `检测新管理端端口 ${targetWebPort} 是否已就绪...`\n : `Checking if new admin port ${targetWebPort} is ready...`;\n const ready = await waitForWebAdminReachable(targetWebPort, 120000, 2000);\n if (!ready) {\n throw new Error(\n isZh.value\n ? `新端口 ${targetWebPort} 暂未就绪,请稍后手动访问。`\n : `New port ${targetWebPort} is not ready yet. Please open it manually later.`\n );\n }\n\n const targetOrigin = getOriginByPort(targetWebPort);\n window.location.replace(`${targetOrigin}/home`);\n};\n\nconst runRestartFlowAndRedirect = async () => {\n restarting.value = true;\n restartProgressText.value = isZh.value\n ? \"配置已保存。请手动重启后端进程,系统会自动检测并在服务就绪后跳转。\"\n : \"Configuration saved. Please restart backend manually. The system will auto-detect and redirect once ready.\";\n\n const startedAt = Date.now();\n const maxWaitMs = 10 * 60 * 1000;\n while (Date.now() - startedAt < maxWaitMs) {\n const ready = await checkBackendReadyOnce();\n if (ready) {\n restartProgressText.value = isZh.value ? \"服务已就绪,正在跳转到首页...\" : \"Service is ready. Redirecting to home...\";\n await redirectToReadyHome();\n return;\n }\n await sleep(2000);\n }\n\n restartProgressText.value = isZh.value\n ? \"暂未检测到新后端服务。请完成手动重启后点击“立即检测”,系统会自动跳转。\"\n : \"Backend is not ready yet. Finish manual restart and click \\\"Check now\\\" to continue.\";\n};\n\nconst checkRestartAndRedirectNow = async () => {\n manualCheckLoading.value = true;\n try {\n const ready = await checkBackendReadyOnce();\n if (!ready) {\n toast.add({\n title: isZh.value ? \"后端尚未就绪\" : \"Backend not ready\",\n description: isZh.value ? \"请先手动重启后端,再点击立即检测。\" : \"Please restart backend manually, then check again.\",\n color: \"warning\",\n });\n return;\n }\n restartProgressText.value = isZh.value ? \"服务已就绪,正在跳转到首页...\" : \"Service is ready. Redirecting to home...\";\n await redirectToReadyHome();\n } finally {\n manualCheckLoading.value = false;\n }\n};\n\nconst resetClientStateAfterSetupComplete = () => {\n if (typeof window === \"undefined\") return;\n\n const keepLocalStorageKeys = new Set([\"px_locale\", \"powerx-color-mode\"]);\n try {\n const keys: string[] = [];\n for (let i = 0; i < localStorage.length; i++) {\n const key = localStorage.key(i);\n if (key) keys.push(key);\n }\n for (const key of keys) {\n if (!keepLocalStorageKeys.has(key)) {\n localStorage.removeItem(key);\n }\n }\n } catch {}\n\n try {\n sessionStorage.clear();\n } catch {}\n\n // 清理与登录相关的 cookie,避免安装后首跳仍携带旧登录态。\n const clearCookie = (name: string) => {\n document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;\n };\n [\"token\", \"access_token\", \"refresh_token\", \"px_current_tenant_uuid\"].forEach(clearCookie);\n};\n\n// 完成设置\nconst completeSetup = async () => {\n completing.value = true;\n try {\n await settingsService.saveSetupConfig(setupPayload.value as any);\n\n await $fetch(\"/api/v1/admin/setup/complete\", {\n method: \"POST\",\n body: {\n licenseAccepted: step1Data.licenseAccepted,\n termsAccepted: step1Data.termsAccepted,\n },\n });\n resetClientStateAfterSetupComplete();\n runRestartFlowAndRedirect();\n } catch (error: any) {\n restarting.value = false;\n toast.add({\n title: \"保存失败\",\n description: String(error?.data?.message || error?.message || \"安装配置保存失败\"),\n color: \"error\",\n });\n } finally {\n completing.value = false;\n }\n};\n\n// 执行系统检测\nconst runSystemChecks = async () => {\n const checks = [\"database\", \"storage\", \"cache\", \"email\", \"ai\"];\n for (const check of checks) {\n step1Data.checks[check].status = \"checking\";\n step1Data.checks[check].message = uiCopy.value.checking;\n }\n\n try {\n const resp: any = await settingsService.getSetupStatus();\n const payload = resp?.data ?? resp;\n setupStatus.value = payload;\n\n const hasTenant = Number(payload?.checks?.tenants || 0) > 0;\n const hasUser = Number(payload?.checks?.users || 0) > 0;\n const hasAI = Number(payload?.checks?.ai_profiles || 0) > 0;\n\n if (isSetupInstalled.value) {\n loadLLMProviderOptions();\n }\n\n step1Data.checks.database.status = hasTenant ? \"success\" : \"pending\";\n step1Data.checks.database.message = hasTenant ? uiCopy.value.msgTenantInited : uiCopy.value.msgTenantNotInited;\n step1Data.checks.storage.status = \"pending\";\n step1Data.checks.storage.message = uiCopy.value.msgStoragePending;\n step1Data.checks.cache.status = \"pending\";\n step1Data.checks.cache.message = uiCopy.value.msgCachePending;\n step1Data.checks.email.status = \"pending\";\n step1Data.checks.email.message = uiCopy.value.msgEmailPending;\n step1Data.checks.ai.status = hasAI ? \"success\" : \"pending\";\n step1Data.checks.ai.message = hasAI ? uiCopy.value.msgAIOk : uiCopy.value.msgAINone;\n\n if (hasUser) {\n step4Data.adminUsername = \"\";\n step4Data.adminEmail = \"\";\n }\n } catch {\n for (const check of checks) {\n step1Data.checks[check].status = \"error\";\n step1Data.checks[check].message = uiCopy.value.msgCheckFailed;\n }\n }\n};\n\nconst normalizeStorageType = (raw: string): string => {\n const value = String(raw || \"\").trim().toLowerCase();\n if (value === \"aliyun\") return \"oss\";\n if (value === \"tencent\") return \"cos\";\n if (value === \"aws\") return \"s3\";\n return value || \"local\";\n};\n\nconst denormalizeStorageType = (raw: string): string => {\n const value = String(raw || \"\").trim().toLowerCase();\n if (value === \"oss\") return \"aliyun\";\n if (value === \"cos\") return \"tencent\";\n if (value === \"s3\") return \"aws\";\n return value || \"local\";\n};\n\nconst loadSetupConfig = async () => {\n try {\n const resp: any = await settingsService.getSetupConfig();\n const payload = resp?.data ?? resp;\n const cfg = payload?.config;\n if (!cfg) return;\n\n step2Data.domain = String(cfg.domain?.domain || step2Data.domain);\n step2Data.apiSubdomain = \"\";\n step2Data.enableCdn = Boolean(cfg.domain?.enable_cdn);\n step2Data.cdnDomain = String(cfg.domain?.cdn_domain || step2Data.cdnDomain);\n\n step2Data.httpsMode = String(cfg.https?.mode || step2Data.httpsMode);\n step2Data.certEmail = String(cfg.https?.cert_email || step2Data.certEmail);\n step2Data.certContent = String(cfg.https?.cert_content || step2Data.certContent);\n step2Data.keyContent = String(cfg.https?.key_content || step2Data.keyContent);\n\n step3Data.storageType = denormalizeStorageType(cfg.storage?.type);\n step3Data.localStoragePath = String(cfg.storage?.local_path || step3Data.localStoragePath);\n step3Data.storageAccessKey = String(cfg.storage?.access_key || step3Data.storageAccessKey);\n step3Data.storageSecretKey = String(cfg.storage?.secret_key || step3Data.storageSecretKey);\n step3Data.storageBucket = String(cfg.storage?.bucket || step3Data.storageBucket);\n step3Data.storageRegion = String(cfg.storage?.region || step3Data.storageRegion);\n\n step3Data.cacheType = String(cfg.cache?.type || step3Data.cacheType);\n step3Data.redisHost = String(cfg.cache?.redis_host || step3Data.redisHost);\n step3Data.redisPort = Number(cfg.cache?.redis_port || step3Data.redisPort);\n step3Data.redisDb = Number(cfg.cache?.redis_db || step3Data.redisDb);\n step3Data.redisPassword = String(cfg.cache?.redis_password || step3Data.redisPassword);\n\n step3Data.dbType = String(cfg.database?.type || step3Data.dbType);\n step3Data.dbHost = String(cfg.database?.host || step3Data.dbHost);\n step3Data.dbPort = Number(cfg.database?.port || step3Data.dbPort);\n step3Data.dbName = String(cfg.database?.name || step3Data.dbName);\n step3Data.dbUsername = String(cfg.database?.username || step3Data.dbUsername);\n step3Data.dbPassword = String(cfg.database?.password || step3Data.dbPassword);\n step3Data.dbCharset = String(cfg.database?.charset || step3Data.dbCharset);\n step3Data.sqlitePath = String(cfg.database?.sqlite_path || step3Data.sqlitePath);\n\n const llmEnabled = cfg.llm?.enabled !== false;\n step6Data.skipLLMSetup = !llmEnabled;\n step6Data.provider = String(cfg.llm?.provider || step6Data.provider);\n step6Data.model = String(cfg.llm?.model || step6Data.model);\n step6Data.baseUrl = String(cfg.llm?.base_url || step6Data.baseUrl);\n step6Data.apiKey = String(cfg.llm?.api_key || step6Data.apiKey);\n step6Data.temperature = Number(cfg.llm?.temperature ?? step6Data.temperature);\n step6Data.topP = Number(cfg.llm?.top_p ?? step6Data.topP);\n step6Data.maxTokens = Number(cfg.llm?.max_tokens ?? step6Data.maxTokens);\n step6Data.stream = Boolean(cfg.llm?.stream ?? step6Data.stream);\n\n step3Data.emailEnabled = Boolean(cfg.email?.enabled);\n step3Data.emailSmtpHost = String(cfg.email?.smtp_host || step3Data.emailSmtpHost);\n step3Data.emailSmtpPort = Number(cfg.email?.smtp_port || step3Data.emailSmtpPort);\n step3Data.emailFromName = String(cfg.email?.from_name || step3Data.emailFromName);\n step3Data.emailFromAddress = String(cfg.email?.from_address || step3Data.emailFromAddress);\n\n step4Data.adminUsername = String(cfg.admin?.username || step4Data.adminUsername);\n step4Data.adminEmail = String(cfg.admin?.email || step4Data.adminEmail);\n step4Data.adminName = String(cfg.admin?.display_name || step4Data.adminName);\n step4Data.adminPhone = String(cfg.admin?.phone || step4Data.adminPhone);\n step4Data.tenantName = String(cfg.admin?.tenant_name || step4Data.tenantName);\n step4Data.tenantCode = String(cfg.admin?.tenant_code || step4Data.tenantCode);\n step4Data.tenantDescription = String(cfg.admin?.tenant_description || step4Data.tenantDescription);\n step4Data.createDefaultDepartments = Boolean(cfg.admin?.create_default_departments ?? step4Data.createDefaultDepartments);\n step4Data.companyName = String(cfg.admin?.company_name || step4Data.companyName);\n\n step3Data.backendPort = Number(cfg.ports?.backend_port || step3Data.backendPort);\n step3Data.webAdminPort = Number(cfg.ports?.web_admin_port || step3Data.webAdminPort);\n } catch {\n // 首次没有配置时忽略\n }\n};\n\nconst openLicenseModal = (e: Event) => {\n e.preventDefault();\n showLicenseModal.value = true;\n};\n\nconst openTermsModal = (e: Event) => {\n e.preventDefault();\n showTermsModal.value = true;\n};\n\nconst openPrivacyModal = (e: Event) => {\n e.preventDefault();\n showPrivacyModal.value = true;\n};\n\nonMounted(async () => {\n llmProviderOptions.value = buildFallbackLLMProviderOptions();\n loadLLMProviderOptions();\n await loadSetupConfig();\n applyStep2SmartDefaults();\n runSystemChecks();\n});\n</script>\n\n<template>\n <div\n class=\"min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 dark:from-gray-900 dark:to-gray-800\"\n >\n <div class=\"container mx-auto px-4 py-8\">\n <div class=\"flex justify-end items-center gap-2 mb-4\">\n <LanguageSwitcher />\n <ThemeSwitcher />\n </div>\n\n <!-- 页面标题 -->\n <div class=\"text-center mb-8\">\n <h1 class=\"text-3xl font-bold text-gray-900 dark:text-white mb-2\">\n {{ uiCopy.pageTitle }}\n </h1>\n <p class=\"text-gray-600 dark:text-gray-300\">\n {{ uiCopy.pageDesc }}\n </p>\n </div>\n\n <!-- 步骤指示器和内容 -->\n <div class=\"max-w-4xl mx-auto\">\n <UStepper\n ref=\"stepper\"\n :items=\"stepperItems\"\n v-model=\"currentStep\"\n class=\"w-full\"\n >\n <!-- 步骤1:系统检查 & 许可 -->\n <template #system-check>\n <UCard class=\"shadow-lg mt-6\">\n <div class=\"p-6\">\n <h3 class=\"text-lg font-semibold mb-6\">{{ uiCopy.step1Title }}</h3>\n <p v-if=\"setupStatus?.version\" class=\"text-sm text-gray-500 mb-4\">\n {{ uiCopy.versionLabel }}{{ setupStatus.version }}\n </p>\n\n <!-- 系统检测 -->\n <div class=\"mb-8\">\n <h4 class=\"text-md font-medium mb-4\">{{ uiCopy.envCheckTitle }}</h4>\n <div class=\"space-y-3\">\n <div\n v-for=\"(check, key) in step1Data.checks\"\n :key=\"key\"\n class=\"flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700 rounded-lg\"\n >\n <div class=\"flex items-center space-x-3\">\n <UIcon\n :name=\"\n check.status === 'success'\n ? 'i-lucide-check-circle'\n : check.status === 'error'\n ? 'i-lucide-x-circle'\n : 'i-lucide-clock'\n \"\n :class=\"{\n 'text-green-500': check.status === 'success',\n 'text-red-500': check.status === 'error',\n 'text-yellow-500': check.status === 'checking',\n 'text-gray-400': check.status === 'pending',\n }\"\n class=\"w-5 h-5\"\n />\n <span class=\"font-medium\">\n {{ uiCopy.checkLabelMap[String(key)] || String(key) }}\n </span>\n </div>\n <span\n class=\"text-sm\"\n :class=\"{\n 'text-green-600': check.status === 'success',\n 'text-red-600': check.status === 'error',\n 'text-yellow-600': check.status === 'checking',\n 'text-gray-500': check.status === 'pending',\n }\"\n >\n {{ check.message }}\n </span>\n </div>\n </div>\n </div>\n\n <!-- 部署模式选择 -->\n <div class=\"mb-8\">\n <h4 class=\"text-md font-medium mb-4\">部署模式</h4>\n <div class=\"grid grid-cols-2 gap-4 mb-4\">\n <UCard\n :class=\"{\n 'ring-2 ring-blue-500':\n step1Data.deploymentMode === 'single-tenant',\n 'hover:shadow-md cursor-pointer': true,\n }\"\n @click=\"step1Data.deploymentMode = 'single-tenant'\"\n >\n <div class=\"p-4\">\n <div class=\"flex items-center space-x-3\">\n <input\n v-model=\"step1Data.deploymentMode\"\n type=\"radio\"\n value=\"single-tenant\"\n />\n <div>\n <h5 class=\"font-medium\">单租户模式</h5>\n <p class=\"text-sm text-gray-600 dark:text-gray-300\">\n 适合单一组织使用\n </p>\n </div>\n </div>\n </div>\n </UCard>\n\n <UCard\n :class=\"{\n 'ring-2 ring-blue-500':\n step1Data.deploymentMode === 'multi-tenant',\n 'hover:shadow-md cursor-pointer': true,\n }\"\n @click=\"step1Data.deploymentMode = 'multi-tenant'\"\n >\n <div class=\"p-4\">\n <div class=\"flex items-center space-x-3\">\n <input\n v-model=\"step1Data.deploymentMode\"\n type=\"radio\"\n value=\"multi-tenant\"\n />\n <div>\n <h5 class=\"font-medium\">多租户模式</h5>\n <p class=\"text-sm text-gray-600 dark:text-gray-300\">\n 支持多个组织独立使用\n </p>\n </div>\n </div>\n </div>\n </UCard>\n </div>\n\n <h5 class=\"font-medium mb-3\">接入方式</h5>\n <div class=\"grid grid-cols-2 gap-4\">\n <UCard\n :class=\"{\n 'ring-2 ring-blue-500':\n step1Data.authMode === 'builtin',\n 'hover:shadow-md cursor-pointer': true,\n }\"\n @click=\"step1Data.authMode = 'builtin'\"\n >\n <div class=\"p-4\">\n <div class=\"flex items-center space-x-3\">\n <input\n v-model=\"step1Data.authMode\"\n type=\"radio\"\n value=\"builtin\"\n />\n <div>\n <h5 class=\"font-medium\">内置账户</h5>\n <p class=\"text-sm text-gray-600 dark:text-gray-300\">\n 使用系统内置的用户管理\n </p>\n </div>\n </div>\n </div>\n </UCard>\n\n <UCard\n :class=\"{\n 'ring-2 ring-blue-500': step1Data.authMode === 'sso',\n 'hover:shadow-md cursor-pointer': true,\n }\"\n @click=\"step1Data.authMode = 'sso'\"\n >\n <div class=\"p-4\">\n <div class=\"flex items-center space-x-3\">\n <input v-model=\"step1Data.authMode\" type=\"radio\" value=\"sso\" />\n <div>\n <h5 class=\"font-medium\">SSO 单点登录</h5>\n <p class=\"text-sm text-gray-600 dark:text-gray-300\">\n 集成企业身份认证系统\n </p>\n </div>\n </div>\n </div>\n </UCard>\n </div>\n </div>\n\n <!-- 许可确认 -->\n <div class=\"space-y-4\">\n <h4 class=\"text-md font-medium\">{{ legalCopy.sectionTitle }}</h4>\n\n <div class=\"flex items-start space-x-3\">\n <UCheckbox\n :model-value=\"step1Data.licenseAccepted\"\n @update:model-value=\"(v) => (step1Data.licenseAccepted = !!v)\"\n class=\"mt-1\"\n />\n <div>\n <p class=\"text-sm\">\n {{ legalCopy.agreePrefix }}\n <button\n type=\"button\"\n class=\"text-blue-600 hover:underline\"\n @click=\"openLicenseModal\"\n >\n {{ legalCopy.license }}\n </button>\n </p>\n </div>\n </div>\n\n <div class=\"flex items-start space-x-3\">\n <UCheckbox\n :model-value=\"step1Data.termsAccepted\"\n @update:model-value=\"(v) => (step1Data.termsAccepted = !!v)\"\n class=\"mt-1\"\n />\n <div>\n <p class=\"text-sm\">\n {{ legalCopy.agreePrefix }}\n <button\n type=\"button\"\n class=\"text-blue-600 hover:underline\"\n @click=\"openTermsModal\"\n >\n {{ legalCopy.terms }}\n </button>\n {{ legalCopy.and }}\n <button\n type=\"button\"\n class=\"text-blue-600 hover:underline\"\n @click=\"openPrivacyModal\"\n >\n {{ legalCopy.privacy }}\n </button>\n </p>\n </div>\n </div>\n\n <div class=\"flex items-start space-x-3\">\n <UCheckbox\n :model-value=\"step1Data.telemetryEnabled\"\n @update:model-value=\"(v) => (step1Data.telemetryEnabled = !!v)\"\n class=\"mt-1\"\n />\n <div>\n <p class=\"text-sm\">\n {{ legalCopy.telemetryTitle }}\n </p>\n <p class=\"text-xs text-gray-500 mt-1\">\n {{ legalCopy.telemetryDesc }}\n </p>\n </div>\n </div>\n </div>\n </div>\n\n <!-- 底部按钮 -->\n <template #footer>\n <div class=\"flex justify-between items-center px-6 py-4\">\n <div></div>\n <UButton\n color=\"primary\"\n @click=\"nextStep\"\n :loading=\"isLoading\"\n :disabled=\"!validateCurrentStep()\"\n >\n 下一步\n </UButton>\n </div>\n </template>\n </UCard>\n </template>\n\n <!-- 步骤2:域名 & HTTPS -->\n <template #domain-https>\n <UCard class=\"shadow-lg mt-6\">\n <div class=\"p-6\">\n <h3 class=\"text-lg font-semibold mb-6\">域名 & HTTPS 配置</h3>\n\n <!-- 域名配置 -->\n <div class=\"mb-8\">\n <h4 class=\"text-md font-medium mb-4\">域名设置</h4>\n <div class=\"space-y-4\">\n <UFormField label=\"系统域名\" required>\n <UInput\n v-model=\"step2Data.domain\"\n placeholder=\"例如:powerx.example.com\"\n icon=\"i-lucide-globe\"\n />\n <template #help>\n <span class=\"text-sm text-gray-500\">\n 默认使用当前访问域名;请输入系统完整域名(不含 http/https)。API 路径前缀由 server.api_prefix 控制(默认 /api/v1)\n </span>\n </template>\n </UFormField>\n </div>\n </div>\n\n <!-- HTTPS 配置 -->\n <div class=\"mb-8\">\n <h4 class=\"text-md font-medium mb-4\">HTTPS 配置</h4>\n <div class=\"space-y-4\">\n <URadioGroup\n v-model=\"step2Data.httpsMode\"\n :options=\"[\n {\n value: 'auto',\n label: '自动获取证书 (Let\\'s Encrypt)',\n },\n { value: 'manual', label: '手动上传证书' },\n { value: 'disable', label: '暂不启用 HTTPS' },\n ]\"\n />\n\n <!-- 自动证书配置 -->\n <div\n v-if=\"step2Data.httpsMode === 'auto'\"\n class=\"pl-6 space-y-4\"\n >\n <UFormField label=\"邮箱地址\" required>\n <UInput\n v-model=\"step2Data.certEmail\"\n type=\"email\"\n placeholder=\"admin@example.com\"\n icon=\"i-lucide-mail\"\n />\n <template #help>\n <span class=\"text-sm text-gray-500\">\n 用于 Let's Encrypt 证书申请和续期通知\n </span>\n </template>\n </UFormField>\n </div>\n\n <!-- 手动证书上传 -->\n <div\n v-if=\"step2Data.httpsMode === 'manual'\"\n class=\"pl-6 space-y-4\"\n >\n <UFormField label=\"证书文件 (.crt)\" required>\n <UTextarea\n v-model=\"step2Data.certContent\"\n placeholder=\"-----BEGIN CERTIFICATE-----\"\n rows=\"6\"\n />\n </UFormField>\n\n <UFormField label=\"私钥文件 (.key)\" required>\n <UTextarea\n v-model=\"step2Data.keyContent\"\n placeholder=\"-----BEGIN PRIVATE KEY-----\"\n rows=\"6\"\n />\n </UFormField>\n </div>\n\n <!-- HTTPS 禁用警告 -->\n <UAlert\n v-if=\"step2Data.httpsMode === 'disable'\"\n icon=\"i-lucide-alert-triangle\"\n color=\"amber\"\n variant=\"soft\"\n title=\"安全警告\"\n description=\"不启用 HTTPS 可能存在安全风险,建议在生产环境中启用\"\n />\n </div>\n </div>\n\n <!-- 服务端口 -->\n <div class=\"mb-8\">\n <h4 class=\"text-md font-medium mb-4\">服务端口</h4>\n <div class=\"grid grid-cols-2 gap-4\">\n <UFormField label=\"Backend 端口\" required>\n <UInput\n v-model=\"step3Data.backendPort\"\n type=\"number\"\n placeholder=\"8080\"\n />\n </UFormField>\n\n <UFormField label=\"Web Admin 端口\" required>\n <UInput\n v-model=\"step3Data.webAdminPort\"\n type=\"number\"\n placeholder=\"3000\"\n />\n </UFormField>\n </div>\n <div class=\"mt-4 rounded-lg border border-gray-200 dark:border-gray-700\">\n <div class=\"grid grid-cols-3 gap-2 bg-gray-50 dark:bg-gray-800 px-3 py-2 text-xs font-semibold\">\n <span>{{ isZh ? \"项目\" : \"Item\" }}</span>\n <span>{{ isZh ? \"目标值(Desired)\" : \"Desired\" }}</span>\n <span>{{ isZh ? \"当前生效值(Effective)\" : \"Effective\" }}</span>\n </div>\n <div class=\"grid grid-cols-3 gap-2 px-3 py-2 text-xs border-t border-gray-100 dark:border-gray-700\">\n <span>Backend</span>\n <span>{{ desiredBackendPort || \"-\" }}</span>\n <span>{{ effectiveBackendPort || \"-\" }}</span>\n </div>\n <div class=\"grid grid-cols-3 gap-2 px-3 py-2 text-xs border-t border-gray-100 dark:border-gray-700\">\n <span>Web Admin</span>\n <span>{{ desiredWebAdminPort || \"-\" }}</span>\n <span>{{ effectiveWebAdminPort || \"-\" }}</span>\n </div>\n </div>\n <UAlert\n v-if=\"restartRequired\"\n class=\"mt-3\"\n color=\"warning\"\n variant=\"soft\"\n icon=\"i-lucide-refresh-cw\"\n :title=\"isZh ? '端口配置已变更,需重启服务后生效' : 'Port config changed. Restart services to take effect.'\"\n :description=\"isZh\n ? `desired=${desiredSource || '-'}; effective=${effectiveSource || '-'}`\n : `desired=${desiredSource || '-'}; effective=${effectiveSource || '-'}`\"\n />\n <p class=\"text-xs text-gray-500 mt-2\">\n 默认端口:backend=8080 / web-admin=3000\n </p>\n </div>\n\n <!-- CDN 配置 -->\n <div class=\"space-y-4\">\n <h4 class=\"text-md font-medium mb-4\">CDN 配置(可选)</h4>\n <UCheckbox\n :model-value=\"step2Data.enableCdn\"\n @update:model-value=\"(v) => (step2Data.enableCdn = !!v)\"\n label=\"启用 CDN 加速\"\n />\n\n <div v-if=\"step2Data.enableCdn\" class=\"pl-6 space-y-4\">\n <UFormField label=\"CDN 域名\">\n <UInput\n v-model=\"step2Data.cdnDomain\"\n placeholder=\"例如:cdn.example.com\"\n icon=\"i-lucide-zap\"\n />\n </UFormField>\n </div>\n </div>\n </div>\n\n <template #footer>\n <div class=\"flex justify-between items-center px-6 py-4\">\n <UButton\n variant=\"outline\"\n @click=\"prevStep\"\n :disabled=\"isLoading\"\n >\n 上一步\n </UButton>\n <UButton\n color=\"primary\"\n @click=\"nextStep\"\n :loading=\"isLoading\"\n :disabled=\"!validateCurrentStep()\"\n >\n 下一步\n </UButton>\n </div>\n </template>\n </UCard>\n </template>\n\n <!-- 步骤3:数据库 & 基础配置 -->\n <template #database-config>\n <UCard class=\"shadow-lg mt-6\">\n <div class=\"p-6\">\n <h3 class=\"text-lg font-semibold mb-6\">数据库 & 基础配置</h3>\n\n <!-- 数据库配置 -->\n <div class=\"mb-8\">\n <h4 class=\"text-md font-medium mb-4\">数据库设置</h4>\n <div class=\"grid grid-cols-2 gap-4 mb-4\">\n <UFormField label=\"数据库类型\" required>\n <USelect\n v-model=\"step3Data.dbType\"\n :portal=\"false\"\n :options=\"[\n { label: 'MySQL', value: 'mysql' },\n { label: 'PostgreSQL', value: 'postgresql' },\n { label: 'SQLite', value: 'sqlite' },\n ]\"\n />\n </UFormField>\n\n <UFormField label=\"数据库版本\">\n <UInput\n v-model=\"step3Data.dbVersion\"\n placeholder=\"例如:8.0\"\n :disabled=\"step3Data.dbType === 'sqlite'\"\n />\n </UFormField>\n </div>\n\n <div\n v-if=\"step3Data.dbType !== 'sqlite'\"\n class=\"grid grid-cols-2 gap-4 mb-4\"\n >\n <UFormField label=\"主机地址\" required>\n <UInput\n v-model=\"step3Data.dbHost\"\n placeholder=\"localhost\"\n icon=\"i-lucide-server\"\n />\n </UFormField>\n\n <UFormField label=\"端口\" required>\n <UInput\n v-model=\"step3Data.dbPort\"\n type=\"number\"\n :placeholder=\"\n step3Data.dbType === 'mysql' ? '3306' : '5432'\n \"\n />\n </UFormField>\n </div>\n\n <div\n v-if=\"step3Data.dbType !== 'sqlite'\"\n class=\"grid grid-cols-2 gap-4 mb-4\"\n >\n <UFormField label=\"数据库名\" required>\n <UInput\n v-model=\"step3Data.dbName\"\n placeholder=\"powerx\"\n icon=\"i-lucide-database\"\n />\n </UFormField>\n\n <UFormField label=\"字符集\">\n <USelect\n v-model=\"step3Data.dbCharset\"\n :portal=\"false\"\n :options=\"[\n { label: 'utf8mb4', value: 'utf8mb4' },\n { label: 'utf8', value: 'utf8' },\n ]\"\n />\n </UFormField>\n </div>\n\n <template v-if=\"step3Data.dbType !== 'sqlite'\">\n <div class=\"grid grid-cols-2 gap-4\">\n <UFormField label=\"用户名\" required>\n <UInput\n v-model=\"step3Data.dbUsername\"\n placeholder=\"root\"\n icon=\"i-lucide-user\"\n />\n </UFormField>\n\n <UFormField label=\"密码\" required>\n <UInput\n v-model=\"step3Data.dbPassword\"\n type=\"password\"\n placeholder=\"数据库密码\"\n icon=\"i-lucide-lock\"\n />\n </UFormField>\n </div>\n\n <div class=\"mt-4 grid grid-cols-2 gap-4 items-center\">\n <p\n class=\"text-xs\"\n :class=\"dbTestState.database.ok ? 'text-green-500' : 'text-gray-500'\"\n >\n {{ dbTestState.database.message || '可先测试数据库连通性,再进入下一步。' }}\n </p>\n <div class=\"flex justify-start\">\n <UButton\n color=\"neutral\"\n variant=\"soft\"\n :loading=\"dbTestState.database.testing\"\n @click=\"setupTestDatabaseConnection\"\n >\n 测试数据库连接\n </UButton>\n </div>\n </div>\n </template>\n\n <div v-else class=\"space-y-4\">\n <UFormField label=\"数据库文件路径\">\n <UInput\n v-model=\"step3Data.sqlitePath\"\n placeholder=\"/data/powerx.db\"\n icon=\"i-lucide-file\"\n />\n </UFormField>\n </div>\n </div>\n\n <!-- 缓存配置 -->\n <div class=\"mb-8\">\n <h4 class=\"text-md font-medium mb-4\">缓存配置</h4>\n <div class=\"space-y-4\">\n <UFormField label=\"缓存类型\">\n <USelect\n v-model=\"step3Data.cacheType\"\n :portal=\"false\"\n :options=\"[\n { label: 'Redis', value: 'redis' },\n { label: 'Memcached', value: 'memcached' },\n { label: '文件缓存', value: 'file' },\n ]\"\n />\n </UFormField>\n\n <div\n v-if=\"step3Data.cacheType === 'redis'\"\n class=\"grid grid-cols-2 gap-4\"\n >\n <UFormField label=\"Redis 主机\">\n <UInput\n v-model=\"step3Data.redisHost\"\n placeholder=\"localhost\"\n icon=\"i-lucide-server\"\n />\n </UFormField>\n\n <UFormField label=\"Redis 端口\">\n <UInput\n v-model=\"step3Data.redisPort\"\n type=\"number\"\n placeholder=\"6379\"\n />\n </UFormField>\n </div>\n\n <div\n v-if=\"step3Data.cacheType === 'redis'\"\n class=\"grid grid-cols-2 gap-4\"\n >\n <UFormField label=\"Redis 密码\">\n <UInput\n v-model=\"step3Data.redisPassword\"\n type=\"password\"\n placeholder=\"Redis 密码(可选)\"\n icon=\"i-lucide-lock\"\n />\n </UFormField>\n\n <UFormField label=\"数据库索引\">\n <UInput\n v-model=\"step3Data.redisDb\"\n type=\"number\"\n placeholder=\"0\"\n />\n </UFormField>\n </div>\n\n <div\n v-if=\"step3Data.cacheType === 'redis'\"\n class=\"mt-2 grid grid-cols-2 gap-4 items-center\"\n >\n <p\n class=\"text-xs\"\n :class=\"dbTestState.cache.ok ? 'text-green-500' : 'text-gray-500'\"\n >\n {{ dbTestState.cache.message || '建议测试 Redis 连通性,避免初始化阶段失败。' }}\n </p>\n <div class=\"flex justify-start\">\n <UButton\n color=\"neutral\"\n variant=\"soft\"\n :loading=\"dbTestState.cache.testing\"\n @click=\"setupTestCacheConnection\"\n >\n 测试 Redis 连接\n </UButton>\n </div>\n </div>\n </div>\n </div>\n\n <!-- 存储配置 -->\n <div class=\"space-y-4\">\n <h4 class=\"text-md font-medium mb-4\">文件存储配置</h4>\n <UFormField label=\"存储类型\">\n <USelect\n v-model=\"step3Data.storageType\"\n :portal=\"false\"\n :options=\"[\n { label: '本地存储', value: 'local' },\n { label: '阿里云 OSS', value: 'aliyun' },\n { label: '腾讯云 COS', value: 'tencent' },\n { label: 'AWS S3', value: 'aws' },\n ]\"\n />\n </UFormField>\n\n <div\n v-if=\"step3Data.storageType === 'local'\"\n class=\"space-y-4\"\n >\n <UFormField label=\"存储路径\">\n <UInput\n v-model=\"step3Data.localStoragePath\"\n placeholder=\"/data/uploads\"\n icon=\"i-lucide-folder\"\n />\n </UFormField>\n </div>\n\n <div\n v-if=\"step3Data.storageType !== 'local'\"\n class=\"grid grid-cols-2 gap-4\"\n >\n <UFormField label=\"Access Key ID\" required>\n <UInput\n v-model=\"step3Data.storageAccessKey\"\n placeholder=\"访问密钥 ID\"\n icon=\"i-lucide-key\"\n />\n </UFormField>\n\n <UFormField label=\"Secret Access Key\" required>\n <UInput\n v-model=\"step3Data.storageSecretKey\"\n type=\"password\"\n placeholder=\"访问密钥\"\n icon=\"i-lucide-lock\"\n />\n </UFormField>\n </div>\n\n <div\n v-if=\"step3Data.storageType !== 'local'\"\n class=\"grid grid-cols-2 gap-4\"\n >\n <UFormField label=\"存储桶名称\" required>\n <UInput\n v-model=\"step3Data.storageBucket\"\n placeholder=\"bucket-name\"\n icon=\"i-lucide-archive\"\n />\n </UFormField>\n\n <UFormField label=\"区域\" required>\n <UInput\n v-model=\"step3Data.storageRegion\"\n placeholder=\"例如:cn-hangzhou\"\n icon=\"i-lucide-map-pin\"\n />\n </UFormField>\n </div>\n </div>\n\n <!-- 邮件配置 -->\n <div class=\"mt-8 space-y-4\">\n <h4 class=\"text-md font-medium mb-4\">邮件配置(可选)</h4>\n\n <UFormField label=\"启用 SMTP 邮件\">\n <USwitch v-model=\"step3Data.emailEnabled\" />\n </UFormField>\n\n <div v-if=\"step3Data.emailEnabled\" class=\"grid grid-cols-2 gap-4\">\n <UFormField label=\"SMTP 主机\" required>\n <UInput\n v-model=\"step3Data.emailSmtpHost\"\n placeholder=\"smtp.example.com\"\n icon=\"i-lucide-mail\"\n />\n </UFormField>\n <UFormField label=\"SMTP 端口\" required>\n <UInput\n v-model=\"step3Data.emailSmtpPort\"\n type=\"number\"\n placeholder=\"587\"\n />\n </UFormField>\n </div>\n\n <div v-if=\"step3Data.emailEnabled\" class=\"grid grid-cols-2 gap-4\">\n <UFormField label=\"发件人名称\">\n <UInput\n v-model=\"step3Data.emailFromName\"\n placeholder=\"PowerX\"\n />\n </UFormField>\n <UFormField label=\"发件人邮箱\" required>\n <UInput\n v-model=\"step3Data.emailFromAddress\"\n placeholder=\"noreply@example.com\"\n />\n </UFormField>\n </div>\n </div>\n </div>\n\n <template #footer>\n <div class=\"flex justify-between items-center px-6 py-4\">\n <UButton\n variant=\"outline\"\n @click=\"prevStep\"\n :disabled=\"isLoading\"\n >\n 上一步\n </UButton>\n <div class=\"flex items-center gap-3\">\n <p\n class=\"text-xs\"\n :class=\"dbTestState.provisioned ? 'text-green-500' : 'text-gray-500'\"\n >\n {{\n dbTestState.provisioned\n ? '数据库 migrate/seed 已完成'\n : '下一步会自动保存配置并执行数据库初始化'\n }}\n </p>\n <UButton\n color=\"primary\"\n @click=\"nextStep\"\n :loading=\"isLoading || dbTestState.provisioning\"\n :disabled=\"!validateCurrentStep()\"\n >\n 下一步\n </UButton>\n </div>\n </div>\n </template>\n </UCard>\n </template>\n\n <!-- 步骤4:超级管理员 & 租户初始化 -->\n <template #admin-tenant>\n <UCard class=\"shadow-lg mt-6\">\n <div class=\"p-6\">\n <h3 class=\"text-lg font-semibold mb-6\">\n 超级管理员 & 租户初始化\n </h3>\n\n <!-- 超级管理员配置 -->\n <div class=\"mb-8\">\n <h4 class=\"text-md font-medium mb-4\">超级管理员账户</h4>\n <div class=\"grid grid-cols-2 gap-4 mb-4\">\n <UFormField label=\"管理员用户名\" required>\n <UInput\n v-model=\"step4Data.adminUsername\"\n placeholder=\"admin\"\n icon=\"i-lucide-user\"\n />\n </UFormField>\n\n <UFormField label=\"管理员邮箱\" required>\n <UInput\n v-model=\"step4Data.adminEmail\"\n type=\"email\"\n placeholder=\"admin@example.com\"\n icon=\"i-lucide-mail\"\n />\n </UFormField>\n </div>\n\n <div class=\"grid grid-cols-2 gap-4 mb-4\">\n <UFormField label=\"管理员密码\" required>\n <UInput\n v-model=\"step4Data.adminPassword\"\n type=\"password\"\n placeholder=\"请输入强密码\"\n icon=\"i-lucide-lock\"\n />\n </UFormField>\n\n <UFormField label=\"确认密码\" required>\n <UInput\n v-model=\"step4Data.adminPasswordConfirm\"\n type=\"password\"\n placeholder=\"再次输入密码\"\n icon=\"i-lucide-lock\"\n />\n </UFormField>\n </div>\n\n <div class=\"grid grid-cols-2 gap-4\">\n <UFormField label=\"管理员姓名\">\n <UInput\n v-model=\"step4Data.adminName\"\n placeholder=\"系统管理员\"\n icon=\"i-lucide-user-circle\"\n />\n </UFormField>\n\n <UFormField label=\"手机号码\">\n <UInput\n v-model=\"step4Data.adminPhone\"\n placeholder=\"13800138000\"\n icon=\"i-lucide-phone\"\n />\n </UFormField>\n </div>\n </div>\n\n <!-- 默认租户配置 -->\n <div\n class=\"mb-8\"\n v-if=\"step1Data.deploymentMode === 'multi-tenant'\"\n >\n <h4 class=\"text-md font-medium mb-4\">默认租户设置</h4>\n <div class=\"grid grid-cols-2 gap-4 mb-4\">\n <UFormField label=\"租户名称\" required>\n <UInput\n v-model=\"step4Data.tenantName\"\n placeholder=\"默认租户\"\n icon=\"i-lucide-building\"\n />\n </UFormField>\n\n <UFormField label=\"租户标识\" required>\n <UInput\n v-model=\"step4Data.tenantCode\"\n placeholder=\"default\"\n icon=\"i-lucide-tag\"\n />\n <template #help>\n <span class=\"text-sm text-gray-500\">\n 租户的唯一标识,只能包含字母、数字和下划线\n </span>\n </template>\n </UFormField>\n </div>\n\n <div class=\"space-y-4\">\n <UFormField label=\"租户描述\">\n <UTextarea\n v-model=\"step4Data.tenantDescription\"\n placeholder=\"租户描述信息\"\n :rows=\"3\"\n />\n </UFormField>\n </div>\n </div>\n\n <!-- 组织架构初始化 -->\n <div class=\"space-y-4\">\n <h4 class=\"text-md font-medium mb-4\">组织架构初始化</h4>\n\n <UCheckbox\n :model-value=\"step4Data.createDefaultDepartments\"\n @update:model-value=\"(v) => (step4Data.createDefaultDepartments = !!v)\"\n label=\"创建默认部门结构\"\n />\n\n <div\n v-if=\"step4Data.createDefaultDepartments\"\n class=\"pl-6 space-y-4\"\n >\n <div class=\"grid grid-cols-2 gap-4\">\n <UFormField label=\"公司名称\">\n <UInput\n v-model=\"step4Data.companyName\"\n placeholder=\"我的公司\"\n icon=\"i-lucide-building-2\"\n />\n </UFormField>\n </div>\n </div>\n </div>\n </div>\n\n <template #footer>\n <div class=\"flex justify-between items-center px-6 py-4\">\n <UButton\n variant=\"outline\"\n @click=\"prevStep\"\n :disabled=\"isLoading\"\n >\n 上一步\n </UButton>\n <UButton\n color=\"primary\"\n @click=\"nextStep\"\n :loading=\"isLoading\"\n :disabled=\"!validateCurrentStep()\"\n >\n 下一步\n </UButton>\n </div>\n </template>\n </UCard>\n </template>\n\n <!-- 步骤5:插件与智能体安装 -->\n <template #plugins-install>\n <UCard class=\"shadow-lg mt-6\">\n <div class=\"p-6\">\n <h3 class=\"text-lg font-semibold mb-6\">插件与智能体安装</h3>\n\n <div class=\"mb-6\">\n <p class=\"text-gray-600 dark:text-gray-300 text-sm\">\n 以下为安装后建议启用的基础能力清单(引导说明,不会在此步骤立即执行插件安装)。\n </p>\n </div>\n\n <div class=\"space-y-4\">\n <div\n v-for=\"plugin in step5Data.availablePlugins\"\n :key=\"plugin.id\"\n class=\"flex items-center justify-between p-4 border rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700\"\n >\n <div>\n <h5 class=\"font-medium\">{{ plugin.name }}</h5>\n <p class=\"text-sm text-gray-600 dark:text-gray-300\">\n {{ plugin.description }}\n </p>\n </div>\n <div class=\"flex items-center space-x-2\">\n <span\n class=\"px-2 py-1 text-xs rounded-full\"\n :class=\"{\n 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200':\n plugin.category === 'workflow',\n 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200':\n plugin.category === 'communication',\n 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200':\n plugin.category === 'storage',\n 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200':\n plugin.category === 'ai',\n }\"\n >\n {{\n plugin.category === \"workflow\"\n ? \"工作流\"\n : plugin.category === \"communication\"\n ? \"通信\"\n : plugin.category === \"storage\"\n ? \"存储\"\n : plugin.category === \"ai\"\n ? \"AI\"\n : plugin.category\n }}\n </span>\n <span\n v-if=\"plugin.recommended\"\n class=\"px-2 py-1 text-xs bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200 rounded-full\"\n >\n 推荐\n </span>\n </div>\n </div>\n </div>\n\n <div class=\"mt-6 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg\">\n <div class=\"flex items-start space-x-3\">\n <UIcon\n name=\"i-lucide-info\"\n class=\"w-5 h-5 text-blue-500 mt-0.5\"\n />\n <div>\n <h5 class=\"font-medium text-blue-900 dark:text-blue-100\">\n 提示\n </h5>\n <p class=\"text-sm text-blue-700 dark:text-blue-300 mt-1\">\n 本步骤仅提供建议清单。实际安装与启停请在“插件市场/已安装插件”中操作,再在“模型中心”“工具中心”完成配置与密钥绑定。\n </p>\n </div>\n </div>\n </div>\n </div>\n\n <template #footer>\n <div class=\"flex justify-between items-center px-6 py-4\">\n <UButton\n variant=\"outline\"\n @click=\"prevStep\"\n :disabled=\"isLoading\"\n >\n 上一步\n </UButton>\n <UButton\n color=\"primary\"\n @click=\"nextStep\"\n :loading=\"isLoading\"\n >\n 下一步\n </UButton>\n </div>\n </template>\n </UCard>\n </template>\n\n <!-- 步骤6:LLM 配置(可选) -->\n <template #llm-config>\n <UCard class=\"shadow-lg mt-6\">\n <div class=\"p-6\">\n <h3 class=\"text-lg font-semibold mb-2\">\n {{ isZh ? \"LLM 模型配置(可选)\" : \"LLM Configuration (Optional)\" }}\n </h3>\n <p class=\"text-sm text-gray-500 mb-6\">\n {{\n isZh\n ? \"仅配置文本模型接入参数;图像/音频等其他模态可在安装后继续配置。\"\n : \"Configure text LLM only. Other modalities can be configured after installation.\"\n }}\n </p>\n\n <div class=\"space-y-4\">\n <UCheckbox\n :model-value=\"step6Data.skipLLMSetup\"\n @update:model-value=\"(v) => (step6Data.skipLLMSetup = !!v)\"\n :label=\"isZh ? '跳过此步骤,安装后在模型中心配置' : 'Skip this step and configure later in Model Center'\"\n />\n </div>\n\n <div v-if=\"!step6Data.skipLLMSetup\" class=\"grid grid-cols-1 lg:grid-cols-3 gap-6 mt-6\">\n <div class=\"lg:col-span-2 space-y-6\">\n <div class=\"rounded-lg border border-[var(--border-color)] bg-[var(--card-bg)] p-4\">\n <div class=\"mb-4 font-medium text-[var(--text-primary)]\">\n {{ isZh ? \"LLM 文本 - 通用\" : \"LLM Text - General\" }}\n </div>\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <UFormField class=\"md:col-span-2\" :label=\"isZh ? 'Provider' : 'Provider'\" required>\n <USelect\n v-model=\"step6Data.provider\"\n :items=\"llmProviderOptions\"\n icon=\"i-heroicons-building-library\"\n class=\"w-full\"\n />\n </UFormField>\n <UFormField class=\"md:col-span-2\" :label=\"isZh ? 'Model' : 'Model'\" required>\n <USelect\n v-model=\"step6Data.model\"\n :items=\"llmModelOptions\"\n icon=\"i-heroicons-cpu-chip\"\n class=\"w-full\"\n />\n <p class=\"mt-1 text-xs text-[var(--text-secondary)] break-all leading-5\">\n {{ step6Data.model }}\n </p>\n </UFormField>\n <UFormField class=\"md:col-span-2 w-full\" :label=\"isZh ? 'Base URL(可选)' : 'Base URL (Optional)'\">\n <UInput\n v-model=\"step6Data.baseUrl\"\n :placeholder=\"isZh ? '例如:https://api.openai.com/v1 或 http://127.0.0.1:11434/v1' : 'e.g. https://api.openai.com/v1 or http://127.0.0.1:11434/v1'\"\n class=\"w-full\"\n />\n </UFormField>\n <UFormField class=\"md:col-span-2 w-full\" :label=\"isZh ? 'API Key' : 'API Key'\" required>\n <UInput\n v-model=\"step6Data.apiKey\"\n type=\"password\"\n autocomplete=\"off\"\n :placeholder=\"isZh ? '请输入 API Key' : 'Enter API Key'\"\n class=\"w-full\"\n />\n </UFormField>\n </div>\n </div>\n\n <div class=\"rounded-lg border border-[var(--border-color)] bg-[var(--card-bg)] p-4\">\n <div class=\"mb-4 font-medium text-[var(--text-primary)]\">\n {{ isZh ? \"LLM 文本 - 参数\" : \"LLM Text - Parameters\" }}\n </div>\n <div class=\"grid grid-cols-1 md:grid-cols-2 gap-4\">\n <div>\n <label class=\"block text-sm font-medium text-[var(--text-primary)] mb-2\">\n {{ isZh ? \"温度 (Temperature)\" : \"Temperature\" }}\n </label>\n <USlider v-model=\"step6Data.temperature\" :min=\"0\" :max=\"2\" :step=\"0.1\" class=\"w-full\" />\n <div class=\"text-xs text-[var(--text-secondary)] mt-1\">\n {{ isZh ? \"当前值\" : \"Current\" }}: {{ step6Data.temperature }}\n </div>\n </div>\n <div>\n <label class=\"block text-sm font-medium text-[var(--text-primary)] mb-2\">\n {{ isZh ? \"最大令牌数\" : \"Max Tokens\" }}\n </label>\n <UInput v-model=\"step6Data.maxTokens\" type=\"number\" :min=\"1\" :max=\"32000\" />\n </div>\n <div>\n <label class=\"block text-sm font-medium text-[var(--text-primary)] mb-2\">\n Top P\n </label>\n <USlider v-model=\"step6Data.topP\" :min=\"0\" :max=\"1\" :step=\"0.01\" class=\"w-full\" />\n <div class=\"text-xs text-[var(--text-secondary)] mt-1\">\n {{ isZh ? \"当前值\" : \"Current\" }}: {{ step6Data.topP }}\n </div>\n </div>\n <div class=\"flex items-center\">\n <UCheckbox\n :model-value=\"step6Data.stream\"\n @update:model-value=\"(v) => (step6Data.stream = !!v)\"\n :label=\"isZh ? '启用流式输出' : 'Enable Streaming'\"\n />\n </div>\n </div>\n </div>\n </div>\n\n <div class=\"lg:col-span-1\">\n <TestPanel\n :current-title=\"isZh ? 'LLM 文本' : 'LLM Text'\"\n :current-state=\"{\n provider: step6Data.provider,\n model: step6Data.model,\n apiKey: step6Data.apiKey,\n baseURL: step6Data.baseUrl,\n }\"\n :last-test-message=\"llmLastTestMessage\"\n :last-test-detail=\"llmLastTestDetail\"\n :on-test-connection=\"setupTestConnection\"\n :on-test-quick-call=\"setupTestQuickCall\"\n />\n </div>\n </div>\n </div>\n\n <template #footer>\n <div class=\"flex justify-between items-center px-6 py-4\">\n <UButton\n variant=\"outline\"\n @click=\"prevStep\"\n :disabled=\"isLoading\"\n >\n {{ isZh ? \"上一步\" : \"Previous\" }}\n </UButton>\n <UButton\n color=\"primary\"\n @click=\"nextStep\"\n :loading=\"isLoading\"\n :disabled=\"!validateCurrentStep()\"\n >\n {{ isZh ? \"完成设置\" : \"Finish Setup\" }}\n </UButton>\n </div>\n </template>\n </UCard>\n </template>\n </UStepper>\n </div>\n </div>\n\n <UModal\n v-model:open=\"showLicenseModal\"\n :title=\"legalCopy.licenseModalTitle\"\n :ui=\"{ content: 'max-w-3xl max-h-[85dvh] overflow-y-auto' }\"\n >\n <template #body>\n <div class=\"space-y-3 text-sm leading-6 text-gray-700 dark:text-gray-300\">\n <p v-for=\"line in legalCopy.licenseLines\" :key=\"line\">{{ line }}</p>\n </div>\n </template>\n </UModal>\n\n <UModal\n v-model:open=\"showTermsModal\"\n :title=\"legalCopy.terms\"\n :ui=\"{ content: 'max-w-3xl max-h-[85dvh] overflow-y-auto' }\"\n >\n <template #body>\n <UsersTermsModal />\n </template>\n </UModal>\n\n <UModal\n v-model:open=\"showPrivacyModal\"\n :title=\"legalCopy.privacy\"\n :ui=\"{ content: 'max-w-3xl max-h-[85dvh] overflow-y-auto' }\"\n >\n <template #body>\n <UsersPrivacyModal />\n </template>\n </UModal>\n\n <div\n v-if=\"restarting\"\n class=\"fixed inset-0 z-[1000] bg-black/55 backdrop-blur-sm flex items-center justify-center px-4\"\n >\n <UCard class=\"w-full max-w-2xl shadow-2xl\">\n <div class=\"p-6\">\n <UIcon name=\"i-lucide-loader-circle\" class=\"w-10 h-10 mx-auto mb-4 animate-spin text-primary-500\" />\n <h3 class=\"text-lg font-semibold mb-2 text-center\">\n {{ isZh ? \"安装已完成,请重启后端\" : \"Setup Complete: Restart Backend\" }}\n </h3>\n <p class=\"text-sm text-gray-600 dark:text-gray-300 text-center\">\n {{\n restartProgressText ||\n (isZh\n ? \"请手动重启后端进程,系统检测到服务就绪后会自动跳转首页。\"\n : \"Please restart backend manually. The page will redirect automatically once service is ready.\")\n }}\n </p>\n <div class=\"mt-4 rounded-md border border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/40 p-3\">\n <p class=\"text-xs text-gray-500 dark:text-gray-400 mb-2\">\n {{ isZh ? \"建议操作\" : \"Recommended command\" }}\n </p>\n <code class=\"block text-xs break-all\">\n {{ isZh ? \"在后端目录执行:./powerx(先停止旧进程再启动)\" : \"Run in backend directory: ./powerx (stop old process first)\" }}\n </code>\n <p class=\"text-xs text-gray-500 dark:text-gray-400 mt-2\">\n {{ isZh ? `目标后端端口:${targetBackendPort || \"-\"}` : `Target backend port: ${targetBackendPort || \"-\"}` }}\n </p>\n </div>\n <div class=\"mt-4 flex justify-center\">\n <UButton color=\"primary\" :loading=\"manualCheckLoading\" @click=\"checkRestartAndRedirectNow\">\n {{ isZh ? \"我已重启,立即检测\" : \"I restarted, check now\" }}\n </UButton>\n </div>\n </div>\n </UCard>\n </div>\n </div>\n</template>\n"}}}}