244f0c24d8
模式标签由「聊天/稿件/创意/策划」改为「聊天/写作/图片」,去掉策划链路; 写作与图片两个入口补上 web 端已有的表单与附件能力。 - 新增 pages/assistant/writing-params.js:web 端 writing-params.ts 的等价移植 (零依赖纯函数)。写作模式加「文体/字数」面板,入参优先级改为 「界面显式选择 > 键值入参 > 自然语言推断 > 默认值」——过去没有表单, 只按「键:值」解析,解析不到就一律回落 doc_type=新闻稿 / Number=800, 而上游提示词把这两项当主控指令,会直接压掉用户原话里的诉求(issue 13) - 图片模式加「图片比例 + 参考素材」:附件组装成 user_files(上限 5, 类型 image/video/audio/document),比例作为文本追加进 user_content - chat-service 新增 uploadAiFile(file, app);上传必须带 app='creativeAssistant' —— Dify 的 /files/upload 按应用鉴权,不传 app 的附件必然报 Invalid upload file(issue 14) - 删除 buildOutlinePayload / formatOutlineResult 与 activityOutlineGen import; 顺带清掉本次改动块内的死代码(未使用的 getCurrentUser、pickDisplayableText) 顺带修掉同源缺陷:pages/creative-assistant 的 uploadAiFile(file) 也没传应用凭证, 该页附件同样是坏的(issue 14 记的 gxmu/creativeAssistant 表单页那个缺陷)。 验证:移植等价性用例 8/8 —— 同一张用例表同时喂给小程序与 web 两端实现逐字段比对 JSON,另加 4 条常量/边界断言(防「两边一起错」);模板↔脚本静态一致性检查 (模板绑定名、this.X() 调用点、已删标识符残留)全绿,判别力经 5 处变异测试证伪; 5 个改动文件 node --check 通过,style 块花括号配平。 本机无 HBuilderX CLI 与 package.json,真机/微信开发者工具未跑。 本文件同时带有此前未提交的 issue 01 跟进改动(resolveFailedCreativeText: 创意失败态采用后端按原因映射的文案,但仍过 isDisplayableAnswer 兜底), 同一文件无法拆分。 Refs: .scratch/ai-assistant/issues/15-uniapp-assistant-modes-align-web.md
500 lines
13 KiB
JavaScript
500 lines
13 KiB
JavaScript
import {
|
||
API_BASE_URL,
|
||
DEFAULT_TENANT_ID,
|
||
TOKEN_HEADER_NAME,
|
||
clearAuthStorage,
|
||
getStoredUserInfo,
|
||
getToken,
|
||
redirectToLogin
|
||
} from '../../utils/request'
|
||
|
||
export { API_BASE_URL, getToken }
|
||
|
||
const MODULES_API_URL = API_BASE_URL
|
||
const APP_SECRET = 'ffd6eee985af45e4a75098422d1decbb'
|
||
const AI_REQUEST_TIMEOUT = 10 * 60 * 1000
|
||
|
||
let realAtob
|
||
const b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
|
||
const b64re = /^(?:[A-Za-z\d+/]{4})*?(?:[A-Za-z\d+/]{2}(?:==)?|[A-Za-z\d+/]{3}=?)?$/
|
||
|
||
if (typeof atob !== 'function') {
|
||
realAtob = function(str) {
|
||
let input = String(str || '').replace(/[\t\n\f\r ]+/g, '')
|
||
if (!b64re.test(input)) {
|
||
throw new Error("Failed to execute 'atob': The string to be decoded is not correctly encoded.")
|
||
}
|
||
input += '=='.slice(2 - (input.length & 3))
|
||
let bitmap
|
||
let result = ''
|
||
let r1
|
||
let r2
|
||
let index = 0
|
||
for (; index < input.length;) {
|
||
bitmap =
|
||
(b64.indexOf(input.charAt(index++)) << 18) |
|
||
(b64.indexOf(input.charAt(index++)) << 12) |
|
||
((r1 = b64.indexOf(input.charAt(index++))) << 6) |
|
||
(r2 = b64.indexOf(input.charAt(index++)))
|
||
result +=
|
||
r1 === 64
|
||
? String.fromCharCode((bitmap >> 16) & 255)
|
||
: r2 === 64
|
||
? String.fromCharCode((bitmap >> 16) & 255, (bitmap >> 8) & 255)
|
||
: String.fromCharCode((bitmap >> 16) & 255, (bitmap >> 8) & 255, bitmap & 255)
|
||
}
|
||
return result
|
||
}
|
||
} else {
|
||
realAtob = atob
|
||
}
|
||
|
||
function b64DecodeUnicode(str) {
|
||
return decodeURIComponent(
|
||
realAtob(str)
|
||
.split('')
|
||
.map((char) => `%${`00${char.charCodeAt(0).toString(16)}`.slice(-2)}`)
|
||
.join('')
|
||
)
|
||
}
|
||
|
||
function parseTokenPayload(token) {
|
||
const rawToken = String(token || '').replace(/^Bearer\s+/i, '')
|
||
const parts = rawToken.split('.')
|
||
if (!rawToken || parts.length !== 3) {
|
||
return null
|
||
}
|
||
try {
|
||
const normalized = parts[1].replace(/-/g, '+').replace(/_/g, '/')
|
||
return JSON.parse(b64DecodeUnicode(normalized))
|
||
} catch (error) {
|
||
return null
|
||
}
|
||
}
|
||
|
||
function safeAdd(x, y) {
|
||
const lsw = (x & 0xffff) + (y & 0xffff)
|
||
const msw = (x >> 16) + (y >> 16) + (lsw >> 16)
|
||
return (msw << 16) | (lsw & 0xffff)
|
||
}
|
||
|
||
function bitRotateLeft(num, cnt) {
|
||
return (num << cnt) | (num >>> (32 - cnt))
|
||
}
|
||
|
||
function md5cmn(q, a, b, x, s, t) {
|
||
return safeAdd(bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s), b)
|
||
}
|
||
|
||
function md5ff(a, b, c, d, x, s, t) {
|
||
return md5cmn((b & c) | (~b & d), a, b, x, s, t)
|
||
}
|
||
|
||
function md5gg(a, b, c, d, x, s, t) {
|
||
return md5cmn((b & d) | (c & ~d), a, b, x, s, t)
|
||
}
|
||
|
||
function md5hh(a, b, c, d, x, s, t) {
|
||
return md5cmn(b ^ c ^ d, a, b, x, s, t)
|
||
}
|
||
|
||
function md5ii(a, b, c, d, x, s, t) {
|
||
return md5cmn(c ^ (b | ~d), a, b, x, s, t)
|
||
}
|
||
|
||
function binlMD5(x, len) {
|
||
x[len >> 5] |= 0x80 << len % 32
|
||
x[(((len + 64) >>> 9) << 4) + 14] = len
|
||
|
||
let i
|
||
let olda
|
||
let oldb
|
||
let oldc
|
||
let oldd
|
||
let a = 1732584193
|
||
let b = -271733879
|
||
let c = -1732584194
|
||
let d = 271733878
|
||
|
||
for (i = 0; i < x.length; i += 16) {
|
||
olda = a
|
||
oldb = b
|
||
oldc = c
|
||
oldd = d
|
||
|
||
a = md5ff(a, b, c, d, x[i], 7, -680876936)
|
||
d = md5ff(d, a, b, c, x[i + 1], 12, -389564586)
|
||
c = md5ff(c, d, a, b, x[i + 2], 17, 606105819)
|
||
b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330)
|
||
a = md5ff(a, b, c, d, x[i + 4], 7, -176418897)
|
||
d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426)
|
||
c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341)
|
||
b = md5ff(b, c, d, a, x[i + 7], 22, -45705983)
|
||
a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416)
|
||
d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417)
|
||
c = md5ff(c, d, a, b, x[i + 10], 17, -42063)
|
||
b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162)
|
||
a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682)
|
||
d = md5ff(d, a, b, c, x[i + 13], 12, -40341101)
|
||
c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290)
|
||
b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329)
|
||
|
||
a = md5gg(a, b, c, d, x[i + 1], 5, -165796510)
|
||
d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632)
|
||
c = md5gg(c, d, a, b, x[i + 11], 14, 643717713)
|
||
b = md5gg(b, c, d, a, x[i], 20, -373897302)
|
||
a = md5gg(a, b, c, d, x[i + 5], 5, -701558691)
|
||
d = md5gg(d, a, b, c, x[i + 10], 9, 38016083)
|
||
c = md5gg(c, d, a, b, x[i + 15], 14, -660478335)
|
||
b = md5gg(b, c, d, a, x[i + 4], 20, -405537848)
|
||
a = md5gg(a, b, c, d, x[i + 9], 5, 568446438)
|
||
d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690)
|
||
c = md5gg(c, d, a, b, x[i + 3], 14, -187363961)
|
||
b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501)
|
||
a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467)
|
||
d = md5gg(d, a, b, c, x[i + 2], 9, -51403784)
|
||
c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473)
|
||
b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734)
|
||
|
||
a = md5hh(a, b, c, d, x[i + 5], 4, -378558)
|
||
d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463)
|
||
c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562)
|
||
b = md5hh(b, c, d, a, x[i + 14], 23, -35309556)
|
||
a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060)
|
||
d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353)
|
||
c = md5hh(c, d, a, b, x[i + 7], 16, -155497632)
|
||
b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640)
|
||
a = md5hh(a, b, c, d, x[i + 13], 4, 681279174)
|
||
d = md5hh(d, a, b, c, x[i], 11, -358537222)
|
||
c = md5hh(c, d, a, b, x[i + 3], 16, -722521979)
|
||
b = md5hh(b, c, d, a, x[i + 6], 23, 76029189)
|
||
a = md5hh(a, b, c, d, x[i + 9], 4, -640364487)
|
||
d = md5hh(d, a, b, c, x[i + 12], 11, -421815835)
|
||
c = md5hh(c, d, a, b, x[i + 15], 16, 530742520)
|
||
b = md5hh(b, c, d, a, x[i + 2], 23, -995338651)
|
||
|
||
a = md5ii(a, b, c, d, x[i], 6, -198630844)
|
||
d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415)
|
||
c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905)
|
||
b = md5ii(b, c, d, a, x[i + 5], 21, -57434055)
|
||
a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571)
|
||
d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606)
|
||
c = md5ii(c, d, a, b, x[i + 10], 15, -1051523)
|
||
b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799)
|
||
a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359)
|
||
d = md5ii(d, a, b, c, x[i + 15], 10, -30611744)
|
||
c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380)
|
||
b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649)
|
||
a = md5ii(a, b, c, d, x[i + 4], 6, -145523070)
|
||
d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379)
|
||
c = md5ii(c, d, a, b, x[i + 2], 15, 718787259)
|
||
b = md5ii(b, c, d, a, x[i + 9], 21, -343485551)
|
||
|
||
a = safeAdd(a, olda)
|
||
b = safeAdd(b, oldb)
|
||
c = safeAdd(c, oldc)
|
||
d = safeAdd(d, oldd)
|
||
}
|
||
return [a, b, c, d]
|
||
}
|
||
|
||
function binl2rstr(input) {
|
||
let index
|
||
let output = ''
|
||
const length32 = input.length * 32
|
||
for (index = 0; index < length32; index += 8) {
|
||
output += String.fromCharCode((input[index >> 5] >>> index % 32) & 0xff)
|
||
}
|
||
return output
|
||
}
|
||
|
||
function rstr2binl(input) {
|
||
const output = Array(input.length >> 2)
|
||
let index
|
||
for (index = 0; index < output.length; index += 1) {
|
||
output[index] = 0
|
||
}
|
||
const length8 = input.length * 8
|
||
for (index = 0; index < length8; index += 8) {
|
||
output[index >> 5] |= (input.charCodeAt(index / 8) & 0xff) << index % 32
|
||
}
|
||
return output
|
||
}
|
||
|
||
function rstrMD5(value) {
|
||
return binl2rstr(binlMD5(rstr2binl(value), value.length * 8))
|
||
}
|
||
|
||
function rstr2hex(input) {
|
||
const hexTab = '0123456789abcdef'
|
||
let output = ''
|
||
let index
|
||
let value
|
||
for (index = 0; index < input.length; index += 1) {
|
||
value = input.charCodeAt(index)
|
||
output += hexTab.charAt((value >>> 4) & 0x0f) + hexTab.charAt(value & 0x0f)
|
||
}
|
||
return output
|
||
}
|
||
|
||
function str2rstrUTF8(input) {
|
||
return unescape(encodeURIComponent(input))
|
||
}
|
||
|
||
function md5(value) {
|
||
return rstr2hex(rstrMD5(str2rstrUTF8(String(value || ''))))
|
||
}
|
||
|
||
function objKeySort(obj) {
|
||
const next = {}
|
||
Object.keys(obj || {})
|
||
.sort()
|
||
.forEach((key) => {
|
||
next[key] = obj[key]
|
||
})
|
||
return next
|
||
}
|
||
|
||
function addSignature(payload) {
|
||
const form = payload || {}
|
||
form.timestamp = Date.now()
|
||
form.version = 'v3'
|
||
let sign = ''
|
||
const sorted = objKeySort(form)
|
||
Object.keys(sorted).forEach((key) => {
|
||
const value = form[key]
|
||
if (value !== null && value !== undefined && value !== '') {
|
||
sign = `${sign}${value}-`
|
||
}
|
||
})
|
||
form.sign = md5(`${sign}${APP_SECRET}`)
|
||
return form
|
||
}
|
||
|
||
function getStorageValue(keys) {
|
||
for (let index = 0; index < keys.length; index += 1) {
|
||
const value = uni.getStorageSync(keys[index])
|
||
if (value) {
|
||
return value
|
||
}
|
||
}
|
||
return ''
|
||
}
|
||
|
||
export function getCurrentUser() {
|
||
const primaryToken = getToken()
|
||
const storedUser = getStoredUserInfo()
|
||
const tokenList = [
|
||
primaryToken,
|
||
uni.getStorageSync('uni_id_token'),
|
||
uni.getStorageSync('access_token')
|
||
].filter(Boolean)
|
||
let payload = null
|
||
|
||
for (let index = 0; index < tokenList.length; index += 1) {
|
||
payload = parseTokenPayload(tokenList[index])
|
||
if (payload) {
|
||
break
|
||
}
|
||
}
|
||
|
||
if (!payload) {
|
||
return {
|
||
...storedUser,
|
||
token: primaryToken || ''
|
||
}
|
||
}
|
||
|
||
return {
|
||
...storedUser,
|
||
userId: Number(payload.userId || payload.uid || payload.id || 0) || null,
|
||
nickname: payload.nickname || payload.username || payload.nickName || '',
|
||
realName: payload.realName || payload.realname || payload.name || '',
|
||
token: primaryToken || ''
|
||
}
|
||
}
|
||
|
||
export function apiRequest({
|
||
url,
|
||
method = 'GET',
|
||
data,
|
||
withSignature = true,
|
||
withTenant = false,
|
||
timeout
|
||
}) {
|
||
const token = getToken()
|
||
const requestData = data ? { ...data } : undefined
|
||
const headers = {}
|
||
|
||
if (token) {
|
||
headers[TOKEN_HEADER_NAME] = token
|
||
}
|
||
|
||
if (requestData && withTenant) {
|
||
requestData.tenantId = DEFAULT_TENANT_ID
|
||
}
|
||
|
||
if (requestData && withSignature) {
|
||
addSignature(requestData)
|
||
}
|
||
|
||
return uni.request({
|
||
url,
|
||
method,
|
||
data: requestData,
|
||
header: headers,
|
||
...(timeout !== undefined && { timeout })
|
||
}).then((response) => {
|
||
const responseHeader = (response && response.header) || {}
|
||
const nextToken =
|
||
responseHeader[TOKEN_HEADER_NAME] ||
|
||
responseHeader[TOKEN_HEADER_NAME.toLowerCase()]
|
||
if (nextToken) {
|
||
uni.setStorageSync('access_token', nextToken)
|
||
}
|
||
|
||
const { statusCode } = response || {}
|
||
const body = (response && response.data) || {}
|
||
if (statusCode && statusCode >= 400) {
|
||
return Promise.reject(new Error(body.message || '请求失败'))
|
||
}
|
||
if (Number(body.code) === 401) {
|
||
clearAuthStorage()
|
||
redirectToLogin()
|
||
return Promise.reject(new Error(body.message || '登录已失效'))
|
||
}
|
||
if (body.code === 0) {
|
||
return body.data !== undefined ? body.data : body.message
|
||
}
|
||
return Promise.reject(new Error(body.message || '请求失败'))
|
||
})
|
||
}
|
||
|
||
export function listAiChatList(params) {
|
||
return apiRequest({
|
||
url: `${MODULES_API_URL}/ai/ai-chat-list`,
|
||
method: 'GET',
|
||
data: params
|
||
})
|
||
}
|
||
|
||
export function listAiChatHistory(params) {
|
||
return apiRequest({
|
||
url: `${MODULES_API_URL}/ai/ai-chat-history`,
|
||
method: 'GET',
|
||
data: params
|
||
})
|
||
}
|
||
|
||
export function removeAiChatList(id) {
|
||
return apiRequest({
|
||
url: `${MODULES_API_URL}/ai/ai-chat-list/${id}`,
|
||
method: 'DELETE',
|
||
withSignature: false
|
||
})
|
||
}
|
||
|
||
export function sendMessage(data) {
|
||
return apiRequest({
|
||
url: `${API_BASE_URL}/ai/chat/message`,
|
||
method: 'POST',
|
||
data,
|
||
withTenant: true,
|
||
timeout: AI_REQUEST_TIMEOUT
|
||
})
|
||
}
|
||
|
||
export function manuscriptGen(data) {
|
||
return apiRequest({
|
||
url: `${API_BASE_URL}/ai/manuscriptGen`,
|
||
method: 'POST',
|
||
data,
|
||
withTenant: true,
|
||
timeout: AI_REQUEST_TIMEOUT
|
||
})
|
||
}
|
||
|
||
export function creativeAssistant(data) {
|
||
return apiRequest({
|
||
url: `${API_BASE_URL}/ai/creativeAssistant`,
|
||
method: 'POST',
|
||
data,
|
||
withTenant: true,
|
||
timeout: AI_REQUEST_TIMEOUT
|
||
})
|
||
}
|
||
|
||
export function activityOutlineGen(data) {
|
||
return apiRequest({
|
||
url: `${API_BASE_URL}/ai/activityOutlineGen`,
|
||
method: 'POST',
|
||
data,
|
||
withTenant: true,
|
||
timeout: AI_REQUEST_TIMEOUT
|
||
})
|
||
}
|
||
|
||
/**
|
||
* AI 文件上传(喂给 Dify 开始节点的 file-list 变量)。
|
||
*
|
||
* @param {object} file `uni.chooseImage` / `uni.chooseMessageFile` 拿到的临时文件(需有 `path`)
|
||
* @param {string} [app] 应用代号(后端按代号选择 Dify 应用凭证)
|
||
*
|
||
* `app` 不是可选项而是**硬约束**:Dify 的 `/files/upload` 按应用鉴权,
|
||
* 文件只能被「上传时所用应用」的工作流引用,换个应用引用同一个 `upload_file_id`
|
||
* 会直接报 `Invalid upload file`(issue 14 实测:chat Key 上传 → 0.2s 失败,
|
||
* creativeAssistant Key 上传 → succeeded)。
|
||
*
|
||
* 后端 `/ai/file/upload` 不传 `app` 时保持旧行为(`type` 为空 → chat 凭证),
|
||
* 所以喂给创意助手 file-list 的附件**必须**传 `app: 'creativeAssistant'`。
|
||
*/
|
||
export function uploadAiFile(file, app) {
|
||
const token = getToken()
|
||
const formData = {
|
||
tenantId: DEFAULT_TENANT_ID
|
||
}
|
||
if (app) {
|
||
formData.app = app
|
||
}
|
||
return new Promise((resolve, reject) => {
|
||
uni.uploadFile({
|
||
url: `${API_BASE_URL}/ai/file/upload`,
|
||
filePath: file.path,
|
||
name: 'file',
|
||
header: token
|
||
? {
|
||
Authorization: token
|
||
}
|
||
: {},
|
||
formData,
|
||
success: (response) => {
|
||
try {
|
||
const result = JSON.parse(response.data || '{}')
|
||
if (result.code === 0 && result.data) {
|
||
resolve(result.data)
|
||
return
|
||
}
|
||
reject(new Error(result.message || '上传失败'))
|
||
} catch (error) {
|
||
reject(new Error('上传响应解析失败'))
|
||
}
|
||
},
|
||
fail: (error) => {
|
||
reject(new Error(error.errMsg || '上传失败'))
|
||
}
|
||
})
|
||
})
|
||
}
|
||
|
||
export function buildWebSocketUrl(userId) {
|
||
const url = String(API_BASE_URL || '')
|
||
if (/^https?:\/\//.test(url)) {
|
||
return `${url
|
||
.replace(/^https:/, 'wss:')
|
||
.replace(/^http:/, 'ws:')
|
||
.replace(/\/api\/?$/, '')
|
||
.replace(/\/$/, '')}/chat/${userId}`
|
||
}
|
||
return `${url.replace(/\/api\/?$/, '')}/chat/${userId}`
|
||
}
|