chore:添加 chat_ui_config 支持,优化前端 UI 行为开关;实现附件类型动态控制,确保与后端配置同步
This commit is contained in:
@@ -436,6 +436,43 @@ function resetServiceConfigCache() {
|
||||
_serviceConfigCacheAt = 0;
|
||||
}
|
||||
|
||||
// PR-CHAT-UI-CONFIG (2026-09-20): chat_ui_config 前端 fallback 默认值
|
||||
// 与后端 addon/aikefu/model/Config.php::DEFAULT_UI_CONFIG 字段一一对应
|
||||
// 改任何一项必须同步后端常量, 否则前后端默认分叉
|
||||
//
|
||||
// PR-CHAT-UI-CONFIG-SERVER-ONLY (2026-09-20):
|
||||
// ui_available_models 必须完全来自后端 (Config.php::DEFAULT_UI_CONFIG),
|
||||
// 前端不维护实际清单。仅保留 'Auto' 兜底: 后端 /info 调用失败 / 返回空数组时,
|
||||
// 模型选择器仍可显示 Auto。
|
||||
// 数据源 (后端维护): https://docs.openclaw.ai/providers
|
||||
// 商家可在后台自由增删任意自定义模型名 (自部署/微调), 仅 trim + 去空 + 去重 + 单条 ≤30 字符 + 总数 ≤100。
|
||||
const CHAT_UI_DEFAULTS = {
|
||||
ui_show_thinking_process: true,
|
||||
ui_enable_attachment_types: ['image', 'file', 'audio', 'video'],
|
||||
ui_input_max_length: 500,
|
||||
ui_available_models: ['Auto'],
|
||||
ui_enable_feedback: true
|
||||
};
|
||||
|
||||
// chat_ui_config 自带短 TTL 缓存(60s, 与 service_config 同步);
|
||||
// 不在 _serviceConfigCache 里读是因为 chat 组件可能在 getServiceConfig 还没返回前就调它
|
||||
var _chatUiConfigCache = null;
|
||||
var _chatUiConfigCacheAt = 0;
|
||||
|
||||
function mergeChatUiConfig(remote) {
|
||||
// remote 是 /info 响应里的 chat_ui_config 对象, 缺字段用 defaults 兜底
|
||||
// 注意: 不深拷贝数组 → 后端返回的就是最终数组, 不再覆盖
|
||||
var merged = {};
|
||||
Object.keys(CHAT_UI_DEFAULTS).forEach(function(k) {
|
||||
if (remote && Object.prototype.hasOwnProperty.call(remote, k) && remote[k] !== null && remote[k] !== undefined) {
|
||||
merged[k] = remote[k];
|
||||
} else {
|
||||
merged[k] = CHAT_UI_DEFAULTS[k];
|
||||
}
|
||||
});
|
||||
return merged;
|
||||
}
|
||||
|
||||
export default {
|
||||
/**
|
||||
* 获取uniacid
|
||||
@@ -935,6 +972,35 @@ export default {
|
||||
*/
|
||||
clearServiceConfigCache() {
|
||||
resetServiceConfigCache();
|
||||
_chatUiConfigCache = null;
|
||||
_chatUiConfigCacheAt = 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* PR-CHAT-UI-CONFIG (2026-09-20): 获取前端 UI 行为开关
|
||||
* 5 个 P0 开关由后端 Config::getChatUiConfig() 下发, 字段缺失用前端 CHAT_UI_DEFAULTS 兜底
|
||||
* 缓存策略: 60s TTL, 失败不缓存 (下次重试), 与 getServiceConfig 共享 /info 调用
|
||||
* 入参: force=true 跳过缓存 (用于用户切换站点/账号后强制刷新)
|
||||
*/
|
||||
async getChatUiConfig(force) {
|
||||
const nowTs = Date.now();
|
||||
if (!force && _chatUiConfigCache && (nowTs - _chatUiConfigCacheAt) < SERVICE_CONFIG_CACHE_TTL) {
|
||||
return _chatUiConfigCache;
|
||||
}
|
||||
try {
|
||||
// 复用 getServiceConfig() 触发 /info; 它的缓存会带回 chat_ui_config 字段
|
||||
// 若 service_config 缓存未命中, getServiceConfig() 内部会发请求并填充 _serviceConfigCache
|
||||
const serviceData = await this.getServiceConfig();
|
||||
const remote = (serviceData && serviceData.chat_ui_config) || null;
|
||||
const merged = mergeChatUiConfig(remote);
|
||||
_chatUiConfigCache = merged;
|
||||
_chatUiConfigCacheAt = Date.now();
|
||||
return merged;
|
||||
} catch (e) {
|
||||
console.warn('[AI Service] getChatUiConfig 失败, 使用前端默认值:', (e && e.message) || e);
|
||||
// 失败也要返回可用配置 (前端默认), 但不写缓存 — 下次重试
|
||||
return Object.assign({}, CHAT_UI_DEFAULTS);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -148,7 +148,8 @@
|
||||
</view>
|
||||
<view class="message-content">
|
||||
<!-- 思考流折叠面板 (AI 思考过程) -->
|
||||
<view v-if="message.role === 'ai' && message.thoughtsText" class="thought-panel" :class="{ streaming: message.isStreaming && message.thoughtsText }">
|
||||
<!-- PR-CHAT-UI-CONFIG (2026-09-20): 后端可关闭 ui_show_thinking_process, 默认 true 不变体感 -->
|
||||
<view v-if="message.role === 'ai' && message.thoughtsText && chatUiConfig.ui_show_thinking_process" class="thought-panel" :class="{ streaming: message.isStreaming && message.thoughtsText }">
|
||||
<view class="thought-header" @tap.stop="toggleThought(message)">
|
||||
<text class="thought-icon">{{ message.isStreaming ? '🧠' : '💡' }}</text>
|
||||
<text class="thought-label">{{ message.isStreaming ? '思考中...' : ('思考过程 (' + (message.thoughtSteps || 1) + ' 步)') }}</text>
|
||||
@@ -356,7 +357,8 @@
|
||||
</view>
|
||||
|
||||
<!-- AI 消息下方操作栏: 复制 / 点赞 / 不喜欢 / 更多 -->
|
||||
<view class="ai-actions" v-if="message.role === 'ai' && message.type !== 'loading'">
|
||||
<!-- PR-CHAT-UI-CONFIG (2026-09-20): 后端可关闭 ui_enable_feedback (隐藏点赞/点踩/更多), 默认 true -->
|
||||
<view class="ai-actions" v-if="message.role === 'ai' && message.type !== 'loading' && chatUiConfig.ui_enable_feedback">
|
||||
<view class="ai-action-btn" @click="onAiActionCopy(message)">
|
||||
<text class="iconfont icon-copy">⎘</text>
|
||||
</view>
|
||||
@@ -441,7 +443,7 @@
|
||||
class="message-input"
|
||||
placeholder="发消息或按住说话"
|
||||
placeholder-class="message-input-placeholder"
|
||||
:maxlength="500"
|
||||
:maxlength="chatUiConfig.ui_input_max_length"
|
||||
:auto-height="true"
|
||||
:show-confirm-bar="false"
|
||||
:adjust-position="adjustPosition"
|
||||
@@ -523,8 +525,10 @@
|
||||
</view>
|
||||
|
||||
<!-- 更多工具面板 -->
|
||||
<!-- PR-CHAT-UI-CONFIG (2026-09-20): 透传后端 ui_enable_attachment_types 控制各 tab 显隐 -->
|
||||
<ai-chat-tools-panel
|
||||
:visible.sync="showToolsPanel"
|
||||
:attachment-types="chatUiConfig.ui_enable_attachment_types"
|
||||
@image-picked="onImagePicked"
|
||||
@file-selected="onFileSelected"
|
||||
@voice-start="showVoicePanel"
|
||||
@@ -547,11 +551,11 @@
|
||||
<text>×</text>
|
||||
</button>
|
||||
</view>
|
||||
<textarea
|
||||
v-model="expandInputText"
|
||||
<textarea
|
||||
v-model="expandInputText"
|
||||
class="expand-editor-input"
|
||||
:placeholder="$lang('ai-chat.index.inputPlaceholder')"
|
||||
:maxlength="500"
|
||||
:maxlength="chatUiConfig.ui_input_max_length"
|
||||
:auto-height="true"
|
||||
:show-confirm-bar="false"
|
||||
:adjust-position="true"
|
||||
@@ -708,7 +712,29 @@ export default {
|
||||
avatarError: false,
|
||||
// 当前选中的 AI 模型, 显示在输入框左侧(如 Auto)
|
||||
selectedModel: 'Auto',
|
||||
availableModels: ['Auto', 'GPT-4', 'Claude', '文心一言', '通义千问', 'Kimi'],
|
||||
// PR-CHAT-UI-CONFIG-SERVER-ONLY (2026-09-20): availableModels 由后端 chat_ui_config.ui_available_models 注入
|
||||
// 初始仅 'Auto' 兜底, created() 里后端 /info 返回后异步覆盖完整列表
|
||||
// 数据源 (后端维护): https://docs.openclaw.ai/providers
|
||||
availableModels: ['Auto'],
|
||||
|
||||
// PR-CHAT-UI-CONFIG (2026-09-20): 前端 UI 行为开关
|
||||
// 字段与 ai-service.js::CHAT_UI_DEFAULTS 一一对应, 是后端 getChatUiConfig() 的缓存副本
|
||||
// 5 个 P0 开关:
|
||||
// ui_show_thinking_process - 控制是否显示 AI 思考过程面板
|
||||
// ui_enable_attachment_types - 控制 tools-panel 4 类附件 tab 显隐 (image/file/audio/video)
|
||||
// ui_input_max_length - 输入框最大字符数, 替代硬编 500
|
||||
// ui_available_models - 模型选择器列表 (PR-CHAT-UI-CONFIG-SERVER-ONLY: 后端注入)
|
||||
// ui_enable_feedback - 控制是否显示 AI 消息下方操作栏 (复制/点赞/点踩/更多)
|
||||
// 加载失败/未配置时全走前端默认值 (ai-service.js 兜底), 这里手填一份做双重保险
|
||||
// - ui_available_models 兜底仅 'Auto', 实际清单完全来自后端 (Config.php::DEFAULT_UI_CONFIG)
|
||||
chatUiConfig: {
|
||||
ui_show_thinking_process: true,
|
||||
ui_enable_attachment_types: ['image', 'file', 'audio', 'video'],
|
||||
ui_input_max_length: 500,
|
||||
ui_available_models: ['Auto'],
|
||||
ui_enable_feedback: true
|
||||
},
|
||||
chatUiConfigLoaded: false,
|
||||
|
||||
// AiChat API相关
|
||||
currentConversationId: null,
|
||||
@@ -834,6 +860,8 @@ export default {
|
||||
|
||||
// 异步获取服务配置(含欢迎语)
|
||||
this._loadWelcomeMessage()
|
||||
// PR-CHAT-UI-CONFIG (2026-09-20): 异步加载 UI 开关 (与欢迎语并行, 不阻塞 created)
|
||||
this._loadChatUiConfig()
|
||||
},
|
||||
mounted() {
|
||||
// 初始加载时滚底(仅一次)
|
||||
@@ -1016,6 +1044,29 @@ export default {
|
||||
},
|
||||
|
||||
// 加载欢迎语:优先 API,API 失败或禁用时用父页面传入的 fallbackWelcome
|
||||
async _loadChatUiConfig() {
|
||||
// PR-CHAT-UI-CONFIG (2026-09-20): 拉取前端 UI 行为配置 (5 个 P0 开关)
|
||||
// ai-service.getChatUiConfig() 内部已带 60s TTL + 与 getServiceConfig 共享 /info 调用
|
||||
// 这里失败不影响主流程, 失败时保留 data() 里的初始默认值
|
||||
try {
|
||||
const cfg = await aiService.getChatUiConfig()
|
||||
if (cfg && typeof cfg === 'object') {
|
||||
this.chatUiConfig = cfg
|
||||
// ui_available_models 同步覆盖 availableModels (UI 模型选择器用)
|
||||
if (Array.isArray(cfg.ui_available_models) && cfg.ui_available_models.length) {
|
||||
this.availableModels = cfg.ui_available_models.slice()
|
||||
// 当前 selectedModel 已不在新列表里 → 回落 'Auto' 或第一项
|
||||
if (!this.availableModels.includes(this.selectedModel)) {
|
||||
this.selectedModel = this.availableModels[0] || 'Auto'
|
||||
}
|
||||
}
|
||||
this.chatUiConfigLoaded = true
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[AI Chat] 加载 chatUiConfig 失败, 使用前端默认:', (e && e.message) || e)
|
||||
}
|
||||
},
|
||||
|
||||
async _loadWelcomeMessage() {
|
||||
// PR-FIX (2026-09-20): 防重入 (created 异步加载期间 mounted/loadHistoryIfNeeded
|
||||
// 等路径再触发会造成欢迎气泡重复 push; 网络慢时尤其容易)
|
||||
|
||||
@@ -9,26 +9,29 @@
|
||||
</button>
|
||||
</view>
|
||||
<view class="tools-grid">
|
||||
<view class="tool-item" @click="selectImage">
|
||||
<!-- PR-CHAT-UI-CONFIG (2026-09-20): 每个附件 tab 由 attachmentTypes 控制显隐
|
||||
支持的 kind: image / file / audio / video, 缺失的 kind 自动隐藏
|
||||
location 不属于附件, 不受此开关控制 -->
|
||||
<view v-if="isTypeEnabled('image')" class="tool-item" @click="selectImage">
|
||||
<view class="tool-icon">
|
||||
<text class="icondiy icon-system-image-line"></text>
|
||||
</view>
|
||||
<text class="tool-text">{{ $lang('ai-chat.index.image') }}</text>
|
||||
</view>
|
||||
<view class="tool-item" @click="selectVideo">
|
||||
<view v-if="isTypeEnabled('video')" class="tool-item" @click="selectVideo">
|
||||
<view class="tool-icon">
|
||||
<text class="icondiy icon-system-video"></text>
|
||||
</view>
|
||||
<!-- 第二个参数是 $lang 的默认值: 后端 bundle 尚未下发该 key 时也不会显示 raw key -->
|
||||
<text class="tool-text">{{ $lang('ai-chat.index.video', '视频/音频') }}</text>
|
||||
</view>
|
||||
<view class="tool-item" @click="selectFile">
|
||||
<view v-if="isTypeEnabled('file')" class="tool-item" @click="selectFile">
|
||||
<view class="tool-icon">
|
||||
<text class="icondiy icon-system-file-2-line"></text>
|
||||
</view>
|
||||
<text class="tool-text">{{ $lang('ai-chat.index.file') }}</text>
|
||||
</view>
|
||||
<view class="tool-item" @click="selectVoiceOrAudio">
|
||||
<view v-if="isTypeEnabled('audio')" class="tool-item" @click="selectVoiceOrAudio">
|
||||
<view class="tool-icon">
|
||||
<text class="icondiy icon-system-chat-voice-line"></text>
|
||||
</view>
|
||||
@@ -52,6 +55,9 @@ import { uploadImage, uploadMedia } from '@/common/js/media-upload.js'
|
||||
*
|
||||
* Props:
|
||||
* visible: Boolean - 控制显示/隐藏
|
||||
* attachmentTypes: Array<String> - PR-CHAT-UI-CONFIG (2026-09-20) 可启用的附件 kind 列表
|
||||
* 默认 ['image','file','audio','video'], 后端 chat_ui_config.ui_enable_attachment_types 可裁剪
|
||||
* location 不属于附件, 不受此 prop 影响
|
||||
*
|
||||
* Events:
|
||||
* update:visible - 同步关闭状态
|
||||
@@ -67,6 +73,12 @@ export default {
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
attachmentTypes: {
|
||||
type: Array,
|
||||
default: function () {
|
||||
return ['image', 'file', 'audio', 'video']
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -81,6 +93,13 @@ export default {
|
||||
this.$emit('update:visible', false)
|
||||
},
|
||||
|
||||
// PR-CHAT-UI-CONFIG (2026-09-20): 判断某个附件 kind 是否启用
|
||||
// undefined/空数组 → 兜底全部启用 (保留老调用方的体感)
|
||||
isTypeEnabled(kind) {
|
||||
if (!this.attachmentTypes || !this.attachmentTypes.length) return true
|
||||
return this.attachmentTypes.indexOf(kind) !== -1
|
||||
},
|
||||
|
||||
selectImage() {
|
||||
const that = this
|
||||
uni.chooseImage({
|
||||
|
||||
Reference in New Issue
Block a user