浏览代码

Mockup blocks: key JSON-stored blocks by id, stop silent truncation (for #5700) (#5716)

* AI agents: mockup_upsert tool and get_mockup_block service block for NEO mockups

- BxDolAIToolMockup (`mockup_upsert`): agent saves a normalized JSON tree
  (view/row/text/image/icon/button) into sys_pages_blocks_data for an existing
  `system / get_mockup_block / TemplServices` service block; refuses other blocks,
  drops unknown/empty nodes, limits button href to relative, http(s), mailto.
- system/get_mockup_block returns the tree as a `mockup` API block (placeholder
  when nothing is saved yet).
- BxBasePage::_getBlockService keys get_mockup_block calls by block id so several
  mockup blocks on one page do not share BxDolService's memory cache.
- install/sql/system.sql: mockup_upsert tool row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Mockup blocks: key JSON-stored blocks by id, stop silent truncation

- BxBasePage::_getBlockService keyed get_mockup_block calls by block id
  only for PHP-serialized block content, read with a bare @unserialize().
  New service blocks are stored as JSON (BxDolServiceCallCodec, since
  dce212e70a), so for them the rewrite never happened and every mockup
  block on a page got the first block's tree again. Decode and re-encode
  through the codec, which also keeps objects out of unserialize().
- BxDolAIToolMockup::saveTree: sys_pages_blocks_data.data is TEXT
  (65,535 bytes) while the tool accepted up to 200,000 bytes, and the tree
  was stored with every non-Latin character escaped as \uXXXX (6 bytes).
  With a non-strict sql_mode MariaDB cut the JSON off: the tool said
  "saved" and the block showed nothing. Store it unescaped (a Russian
  300-line page drops from 80 KB to 38 KB) and refuse anything over the
  column size with an error the model can act on.
- saveTree: saving an identical tree again returned "failed to write",
  because an unchanged upsert affects 0 rows; only false is a failure now.
- isMockupBlock: decode the block's service call and compare module and
  method instead of searching the raw content for the method name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* no unnecessary class_exists

* fix field size

---------

Co-authored-by: romanlesnikov <roman.lesnikov@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Aleksandr Trofimov <at@unacms.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Andrey Yasko 2 周之前
父节点
当前提交
1a2507cb56
共有 3 个文件被更改,包括 19 次插入 和 9 次删除
  1. 13 3
      inc/classes/BxDolAIToolMockup.php
  2. 1 1
      install/sql/system.sql
  3. 5 5
      template/scripts/BxBasePage.php

+ 13 - 3
inc/classes/BxDolAIToolMockup.php

@@ -21,6 +21,8 @@ class BxDolAIToolMockup extends BxDolAITool
 {
     const MAX_DEPTH = 24;
     const MAX_JSON = 200000;
+    /** sys_pages_blocks_data.data is MEDIUMTEXT: a longer tree is cut off and no longer parses. */
+    const MAX_STORED = 16777215;
 
     public function __construct()
     {
@@ -93,7 +95,8 @@ class BxDolAIToolMockup extends BxDolAITool
         $sContent = BxDolDb::getInstance()->getOne("SELECT `content` FROM `sys_pages_blocks` WHERE `id` = :id AND `type` = 'service' LIMIT 1", [
             'id' => $iBlockId,
         ]);
-        return is_string($sContent) && strpos($sContent, 'get_mockup_block') !== false;
+        $aCall = BxDolService::decodeServiceCall($sContent);
+        return is_array($aCall) && ($aCall['module'] ?? '') === 'system' && ($aCall['method'] ?? '') === 'get_mockup_block';
     }
 
     public static function getTree($iBlockId = 0)
@@ -122,9 +125,16 @@ class BxDolAIToolMockup extends BxDolAITool
         if ($iBlockId <= 0)
             return 'error: block_id missing';
 
+        // Unescaped: a non-Latin character costs 2-4 bytes here instead of 6 as \uXXXX.
+        $sData = json_encode($aTree, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
+        if (!is_string($sData))
+            return 'error: the tree cannot be encoded as JSON';
+        if (strlen($sData) > self::MAX_STORED)
+            return 'error: the tree is too large to save (' . strlen($sData) . ' bytes, the limit is ' . self::MAX_STORED . '). Simplify it: fewer nodes, shorter texts and class names.';
+
         $oDb = new BxDolPageQuery([]);
-        $b = $oDb->setPageBlockData($iBlockId, 0, '', $aTree);
-        if (!$b)
+        $b = $oDb->setPageBlockData($iBlockId, 0, '', $sData);
+        if ($b === false)
             return 'error: failed to write sys_pages_blocks_data';
 
         return '';

+ 1 - 1
install/sql/system.sql

@@ -6422,7 +6422,7 @@ CREATE TABLE IF NOT EXISTS `sys_pages_blocks_data` (
   `block_id` int(11) NOT NULL DEFAULT 0,
   `content_id` int(11) NOT NULL DEFAULT 0,
   `content_module` varchar(32) NOT NULL,
-  `data` text NOT NULL,
+  `data` mediumtext NOT NULL,
   PRIMARY KEY (`id`),
   UNIQUE KEY `block` (`block_id`, `content_id`, `content_module`)
 );

+ 5 - 5
template/scripts/BxBasePage.php

@@ -1414,11 +1414,11 @@ class BxBasePage extends BxDolPage
         $sContent = $aBlock['content'];
         // get_mockup_block blocks are stored with empty params, so BxDolService's memory cache would
         // hand every such block on the page the same tree; key the call by block id instead.
-        $aCall = is_string($sContent) ? @unserialize($sContent) : false;
-        if (is_array($aCall) && !empty($aCall['method']) && $aCall['method'] === 'get_mockup_block') {
-            $aCall['params'] = [(int)$aBlock['id']];
-            $sContent = serialize($aCall);
-        }
+        // Read and rewritten through the service-call codec: new blocks are stored as JSON, and
+        // old serialized ones must never be unserialized with objects allowed.
+        $aCall = BxDolService::decodeServiceCall($sContent);
+        if (is_array($aCall) && ($aCall['method'] ?? '') === 'get_mockup_block')
+            $sContent = BxDolService::getSerializedService($aCall['module'], $aCall['method'], [(int)$aBlock['id']], $aCall['class'] ?? '');
 
         $aResult = BxDolService::callSerialized($sContent, array_merge($this->_aMarkers, [
             'block_id' => $aBlock['id']