|
@@ -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();
|
|
$iProfileId = (int)bx_get_logged_profile_id();
|
|
|
$oSession = BxDolSession::getInstance();
|
|
$oSession = BxDolSession::getInstance();
|
|
|
- $sHeaderKey = $this->readAiGuestKeyFromRequest();
|
|
|
|
|
- $sSessionKey = 'ai_chat_guest_subindex';
|
|
|
|
|
|
|
+ $sSessionKey = 'ai_chat_agent_ids';
|
|
|
|
|
|
|
|
if ($iProfileId) {
|
|
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);
|
|
$oSession->unsetValue($sSessionKey);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
return [
|
|
return [
|
|
|
'sender_profile_id' => $iProfileId,
|
|
'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 [
|
|
return [
|
|
|
'sender_profile_id' => 0,
|
|
'sender_profile_id' => 0,
|
|
|
- 'chat_history_subindex' => $sSubindex,
|
|
|
|
|
|
|
+ 'chat_history_subindex' => (string)$oSession->getId(),
|
|
|
];
|
|
];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* `{trigger}:{agentId}:{contextPid}:{userSubindex}`.
|
|
* `{trigger}:{agentId}:{contextPid}:{userSubindex}`.
|
|
|
* Context is omitted when 0 so existing site-wide threads still load.
|
|
* 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 = [])
|
|
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
|
|
* @return int|false
|
|
|
*/
|
|
*/
|
|
|
- public function resolveChatHistoryContextPid()
|
|
|
|
|
|
|
+ public function resolveChatHistoryContextPid($bRequirePost = true)
|
|
|
{
|
|
{
|
|
|
$mixed = bx_get('context');
|
|
$mixed = bx_get('context');
|
|
|
if ($mixed === false)
|
|
if ($mixed === false)
|
|
@@ -561,55 +570,69 @@ class BxDolAI extends BxDolFactory implements iBxDolSingleton
|
|
|
if (!$oProfile || !bx_srv('system', 'is_module_context', [$oProfile->getModule()]))
|
|
if (!$oProfile || !bx_srv('system', 'is_module_context', [$oProfile->getModule()]))
|
|
|
return false;
|
|
return false;
|
|
|
|
|
|
|
|
- if ($oProfile->checkAllowedProfileView() !== CHECK_ACTION_RESULT_ALLOWED)
|
|
|
|
|
|
|
+ if (!$this->isAllowedContextChat($oProfile, $bRequirePost))
|
|
|
return false;
|
|
return false;
|
|
|
|
|
|
|
|
return $iPid;
|
|
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);
|
|
$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;
|
|
return 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- protected function filterViewableContextPid($iPid)
|
|
|
|
|
|
|
+ public function filterContextPid($iPid, $bRequirePost = false)
|
|
|
{
|
|
{
|
|
|
$iPid = (int)$iPid;
|
|
$iPid = (int)$iPid;
|
|
|
if ($iPid <= 0)
|
|
if ($iPid <= 0)
|
|
|
return 0;
|
|
return 0;
|
|
|
|
|
|
|
|
$oProfile = BxDolProfile::getInstance($iPid);
|
|
$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 0;
|
|
|
|
|
|
|
|
return $iPid;
|
|
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`.
|
|
* Transcript for TanStack `useChat` hydrate / `initialMessages`.
|
|
|
* Loads the same NeuronAI chat history the agent uses when streaming.
|
|
* 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 = [])
|
|
public function getChatHistoryUiMessages($iAgentId, $aParams = [])
|
|
|
{
|
|
{
|
|
|
if (!isset($aParams['chat_history_subindex']))
|
|
if (!isset($aParams['chat_history_subindex']))
|
|
|
- $aParams = array_merge($this->resolveChatHistoryParams(), $aParams);
|
|
|
|
|
|
|
+ $aParams = array_merge($this->resolveChatHistoryParams($iAgentId), $aParams);
|
|
|
|
|
|
|
|
$aAgent = BxDolAiQuery::getAgentObject((int)$iAgentId);
|
|
$aAgent = BxDolAiQuery::getAgentObject((int)$iAgentId);
|
|
|
if (!$aAgent || empty($aAgent['chat_history_context']))
|
|
if (!$aAgent || empty($aAgent['chat_history_context']))
|
|
@@ -691,38 +714,10 @@ class BxDolAI extends BxDolFactory implements iBxDolSingleton
|
|
|
return $aResult;
|
|
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)
|
|
public function streamAgentChat($iAgentId, $sPrompt, $aParams = [], $sThreadId = null)
|
|
|
{
|
|
{
|
|
|
if (!isset($aParams['chat_history_subindex']))
|
|
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);
|
|
$oAdapter = new NeuronAI\Chat\Messages\Stream\Adapters\AGUIAdapter($sThreadId);
|
|
|
|
|
|