feat(auto-reply): 接入万川平台模型配置 + 各模型独立网关回退
万川平台对接 - 新增 wanchuan_proxy.go:WanchuanLogin/WanchuanGetModel 代理登录与按 code 拉取模型, 日志对 password/token/apiKey 打码(含 encryptedConfig 二次解析) - 新增 PlatformConfig(baseUrl/username/password)及 Get/SavePlatformConfig 持久化 - 前端万川卡片:登录→拉取 chat/vision/embedding/rerank/voice→回填 form 并保存→必要时重建向量索引 各模型独立网关(url+key),留空回退聊天网关 - RetrievalConfig 新增 embeddingBaseUrl/embeddingApiKey、rerankBaseUrl/rerankApiKey - embeddingRequestConfig/rerankRequestConfig:优先独立网关,未配置回退 AI.BaseURL/APIKey - vision/audio 同模式:非 DashScope 网关下视觉/语音模型留空时不再锁死或强写 DashScope, 运行期由 fallbackString(VisionModel, Model) 动态复用聊天模型 陈旧向量空间防护 - loadEmbeddingIndex 检测磁盘索引与当前 embedding 模型/维度不一致时清空向量、回退关键词检索, 并提示重建(embeddingIndexStaleReason,兼容旧版无模型名索引) UI 状态修复 - 登录拉模型期间统一置全局 busy,禁用闸门收敛为 busy(与刷新联系人等按钮同范式), platformBusy 仅保留用于按钮「处理中…」文案,杜绝并发读写 form 与反向可点洞 其他 - 删除遗留 helper/auto_reply_ai.go.bak - 补充 config/helper 单元测试(视觉回退分支、陈旧索引判定)
This commit is contained in:
@@ -54,7 +54,8 @@
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span>身份缓存</span>
|
||||
<strong :class="status.identityRefreshing ? 'muted' : ''">{{ status.identityRefreshing ? '刷新中' : formatUnixTime(status.identityLastRefreshAt) }}</strong>
|
||||
<strong :class="status.identityRefreshing ? 'muted' : ''">{{ status.identityRefreshing ? '刷新中' :
|
||||
formatUnixTime(status.identityLastRefreshAt) }}</strong>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span>协同等待中</span>
|
||||
@@ -79,13 +80,8 @@
|
||||
</div>
|
||||
|
||||
<nav class="section-nav" aria-label="自动客服模块导航">
|
||||
<button
|
||||
v-for="item in sectionNavItems"
|
||||
:key="item.id"
|
||||
type="button"
|
||||
class="section-nav-btn"
|
||||
@click="scrollToSection(item.id)"
|
||||
>
|
||||
<button v-for="item in sectionNavItems" :key="item.id" type="button" class="section-nav-btn"
|
||||
@click="scrollToSection(item.id)">
|
||||
{{ item.label }}
|
||||
</button>
|
||||
</nav>
|
||||
@@ -167,13 +163,49 @@
|
||||
<div class="section-meta">
|
||||
<span>{{ form.ai.provider === 'local' ? '本地模型' : '兼容接口' }}</span>
|
||||
<span>文本:{{ form.ai.model || '未配置' }}</span>
|
||||
<span>图片:{{ form.ai.visionModel || defaultVisionModel }}</span>
|
||||
<span>图片:{{ form.ai.visionModel || form.ai.model || defaultVisionModel }}{{ form.ai.visionModel ? '' :
|
||||
'(复用文本模型)' }}</span>
|
||||
<span>语音:{{ form.ai.audioModel || '未配置' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="sectionAlerts('ai').length" class="section-alerts">
|
||||
<div v-for="alert in sectionAlerts('ai')" :key="alert" class="section-alert">{{ alert }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 万川平台配置卡片 -->
|
||||
<div class="config-subsection wanchuan-platform-card">
|
||||
<div class="config-subsection-header">
|
||||
<strong>万川平台</strong>
|
||||
<span>从万川 AI 平台自动获取模型配置</span>
|
||||
</div>
|
||||
<div class="form-grid wanchuan-form-row">
|
||||
<label>
|
||||
<span>平台地址</span>
|
||||
<input v-model="platformForm.baseUrl" placeholder="https://platform.example.com">
|
||||
</label>
|
||||
<label>
|
||||
<span>账号</span>
|
||||
<input v-model="platformForm.username" placeholder="用户名">
|
||||
</label>
|
||||
<label>
|
||||
<span>密码</span>
|
||||
<input v-model="platformForm.password" type="password" placeholder="密码">
|
||||
</label>
|
||||
<div class="wanchuan-actions">
|
||||
<!-- 禁用统一走全局 busy(与「刷新联系人」等按钮同范式:busy || 本按钮字段守卫),
|
||||
登录时 loginAndFetchModels 已置 busy=true;platformBusy 仅保留用于本按钮「处理中…」文案,
|
||||
避免别处操作占用 busy 时这里误显「处理中…」。 -->
|
||||
<button class="ghost-btn" @click="loginAndFetchModels"
|
||||
:disabled="busy || !platformForm.baseUrl || !platformForm.username || !platformForm.password">
|
||||
{{ platformBusy ? '处理中...' : '登录并获取模型' }}
|
||||
</button>
|
||||
<button class="ghost-btn" @click="resetPlatformConfig"
|
||||
:disabled="busy || !platformForm.baseUrl || !platformForm.username || !platformForm.password">重新获取模型</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="platformMessage" class="inline-message" :class="platformMessageType">{{ platformMessage }}</div>
|
||||
</div>
|
||||
|
||||
<div class="ai-config-groups">
|
||||
<div class="config-subsection">
|
||||
<div class="config-subsection-header">
|
||||
@@ -342,6 +374,17 @@
|
||||
用于文本向量化,例如:text-embedding-v4, text-embedding-v3
|
||||
</small>
|
||||
</label>
|
||||
<label>
|
||||
<span>Embedding Base URL</span>
|
||||
<input v-model="form.retrieval.embeddingBaseUrl" placeholder="留空则复用文本模型网关">
|
||||
<small style="color: #666; font-size: 12px; margin-top: 4px; display: block;">
|
||||
向量模型独立网关,留空回退到「文本回复」的 Base URL
|
||||
</small>
|
||||
</label>
|
||||
<label>
|
||||
<span>Embedding API Key</span>
|
||||
<input v-model="form.retrieval.embeddingApiKey" type="password" placeholder="留空则复用文本模型密钥">
|
||||
</label>
|
||||
<label>
|
||||
<span>Embedding 维度</span>
|
||||
<input type="number" v-model.number="form.retrieval.embeddingDimensions" min="128">
|
||||
@@ -353,6 +396,17 @@
|
||||
用于结果重排序,例如:qwen3-rerank, gte-rerank-v2
|
||||
</small>
|
||||
</label>
|
||||
<label>
|
||||
<span>Rerank Base URL</span>
|
||||
<input v-model="form.retrieval.rerankBaseUrl" placeholder="留空则复用文本模型网关">
|
||||
<small style="color: #666; font-size: 12px; margin-top: 4px; display: block;">
|
||||
重排模型独立网关,留空回退到「文本回复」的 Base URL
|
||||
</small>
|
||||
</label>
|
||||
<label>
|
||||
<span>Rerank API Key</span>
|
||||
<input v-model="form.retrieval.rerankApiKey" type="password" placeholder="留空则复用文本模型密钥">
|
||||
</label>
|
||||
<label>
|
||||
<span>召回 TopK</span>
|
||||
<input type="number" v-model.number="form.retrieval.recallTopK" min="1">
|
||||
@@ -425,24 +479,13 @@
|
||||
<label>
|
||||
<span>人工同事</span>
|
||||
<div class="handoff-human-combobox">
|
||||
<input
|
||||
v-model="handoffHumanQuery"
|
||||
class="handoff-human-search"
|
||||
placeholder="输入姓名、拼音、userID 模糊搜索"
|
||||
@focus="startHandoffHumanEditing"
|
||||
@input="handleHandoffHumanInput"
|
||||
@keydown.enter.prevent="previewFirstHandoffHuman"
|
||||
@keydown.esc.prevent="closeHandoffHumanDropdown"
|
||||
>
|
||||
<input v-model="handoffHumanQuery" class="handoff-human-search" placeholder="输入姓名、拼音、userID 模糊搜索"
|
||||
@focus="startHandoffHumanEditing" @input="handleHandoffHumanInput"
|
||||
@keydown.enter.prevent="previewFirstHandoffHuman" @keydown.esc.prevent="closeHandoffHumanDropdown">
|
||||
<div v-if="handoffHumanDropdownOpen" class="handoff-human-dropdown">
|
||||
<button
|
||||
v-for="item in handoffHumanOptions"
|
||||
:key="item.userId"
|
||||
type="button"
|
||||
class="handoff-human-option"
|
||||
<button v-for="item in handoffHumanOptions" :key="item.userId" type="button" class="handoff-human-option"
|
||||
:class="{ selected: pendingHandoffHumanId === item.userId }"
|
||||
@mousedown.prevent="previewHandoffHuman(item)"
|
||||
>
|
||||
@mousedown.prevent="previewHandoffHuman(item)">
|
||||
<strong>{{ identityOptionName(item) }}</strong>
|
||||
<span>{{ item.userId }}</span>
|
||||
<small>{{ identitySourceLabel(item.source) }}</small>
|
||||
@@ -453,7 +496,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="select-action-row">
|
||||
<button type="button" class="mini-btn" @click="confirmHandoffHuman" :disabled="!canConfirmHandoffHuman">确认选择</button>
|
||||
<button type="button" class="mini-btn" @click="confirmHandoffHuman"
|
||||
:disabled="!canConfirmHandoffHuman">确认选择</button>
|
||||
<button type="button" class="mini-btn" @click="clearHandoffHuman">清空</button>
|
||||
</div>
|
||||
<small class="field-hint">{{ selectedHandoffHumanSummary }}</small>
|
||||
@@ -480,7 +524,8 @@
|
||||
</label>
|
||||
<label class="wide">
|
||||
<span>通知模板</span>
|
||||
<textarea v-model="form.handoff.messageTemplate" rows="8" :placeholder="defaultHandoffTemplateHint"></textarea>
|
||||
<textarea v-model="form.handoff.messageTemplate" rows="8"
|
||||
:placeholder="defaultHandoffTemplateHint"></textarea>
|
||||
<small class="field-hint">留空时会按私聊/群聊自动使用上面的默认模板;填写后则使用你自定义的模板。</small>
|
||||
</label>
|
||||
</div>
|
||||
@@ -499,7 +544,8 @@
|
||||
<div class="section-meta">
|
||||
<span>内部 {{ status.internalContactCount || 0 }}</span>
|
||||
<span>外部 {{ status.externalContactCount || 0 }}</span>
|
||||
<span>缓存 {{ status.identityInitializing ? '初始化中' : (status.identityRefreshing ? '刷新中' : formatUnixTime(status.identityLastRefreshAt)) }}</span>
|
||||
<span>缓存 {{ status.identityInitializing ? '初始化中' : (status.identityRefreshing ? '刷新中' :
|
||||
formatUnixTime(status.identityLastRefreshAt)) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="sectionAlerts('identity').length" class="section-alerts">
|
||||
@@ -550,20 +596,24 @@
|
||||
<input v-model="internalGroupQuery" placeholder="搜索总群名称或 conversationId">
|
||||
<select v-model="selectedInternalGroupId" :disabled="internalGroupFilteredOptions.length === 0">
|
||||
<option value="">{{ groupSelectHint() }}</option>
|
||||
<option v-for="item in internalGroupFilteredOptions" :key="item.conversationId" :value="item.conversationId">
|
||||
<option v-for="item in internalGroupFilteredOptions" :key="item.conversationId"
|
||||
:value="item.conversationId">
|
||||
{{ groupOptionLabel(item) }}
|
||||
</option>
|
||||
</select>
|
||||
<button type="button" class="ghost-btn" @click="addSelectedInternalGroup" :disabled="!canAddInternalGroup">添加</button>
|
||||
<button type="button" class="ghost-btn" @click="addSelectedInternalGroup"
|
||||
:disabled="!canAddInternalGroup">添加</button>
|
||||
</div>
|
||||
<div v-if="groupOptions.length === 0" class="field-hint">先点击“刷新群列表”,再选择企业总群;未选择总群时只同步企微联系人列表。</div>
|
||||
<div v-if="internalSelectedGroups.length" class="identity-selected-list">
|
||||
<div v-for="item in internalSelectedGroups" :key="item.conversationId" class="identity-selected-row group-selected-row">
|
||||
<div v-for="item in internalSelectedGroups" :key="item.conversationId"
|
||||
class="identity-selected-row group-selected-row">
|
||||
<strong>{{ item.name || '未命名群聊' }}</strong>
|
||||
<span class="identity-user-id">{{ item.conversationId }}</span>
|
||||
<span>{{ item.source || '群列表' }}</span>
|
||||
<span></span>
|
||||
<button type="button" class="ghost-btn small-btn" @click="removeInternalGroup(item.conversationId)">删除</button>
|
||||
<button type="button" class="ghost-btn small-btn"
|
||||
@click="removeInternalGroup(item.conversationId)">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="identity-empty">暂无内部成员同步群</div>
|
||||
@@ -624,23 +674,21 @@
|
||||
{{ identityOptionLabel(item) }}
|
||||
</option>
|
||||
</select>
|
||||
<button type="button" class="ghost-btn" @click="addSelectedIdentity('internal')" :disabled="!canAddIdentity('internal')">添加</button>
|
||||
<button type="button" class="ghost-btn" @click="addSelectedIdentity('internal')"
|
||||
:disabled="!canAddIdentity('internal')">添加</button>
|
||||
</div>
|
||||
<div v-if="internalSelectedContacts.length" class="identity-selected-list">
|
||||
<div v-for="item in internalSelectedContacts" :key="item.userId" class="identity-selected-row">
|
||||
<strong>{{ item.name || '未填写名称' }}</strong>
|
||||
<span class="identity-user-id">{{ item.userId }}</span>
|
||||
<span>{{ identitySourceLabel(item.source) }}</span>
|
||||
<input
|
||||
v-if="item.needsLabel"
|
||||
:value="identityManualLabelDraft('internal', item.userId)"
|
||||
placeholder="补充名称"
|
||||
@input="setIdentityLabelDraft('internal', item.userId, $event.target.value)"
|
||||
<input v-if="item.needsLabel" :value="identityManualLabelDraft('internal', item.userId)"
|
||||
placeholder="补充名称" @input="setIdentityLabelDraft('internal', item.userId, $event.target.value)"
|
||||
@blur="commitIdentityLabelDraft('internal', item.userId)"
|
||||
@keydown.enter.prevent="commitIdentityLabelDraft('internal', item.userId)"
|
||||
>
|
||||
@keydown.enter.prevent="commitIdentityLabelDraft('internal', item.userId)">
|
||||
<span v-else></span>
|
||||
<button type="button" class="ghost-btn small-btn" @click="removeIdentity('internal', item.userId)">删除</button>
|
||||
<button type="button" class="ghost-btn small-btn"
|
||||
@click="removeIdentity('internal', item.userId)">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="identity-empty">暂无手动内部员工</div>
|
||||
@@ -658,30 +706,29 @@
|
||||
{{ identityOptionLabel(item) }}
|
||||
</option>
|
||||
</select>
|
||||
<button type="button" class="ghost-btn" @click="addSelectedIdentity('external')" :disabled="!canAddIdentity('external')">添加</button>
|
||||
<button type="button" class="ghost-btn" @click="addSelectedIdentity('external')"
|
||||
:disabled="!canAddIdentity('external')">添加</button>
|
||||
</div>
|
||||
<div v-if="externalSelectedContacts.length" class="identity-selected-list">
|
||||
<div v-for="item in externalSelectedContacts" :key="item.userId" class="identity-selected-row">
|
||||
<strong>{{ item.name || '未填写名称' }}</strong>
|
||||
<span class="identity-user-id">{{ item.userId }}</span>
|
||||
<span>{{ identitySourceLabel(item.source) }}</span>
|
||||
<input
|
||||
v-if="item.needsLabel"
|
||||
:value="identityManualLabelDraft('external', item.userId)"
|
||||
placeholder="补充名称"
|
||||
@input="setIdentityLabelDraft('external', item.userId, $event.target.value)"
|
||||
<input v-if="item.needsLabel" :value="identityManualLabelDraft('external', item.userId)"
|
||||
placeholder="补充名称" @input="setIdentityLabelDraft('external', item.userId, $event.target.value)"
|
||||
@blur="commitIdentityLabelDraft('external', item.userId)"
|
||||
@keydown.enter.prevent="commitIdentityLabelDraft('external', item.userId)"
|
||||
>
|
||||
@keydown.enter.prevent="commitIdentityLabelDraft('external', item.userId)">
|
||||
<span v-else></span>
|
||||
<button type="button" class="ghost-btn small-btn" @click="removeIdentity('external', item.userId)">删除</button>
|
||||
<button type="button" class="ghost-btn small-btn"
|
||||
@click="removeIdentity('external', item.userId)">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="identity-empty">暂无手动外部客户</div>
|
||||
</div>
|
||||
<label>
|
||||
<span>批量粘贴内部员工 ID</span>
|
||||
<textarea v-model="internalUserIdsText" rows="3" placeholder="每行一个 user_id,例如 1688855899845302"></textarea>
|
||||
<textarea v-model="internalUserIdsText" rows="3"
|
||||
placeholder="每行一个 user_id,例如 1688855899845302"></textarea>
|
||||
</label>
|
||||
<label>
|
||||
<span>批量粘贴外部客户 ID</span>
|
||||
@@ -705,7 +752,8 @@
|
||||
<button class="ghost-btn" @click="saveConfig('identity')" :disabled="busy">保存配置</button>
|
||||
<button class="ghost-btn" @click="refreshContacts" :disabled="busy || status.identityRefreshing">刷新联系人</button>
|
||||
<button class="ghost-btn" @click="refreshGroups" :disabled="busy">刷新群列表</button>
|
||||
<button class="ghost-btn" @click="syncInternalGroups" :disabled="busy || status.identityRefreshing || !form.identity.internalGroupConversationIds.length">同步内部成员群</button>
|
||||
<button class="ghost-btn" @click="syncInternalGroups"
|
||||
:disabled="busy || status.identityRefreshing || !form.identity.internalGroupConversationIds.length">同步内部成员群</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -756,11 +804,15 @@
|
||||
<div v-if="!status.lastMessages || status.lastMessages.length === 0" class="empty">暂无处理记录</div>
|
||||
</div>
|
||||
<div v-if="recordTotalCount > 0" class="record-pagination">
|
||||
<button class="ghost-btn small-btn" type="button" @click="goRecordPage(1)" :disabled="recordCurrentPage <= 1">首页</button>
|
||||
<button class="ghost-btn small-btn" type="button" @click="prevRecordPage" :disabled="recordCurrentPage <= 1">上一页</button>
|
||||
<button class="ghost-btn small-btn" type="button" @click="goRecordPage(1)"
|
||||
:disabled="recordCurrentPage <= 1">首页</button>
|
||||
<button class="ghost-btn small-btn" type="button" @click="prevRecordPage"
|
||||
:disabled="recordCurrentPage <= 1">上一页</button>
|
||||
<span>{{ recordPageSummary }},显示 {{ recordStartIndex }}-{{ recordEndIndex }} / {{ recordTotalCount }}</span>
|
||||
<button class="ghost-btn small-btn" type="button" @click="nextRecordPage" :disabled="recordCurrentPage >= recordTotalPages">下一页</button>
|
||||
<button class="ghost-btn small-btn" type="button" @click="goRecordPage(recordTotalPages)" :disabled="recordCurrentPage >= recordTotalPages">末页</button>
|
||||
<button class="ghost-btn small-btn" type="button" @click="nextRecordPage"
|
||||
:disabled="recordCurrentPage >= recordTotalPages">下一页</button>
|
||||
<button class="ghost-btn small-btn" type="button" @click="goRecordPage(recordTotalPages)"
|
||||
:disabled="recordCurrentPage >= recordTotalPages">末页</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -783,7 +835,11 @@ import {
|
||||
TestAIConnection,
|
||||
TestHumanHandoff,
|
||||
SendWxWorkData,
|
||||
LogFrontend
|
||||
LogFrontend,
|
||||
GetPlatformConfig,
|
||||
SavePlatformConfig,
|
||||
WanchuanLogin,
|
||||
WanchuanGetModel
|
||||
} from '../../wailsjs/go/main/App.js'
|
||||
|
||||
const busy = ref(false)
|
||||
@@ -815,6 +871,16 @@ let identityOptionsKey = ''
|
||||
let currentIdentityScopeKey = ''
|
||||
let timer = null
|
||||
|
||||
// 万川平台相关状态
|
||||
const platformBusy = ref(false)
|
||||
const platformMessage = ref('')
|
||||
const platformMessageType = ref('success')
|
||||
const platformForm = reactive({
|
||||
baseUrl: '',
|
||||
username: '',
|
||||
password: ''
|
||||
})
|
||||
|
||||
const form = reactive(defaultConfig())
|
||||
const sectionNavItems = [
|
||||
{ id: 'listen', label: '监听策略' },
|
||||
@@ -1055,8 +1121,12 @@ function defaultConfig() {
|
||||
retrievalMode: 'hybrid_rerank',
|
||||
embeddingIndexPath: 'config/knowledge/embedding_index.json',
|
||||
embeddingModel: 'text-embedding-v4',
|
||||
embeddingBaseUrl: '',
|
||||
embeddingApiKey: '',
|
||||
embeddingDimensions: 512,
|
||||
rerankModel: 'qwen3-rerank',
|
||||
rerankBaseUrl: '',
|
||||
rerankApiKey: '',
|
||||
recallTopK: 50,
|
||||
rerankTopK: 30,
|
||||
finalTopK: 8
|
||||
@@ -1397,15 +1467,15 @@ function filterHandoffHumanOptions(options, query) {
|
||||
}
|
||||
|
||||
function handoffHumanMatchScore(item, query, selectedId) {
|
||||
const userId = String(item.userId || '').trim()
|
||||
const userId = String(item.userId || '').trim()
|
||||
if (!userId || isRobotUserId(userId)) return -1
|
||||
let score = userId === selectedId ? 20 : 0
|
||||
if (!query) return score
|
||||
const name = String(item.name || '').toLowerCase()
|
||||
const source = String(item.source || '').toLowerCase()
|
||||
const accountName = String(item.sourceAccountName || '').toLowerCase()
|
||||
const accountUserId = String(item.sourceAccountUserId || '').toLowerCase()
|
||||
const clientId = String(item.clientId || '').toLowerCase()
|
||||
const name = String(item.name || '').toLowerCase()
|
||||
const source = String(item.source || '').toLowerCase()
|
||||
const accountName = String(item.sourceAccountName || '').toLowerCase()
|
||||
const accountUserId = String(item.sourceAccountUserId || '').toLowerCase()
|
||||
const clientId = String(item.clientId || '').toLowerCase()
|
||||
const userIdLower = userId.toLowerCase()
|
||||
const fields = [name, userIdLower, source, accountName, accountUserId, clientId].filter(Boolean)
|
||||
if (name === query || userIdLower === query) return score + 100
|
||||
@@ -1911,14 +1981,29 @@ function isLikelyTextOnlyQwenModel(model) {
|
||||
['turbo', 'plus', 'max', 'long', 'coder', 'math', 'instruct'].some(token => name.includes(token))
|
||||
}
|
||||
|
||||
function isDashScopeGateway(url) {
|
||||
return String(url || '').toLowerCase().includes('dashscope.aliyuncs.com')
|
||||
}
|
||||
|
||||
function normalizeVisionModelConfig() {
|
||||
if (!form.ai) return
|
||||
const visionModel = String(form.ai.visionModel || '').trim()
|
||||
const textModel = String(form.ai.model || '').trim()
|
||||
if (!visionModel ||
|
||||
(visionModel.toLowerCase() === textModel.toLowerCase() && !isVisionCapableModelName(visionModel)) ||
|
||||
isLikelyTextOnlyQwenModel(visionModel)) {
|
||||
form.ai.visionModel = defaultVisionModel
|
||||
const visionGateway = String(form.ai.visionBaseUrl || '').trim() || String(form.ai.baseUrl || '').trim()
|
||||
if (isDashScopeGateway(visionGateway)) {
|
||||
// DashScope 网关:空/文本模型一律回退到专用视觉模型 qwen3-vl-plus
|
||||
if (!visionModel ||
|
||||
(visionModel.toLowerCase() === textModel.toLowerCase() && !isVisionCapableModelName(visionModel)) ||
|
||||
isLikelyTextOnlyQwenModel(visionModel)) {
|
||||
form.ai.visionModel = defaultVisionModel
|
||||
}
|
||||
} else if (!String(form.ai.visionBaseUrl || '').trim()) {
|
||||
// 非 DashScope 且无独立视觉网关(如万川统一网关):
|
||||
// 视觉模型与聊天模型相同时清空,让后端 fallbackString(visionModel, model) 动态复用聊天模型,
|
||||
// 这样改聊天模型时视觉会自动跟随,不会锁死在旧值。仅保留用户在同网关上显式填的不同视觉模型。
|
||||
if (visionModel.toLowerCase() === textModel.toLowerCase()) {
|
||||
form.ai.visionModel = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1933,7 +2018,9 @@ function normalizeAIConfigBeforeSave() {
|
||||
if (!form.ai.audioProvider) form.ai.audioProvider = 'auto'
|
||||
if (!form.ai.audioMode) form.ai.audioMode = 'openai_audio_chat'
|
||||
if (!form.ai.audioModel) form.ai.audioModel = 'qwen3-asr-flash'
|
||||
if (!form.ai.audioBaseUrl) form.ai.audioBaseUrl = 'https://dashscope.aliyuncs.com/compatible-mode/v1'
|
||||
// 语音网关与 vision/embedding/rerank 一致:audioBaseUrl 留空时不强写 DashScope,
|
||||
// 运行期由后端 audioRequestConfig 回退复用聊天网关。否则非 DashScope(如万川)会出现
|
||||
// 「DashScope URL + 万川聊天 Key」的错配导致鉴权失败。用户填了独立语音网关则保留其值。
|
||||
if (!['concise', 'medium', 'detailed'].includes(form.ai.replyDetail)) form.ai.replyDetail = 'detailed'
|
||||
if (looksLikeUrl(form.ai.audioApiKey)) {
|
||||
form.ai.audioApiKey = ''
|
||||
@@ -1969,6 +2056,267 @@ async function loadConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
// 加载万川平台配置
|
||||
async function loadPlatformConfig() {
|
||||
try {
|
||||
const cfg = await GetPlatformConfig()
|
||||
if (cfg) {
|
||||
platformForm.baseUrl = cfg.baseUrl || ''
|
||||
platformForm.username = cfg.username || ''
|
||||
platformForm.password = cfg.password || ''
|
||||
}
|
||||
} catch (err) {
|
||||
LogFrontend('warn', `加载平台配置失败: ${err.message || err}`)
|
||||
}
|
||||
}
|
||||
|
||||
// 万川平台模型编码(平台按用途用不同 code 区分)
|
||||
// 知识库三件套(chat/embedding/rerank)注册在 preSaleInSaleAfterSale- 业务前缀下;
|
||||
// vision/voice 是平台系统级共享模型,用裸 code(带前缀的 preSaleInSaleAfterSale-voice 平台会返回 500「功能模型不存在」)。
|
||||
// 一切以平台 getByCode 实际返回为准。
|
||||
const WANCHUAN_CODE_CHAT = 'preSaleInSaleAfterSale-chat' // 聊天/文本模型 → form.ai
|
||||
const WANCHUAN_CODE_EMBEDDING = 'preSaleInSaleAfterSale-text-embedding' // 向量模型 → form.retrieval.embeddingModel
|
||||
const WANCHUAN_CODE_RERANK = 'preSaleInSaleAfterSale-rerank' // 重排模型 → form.retrieval.rerankModel
|
||||
const WANCHUAN_CODE_VOICE = 'voice' // 语音模型(ASR) → form.ai.audio*(裸 code,paraformer-v2)
|
||||
// 图片识别模型 → form.ai.vision*。平台把视觉模型注册在裸 code「vision」下(functionName=售前售后视觉),
|
||||
// 带独立 endpointUrl/apiKey,与聊天网关不同,必须按此 code 拉取并回填,不能复用聊天模型。
|
||||
// 仍按「可选」处理:万一平台某天下线该 code,自动回退运行时复用聊天模型,不中断主流程。
|
||||
const WANCHUAN_CODE_VISION = 'vision'
|
||||
|
||||
// 拉取一个「可选」模型配置:失败/平台无此 code 时返回 null,不中断主流程。
|
||||
// code 为空表示该用途万川暂未提供独立模型,直接跳过(运行时由后端回退复用聊天网关)。
|
||||
async function fetchOptionalWanchuanModel(code, label, token) {
|
||||
if (!code) {
|
||||
LogFrontend('info', `[万川平台] 未配置 ${label} code,跳过(运行时回退复用聊天网关)`)
|
||||
return null
|
||||
}
|
||||
try {
|
||||
LogFrontend('info', `[万川平台] 开始获取 ${label} 模型配置 (${code})`)
|
||||
const response = await WanchuanGetModel(platformForm.baseUrl, code, token)
|
||||
const model = parseModelConfig(JSON.parse(response), code)
|
||||
if (!model || !model.modelName) return null
|
||||
LogFrontend('info', `[万川平台] ${label} 解析结果 - modelName: ${model.modelName}, endpointUrl: ${model.endpointUrl}, apiKey 长度: ${model.apiKey?.length || 0}`)
|
||||
return model
|
||||
} catch (err) {
|
||||
LogFrontend('warn', `[万川平台] ${label} 模型获取失败(跳过,运行时回退复用聊天网关): ${err.message || err}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 登录并获取模型配置
|
||||
async function loginAndFetchModels() {
|
||||
platformBusy.value = true
|
||||
// 同时锁住全局 busy,避免登录拉模型的数秒内用户点别处「保存配置」等按钮,
|
||||
// 与本函数内部的 saveConfig 并发读写同一个 form。saveConfig 的 ownsBusy 机制会识别
|
||||
// busy 已被外层置位而不重复开关,逻辑自洽。
|
||||
busy.value = true
|
||||
platformMessage.value = ''
|
||||
platformMessageType.value = 'success'
|
||||
|
||||
try {
|
||||
// 记录回填前的 embedding 模型/维度,回填保存后若发生变化需重建向量索引(旧向量属于不同向量空间)
|
||||
const prevEmbeddingModel = String(form.retrieval?.embeddingModel || '').trim()
|
||||
const prevEmbeddingDims = Number(form.retrieval?.embeddingDimensions || 0)
|
||||
|
||||
// 1. 先保存凭证
|
||||
// Wails 把 Go 的 (bool, string) 多返回值序列化的形态不固定:可能是数组、标量 true 或对象,需归一化判断
|
||||
const saveResult = await SavePlatformConfig(JSON.stringify(platformForm))
|
||||
const saveOk = Array.isArray(saveResult) ? saveResult[0] : Boolean(saveResult)
|
||||
const saveMsg = Array.isArray(saveResult) ? saveResult[1] : ''
|
||||
if (!saveOk) {
|
||||
throw new Error(saveMsg || '保存平台配置失败')
|
||||
}
|
||||
|
||||
// 2. 登录获取 token
|
||||
LogFrontend('info', `[万川平台] 开始登录 - URL: ${platformForm.baseUrl}`)
|
||||
const loginResponse = await WanchuanLogin(platformForm.baseUrl, platformForm.username, platformForm.password)
|
||||
const loginData = JSON.parse(loginResponse)
|
||||
|
||||
LogFrontend('info', `[万川平台] 登录响应: ${JSON.stringify(loginData)}`)
|
||||
|
||||
// 多路径提取 token
|
||||
const token = loginData.token ||
|
||||
loginData.data?.token ||
|
||||
loginData.data?.access_token ||
|
||||
loginData.access_token
|
||||
|
||||
if (!token) {
|
||||
throw new Error('登录失败:未获取到 token')
|
||||
}
|
||||
|
||||
LogFrontend('info', `[万川平台] 登录成功,token 长度: ${token.length}`)
|
||||
|
||||
// 3. 获取 chat 模型(必拉)→ 回填 form.ai(baseUrl/apiKey/model 作为统一网关)
|
||||
LogFrontend('info', `[万川平台] 开始获取 chat 模型配置 (${WANCHUAN_CODE_CHAT})`)
|
||||
const chatResponse = await WanchuanGetModel(platformForm.baseUrl, WANCHUAN_CODE_CHAT, token)
|
||||
const chatData = JSON.parse(chatResponse)
|
||||
LogFrontend('info', `[万川平台] chat 响应: ${JSON.stringify(chatData)}`)
|
||||
|
||||
const chatModel = parseModelConfig(chatData, WANCHUAN_CODE_CHAT)
|
||||
if (!chatModel || !chatModel.modelName) {
|
||||
throw new Error('获取聊天模型配置失败')
|
||||
}
|
||||
LogFrontend('info', `[万川平台] chat 解析结果 - modelName: ${chatModel.modelName}, endpointUrl: ${chatModel.endpointUrl}, apiKey 长度: ${chatModel.apiKey?.length || 0}`)
|
||||
|
||||
// 回填聊天模型(网关地址 + 密钥 + 模型名)
|
||||
if (chatModel.endpointUrl) form.ai.baseUrl = chatModel.endpointUrl
|
||||
if (chatModel.apiKey) form.ai.apiKey = chatModel.apiKey
|
||||
form.ai.model = chatModel.modelName
|
||||
|
||||
// 图片识别模型先置空,再按 vision code 拉取覆盖:
|
||||
// 平台 vision 是独立模型(独立 endpointUrl/apiKey),下面 helper 拉到就显式回填。
|
||||
// 万一平台下线该 code(helper 返回 null),保持留空,后端 visionRequestConfig 用
|
||||
// fallbackString(visionModel, model) 运行时复用聊天模型,不锁死旧值。
|
||||
form.ai.visionBaseUrl = ''
|
||||
form.ai.visionApiKey = ''
|
||||
form.ai.visionModel = ''
|
||||
|
||||
// 3.5 获取 vision 模型(独立 code「vision」,拉到则回填独立网关;拉不到回退复用 chat)→ 回填 form.ai.vision*
|
||||
const visionModel = await fetchOptionalWanchuanModel(WANCHUAN_CODE_VISION, 'vision', token)
|
||||
if (visionModel) {
|
||||
form.ai.visionModel = visionModel.modelName
|
||||
form.ai.visionBaseUrl = visionModel.endpointUrl || ''
|
||||
form.ai.visionApiKey = visionModel.apiKey || ''
|
||||
} else {
|
||||
LogFrontend('info', `[万川平台] 未获取到独立视觉模型,图片识别运行时复用聊天模型: ${chatModel.modelName}`)
|
||||
}
|
||||
|
||||
// 4. 获取 text-embedding 模型(可选)→ 回填 form.retrieval(模型名 + 独立 url/key)
|
||||
const embeddingModel = await fetchOptionalWanchuanModel(WANCHUAN_CODE_EMBEDDING, 'embedding', token)
|
||||
if (embeddingModel) {
|
||||
form.retrieval.embeddingModel = embeddingModel.modelName
|
||||
form.retrieval.embeddingBaseUrl = embeddingModel.endpointUrl || ''
|
||||
form.retrieval.embeddingApiKey = embeddingModel.apiKey || ''
|
||||
}
|
||||
|
||||
// 5. 获取 rerank 模型(可选)→ 回填 form.retrieval(模型名 + 独立 url/key)
|
||||
const rerankModel = await fetchOptionalWanchuanModel(WANCHUAN_CODE_RERANK, 'rerank', token)
|
||||
if (rerankModel) {
|
||||
form.retrieval.rerankModel = rerankModel.modelName
|
||||
form.retrieval.rerankBaseUrl = rerankModel.endpointUrl || ''
|
||||
form.retrieval.rerankApiKey = rerankModel.apiKey || ''
|
||||
}
|
||||
|
||||
// 6. 获取 voice 模型(可选)→ 回填 form.ai.audio*(独立 url/key/model)
|
||||
// 先清空语音网关覆盖项:拉到了显式回填;拉不到则留空,运行时由后端 audioRequestConfig 回退复用聊天网关,
|
||||
// 避免残留的 DashScope 默认 URL 与万川聊天 Key 错配。(语音模型名仍由 ApplyDefaults 兜底/缺失提示引导手动配)
|
||||
form.ai.audioBaseUrl = ''
|
||||
form.ai.audioApiKey = ''
|
||||
const voiceModel = await fetchOptionalWanchuanModel(WANCHUAN_CODE_VOICE, 'voice', token)
|
||||
if (voiceModel) {
|
||||
form.ai.audioModel = voiceModel.modelName
|
||||
form.ai.audioBaseUrl = voiceModel.endpointUrl || ''
|
||||
form.ai.audioApiKey = voiceModel.apiKey || ''
|
||||
}
|
||||
|
||||
console.log('[万川平台] 模型配置获取完成:')
|
||||
console.log(' chat:', chatModel)
|
||||
console.log(' vision:', visionModel)
|
||||
console.log(' embedding:', embeddingModel)
|
||||
console.log(' rerank:', rerankModel)
|
||||
console.log(' voice:', voiceModel)
|
||||
|
||||
// 7. 落盘并通知 helper 重载(saveConfig 内部调用 SaveAutoReplyConfig → /api/auto-reply/reload)
|
||||
const saved = await saveConfig('ai', true)
|
||||
if (!saved) {
|
||||
throw new Error('模型已获取但保存失败,请检查 AI 配置后重试')
|
||||
}
|
||||
|
||||
const visionLogText = visionModel ? visionModel.modelName : `复用chat(${chatModel.modelName})`
|
||||
LogFrontend('info', `[万川平台] 模型配置已回填并保存 - chat: ${chatModel.modelName}, vision: ${visionLogText}, embedding: ${embeddingModel?.modelName || '无'}, rerank: ${rerankModel?.modelName || '无'}, voice: ${voiceModel?.modelName || '无'}`)
|
||||
|
||||
// 7.1 若 embedding 模型/维度发生变化,磁盘旧向量属于不同向量空间,需重建索引,
|
||||
// 否则向量检索会静默回退关键词。无知识库内容时重建会快速跳过,不影响。
|
||||
let rebuildNote = ''
|
||||
if (embeddingModel && embeddingModel.modelName) {
|
||||
const newEmbeddingModel = String(form.retrieval?.embeddingModel || '').trim()
|
||||
const newEmbeddingDims = Number(form.retrieval?.embeddingDimensions || 0)
|
||||
const embeddingChanged =
|
||||
newEmbeddingModel.toLowerCase() !== prevEmbeddingModel.toLowerCase() ||
|
||||
newEmbeddingDims !== prevEmbeddingDims
|
||||
if (embeddingChanged) {
|
||||
try {
|
||||
LogFrontend('info', `[万川平台] embedding 模型变更(${prevEmbeddingModel || '空'}→${newEmbeddingModel}),开始重建向量索引`)
|
||||
const rebuildResult = await RebuildKnowledgeIndex()
|
||||
if (rebuildResult?.success) {
|
||||
rebuildNote = ',向量索引已重建'
|
||||
LogFrontend('info', '[万川平台] 向量索引重建成功')
|
||||
} else {
|
||||
rebuildNote = `,但向量索引重建失败(请到知识库页手动重建):${rebuildResult?.message || '未知错误'}`
|
||||
LogFrontend('warn', `[万川平台] 向量索引重建失败: ${rebuildResult?.message || '未知错误'}`)
|
||||
}
|
||||
await loadStatus()
|
||||
} catch (err) {
|
||||
rebuildNote = `,但向量索引重建异常(请到知识库页手动重建):${err.message || err}`
|
||||
LogFrontend('warn', `[万川平台] 向量索引重建异常: ${err.message || err}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 提示仍需手动配置的模型(平台未提供对应 code)
|
||||
const missing = []
|
||||
if (!embeddingModel) missing.push('向量(embedding)')
|
||||
if (!rerankModel) missing.push('重排(rerank)')
|
||||
if (!voiceModel) missing.push('语音(voice/ASR)')
|
||||
|
||||
platformMessage.value = `获取并保存成功!文本模型: ${chatModel.modelName}` +
|
||||
`${visionModel ? ',图片识别: ' + visionModel.modelName : ',图片识别复用文本模型'}` +
|
||||
`${embeddingModel ? ',向量: ' + embeddingModel.modelName : ''}` +
|
||||
`${rerankModel ? ',重排: ' + rerankModel.modelName : ''}` +
|
||||
`${voiceModel ? ',语音: ' + voiceModel.modelName : ''}` +
|
||||
rebuildNote +
|
||||
`${missing.length ? '。仍需手动配置:' + missing.join('、') : '。全部模型已就绪'}。`
|
||||
platformMessageType.value = rebuildNote.includes('失败') || rebuildNote.includes('异常') ? 'error' : 'success'
|
||||
|
||||
} catch (err) {
|
||||
const errMsg = err.message || String(err)
|
||||
LogFrontend('error', `[万川平台] 获取失败: ${errMsg}`)
|
||||
platformMessage.value = `获取失败: ${errMsg}`
|
||||
platformMessageType.value = 'error'
|
||||
} finally {
|
||||
platformBusy.value = false
|
||||
busy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 解析模型配置
|
||||
function parseModelConfig(data, code) {
|
||||
try {
|
||||
if (!data || !data.data || !data.data.providerModels) {
|
||||
throw new Error(`平台未返回模型[${code}]配置`)
|
||||
}
|
||||
|
||||
const pm = data.data.providerModels
|
||||
const modelName = pm.modelName
|
||||
|
||||
// encryptedConfig 是 JSON 字符串,需要二次解析
|
||||
let config = {}
|
||||
if (pm.encryptedConfig) {
|
||||
config = JSON.parse(pm.encryptedConfig)
|
||||
}
|
||||
|
||||
return {
|
||||
modelName: modelName,
|
||||
apiKey: config.apiKey || '',
|
||||
endpointUrl: config.endpointUrl || ''
|
||||
}
|
||||
} catch (err) {
|
||||
LogFrontend('error', `[万川平台] 解析模型[${code}]失败: ${err.message || err}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 重置:重新从平台拉取模型并覆盖回填(用于手动改过模型字段后恢复平台值)
|
||||
async function resetPlatformConfig() {
|
||||
if (!platformForm.baseUrl || !platformForm.username || !platformForm.password) {
|
||||
platformMessage.value = '请先填写平台地址、账号和密码'
|
||||
platformMessageType.value = 'error'
|
||||
return
|
||||
}
|
||||
LogFrontend('info', '[万川平台] 重置:重新登录并拉取模型覆盖回填')
|
||||
await loginAndFetchModels()
|
||||
}
|
||||
|
||||
async function loadStatus() {
|
||||
try {
|
||||
const result = await GetAutoReplyStatus()
|
||||
@@ -2305,6 +2653,7 @@ function scrollToSection(id) {
|
||||
|
||||
onMounted(async () => {
|
||||
await loadConfig()
|
||||
await loadPlatformConfig()
|
||||
await loadStatus()
|
||||
await loadIdentityOptions(true)
|
||||
await loadGroupOptions(true)
|
||||
@@ -2579,6 +2928,45 @@ button:disabled {
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.wanchuan-platform-card {
|
||||
background: #fff8e1;
|
||||
border-color: #ffd54f;
|
||||
}
|
||||
|
||||
.wanchuan-platform-card .config-subsection-header strong {
|
||||
color: #f57c00;
|
||||
}
|
||||
|
||||
/* 平台行:三个输入框 + 按钮组同排,按钮靠右并与输入框底部对齐 */
|
||||
.wanchuan-form-row {
|
||||
grid-template-columns: minmax(220px, 1.4fr) repeat(2, minmax(140px, 1fr)) auto;
|
||||
align-items: end;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.wanchuan-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wanchuan-actions .ghost-btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 窄屏回退:列数不够时按钮组换行但仍靠右 */
|
||||
@media (max-width: 720px) {
|
||||
.wanchuan-form-row {
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
}
|
||||
|
||||
.wanchuan-actions {
|
||||
grid-column: 1 / -1;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.advanced-toggle {
|
||||
margin-top: 12px;
|
||||
min-height: 32px;
|
||||
|
||||
8
frontend/wailsjs/go/main/App.d.ts
vendored
8
frontend/wailsjs/go/main/App.d.ts
vendored
@@ -46,6 +46,8 @@ export function GetKingdeeMonitorStatus():Promise<any>;
|
||||
|
||||
export function GetPendingAfterSalesArchiveSummary():Promise<any>;
|
||||
|
||||
export function GetPlatformConfig():Promise<any>;
|
||||
|
||||
export function GetSystemMemoryUsage():Promise<number>;
|
||||
|
||||
export function GetWxWorkAccountList():Promise<any>;
|
||||
@@ -90,6 +92,8 @@ export function SaveIssue(arg1:main.AfterSalesIssue):Promise<boolean|string>;
|
||||
|
||||
export function SaveKingdeeMonitorConfig(arg1:string):Promise<boolean|string>;
|
||||
|
||||
export function SavePlatformConfig(arg1:string):Promise<boolean|string>;
|
||||
|
||||
export function SendWxWorkData(arg1:string,arg2:string):Promise<boolean>;
|
||||
|
||||
export function SetAutoCollectTask(arg1:boolean):Promise<boolean|string>;
|
||||
@@ -113,3 +117,7 @@ export function TestKingdeeMonitorConnection(arg1:string):Promise<any>;
|
||||
export function TriggerManualCollect(arg1:string):Promise<boolean|string>;
|
||||
|
||||
export function UpdateAfterSalesKnowledgeCase(arg1:string,arg2:string):Promise<any>;
|
||||
|
||||
export function WanchuanGetModel(arg1:string,arg2:string,arg3:string):Promise<string>;
|
||||
|
||||
export function WanchuanLogin(arg1:string,arg2:string,arg3:string):Promise<string>;
|
||||
|
||||
@@ -90,6 +90,10 @@ export function GetPendingAfterSalesArchiveSummary() {
|
||||
return window['go']['main']['App']['GetPendingAfterSalesArchiveSummary']();
|
||||
}
|
||||
|
||||
export function GetPlatformConfig() {
|
||||
return window['go']['main']['App']['GetPlatformConfig']();
|
||||
}
|
||||
|
||||
export function GetSystemMemoryUsage() {
|
||||
return window['go']['main']['App']['GetSystemMemoryUsage']();
|
||||
}
|
||||
@@ -178,6 +182,10 @@ export function SaveKingdeeMonitorConfig(arg1) {
|
||||
return window['go']['main']['App']['SaveKingdeeMonitorConfig'](arg1);
|
||||
}
|
||||
|
||||
export function SavePlatformConfig(arg1) {
|
||||
return window['go']['main']['App']['SavePlatformConfig'](arg1);
|
||||
}
|
||||
|
||||
export function SendWxWorkData(arg1, arg2) {
|
||||
return window['go']['main']['App']['SendWxWorkData'](arg1, arg2);
|
||||
}
|
||||
@@ -225,3 +233,11 @@ export function TriggerManualCollect(arg1) {
|
||||
export function UpdateAfterSalesKnowledgeCase(arg1, arg2) {
|
||||
return window['go']['main']['App']['UpdateAfterSalesKnowledgeCase'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function WanchuanGetModel(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['WanchuanGetModel'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
export function WanchuanLogin(arg1, arg2, arg3) {
|
||||
return window['go']['main']['App']['WanchuanLogin'](arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user