romanlesnikov 1 месяц назад
Родитель
Сommit
24b3c85c36
3 измененных файлов с 144 добавлено и 177 удалено
  1. 70 75
      inc/classes/BxDolAI.php
  2. 35 39
      inc/classes/BxDolAIQuery.php
  3. 39 63
      template/scripts/BxBaseServices.php

+ 70 - 75
inc/classes/BxDolAI.php

@@ -488,27 +488,31 @@ class BxDolAI extends BxDolFactory implements iBxDolSingleton
     }
 
     /**
-     * Guest threads: `g:{clientKey}` (NEO header) or `s:{memberSession}` (UNA cookie).
-     * Members: profile id. Guest key is kept in session and adopted on login.
+     * Guests: UNA session id. Members: profile id.
+     * Guest chats remember agent ids in session so login can adopt those threads.
      */
-    public function resolveChatHistoryParams()
+    public function resolveChatHistoryParams($iAgentId = 0)
     {
+        $iAgentId = (int)$iAgentId;
         $iProfileId = (int)bx_get_logged_profile_id();
         $oSession = BxDolSession::getInstance();
-        $sHeaderKey = $this->readAiGuestKeyFromRequest();
-        $sSessionKey = 'ai_chat_guest_subindex';
+        $sSessionKey = 'ai_chat_agent_ids';
 
         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))
+            $aIds = $oSession->getValue($sSessionKey);
+            if (is_array($aIds) && $aIds) {
+                $sSessionId = (string)$oSession->getId();
+                if ($iAgentId)
+                    $aIds[] = $iAgentId;
+                foreach (array_unique(array_map('intval', $aIds)) as $iStoredAgentId) {
+                    if (!$iStoredAgentId || $sSessionId === '')
+                        continue;
+                    $aAgent = BxDolAiQuery::getAgentObject($iStoredAgentId);
+                    if ($aAgent)
+                        $this->_oDb->adoptGuestChatHistory($aAgent, $sSessionId, $iProfileId);
+                }
                 $oSession->unsetValue($sSessionKey);
+            }
 
             return [
                 'sender_profile_id' => $iProfileId,
@@ -516,21 +520,25 @@ class BxDolAI extends BxDolFactory implements iBxDolSingleton
             ];
         }
 
-        $oSession->start(true);
-        $sSessionId = (string)$oSession->getId();
-        $sSubindex = $sHeaderKey ? ('g:' . $sHeaderKey) : ('s:' . ($sSessionId !== '' ? $sSessionId : '0'));
-        $oSession->setValue($sSessionKey, $sSubindex);
+        if ($iAgentId) {
+            $aIds = $oSession->getValue($sSessionKey);
+            $aIds = array_map('intval', is_array($aIds) ? $aIds : []);
+            if (!in_array($iAgentId, $aIds, true)) {
+                $aIds[] = $iAgentId;
+                $oSession->setValue($sSessionKey, $aIds);
+            }
+        }
 
         return [
             'sender_profile_id' => 0,
-            'chat_history_subindex' => $sSubindex,
+            'chat_history_subindex' => (string)$oSession->getId(),
         ];
     }
 
     /**
      * `{trigger}:{agentId}:{contextPid}:{userSubindex}`.
      * Context is omitted when 0 so existing site-wide threads still load.
-     * User suffix stays last so adoptGuestChatHistory keeps matching.
+     * User suffix (session id or profile id) stays last so adopt can replace it.
      */
     public static function chatHistoryThreadId($aAgent, $aParams = [])
     {
@@ -544,10 +552,11 @@ class BxDolAI extends BxDolFactory implements iBxDolSingleton
     }
 
     /**
-     * Context from chat HTTP (`?context=`). Missing/0 = site-wide. Invalid/unviewable = false (403).
+     * Context from chat HTTP (`?context=`). Missing/0 = site-wide. Invalid/denied = false (403).
+     * Posting in a group is not the same as viewing it; chat requires post by default.
      * @return int|false
      */
-    public function resolveChatHistoryContextPid()
+    public function resolveChatHistoryContextPid($bRequirePost = true)
     {
         $mixed = bx_get('context');
         if ($mixed === false)
@@ -561,55 +570,69 @@ class BxDolAI extends BxDolFactory implements iBxDolSingleton
         if (!$oProfile || !bx_srv('system', 'is_module_context', [$oProfile->getModule()]))
             return false;
 
-        if ($oProfile->checkAllowedProfileView() !== CHECK_ACTION_RESULT_ALLOWED)
+        if (!$this->isAllowedContextChat($oProfile, $bRequirePost))
             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.
+     * Context of the current page (AI agent block).
+     * `bx_get_page_info()` reads the URL; App page JSON merges `i`/`id` into $_GET instead.
+     * @param bool $bRequirePost also require posting into the context (membership / post ACL)
      */
-    public function resolveChatHistoryContextPidFromPage()
+    public function resolveChatHistoryContextPidFromPage($bRequirePost = false)
     {
-        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());
-        }
+        $aInfo = bx_get_page_info();
+        if ($aInfo && !empty($aInfo['context_profile_id']))
+            return $this->filterContextPid((int)$aInfo['context_profile_id'], $bRequirePost);
 
-        $oPage = BxDolPage::getObjectInstanceByURI();
-        $sModule = $oPage ? (string)$oPage->getModule() : '';
+        $sUri = bx_get('i');
         $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());
+        if ($sUri && $iContentId > 0) {
+            $oPage = BxDolPage::getObjectInstanceByURI($sUri);
+            $sModule = $oPage ? (string)$oPage->getModule() : '';
+            if ($sModule !== '' && bx_srv('system', 'is_module_context', [$sModule])) {
+                $oProfile = BxDolProfile::getInstanceByContentAndType($iContentId, $sModule);
+                if ($oProfile && ($iPid = $this->filterContextPid((int)$oProfile->id(), $bRequirePost)))
+                    return $iPid;
+            }
         }
 
+        $iPid = (int)bx_process_input(bx_get('profile_id'), BX_DATA_INT);
+        if ($iPid > 0 && ($iPid = $this->filterContextPid($iPid, $bRequirePost)))
+            return $iPid;
+
         return 0;
     }
 
-    protected function filterViewableContextPid($iPid)
+    public function filterContextPid($iPid, $bRequirePost = false)
     {
         $iPid = (int)$iPid;
         if ($iPid <= 0)
             return 0;
 
         $oProfile = BxDolProfile::getInstance($iPid);
-        if (!$oProfile || $oProfile->checkAllowedProfileView() !== CHECK_ACTION_RESULT_ALLOWED)
+        if (!$oProfile || !bx_srv('system', 'is_module_context', [$oProfile->getModule()]))
+            return 0;
+
+        if (!$this->isAllowedContextChat($oProfile, $bRequirePost))
             return 0;
 
         return $iPid;
     }
 
+    protected function isAllowedContextChat($oProfile, $bRequirePost = false)
+    {
+        if ($oProfile->checkAllowedProfileView() !== CHECK_ACTION_RESULT_ALLOWED)
+            return false;
+
+        if ($bRequirePost && $oProfile->checkAllowedPostInProfile() !== CHECK_ACTION_RESULT_ALLOWED)
+            return false;
+
+        return true;
+    }
+
     /**
      * Transcript for TanStack `useChat` hydrate / `initialMessages`.
      * Loads the same NeuronAI chat history the agent uses when streaming.
@@ -617,7 +640,7 @@ class BxDolAI extends BxDolFactory implements iBxDolSingleton
     public function getChatHistoryUiMessages($iAgentId, $aParams = [])
     {
         if (!isset($aParams['chat_history_subindex']))
-            $aParams = array_merge($this->resolveChatHistoryParams(), $aParams);
+            $aParams = array_merge($this->resolveChatHistoryParams($iAgentId), $aParams);
 
         $aAgent = BxDolAiQuery::getAgentObject((int)$iAgentId);
         if (!$aAgent || empty($aAgent['chat_history_context']))
@@ -691,38 +714,10 @@ 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)
     {
         if (!isset($aParams['chat_history_subindex']))
-            $aParams = array_merge($this->resolveChatHistoryParams(), $aParams);
+            $aParams = array_merge($this->resolveChatHistoryParams($iAgentId), $aParams);
 
         $oAdapter = new NeuronAI\Chat\Messages\Stream\Adapters\AGUIAdapter($sThreadId);
 

+ 35 - 39
inc/classes/BxDolAIQuery.php

@@ -987,51 +987,47 @@ class BxDolAIQuery extends BxDolDb
     }
 
     /**
-     * Rename guest threads (`…:g:{key}` / `…:s:{session}`) onto a profile id.
+     * Rename this agent's guest threads (`…:{sessionId}`) onto a profile id.
      * If the member thread already has messages, drop the guest copy.
      */
-    public function adoptGuestChatHistory($sGuestSubindex, $iProfileId)
+    public function adoptGuestChatHistory($aAgent, $sSessionId, $iProfileId)
     {
-        $sGuestSubindex = (string)$sGuestSubindex;
+        $sSessionId = (string)$sSessionId;
         $iProfileId = (int)$iProfileId;
-        if ($sGuestSubindex === '' || !$iProfileId)
+        if ($sSessionId === '' || !$iProfileId || empty($aAgent['id']))
             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,
-            ]);
-        }
+        $sPrefix = $aAgent['trigger'] . ':' . $aAgent['id'] . ':';
+        $sOldSuffix = ':' . $sSessionId;
+        $sNewSuffix = ':' . $iProfileId;
+        $iLen = strlen($sOldSuffix);
+        $aBindings = [
+            'p' => $sPrefix . '%',
+            'o' => $sOldSuffix,
+            'n' => $sNewSuffix,
+        ];
+
+        $this->query("
+            DELETE `g` FROM `sys_agents_chat_history` AS `g`
+            INNER JOIN `sys_agents_chat_history` AS `m`
+                ON `m`.`thread_id` = CONCAT(LEFT(`g`.`thread_id`, CHAR_LENGTH(`g`.`thread_id`) - $iLen), :n)
+            WHERE `g`.`thread_id` LIKE :p AND RIGHT(`g`.`thread_id`, $iLen) = :o
+              AND `m`.`messages` IS NOT NULL AND `m`.`messages` != '' AND `m`.`messages` != '[]'
+        ", $aBindings);
+
+        $this->query("
+            DELETE `m` FROM `sys_agents_chat_history` AS `m`
+            INNER JOIN `sys_agents_chat_history` AS `g`
+                ON `g`.`thread_id` LIKE :p AND RIGHT(`g`.`thread_id`, $iLen) = :o
+               AND `m`.`thread_id` = CONCAT(LEFT(`g`.`thread_id`, CHAR_LENGTH(`g`.`thread_id`) - $iLen), :n)
+            WHERE `m`.`messages` = '' OR `m`.`messages` = '[]' OR `m`.`messages` IS NULL
+        ", $aBindings);
+
+        $iAffected = (int)$this->query("
+            UPDATE `sys_agents_chat_history`
+            SET `thread_id` = CONCAT(LEFT(`thread_id`, CHAR_LENGTH(`thread_id`) - $iLen), :n)
+            WHERE `thread_id` LIKE :p AND RIGHT(`thread_id`, $iLen) = :o
+        ", $aBindings);
 
         return $iAffected;
     }

+ 39 - 63
template/scripts/BxBaseServices.php

@@ -853,7 +853,7 @@ class BxBaseServices extends BxDol implements iBxDolProfileService
         ]]);
     }
 
-    public function serviceGetBlockAiAgent($iId = 0)
+    public function serviceGetBlockAiAgent($iId = 0, $mixedContextPid = false, $bRequirePost = false)
     {
         $bIsApi = bx_is_api();
 
@@ -861,17 +861,22 @@ class BxBaseServices extends BxDol implements iBxDolProfileService
         if(!$aAgent || !is_array($aAgent))
             return $bIsApi ? [] : '';
 
-        $iContextPid = 0;
         $oAi = BxDolAI::getInstance();
-        if ($oAi)
-            $iContextPid = (int)$oAi->resolveChatHistoryContextPidFromPage();
+        $bRequirePost = (bool)$bRequirePost;
+        if ($mixedContextPid === false || $mixedContextPid === null || $mixedContextPid === '')
+            $iContextPid = $oAi ? (int)$oAi->resolveChatHistoryContextPidFromPage($bRequirePost) : 0;
+        else
+            $iContextPid = $oAi ? $oAi->filterContextPid((int)$mixedContextPid, $bRequirePost) : 0;
 
-        return $bIsApi ? [bx_api_get_block('ai_agent', [
+        $aBlock = [
             'agent_id' => $iId,
             'agent_profile' => BxDolProfile::getData($aAgent['profile_id']),
             'context_profile_id' => $iContextPid,
-        ])] : BxDolTemplate::getInstance()->parseHtmlByName('agent_ai_chat.html', [
-            'id' => $iId
+        ];
+
+        return $bIsApi ? [bx_api_get_block('ai_agent', $aBlock)] : BxDolTemplate::getInstance()->parseHtmlByName('agent_ai_chat.html', [
+            'id' => $iId,
+            'context_profile_id' => $iContextPid,
         ]);
     }
 
@@ -1816,72 +1821,58 @@ class BxBaseServices extends BxDol implements iBxDolProfileService
     public function serviceAiChat($iAgentId)
     {
         $sMethod = $_SERVER['REQUEST_METHOD'] ?? '';
-        if ('GET' === $sMethod)
-            return $this->_aiChatHydrate($iAgentId);
+        if ('GET' !== $sMethod && 'POST' !== $sMethod)
+            return echoJson(['code' => 405, 'msg' => _t('_error occured')]);
 
-        if ('POST' !== $sMethod)
+        if ('GET' === $sMethod && false !== bx_get('offset'))
             return echoJson(['code' => 405, 'msg' => _t('_error occured')]);
 
         $aAgent = $this->_aiChatLoadAgent($iAgentId);
         if (!$aAgent)
-            return;
+            return echoJson(['code' => (int)$iAgentId ? 404 : 400, 'msg' => _t('_sys_agents_agent_not_found')]);
+
+        $oAi = BxDolAI::getInstance();
+        if (!$oAi)
+            return echoJson(['code' => 503, 'msg' => _t('_sys_agents_exception')]);
+
+        if (!$oAi->canChatDirectly($aAgent))
+            return echoJson(['code' => 403, 'msg' => _t('_sys_agents_unauthorized')]);
+
+        $aParams = $this->_aiChatHistoryParams($aAgent);
+        if (!$aParams)
+            return echoJson(['code' => 403, 'msg' => _t('_sys_agents_unauthorized')]);
+
+        if ('GET' === $sMethod)
+            return echoJson([
+                'messages' => $oAi->getChatHistoryUiMessages($aAgent['id'], $aParams),
+                'activeRun' => null,
+                'interrupts' => null,
+            ]);
 
         $sJson = file_get_contents('php://input');
         $aData = json_decode($sJson, true);
         if (json_last_error() !== JSON_ERROR_NONE)
             return echoJson(['code' => 400, 'msg' => _t('_sys_agents_json_field_err')]);
 
-        $oAi = BxDolAI::getInstance();
         $sPrompt = $oAi->extractChatPromptFromRequest($aData);
         if (!$sPrompt)
             return echoJson(['code' => 400, 'msg' => _t('_sys_agents_json_field_err')]);
 
-        $aParams = $this->_aiChatHistoryParams();
-        if (!$aParams)
-            return;
-
         $oAi->streamAgentChat($aAgent['id'], $sPrompt, $aParams, $aData['threadId'] ?? null);
     }
 
-    /**
-     * GET hydrate for TanStack `useChat({ persistence: true })`.
-     * Same route as the SSE POST; join/resume (`?offset=`) is not supported.
-     */
-    protected function _aiChatHydrate($iAgentId)
-    {
-        if (false !== bx_get('offset'))
-            return echoJson(['code' => 405, 'msg' => _t('_error occured')]);
-
-        $aAgent = $this->_aiChatLoadAgent($iAgentId);
-        if (!$aAgent)
-            return;
-
-        $oAi = BxDolAI::getInstance();
-        $aParams = $this->_aiChatHistoryParams();
-        if (!$aParams)
-            return;
-
-        return echoJson([
-            '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()
+    protected function _aiChatHistoryParams($aAgent)
     {
         $oAi = BxDolAI::getInstance();
         $mixedContext = $oAi->resolveChatHistoryContextPid();
-        if ($mixedContext === false) {
-            echoJson(['code' => 403, 'msg' => _t('_sys_agents_unauthorized')]);
+        if ($mixedContext === false)
             return false;
-        }
 
-        $aParams = $oAi->resolveChatHistoryParams();
+        $aParams = $oAi->resolveChatHistoryParams((int)$aAgent['id']);
         $aParams['chat_history_context_pid'] = (int)$mixedContext;
         return $aParams;
     }
@@ -1889,27 +1880,12 @@ class BxBaseServices extends BxDol implements iBxDolProfileService
     protected function _aiChatLoadAgent($iAgentId)
     {
         $iAgentId = (int)$iAgentId;
-        if (!$iAgentId) {
-            echoJson(['code' => 400, 'msg' => _t('_sys_agents_agent_not_found')]);
+        if (!$iAgentId)
             return false;
-        }
 
         $aAgent = BxDolAiQuery::getAgentObject($iAgentId);
-        if (!$aAgent || !$aAgent['active']) {
-            echoJson(['code' => 404, 'msg' => _t('_sys_agents_agent_not_found')]);
+        if (!$aAgent || !$aAgent['active'])
             return false;
-        }
-
-        $oAi = BxDolAI::getInstance();
-        if (!$oAi) {
-            echoJson(['code' => 503, 'msg' => _t('_sys_agents_exception')]);
-            return false;
-        }
-
-        if (!$oAi->canChatDirectly($aAgent)) {
-            echoJson(['code' => 403, 'msg' => _t('_sys_agents_unauthorized')]);
-            return false;
-        }
 
         return $aAgent;
     }