feat: 小程序 AI 底栏与聊天页、出站推送调试与 tabbar 迁移

- 自定义 tabBar:AI 入口图标与样式,相关页面与审核门控调整

- API:MpTabbar、出站推送 Hook、后台设置与迁移 SQL(含 FAB 居中排序)

- 管理端:推送 Hook 面板、出站推送调试工具与生产环境注释说明

Made-with: Cursor
This commit is contained in:
Ghost
2026-04-20 15:29:10 +08:00
parent ce55c48c64
commit d07235b64c
26 changed files with 486 additions and 317 deletions

View File

@@ -511,10 +511,12 @@ class OutboundPushHookService
'X-MBTI-Internal-Signature: ' . self::signAsyncInternalDispatch($body, $timestamp),
];
if (!self::postJsonAsyncNoWait($url, $body, $headers)) {
$verifyHttp2xx = (($payload['job'] ?? '') === 'ai.chat_turn');
if (!self::postJsonAsyncNoWait($url, $body, $headers, $verifyHttp2xx)) {
// 偶发 TLS 握手/链路抖动:短间隔重试一次再回落 shutdown
usleep(150000);
if (!self::postJsonAsyncNoWait($url, $body, $headers)) {
if (!self::postJsonAsyncNoWait($url, $body, $headers, $verifyHttp2xx)) {
Log::warning('OutboundPushHook async enqueue failed', [
'url' => self::maskUrl($url),
'payload' => $payload,
@@ -566,11 +568,13 @@ class OutboundPushHookService
}
/**
* fire-and-forget 异步 POST只负责把请求投出去,不等待接口处理完成
* fire-and-forget 异步 POST把请求写出;可选读取 HTTP 首行状态码
* - verifyHttp2xx=false与旧版一致仅判断 fwrite其它 job 可能同步较慢,不宜等响应头)。
* - verifyHttp2xx=true用于 ai.chat_turndispatch 须先快速返回 2xx否则会误判失败且无法触发 AiChat shutdown 兜底)。
*
* @param array<int,string> $headers
*/
private static function postJsonAsyncNoWait(string $url, string $body, array $headers): bool
private static function postJsonAsyncNoWait(string $url, string $body, array $headers, bool $verifyHttp2xx = false): bool
{
$parts = parse_url($url);
if (!is_array($parts) || empty($parts['host'])) {
@@ -633,9 +637,44 @@ class OutboundPushHookService
$rawRequest = implode("\r\n", $requestLines) . "\r\n\r\n" . $body;
$written = @fwrite($socket, $rawRequest);
@fclose($socket);
if ($written === false) {
@fclose($socket);
return $written !== false;
return false;
}
if (!$verifyHttp2xx) {
@fclose($socket);
return true;
}
$statusLine = @fgets($socket);
@fclose($socket);
if ($statusLine === false || $statusLine === '') {
Log::warning('OutboundPushHook async: no HTTP status line', ['url' => self::maskUrl($url)]);
return false;
}
if (!preg_match('#HTTP/\d\.\d\s+(\d{3})#', $statusLine, $m)) {
Log::warning('OutboundPushHook async: bad status line', [
'url' => self::maskUrl($url),
'prefix' => mb_substr($statusLine, 0, 80),
]);
return false;
}
$code = (int) $m[1];
if ($code < 200 || $code >= 300) {
Log::warning('OutboundPushHook async: non-2xx from internal dispatch', [
'url' => self::maskUrl($url),
'http' => $code,
]);
return false;
}
return true;
}
/**