Просмотр исходного кода

Merge branch 'master' of https://github.com/unacms/una

AntonLV 1 месяц назад
Родитель
Сommit
44dba340e8

+ 156 - 3
inc/classes/BxDolAI.php

@@ -487,19 +487,143 @@ class BxDolAI extends BxDolFactory implements iBxDolSingleton
         return '';
     }
 
+    /**
+     * Guest threads: `g:{clientKey}` (NEO header) or `s:{memberSession}` (UNA cookie).
+     * Members: profile id. Guest key is kept in session and adopted on login.
+     */
+    public function resolveChatHistoryParams()
+    {
+        $iProfileId = (int)bx_get_logged_profile_id();
+        $oSession = BxDolSession::getInstance();
+        $sHeaderKey = $this->readAiGuestKeyFromRequest();
+        $sSessionKey = 'ai_chat_guest_subindex';
+
+        if ($iProfileId) {
+            $sGuest = $sHeaderKey ? ('g:' . $sHeaderKey) : '';
+            if ($sGuest === '') {
+                $sStored = $oSession->getValue($sSessionKey);
+                if ($this->isAiGuestSubindex($sStored))
+                    $sGuest = $sStored;
+            }
+            if ($sGuest !== '')
+                $this->_oDb->adoptGuestChatHistory($sGuest, $iProfileId);
+            if ($oSession->isValue($sSessionKey))
+                $oSession->unsetValue($sSessionKey);
+
+            return [
+                'sender_profile_id' => $iProfileId,
+                'chat_history_subindex' => (string)$iProfileId,
+            ];
+        }
+
+        $oSession->start(true);
+        $sSessionId = (string)$oSession->getId();
+        $sSubindex = $sHeaderKey ? ('g:' . $sHeaderKey) : ('s:' . ($sSessionId !== '' ? $sSessionId : '0'));
+        $oSession->setValue($sSessionKey, $sSubindex);
+
+        return [
+            'sender_profile_id' => 0,
+            'chat_history_subindex' => $sSubindex,
+        ];
+    }
+
+    /**
+     * `{trigger}:{agentId}:{contextPid}:{userSubindex}`.
+     * Context is omitted when 0 so existing site-wide threads still load.
+     * User suffix stays last so adoptGuestChatHistory keeps matching.
+     */
+    public static function chatHistoryThreadId($aAgent, $aParams = [])
+    {
+        $s = ($aAgent['trigger'] ?? '') . ':' . ($aAgent['id'] ?? '');
+        $iContextPid = (int)($aParams['chat_history_context_pid'] ?? 0);
+        if ($iContextPid > 0)
+            $s .= ':' . $iContextPid;
+        if (isset($aParams['chat_history_subindex']) && $aParams['chat_history_subindex'] !== '')
+            $s .= ':' . (string)$aParams['chat_history_subindex'];
+        return $s;
+    }
+
+    /**
+     * Context from chat HTTP (`?context=`). Missing/0 = site-wide. Invalid/unviewable = false (403).
+     * @return int|false
+     */
+    public function resolveChatHistoryContextPid()
+    {
+        $mixed = bx_get('context');
+        if ($mixed === false)
+            return 0;
+
+        $iPid = (int)$mixed;
+        if ($iPid <= 0)
+            return 0;
+
+        $oProfile = BxDolProfile::getInstance($iPid);
+        if (!$oProfile || !bx_srv('system', 'is_module_context', [$oProfile->getModule()]))
+            return false;
+
+        if ($oProfile->checkAllowedProfileView() !== CHECK_ACTION_RESULT_ALLOWED)
+            return false;
+
+        return $iPid;
+    }
+
+    /**
+     * Context of the current page (AI agent block JSON).
+     * Prefer `bx_get_page_info()`; API page JSON falls back to $_GET after getPageAPI merges params.
+     */
+    public function resolveChatHistoryContextPidFromPage()
+    {
+        if (function_exists('bx_get_page_info')) {
+            $aInfo = bx_get_page_info();
+            if ($aInfo && !empty($aInfo['context_profile_id']))
+                return $this->filterViewableContextPid((int)$aInfo['context_profile_id']);
+        }
+
+        if (($iPid = (int)bx_process_input(bx_get('profile_id'), BX_DATA_INT)) > 0) {
+            $oProfile = BxDolProfile::getInstance($iPid);
+            if ($oProfile && bx_srv('system', 'is_module_context', [$oProfile->getModule()]))
+                return $this->filterViewableContextPid((int)$oProfile->id());
+        }
+
+        $oPage = BxDolPage::getObjectInstanceByURI();
+        $sModule = $oPage ? (string)$oPage->getModule() : '';
+        $iContentId = (int)bx_process_input(bx_get('id'), BX_DATA_INT);
+        if ($iContentId > 0 && $sModule !== '' && bx_srv('system', 'is_module_context', [$sModule])) {
+            $oProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $sModule);
+            if ($oProfile)
+                return $this->filterViewableContextPid((int)$oProfile->id());
+        }
+
+        return 0;
+    }
+
+    protected function filterViewableContextPid($iPid)
+    {
+        $iPid = (int)$iPid;
+        if ($iPid <= 0)
+            return 0;
+
+        $oProfile = BxDolProfile::getInstance($iPid);
+        if (!$oProfile || $oProfile->checkAllowedProfileView() !== CHECK_ACTION_RESULT_ALLOWED)
+            return 0;
+
+        return $iPid;
+    }
+
     /**
      * Transcript for TanStack `useChat` hydrate / `initialMessages`.
      * Loads the same NeuronAI chat history the agent uses when streaming.
      */
     public function getChatHistoryUiMessages($iAgentId, $aParams = [])
     {
-        $aParams['chat_history_subindex'] = (int)($aParams['sender_profile_id'] ?? 0);
+        if (!isset($aParams['chat_history_subindex']))
+            $aParams = array_merge($this->resolveChatHistoryParams(), $aParams);
 
         $aAgent = BxDolAiQuery::getAgentObject((int)$iAgentId);
         if (!$aAgent || empty($aAgent['chat_history_context']))
             return [];
 
-        $sThreadId = $aAgent['trigger'] . ':' . $aAgent['id'] . ':' . $aParams['chat_history_subindex'];
+        $sThreadId = self::chatHistoryThreadId($aAgent, $aParams);
         $sJson = $this->_oDb->getOne("SELECT `messages` FROM `sys_agents_chat_history` WHERE `thread_id` = :t", [
             't' => $sThreadId,
         ]);
@@ -567,9 +691,38 @@ class BxDolAI extends BxDolFactory implements iBxDolSingleton
         return $aResult;
     }
 
+    protected function readAiGuestKeyFromRequest()
+    {
+        $s = $_SERVER['HTTP_X_UNA_AI_GUEST_KEY'] ?? '';
+        if ($s === '' && function_exists('apache_request_headers')) {
+            foreach (apache_request_headers() as $sName => $sValue) {
+                if (strtolower((string)$sName) === 'x-una-ai-guest-key') {
+                    $s = $sValue;
+                    break;
+                }
+            }
+        }
+        return $this->sanitizeAiGuestKey($s);
+    }
+
+    protected function sanitizeAiGuestKey($s)
+    {
+        $s = is_string($s) ? $s : '';
+        return preg_match('/^[A-Za-z0-9_-]{16,64}$/', $s) ? $s : '';
+    }
+
+    protected function isAiGuestSubindex($s)
+    {
+        $s = is_string($s) ? $s : '';
+        if ($s === '' || (strncmp($s, 'g:', 2) !== 0 && strncmp($s, 's:', 2) !== 0))
+            return false;
+        return $this->sanitizeAiGuestKey(substr($s, 2)) !== '';
+    }
+
     public function streamAgentChat($iAgentId, $sPrompt, $aParams = [], $sThreadId = null)
     {
-        $aParams['chat_history_subindex'] = (int)($aParams['sender_profile_id'] ?? 0);
+        if (!isset($aParams['chat_history_subindex']))
+            $aParams = array_merge($this->resolveChatHistoryParams(), $aParams);
 
         $oAdapter = new NeuronAI\Chat\Messages\Stream\Adapters\AGUIAdapter($sThreadId);
 

+ 50 - 0
inc/classes/BxDolAIQuery.php

@@ -986,6 +986,56 @@ class BxDolAIQuery extends BxDolDb
         return $iAffected;
     }
 
+    /**
+     * Rename guest threads (`…:g:{key}` / `…:s:{session}`) onto a profile id.
+     * If the member thread already has messages, drop the guest copy.
+     */
+    public function adoptGuestChatHistory($sGuestSubindex, $iProfileId)
+    {
+        $sGuestSubindex = (string)$sGuestSubindex;
+        $iProfileId = (int)$iProfileId;
+        if ($sGuestSubindex === '' || !$iProfileId)
+            return 0;
+
+        $aRows = $this->getAll("SELECT `thread_id` FROM `sys_agents_chat_history`");
+        if (!$aRows)
+            return 0;
+
+        $sSuffix = ':' . $sGuestSubindex;
+        $iSuffixLen = strlen($sSuffix);
+        $iAffected = 0;
+        foreach ($aRows as $aRow) {
+            $sOld = $aRow['thread_id'] ?? '';
+            if ($sOld === '' || substr($sOld, -$iSuffixLen) !== $sSuffix)
+                continue;
+
+            $sNew = substr($sOld, 0, -$iSuffixLen) . ':' . $iProfileId;
+            if ($sNew === $sOld)
+                continue;
+
+            $sExisting = $this->getOne("SELECT `messages` FROM `sys_agents_chat_history` WHERE `thread_id` = :t", [
+                't' => $sNew,
+            ]);
+            $aExisting = json_decode((string)$sExisting, true);
+            if (is_array($aExisting) && $aExisting !== []) {
+                $iAffected += (int)$this->query("DELETE FROM `sys_agents_chat_history` WHERE `thread_id` = :t", [
+                    't' => $sOld,
+                ]);
+                continue;
+            }
+
+            if ($sExisting !== false && $sExisting !== null)
+                $this->query("DELETE FROM `sys_agents_chat_history` WHERE `thread_id` = :t", ['t' => $sNew]);
+
+            $iAffected += (int)$this->query("UPDATE `sys_agents_chat_history` SET `thread_id` = :n WHERE `thread_id` = :o", [
+                'n' => $sNew,
+                'o' => $sOld,
+            ]);
+        }
+
+        return $iAffected;
+    }
+
     static public function getAlert($s) {
         if (!$s)
             return false;        

+ 1 - 4
inc/classes/BxDolAiAgent.php

@@ -123,10 +123,7 @@ class BxDolAiAgent extends RAG
 
     protected function getСhatHistoryThreadId(): string
     {
-        $s = $this->aAgent['trigger'] . ':' . $this->aAgent['id'];
-        if (isset($this->aParams['chat_history_subindex']))
-            $s .=  ':' . $this->aParams['chat_history_subindex'];
-        return $s;
+        return BxDolAI::chatHistoryThreadId($this->aAgent, $this->aParams);
     }
 
     protected function getAlertTriggerModifyableKeys(): string

+ 38 - 7
template/scripts/BxBaseServices.php

@@ -861,7 +861,16 @@ class BxBaseServices extends BxDol implements iBxDolProfileService
         if(!$aAgent || !is_array($aAgent))
             return $bIsApi ? [] : '';
 
-        return $bIsApi ? [bx_api_get_block('ai_agent', ['agent_id' => $iId])] : BxDolTemplate::getInstance()->parseHtmlByName('agent_ai_chat.html', [
+        $iContextPid = 0;
+        $oAi = BxDolAI::getInstance();
+        if ($oAi)
+            $iContextPid = (int)$oAi->resolveChatHistoryContextPidFromPage();
+
+        return $bIsApi ? [bx_api_get_block('ai_agent', [
+            'agent_id' => $iId,
+            'agent_profile' => BxDolProfile::getData($aAgent['profile_id']),
+            'context_profile_id' => $iContextPid,
+        ])] : BxDolTemplate::getInstance()->parseHtmlByName('agent_ai_chat.html', [
             'id' => $iId
         ]);
     }
@@ -1827,9 +1836,11 @@ class BxBaseServices extends BxDol implements iBxDolProfileService
         if (!$sPrompt)
             return echoJson(['code' => 400, 'msg' => _t('_sys_agents_json_field_err')]);
 
-        $oAi->streamAgentChat($aAgent['id'], $sPrompt, [
-            'sender_profile_id' => bx_get_logged_profile_id(),
-        ], $aData['threadId'] ?? null);
+        $aParams = $this->_aiChatHistoryParams();
+        if (!$aParams)
+            return;
+
+        $oAi->streamAgentChat($aAgent['id'], $sPrompt, $aParams, $aData['threadId'] ?? null);
     }
 
     /**
@@ -1846,15 +1857,35 @@ class BxBaseServices extends BxDol implements iBxDolProfileService
             return;
 
         $oAi = BxDolAI::getInstance();
+        $aParams = $this->_aiChatHistoryParams();
+        if (!$aParams)
+            return;
+
         return echoJson([
-            'messages' => $oAi->getChatHistoryUiMessages($aAgent['id'], [
-                'sender_profile_id' => bx_get_logged_profile_id(),
-            ]),
+            'messages' => $oAi->getChatHistoryUiMessages($aAgent['id'], $aParams),
             'activeRun' => null,
             'interrupts' => null,
         ]);
     }
 
+    /**
+     * Member/guest subindex + optional `?context=` (group/org profile id).
+     * Invalid context is 403 — never fall back to the site-wide thread.
+     */
+    protected function _aiChatHistoryParams()
+    {
+        $oAi = BxDolAI::getInstance();
+        $mixedContext = $oAi->resolveChatHistoryContextPid();
+        if ($mixedContext === false) {
+            echoJson(['code' => 403, 'msg' => _t('_sys_agents_unauthorized')]);
+            return false;
+        }
+
+        $aParams = $oAi->resolveChatHistoryParams();
+        $aParams['chat_history_context_pid'] = (int)$mixedContext;
+        return $aParams;
+    }
+
     protected function _aiChatLoadAgent($iAgentId)
     {
         $iAgentId = (int)$iAgentId;