window.checkZAIPanel = async function(showMessage) { const zaiPanel = zui.AIPanel.shared; const store = zaiPanel ? zaiPanel.store : null; if(!store || !store.isConfigOK) { if(showMessage) zui.Modal.alert((store ? store.error : '') || {content: {html: zaiLang.zaiConfigNotValid}}); return; } const isOK = await store.isOK(); if(!isOK) { if(showMessage) zui.Modal.alert((store ? store.error : '') || {content: {html: zaiLang.unauthorizedError}}); return; } return zaiPanel; }; window.openPageForm = function(url, data, callback) { return new Promise((resolve, reject) => { localStorage.setItem('aiResult', JSON.stringify(data)); const openedApp = openUrl(url); if(!openedApp) return; let updateTimer = 0; const tryUpdateForm = () => { if(updateTimer) clearTimeout(updateTimer); updateTimer = setTimeout(() => { try { if(data) { const iframe = openedApp.iframe; iframe.contentWindow.applyFormData(data); } callback && callback(openedApp); resolve(openedApp); } catch (error) {reject(error)} }, 2000); }; openedApp.$app.one('updateapp.apps updatepage.app', tryUpdateForm); setTimeout(() => openedApp.$app.off('updateapp.apps', tryUpdateForm), 5000); }); } function getPromptFormConfig(fields, extraConfig) { if(!Array.isArray(fields) || !fields.length) return; const typeMap = {radio: 'picker', checkbox: 'multiPicker', text: 'input'}; const properties = fields.reduce((properties, field, index) => { field.code = `field-${field.id}`; properties[field.code] = { type : 'string', widget : typeMap[field.type] || field.type, title : field.name, placeholder: field.placeholder, order : index, required : field.required && field.required !== '0', props : zui.isNotEmptyString(field.options) ? {items: field.options.split(',').map(x => ({text: x, value: x}))}: undefined }; return properties; }, {}); return $.extend( { schema: {type: 'object', properties: properties}, prompt: (data) => fields.map(x => `* ${x.name}: ${data[x.code] || ''}`).join('\n') }, extraConfig); } window.executeZentaoPrompt = async function(info, testingMode) { testingMode = testingMode && testingMode !== '0'; const zaiPanel = await checkZAIPanel(true); if(!zaiPanel) return; const langData = zaiPanel.options.langData || {}; const noTargetForm = !info.targetForm || info.targetForm === 'empty.empty'; const toolName = `zentao_tool_${info.promptID}`; const agentTool = noTargetForm ? null : { name : toolName, displayName: info.name, description: info.name, parameters : { type: 'object', properties: { data: info.schema, title: {type: 'string', description: langData.promptResultTitle}, summary: {type: 'string', description: langData.agentResultSummary}, }, required: ['data', 'summary'], }, }; const tools = noTargetForm ? [] : [{ ...agentTool, fn: (response) => { const result = response.data; const targetForm = info.targetForm; if(!targetForm) return {result: result}; const taskResult = { agentID : info.promptID, id : `zentao-agent-result-${info.promptID}`, tool : agentTool, title : response.title, result : response, formLocation : info.formLocation, targetFormName: info.targetFormName, targetForm : info.targetForm, objectID : info.objectID, objectType : info.objectType, objectData : info.objectData, objectProps : info.dataPropNames, actions: info.promptAudit ? [{ text : langData.goTesting, url : $.createLink('ai', 'promptAudit', `promptId=${info.promptID}&objectId=${info.objectID || 0}`), type : 'primary-pale', 'data-toggle': 'modal', }] : [], }; const message = { role: 'user', content: [response.title, zui.formatString(langData.processedDataResult, {data: JSON.stringify(result)}), response.summary, zui.formatString(langData.promptResultReturn, {formName: info.targetFormName})].join('\n\n'), custom_data: {taskResults: [taskResult], asRole: 'assistant'} }; return {message: message}; }, }]; const klibs = (info.knowledgeLib ? info.knowledgeLib.split(',') : []).filter(Boolean).map(x => `zentao:${x}`); const formConfig = getPromptFormConfig(info.fields, info.formConfig); const popupOptions = { id : 'zentao-prompt-popoup', viewType : 'chat', width : info.content ? 800 : 600, postMessage: {content: [{role: 'user', content: info.purpose, custom_data: {invisible: true}}]}, creatingChat: { title : info.name, type : 'agent', model : info.model, tools : tools, prompt : [info.role, zui.formatString(langData.processDataPrefix, {data: info.dataPrompt}), noTargetForm ? null : zui.formatString(langData.promptExtraLimit, {toolName: toolName})].filter(Boolean).join('\n\n'), form : formConfig, memories : klibs.length ? [{collections: klibs}] : undefined, }, }; zaiPanel.openPopup(popupOptions); }; window.openAITaskPopup = async function(taskID) { const zaiPanel = await checkZAIPanel(true); if(!zaiPanel) return; const popupOptions = { id : 'zentao-task-popoup', viewType : 'task', width : 600, chatID : `task-${taskID}`, }; zaiPanel.openPopup(popupOptions); }; window.callZentaoAgent = async function(agentID, objectID) { const res = await $.ajax({url: $.createLink('ai', 'promptExecute', `promptId=${agentID}&objectId=${objectID}`), 'dataType': 'json'}); if(!res || res.result !== 'success' || !res.callback) return; return executeZentaoPrompt(res.callback.params[0], res.callback.params[1]); }; /* 加载数字员工列表,并注册菜单 */ function loadAndRegisterAiTeammates(lang, plugin) { plugin.defineContextProvider({ code: 'ai-teammate', title: lang.teammate, icon: 'hand-right', items: async function() { const res = await zui.fetchData($.createLink('ai', 'ajaxGetTeammates')); if(!res || res.result !== 'success' || !res.data) return []; const teammates = res.data; if(!teammates.length) return; const items = teammates.map((item) => { const collections = []; if(item.klibs && item.klibs.length) { item.klibs.forEach(klibID => collections.push(`zentao:${klibID}`)); } const promptParts = []; if(item.roleName) { const prefix = lang.teammatePromptPrefix; promptParts.push(`${prefix}${item.roleName}`); } if(item.desc) promptParts.push(item.desc); if(item.klibNames && item.klibNames.length) { const klibNamesStr = item.klibNames.join(', '); const knowledgePrefix = lang.teammateKnowledgePrefix; const knowledgeSuffix = lang.teammateKnowledgeSuffix; promptParts.push(`${knowledgePrefix}${klibNamesStr}${knowledgeSuffix}`); } const data = { prompt: promptParts.join(', '), }; if(collections.length) data.memory = {collections}; return { code: `zentao-aiteammate-${item.id}`, title: item.name, hint: item.desc || item.name, data, llm: item.llm || undefined }; }); return items; }, }); } function registerZentaoAIPlugin(lang) { const plugin = zui.AIPlugin.define('zentao', {name: lang.name, icon: 'zentao'}); plugin.defineContextProvider( { code: 'currentPage', title: lang.currentPage, icon: 'globe', recommend: true, when: () => $.apps, data: () => { const pageWindow = $.apps.getLastApp().iframe.contentWindow; const page$ = pageWindow.$; const $mainContainer = page$('#mainContainer'); const pageContent = $mainContainer.length ? $mainContainer.text() : page$('body').text(); return { prompt: [ `当前页面标题:${document.title}`, "当前页面内容:", pageContent ].join('\n\n') }; }, generate: ({userPrompt}) => { if(new RegExp(`@(${lang.currentPage})`, 'i').test(userPrompt)) return {}; } }); const objectIcons = { story : 'file-text', demand : 'file-text', bug : 'bug', doc : 'doc', design : 'design', feedback: 'feedback', }; const zentaoVersion = window.config?.version || ''; const [_, zentaoEdition] = zentaoVersion.match(/^([a-zA-Z]+)?(\d+\.\d+(\.\d+)?)$/) || []; ['story', 'demand', 'bug', 'doc', 'design', 'feedback'].forEach(objectType => { if(objectType === 'feedback' && !zentaoEdition) return; if(objectType === 'demand' && zentaoEdition !== 'ipd') return; plugin.defineContextProvider({ code: `${objectType}Lib`, title: lang[objectType], icon: objectIcons[objectType], when: ({store}) => !!store.globalMemory, data: { memory: {collections: ['zentao:global'], content_filter: {attrs: {objectType}}}, }, generate: ({userPrompt}) => { const objectName = lang[objectType] || objectType; const matches = [...userPrompt.matchAll(new RegExp(`@(${objectName}${objectType !== objectName ? `|${objectType}` : ''})\\s?#?(\\d+)`, 'gi'))]; if(matches.length) { return matches.map(match => { const objectID = match[2]; return { code: `${objectType}-${objectID}`, recommend: true, title: `${objectName} #${objectID}`, data: () => ({ memory: { collections: ['zentao:global'], content_filter: {attrs: {objectKey: `${objectType}-${objectID}`}}, }, }) }; }); } if(new RegExp(`@(${objectName}${objectType !== objectName ? `|${objectType}` : ''})`, 'i').test(userPrompt)) return {}; } }) }); plugin.defineContextProvider( { code : 'currentDocContent', title : lang.currentDocContent, icon : 'doc', recommend: true, hidden : true, when: () => { if(!window.config) return; const pageWindow = $.apps.getLastApp().iframe.contentWindow; const page$ = pageWindow.$; const editor = page$("[z-use-editor]").zui(); return !!editor; }, data: async () => { const pageWindow = $.apps.getLastApp().iframe.contentWindow; const page$ = pageWindow.$; const editor = page$("[z-use-editor]").zui(); const html = await editor.getHtml(); const text = $(html).text(); return {prompt: ["当前文档内容:", text].join('\n\n')}; }, generate: ({userPrompt}) => { if (new RegExp(`@(${lang.currentDocContent})`, 'i').test(userPrompt)) return {}; } }); plugin.defineContextProvider({ code : 'globalMemory', title: lang.globalMemoryTitle, icon : 'book', when : context => !!context.store.globalMemory, data : {memory: {collections: ['zentao:global']}}, }); if(lang.knowledgeLib) { plugin.defineContextProvider({ code : 'knowledgeLibs', title: lang.knowledgeLib, icon : 'book', contexts : function() { return new Promise((resolve) => { zui.Modal.open({url: $.createLink('ai', 'selectknowledgelib', `selectedID=&callback=getKnowledgeLibsByForm`), size: 'sm'}); window.getKnowledgeLibsByForm = function(libs) { if(!libs.length) return resolve(); const res = []; libs.forEach(item => { res.push({ title: item.name, hint: item.name, code: `zentao-knowledgeLib-${item.id}`, data: { memory: {collections: [`zentao:${item.id}`]} } }) }); resolve(res); } }); }, }); } window.enableAITeammate && loadAndRegisterAiTeammates(lang, plugin); plugin.defineSuggestion( { when: ({state}) => { const page = state ? state.zentaoPage : null; if(!page) return; const openedApp = $.apps.openedApps[page.app]; if(!openedApp) return; const aiSuggestions = openedApp.iframe.contentWindow ? openedApp.iframe.contentWindow.aiSuggestions : null; return Array.isArray(aiSuggestions) && aiSuggestions.length; }, items: function({state}) { const zentaoPage = state ? state.zentaoPage : null; if(!zentaoPage) return; const openedApp = $.apps.openedApps[zentaoPage.app]; if(!openedApp) return; const aiSuggestions = openedApp.iframe.contentWindow ? openedApp.iframe.contentWindow.aiSuggestions : null; return aiSuggestions.map(suggestion => { const {page = '', zentaoAgent, ...others} = suggestion; const pageList = page.split(',').filter(Boolean); if(pageList && !pageList.some(x => x === zentaoPage.path || x === zentaoPage.currentModule)) return; return { ...others, ...(zentaoAgent ? {action: () => callZentaoAgent(zentaoAgent.agentID, zentaoAgent.objectID)} : {}), }; }).filter(Boolean); } }); plugin.defineCallback('onCreateChat', async function(info) { if(info.isLocal) return; const originMemories = info.options.memories; if(!originMemories || !originMemories.length) return; const knowledgeLibs = {}; const otherMemories = originMemories.reduce((others, memory) => { const ohterCollections = []; for(const collection of memory.collections) { if(collection.startsWith('zentao:')) { const lib = collection.substr(7); const newFilter = $.extend(true, {}, memory.content_filter); if(!Object.keys(newFilter).length) { knowledgeLibs[lib] = {}; break;; } const oldFilter = knowledgeLibs[lib] ? knowledgeLibs[lib] : null; const finalFilter = $.extend(true, {}, oldFilter, newFilter); if(newFilter && newFilter.attrs && oldFilter && oldFilter.attrs) { Object.keys(oldFilter.attrs).forEach(attrName => { const oldAttr = oldFilter.attrs[attrName]; const newAttr = newFilter.attrs[attrName]; if(oldAttr === undefined || newAttr === undefined) return; const finalAttr = typeof oldAttr === 'object' ? oldAttr : {$in: [oldAttr]}; if(typeof newAttr === 'object') finalAttr.$in = [...finalAttr.$in, ...newAttr.$in]; else finalAttr.$in = [...finalAttr.$in, newAttr]; finalFilter.attrs[attrName] = finalAttr; }); } knowledgeLibs[lib] = finalFilter; continue; } ohterCollections.push(collection); } if(ohterCollections.length) others.push($.extend({}, memory, {collections: ohterCollections})); return others; }, []); if(!Object.keys(knowledgeLibs).length) return; return {memories: otherMemories, customData: {ztklibs: knowledgeLibs}}; }); plugin.defineCallback('onPostMessage', async function(info) { if(!info.postingMessages || !info.postingMessages.length) return; if(!info.chat.custom_data || !info.chat.custom_data.ztklibs) return; const userPrompts = []; const systemPrompts = []; info.postingMessages.forEach(x => { if(x.role === 'user') userPrompts.push(x.content); else if(x.role === 'system') systemPrompts.push(x.content); }); let searchPrompt = userPrompts.filter(Boolean).join('\n').trim(); if(!searchPrompt.length) searchPrompt = systemPrompts.filter(Boolean).join('\n').trim(); if(!searchPrompt.length) return; info.updateState(lang.searchingKLibs); const ztklibs = info.chat.custom_data.ztklibs; const ztChunks = info.chat.$local.ztChunks || {}; const [response] = await $.ajaxSubmit( { url: $.createLink('zai', 'ajaxSearchKnowledges'), data: {userPrompt: searchPrompt, filters: JSON.stringify(ztklibs)} }); if(response && response.result === 'success' && response.data && Array.isArray(response.data) && response.data.length) { const newPropms = []; const newRefs = []; const refKeys = new Set(); response.data.forEach(item => { if(ztChunks[item.id]) return; ztChunks[item.id] = 1; newPropms.push(item.content); if(refKeys.has(item.key)) return; const itemAttrs = item.attrs || {}; newRefs.push({key: item.key, name: itemAttrs.objectTitle || item.knowledgeTitle, type: itemAttrs.objectType || 'knowledge', id: itemAttrs.objectID || item.knowledgeID}) refKeys.add(item.key); }); info.chat.$local.ztChunks = ztChunks; return {systemPrompt: newPropms.filter(Boolean).join('\n\n'), refs: newRefs}; } }); } /* Bind AI commands in app when app is loaded, example: $(document).on('loadapp.apps updateapp.apps', (e, args) => { const win = (e.type === 'updateapp' ? $.apps.openedApps[args[0]] : args).iframe.contentWindow; bindAICommandsInApp(win, 1000); });*/ function bindAICommandsInApp(win, delay) { if(!win || !win.zui || win._bindedAICommands !== undefined) return; const panel = win.zui.AIPanel.shared; if(!panel) return; if(win._bindedAICommands) clearTimeout(win._bindedAICommands); win._bindedAICommands = setTimeout(() => { win.zui.bindCommands(win.document.body, { commands: {}, scope: panel.commandScope, onCommand: panel.executeCommand.bind(panel) }); win._bindedAICommands = 0; }, delay || 0); } $(() => { if(getZentaoPageType() !== 'home') return bindAICommandsInApp(window); const zentaoConfig = window.config if(!zentaoConfig || zentaoConfig.currentModule !== 'index' || zentaoConfig.currentMethod !== 'index') return; const zaiConfig = window.zai || window.top.zai; const isOpenVersion = /^\d/.test(config.version || $('#zuiCSS').attr('href').split('?v=').pop()); if(zaiConfig) { registerZentaoAIPlugin(zaiLang); let userAvatarProps; const getAvatar = (type, info) => { if(type === 'role' && info.role === 'user') { if(userAvatarProps) return userAvatarProps; const $avatar = $.apps.getLastApp().iframe?.contentWindow.$('#userMenu-toggle>.avatar'); if($avatar?.length) { userAvatarProps = { text : $avatar.find('.avatar-text').text(), code : window.config.account, src : $avatar.find('img').attr('src'), icon : undefined, background: $avatar.css('backgroundColor'), foreColor : $avatar.css('color'), }; } return userAvatarProps; } if(type === 'chat' && info.chat.teammate) { const teammate = zaiConfig.teammateMap[info.chat.teammate] || {id: info.chat.teammate, name: info.chat.teammate}; return {src: teammate.avatar, size: 24, code: teammate.id}; } }; const aiStore = zui.ZAIStore.createFromZentao($.extend({getAvatar: getAvatar}, zaiConfig)); if(!aiStore) return zui.AIPanel.init( { store : aiStore, position : {bottom: +window.config.debug > 4 ? 56 : 40, right: 16}, maximizedPosition: {left: 'calc(var(--zt-menu-width) + 4px)', top: 4, bottom: 'calc(var(--zt-apps-bar-height) + 4px)', right: 16}, langData : zaiLang, getErrorContent: (error) => { let html = ''; if(error.type === 'unauthorized' && zaiLang.unauthorizedError) html = zui.formatString(zaiLang.unauthorizedError, {zaiConfigUrl: $.createLink('zai', 'setting')}) else if(error.type === 'configNotValid' && zaiLang.zaiConfigNotValid) html = zui.formatString(zaiLang.zaiConfigNotValid, {zaiConfigUrl: $.createLink('zai', 'setting')}) if(html.length) return {html: `