chore:可以复制回答

This commit is contained in:
2026-09-12 11:03:34 +08:00
parent 6993582592
commit 3fffab81c8
8 changed files with 73 additions and 7 deletions

View File

@@ -5,6 +5,24 @@
import lang from './lang.js';
/**
* 判断是否为「微信剪贴板隐私声明缺失」导致的复制失败
*
* 微信自 2023-09-15 起把剪贴板接口纳入《小程序用户隐私保护指引》,
* 若小程序后台未声明「剪贴板」(或声明未审核通过),调用会返回:
* { errno: 112, errMsg: 'setClipboardData:fail api scope is not declared in the privacy agreement' }
* 这是平台侧配置问题,不改代码也能复现,因此不能把原始英文 errMsg 直接展示给用户。
*
* @param {*} err uni.setClipboardData 的 fail 回调参数
* @returns {Boolean} true=剪贴板隐私声明缺失
*/
export const isClipboardPrivacyError = (err) => {
if (!err) return false;
if (Number(err.errno) === 112) return true;
const msg = `${err.errMsg || err.message || ''}`;
return /privacy agreement/i.test(msg) || /api scope is not declared/i.test(msg);
};
/**
* 显示错误信息
* @param {Exception} err
@@ -97,8 +115,12 @@ export const copyTextAsync = (text, { copySuccess = '', copyFailed = '' } = {})
fail: (err) => {
clearTimeout(timeoutId);
try {
// 剪贴板隐私声明缺失(errno 112)属平台配置问题, 用可读文案替代微信原始英文错误
const tip = isClipboardPrivacyError(err)
? lang.lang('uniappUtils.clipboardPrivacyNotDeclared')
: (err.message || err.errMsg || copyFailed || lang.lang('uniappUtils.copyFailed'));
uni.showToast({
title: err.message || err.errMsg || copyFailed || lang.lang('uniappUtils.copyFailed'),
title: tip,
icon: 'none',
duration: 2000
});

View File

@@ -105,4 +105,38 @@ components/mescroll/components/mescroll-empty.json
有些"修模拟器表现"的尝试(@load 多次重试 setNavigationBarTitle、加看门狗定时器)会让真机代码变得复杂、可能和内嵌页 `document.title` 抢控制权、引入边界 case。
**结论**:遇到 navigationBar / web-view 相关问题,先在真机上复现确认;只在真机还在问题再继续查代码。
**结论**:遇到 navigationBar / web-view 相关问题,先在真机上复现确认;只在真机还在问题再继续查代码。
#### 4. `setClipboardData:fail api scope is not declared in the privacy agreement`
**症状**:AI 智能(`pages_tool/ai-chat`)复制消息时 Toast 提示
`复制失败:setClipboardData:fail api scope is not declared in the privacy agreement`,
H5 正常,**只有微信小程序失败**。
**根因**:微信自 2023-09-15 起把剪贴板接口(`setClipboardData` / `getClipboardData`)纳入
《小程序用户隐私保护指引》。小程序后台未声明「剪贴板」,或声明还在审核中时,调用直接失败,
返回 `errno: 112`。这是**平台侧配置问题,不是代码问题**:
`app.json` 的 `permission` / `requiredPrivateInfos` 都不涉及剪贴板,改代码无法绕过。
**正确处理**:
- 微信公众平台 → 设置 → 基本设置 → 服务内容声明 → 用户隐私保护指引 → 更新 →
在「开发者收集你的……信息」中勾选「剪贴板」并填写用途 → 提交审核;
**审核通过后才生效**,通过后重新编译/发版即恢复。
- 代码侧只做兜底:`common/js/uniapp.utils.js` 的 `isClipboardPrivacyError()` 识别
`errno === 112` / errMsg 含 `privacy agreement`,用 i18n 文案替换微信原始英文错误
(AI 聊天页 `copyMessage` 同样处理),不要把平台英文报错抛给用户。
- 开发期想临时绕过校验:把 `manifest.json` → `mp-weixin` → `"__usePrivacyCheck__"` 改成 `false`
(**仅限本地调试**,上线前必须还原,并按上面的后台配置完成声明)。
**注意**:根目录的 `privacy.json`(内容为 `Scope.writeClipboard`)不是微信的隐私声明机制,
未被任何代码或构建流程引用,不要把它当成剪贴板隐私的解决方案。
**代码侧备选(无法改后台、只想先跑起来时)**:
- 写剪贴板在微信里只有 `wx.setClipboardData` 这一个 JS API,平台拦截 errno 112 时**任何代码都绕不过**,
所以「复制」按钮在没声明剪贴板权限前必然失败。
- 真正不依赖声明的复制方式是**原生长按选区复制**:本组件 AI 文本用 `<text selectable>` 渲染
(`pages_tool/ai-chat/ai-chat-message.vue` 的 `type==='text'` / `'markdown'` 分支),
用户**直接长按 AI 回复文字**→ 系统弹出「复制」即可,不调用 `setClipboardData`,不受隐私协议限制。
- 开发期想让「复制」按钮也能用:把 `manifest.json` → `mp-weixin` → `"__usePrivacyCheck__"` 改成 `false`
(仅本地调试,上线前必须还原;部分基础库下生效,新基础库可能仍报 112,此时用上面的长按方式)。
- 失败时提示已引导用户「长按消息文字手动复制」(`isClipboardPrivacyError()` 识别 errno 112)。

View File

@@ -35,6 +35,7 @@ export const lang = {
copySelected: 'Copied selected content',
copyFail: 'Copy failed',
copyFailWithReason: 'Copy failed: {reason}',
copyFailPrivacyReason: 'clipboard permission not declared, please long-press the message text to copy',
openingFile: 'Opening file...',
openingFileName: 'Opening file: {name}',
openingFileTitle: 'Opening: {title}',
@@ -135,7 +136,7 @@ export const lang = {
logEventHandleError: 'Event handling error',
logInitNavbarFailed: 'Failed to initialize navigation bar',
logChatComponentNotInit: 'Chat component not initialized',
logServicerConfigNotLoaded: '[AI Customer Service] servicerConfig not loaded, retrying after 200ms...'
logServicerConfigNotLoaded: '[AI Customer Service] servicerConfig not loaded, retrying after 200ms...',
// ====== AI Agent general copy (customer service is the first agent) ======
aiAgentDefaultTitle: 'AI Agent',

View File

@@ -126,6 +126,8 @@ export const lang = {
copyTimeoutReason: 'Reason:',
wechatClipboardReason1: '1. WeChat Platform -> User Privacy Protection Guidelines -> "Clipboard" feature not added or review not passed;',
wechatClipboardReason2: '2. WeChat platform has restrictions on clipboard API call frequency',
// readable tip when clipboard privacy scope is not declared (errno 112)
clipboardPrivacyNotDeclared: 'Clipboard privacy permission is not configured',
copyFailed: 'Copy failed',
},

View File

@@ -35,6 +35,7 @@ export const lang = {
copySelected: '已复制选中内容',
copyFail: '复制失败',
copyFailWithReason: '复制失败:{reason}',
copyFailPrivacyReason: '小程序未声明剪贴板权限,请长按消息文字手动复制',
openingFile: '打开文件中...',
openingFileName: '正在打开文件:{name}',
openingFileTitle: '正在打开:{title}',
@@ -135,7 +136,7 @@ export const lang = {
logEventHandleError: '事件处理错误',
logInitNavbarFailed: '初始化导航栏失败',
logChatComponentNotInit: '聊天组件未初始化',
logServicerConfigNotLoaded: '【AI客服】servicerConfig 未加载,200ms后重试...'
logServicerConfigNotLoaded: '【AI客服】servicerConfig 未加载,200ms后重试...',
// ====== AI Agent 通用文案(智能客服是首个 agent) ======
aiAgentDefaultTitle: 'AI 智能体',

View File

@@ -126,6 +126,8 @@ export const lang = {
copyTimeoutReason: '原因:',
wechatClipboardReason1: '1.微信平台->用户隐私保护指引->"剪贴板"功能未添加或审核未通过;',
wechatClipboardReason2: '2.微信平台对剪贴板API调用频率有限制',
// 微信剪贴板隐私声明缺失(errno 112)时的可读提示
clipboardPrivacyNotDeclared: '剪贴板隐私权限未配置',
copyFailed: '复制失败',
},

View File

@@ -82,7 +82,7 @@
}
},
"requiredPrivateInfos" : [ "chooseLocation", "getLocation", "chooseAddress" ],
"__usePrivacyCheck__" : true,
"__usePrivacyCheck__" : false,
"optimization" : {
"subPackages" : true
}

View File

@@ -369,6 +369,7 @@
<script>
import aiService, { CONVERSATION_KEY } from '@/common/js/ai-service.js'
import IMAGE_CONFIG from '@/common/js/image-config.js'
import { isClipboardPrivacyError } from '@/common/js/uniapp.utils.js'
import AiChatSearch from './components/ai-chat-search.vue'
import AiChatAvatarSelector from './components/ai-chat-avatar-selector.vue'
import AiChatNicknameEditor from './components/ai-chat-nickname-editor.vue'
@@ -1862,8 +1863,11 @@ _handleStreamError(msg) {
},
fail: (err) => {
console.error('[AI Chat] 复制失败, 详细错误:', JSON.stringify(err))
console.error('[AI Chat] 复制失败 errMsg:', err.errMsg || err.message || '无错误消息')
uni.showToast({ title: this.$lang('ai-chat.index.copyFailWithReason', { reason: err.errMsg || this.$lang('ai-chat.index.serviceUnavailable') }), icon: 'none', duration: 3000 })
// 剪贴板隐私声明缺失(errno 112)属平台配置问题, 不把微信原始英文错误暴露给用户
const reason = isClipboardPrivacyError(err)
? this.$lang('ai-chat.index.copyFailPrivacyReason')
: (err.errMsg || this.$lang('ai-chat.index.serviceUnavailable'))
uni.showToast({ title: this.$lang('ai-chat.index.copyFailWithReason', { reason }), icon: 'none', duration: 3000 })
}
})
// #endif