Compare commits
6 Commits
5027a92314
...
b0876e6542
| Author | SHA1 | Date | |
|---|---|---|---|
| b0876e6542 | |||
| 6c4f88d4c8 | |||
| 9f42024682 | |||
| ade336f30a | |||
| e55be0da3a | |||
| c003af4f73 |
5
Makefile
5
Makefile
@ -27,7 +27,8 @@ help:
|
|||||||
@echo " make clean - Clean up processes and temporary files"
|
@echo " make clean - Clean up processes and temporary files"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Docker Production Commands:"
|
@echo "Docker Production Commands:"
|
||||||
@echo " make up - Build and start production Docker services (localhost:2026)"
|
@echo " make up - Start production Docker services from existing images (localhost:2026)"
|
||||||
|
@echo " BUILD=1 make up - Build images then start production Docker services"
|
||||||
@echo " make down - Stop and remove production Docker containers"
|
@echo " make down - Stop and remove production Docker containers"
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Docker Development Commands:"
|
@echo "Docker Development Commands:"
|
||||||
@ -183,7 +184,7 @@ docker-logs-gateway:
|
|||||||
# Production Docker Commands
|
# Production Docker Commands
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|
||||||
# Build and start production services
|
# Start production services (set BUILD=1 to force rebuild)
|
||||||
up:
|
up:
|
||||||
@./scripts/deploy.sh
|
@./scripts/deploy.sh
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,95 @@
|
|||||||
|
"""JSON extraction helpers for LLM-generated memory payloads."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
def strip_code_fence(text: str) -> str:
|
||||||
|
cleaned = text.strip()
|
||||||
|
if not cleaned.startswith("```"):
|
||||||
|
return cleaned
|
||||||
|
lines = cleaned.split("\n")
|
||||||
|
return "\n".join(lines[1:-1] if lines[-1].strip() == "```" else lines[1:]).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def escape_inner_quotes_in_json_strings(text: str) -> str:
|
||||||
|
"""Heuristically repair unescaped inner double quotes inside JSON strings."""
|
||||||
|
out: list[str] = []
|
||||||
|
in_string = False
|
||||||
|
escape = False
|
||||||
|
n = len(text)
|
||||||
|
i = 0
|
||||||
|
while i < n:
|
||||||
|
ch = text[i]
|
||||||
|
if not in_string:
|
||||||
|
out.append(ch)
|
||||||
|
if ch == '"':
|
||||||
|
in_string = True
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if escape:
|
||||||
|
out.append(ch)
|
||||||
|
escape = False
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if ch == "\\":
|
||||||
|
out.append(ch)
|
||||||
|
escape = True
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if ch == '"':
|
||||||
|
j = i + 1
|
||||||
|
while j < n and text[j].isspace():
|
||||||
|
j += 1
|
||||||
|
next_char = text[j] if j < n else ""
|
||||||
|
if next_char in {":", ",", "}", "]", ""}:
|
||||||
|
out.append(ch)
|
||||||
|
in_string = False
|
||||||
|
else:
|
||||||
|
out.append('\\"')
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
out.append(ch)
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
return "".join(out)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_json_object(text: str) -> dict[str, Any] | None:
|
||||||
|
cleaned = strip_code_fence(text)
|
||||||
|
try:
|
||||||
|
parsed = json.loads(cleaned)
|
||||||
|
return parsed if isinstance(parsed, dict) else None
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
repaired = escape_inner_quotes_in_json_strings(cleaned)
|
||||||
|
if repaired != cleaned:
|
||||||
|
try:
|
||||||
|
parsed = json.loads(repaired)
|
||||||
|
return parsed if isinstance(parsed, dict) else None
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
match = re.search(r"\{.*\}", cleaned, flags=re.DOTALL)
|
||||||
|
if not match:
|
||||||
|
return None
|
||||||
|
|
||||||
|
candidate = match.group(0)
|
||||||
|
try:
|
||||||
|
parsed = json.loads(candidate)
|
||||||
|
return parsed if isinstance(parsed, dict) else None
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
repaired = escape_inner_quotes_in_json_strings(candidate)
|
||||||
|
if repaired != candidate:
|
||||||
|
try:
|
||||||
|
parsed = json.loads(repaired)
|
||||||
|
return parsed if isinstance(parsed, dict) else None
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return None
|
||||||
|
return None
|
||||||
@ -4,10 +4,14 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import re
|
|
||||||
import hashlib
|
import hashlib
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from deerflow.agents.memory.json_utils import (
|
||||||
|
escape_inner_quotes_in_json_strings as _escape_inner_quotes_in_json_strings,
|
||||||
|
)
|
||||||
|
from deerflow.agents.memory.json_utils import extract_json_object as _extract_json_object
|
||||||
|
from deerflow.agents.memory.json_utils import strip_code_fence as _strip_code_fence
|
||||||
from deerflow.agents.memory.thread_prompt import create_empty_thread_memory
|
from deerflow.agents.memory.thread_prompt import create_empty_thread_memory
|
||||||
from deerflow.agents.memory.thread_storage import get_thread_memory_storage
|
from deerflow.agents.memory.thread_storage import get_thread_memory_storage
|
||||||
from deerflow.agents.memory.thread_updater import ThreadMemoryUpdater
|
from deerflow.agents.memory.thread_updater import ThreadMemoryUpdater
|
||||||
@ -74,106 +78,6 @@ def _get_summary_model():
|
|||||||
config = get_thread_memory_config()
|
config = get_thread_memory_config()
|
||||||
return create_chat_model(name=config.model_name, thinking_enabled=False, stream_usage=False)
|
return create_chat_model(name=config.model_name, thinking_enabled=False, stream_usage=False)
|
||||||
|
|
||||||
|
|
||||||
def _strip_code_fence(text: str) -> str:
|
|
||||||
cleaned = text.strip()
|
|
||||||
if not cleaned.startswith("```"):
|
|
||||||
return cleaned
|
|
||||||
lines = cleaned.split("\n")
|
|
||||||
return "\n".join(lines[1:-1] if lines[-1].strip() == "```" else lines[1:]).strip()
|
|
||||||
|
|
||||||
|
|
||||||
def _extract_json_object(text: str) -> dict[str, Any] | None:
|
|
||||||
cleaned = _strip_code_fence(text)
|
|
||||||
try:
|
|
||||||
parsed = json.loads(cleaned)
|
|
||||||
return parsed if isinstance(parsed, dict) else None
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
repaired = _escape_inner_quotes_in_json_strings(cleaned)
|
|
||||||
if repaired != cleaned:
|
|
||||||
try:
|
|
||||||
parsed = json.loads(repaired)
|
|
||||||
if isinstance(parsed, dict):
|
|
||||||
logger.warning("THREAD_SUMMARY_DEBUG parse_repaired mode=full_text")
|
|
||||||
return parsed
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
match = re.search(r"\{.*\}", cleaned, flags=re.DOTALL)
|
|
||||||
if not match:
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
parsed = json.loads(match.group(0))
|
|
||||||
return parsed if isinstance(parsed, dict) else None
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
candidate = match.group(0)
|
|
||||||
repaired = _escape_inner_quotes_in_json_strings(candidate)
|
|
||||||
if repaired != candidate:
|
|
||||||
try:
|
|
||||||
parsed = json.loads(repaired)
|
|
||||||
if isinstance(parsed, dict):
|
|
||||||
logger.warning("THREAD_SUMMARY_DEBUG parse_repaired mode=regex_object")
|
|
||||||
return parsed
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
return None
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _escape_inner_quotes_in_json_strings(text: str) -> str:
|
|
||||||
"""Heuristically repair unescaped inner double quotes inside JSON strings.
|
|
||||||
|
|
||||||
If a quote appears while inside a string but the next non-space character is
|
|
||||||
not a valid string terminator (comma, object/array close, or key colon), it is
|
|
||||||
treated as content and escaped.
|
|
||||||
"""
|
|
||||||
out: list[str] = []
|
|
||||||
in_string = False
|
|
||||||
escape = False
|
|
||||||
n = len(text)
|
|
||||||
i = 0
|
|
||||||
while i < n:
|
|
||||||
ch = text[i]
|
|
||||||
if not in_string:
|
|
||||||
out.append(ch)
|
|
||||||
if ch == '"':
|
|
||||||
in_string = True
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
if escape:
|
|
||||||
out.append(ch)
|
|
||||||
escape = False
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
if ch == "\\":
|
|
||||||
out.append(ch)
|
|
||||||
escape = True
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
if ch == '"':
|
|
||||||
j = i + 1
|
|
||||||
while j < n and text[j].isspace():
|
|
||||||
j += 1
|
|
||||||
next_char = text[j] if j < n else ""
|
|
||||||
# Valid JSON string terminators in context:
|
|
||||||
# - key string: :
|
|
||||||
# - value string: , } ]
|
|
||||||
if next_char in {":", ",", "}", "]", ""}:
|
|
||||||
out.append(ch)
|
|
||||||
in_string = False
|
|
||||||
else:
|
|
||||||
out.append('\\"')
|
|
||||||
i += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
out.append(ch)
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
return "".join(out)
|
|
||||||
|
|
||||||
|
|
||||||
def _merge_summary_patch(base: dict[str, Any], patch: dict[str, Any]) -> dict[str, Any]:
|
def _merge_summary_patch(base: dict[str, Any], patch: dict[str, Any]) -> dict[str, Any]:
|
||||||
merged = {"ownerId": base.get("ownerId"), **create_empty_thread_memory()}
|
merged = {"ownerId": base.get("ownerId"), **create_empty_thread_memory()}
|
||||||
merged["user"] = dict(base.get("user", {})) if isinstance(base.get("user"), dict) else merged["user"]
|
merged["user"] = dict(base.get("user", {})) if isinstance(base.get("user"), dict) else merged["user"]
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import uuid
|
|||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from deerflow.agents.memory.json_utils import extract_json_object
|
||||||
from deerflow.agents.memory.updater import _extract_text
|
from deerflow.agents.memory.updater import _extract_text
|
||||||
from deerflow.agents.memory.thread_prompt import build_thread_memory_prompt, create_empty_thread_memory
|
from deerflow.agents.memory.thread_prompt import build_thread_memory_prompt, create_empty_thread_memory
|
||||||
from deerflow.agents.memory.thread_storage import get_thread_memory_storage
|
from deerflow.agents.memory.thread_storage import get_thread_memory_storage
|
||||||
@ -128,10 +129,9 @@ class ThreadMemoryUpdater:
|
|||||||
try:
|
try:
|
||||||
response = self._get_model().invoke(prompt)
|
response = self._get_model().invoke(prompt)
|
||||||
response_text = _extract_text(response.content).strip()
|
response_text = _extract_text(response.content).strip()
|
||||||
if response_text.startswith("```"):
|
parsed = extract_json_object(response_text)
|
||||||
lines = response_text.split("\n")
|
if not isinstance(parsed, dict):
|
||||||
response_text = "\n".join(lines[1:-1] if lines[-1] == "```" else lines[1:])
|
raise json.JSONDecodeError("No valid JSON object found", response_text, 0)
|
||||||
parsed = json.loads(response_text)
|
|
||||||
cleaned = self._scrub_sensitive(parsed, thread_id)
|
cleaned = self._scrub_sensitive(parsed, thread_id)
|
||||||
|
|
||||||
expected_version = 0 if current is None else int(current.get("memoryVersion", 0))
|
expected_version = 0 if current is None else int(current.get("memoryVersion", 0))
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from deerflow.agents.memory.thread_updater import ThreadMemoryUpdater
|
from deerflow.agents.memory.thread_updater import ThreadMemoryUpdater
|
||||||
|
|
||||||
|
|
||||||
@ -18,3 +20,65 @@ def test_scrub_sensitive_tolerates_non_numeric_confidence():
|
|||||||
assert len(cleaned["facts"]) == 2
|
assert len(cleaned["facts"]) == 2
|
||||||
assert cleaned["facts"][0]["confidence"] == 0.5
|
assert cleaned["facts"][0]["confidence"] == 0.5
|
||||||
assert cleaned["facts"][1]["confidence"] == 0.5
|
assert cleaned["facts"][1]["confidence"] == 0.5
|
||||||
|
|
||||||
|
|
||||||
|
def test_update_memory_repairs_model_json_with_unescaped_inner_quotes():
|
||||||
|
class _Storage:
|
||||||
|
def __init__(self):
|
||||||
|
self.saved = None
|
||||||
|
|
||||||
|
def load(self, _thread_id):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def save(self, _thread_id, data, expected_version=None):
|
||||||
|
self.saved = {
|
||||||
|
"thread_id": _thread_id,
|
||||||
|
"data": data,
|
||||||
|
"expected_version": expected_version,
|
||||||
|
}
|
||||||
|
return True
|
||||||
|
|
||||||
|
fake_storage = _Storage()
|
||||||
|
fake_model = type(
|
||||||
|
"M",
|
||||||
|
(),
|
||||||
|
{
|
||||||
|
"invoke": lambda self, prompt: type(
|
||||||
|
"R",
|
||||||
|
(),
|
||||||
|
{
|
||||||
|
"content": """
|
||||||
|
{
|
||||||
|
"user": {
|
||||||
|
"topOfMind": {
|
||||||
|
"summary": "反感“作为 AI"这种句式,认为回答不用寒暄直接说重点。",
|
||||||
|
"updatedAt": "2026-06-11T07:13:11Z"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"history": {},
|
||||||
|
"facts": [
|
||||||
|
{
|
||||||
|
"content": "偏好直接回答,不喜欢“作为 AI"式开场",
|
||||||
|
"category": "preference",
|
||||||
|
"confidence": 0.92
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""".strip()
|
||||||
|
},
|
||||||
|
)()
|
||||||
|
},
|
||||||
|
)()
|
||||||
|
messages = [type("Msg", (), {"type": "human", "content": "请直接回答重点,不要寒暄。"})()]
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("deerflow.agents.memory.thread_updater.get_thread_memory_storage", return_value=fake_storage),
|
||||||
|
patch.object(ThreadMemoryUpdater, "_get_model", return_value=fake_model),
|
||||||
|
):
|
||||||
|
result = ThreadMemoryUpdater().update_memory(messages, "thread-test")
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
assert fake_storage.saved is not None
|
||||||
|
assert fake_storage.saved["expected_version"] == 0
|
||||||
|
assert fake_storage.saved["data"]["user"]["topOfMind"]["summary"].startswith("反感“作为 AI")
|
||||||
|
assert fake_storage.saved["data"]["facts"][0]["content"].startswith("偏好直接回答")
|
||||||
|
|||||||
@ -40,6 +40,7 @@ services:
|
|||||||
|
|
||||||
# ── Frontend: Next.js Production ───────────────────────────────────────────
|
# ── Frontend: Next.js Production ───────────────────────────────────────────
|
||||||
frontend:
|
frontend:
|
||||||
|
image: registry.xueai.art/deerflow/deer-flow-frontend:latest
|
||||||
build:
|
build:
|
||||||
context: ../
|
context: ../
|
||||||
dockerfile: frontend/Dockerfile
|
dockerfile: frontend/Dockerfile
|
||||||
@ -60,6 +61,7 @@ services:
|
|||||||
|
|
||||||
# ── Gateway API ────────────────────────────────────────────────────────────
|
# ── Gateway API ────────────────────────────────────────────────────────────
|
||||||
gateway:
|
gateway:
|
||||||
|
image: registry.xueai.art/deerflow/deer-flow-gateway:latest
|
||||||
build:
|
build:
|
||||||
context: ../
|
context: ../
|
||||||
dockerfile: backend/Dockerfile
|
dockerfile: backend/Dockerfile
|
||||||
@ -120,6 +122,7 @@ services:
|
|||||||
# TODO: switch to langchain/langgraph-api (licensed) once a license key is available.
|
# TODO: switch to langchain/langgraph-api (licensed) once a license key is available.
|
||||||
# For now, use `langgraph dev` (no license required) with the standard backend image.
|
# For now, use `langgraph dev` (no license required) with the standard backend image.
|
||||||
langgraph:
|
langgraph:
|
||||||
|
image: registry.xueai.art/deerflow/deer-flow-langgraph:latest
|
||||||
build:
|
build:
|
||||||
context: ../
|
context: ../
|
||||||
dockerfile: backend/Dockerfile
|
dockerfile: backend/Dockerfile
|
||||||
|
|||||||
7
frontend/.gitignore
vendored
7
frontend/.gitignore
vendored
@ -47,3 +47,10 @@ test-results
|
|||||||
|
|
||||||
# idea files
|
# idea files
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
node_modules/
|
||||||
|
/playwright-report/
|
||||||
|
/blob-report/
|
||||||
|
/playwright/.cache/
|
||||||
|
/playwright/.auth/
|
||||||
|
|||||||
18
frontend/e2e/example.spec.ts
Normal file
18
frontend/e2e/example.spec.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test('has title', async ({ page }) => {
|
||||||
|
await page.goto('https://playwright.dev/');
|
||||||
|
|
||||||
|
// Expect a title "to contain" a substring.
|
||||||
|
await expect(page).toHaveTitle(/Playwright/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('get started link', async ({ page }) => {
|
||||||
|
await page.goto('https://playwright.dev/');
|
||||||
|
|
||||||
|
// Click the get started link.
|
||||||
|
await page.getByRole('link', { name: 'Get started' }).click();
|
||||||
|
|
||||||
|
// Expects page to have a heading with the name of Installation.
|
||||||
|
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
|
||||||
|
});
|
||||||
@ -5,11 +5,10 @@ import { defineConfig, devices } from "@playwright/test";
|
|||||||
import { config as loadEnv } from "dotenv";
|
import { config as loadEnv } from "dotenv";
|
||||||
|
|
||||||
const configDir = path.dirname(fileURLToPath(import.meta.url));
|
const configDir = path.dirname(fileURLToPath(import.meta.url));
|
||||||
// Load local e2e env defaults from frontend/.env(.local), while keeping shell env highest priority.
|
|
||||||
loadEnv({ path: path.resolve(configDir, ".env.local") });
|
loadEnv({ path: path.resolve(configDir, ".env.local") });
|
||||||
loadEnv({ path: path.resolve(configDir, ".env") });
|
loadEnv({ path: path.resolve(configDir, ".env") });
|
||||||
|
|
||||||
const baseURL = process.env.FRONTEND_E2E_BASE_URL ?? "http://127.0.0.1:3000";
|
const baseURL = process.env.FRONTEND_E2E_BASE_URL ?? "http://localhost:2026";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: "./tests/e2e",
|
testDir: "./tests/e2e",
|
||||||
@ -24,7 +23,7 @@ export default defineConfig({
|
|||||||
baseURL,
|
baseURL,
|
||||||
trace: "on-first-retry",
|
trace: "on-first-retry",
|
||||||
screenshot: "only-on-failure",
|
screenshot: "only-on-failure",
|
||||||
video: "retain-on-failure",
|
video: process.env.CI ? "retain-on-failure" : "off",
|
||||||
},
|
},
|
||||||
projects: [
|
projects: [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -665,89 +665,105 @@ packages:
|
|||||||
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||||
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||||
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||||
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||||
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||||
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||||
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||||
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@img/sharp-linux-arm64@0.34.5':
|
'@img/sharp-linux-arm64@0.34.5':
|
||||||
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-linux-arm@0.34.5':
|
'@img/sharp-linux-arm@0.34.5':
|
||||||
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [arm]
|
cpu: [arm]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-linux-ppc64@0.34.5':
|
'@img/sharp-linux-ppc64@0.34.5':
|
||||||
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-linux-riscv64@0.34.5':
|
'@img/sharp-linux-riscv64@0.34.5':
|
||||||
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-linux-s390x@0.34.5':
|
'@img/sharp-linux-s390x@0.34.5':
|
||||||
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-linux-x64@0.34.5':
|
'@img/sharp-linux-x64@0.34.5':
|
||||||
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||||
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||||
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@img/sharp-wasm32@0.34.5':
|
'@img/sharp-wasm32@0.34.5':
|
||||||
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
||||||
@ -902,24 +918,28 @@ packages:
|
|||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.97':
|
'@napi-rs/canvas-linux-riscv64-gnu@0.1.97':
|
||||||
resolution: {integrity: sha512-Jc7I3A51jnEOIAXeLsN/M/+Z28LUeakcsXs07FLq9prXc0eYOtVwsDEv913Gr+06IRo34gJJVgT0TXvmz+N2VA==}
|
resolution: {integrity: sha512-Jc7I3A51jnEOIAXeLsN/M/+Z28LUeakcsXs07FLq9prXc0eYOtVwsDEv913Gr+06IRo34gJJVgT0TXvmz+N2VA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-x64-gnu@0.1.97':
|
'@napi-rs/canvas-linux-x64-gnu@0.1.97':
|
||||||
resolution: {integrity: sha512-iDUBe7AilfuBSRbSa8/IGX38Mf+iCSBqoVKLSQ5XaY2JLOaqz1TVyPFEyIck7wT6mRQhQt5sN6ogfjIDfi74tg==}
|
resolution: {integrity: sha512-iDUBe7AilfuBSRbSa8/IGX38Mf+iCSBqoVKLSQ5XaY2JLOaqz1TVyPFEyIck7wT6mRQhQt5sN6ogfjIDfi74tg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@napi-rs/canvas-linux-x64-musl@0.1.97':
|
'@napi-rs/canvas-linux-x64-musl@0.1.97':
|
||||||
resolution: {integrity: sha512-AKLFd/v0Z5fvgqBDqhvqtAdx+fHMJ5t9JcUNKq4FIZ5WH+iegGm8HPdj00NFlCSnm83Fp3Ln8I2f7uq1aIiWaA==}
|
resolution: {integrity: sha512-AKLFd/v0Z5fvgqBDqhvqtAdx+fHMJ5t9JcUNKq4FIZ5WH+iegGm8HPdj00NFlCSnm83Fp3Ln8I2f7uq1aIiWaA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@napi-rs/canvas-win32-arm64-msvc@0.1.97':
|
'@napi-rs/canvas-win32-arm64-msvc@0.1.97':
|
||||||
resolution: {integrity: sha512-u883Yr6A6fO7Vpsy9YE4FVCIxzzo5sO+7pIUjjoDLjS3vQaNMkVzx5bdIpEL+ob+gU88WDK4VcxYMZ6nmnoX9A==}
|
resolution: {integrity: sha512-u883Yr6A6fO7Vpsy9YE4FVCIxzzo5sO+7pIUjjoDLjS3vQaNMkVzx5bdIpEL+ob+gU88WDK4VcxYMZ6nmnoX9A==}
|
||||||
@ -978,36 +998,42 @@ packages:
|
|||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@napi-rs/simple-git-linux-arm64-musl@0.1.22':
|
'@napi-rs/simple-git-linux-arm64-musl@0.1.22':
|
||||||
resolution: {integrity: sha512-MOs7fPyJiU/wqOpKzAOmOpxJ/TZfP4JwmvPad/cXTOWYwwyppMlXFRms3i98EU3HOazI/wMU2Ksfda3+TBluWA==}
|
resolution: {integrity: sha512-MOs7fPyJiU/wqOpKzAOmOpxJ/TZfP4JwmvPad/cXTOWYwwyppMlXFRms3i98EU3HOazI/wMU2Ksfda3+TBluWA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@napi-rs/simple-git-linux-ppc64-gnu@0.1.22':
|
'@napi-rs/simple-git-linux-ppc64-gnu@0.1.22':
|
||||||
resolution: {integrity: sha512-L59dR30VBShRUIZ5/cQHU25upNgKS0AMQ7537J6LCIUEFwwXrKORZKJ8ceR+s3Sr/4jempWVvMdjEpFDE4HYww==}
|
resolution: {integrity: sha512-L59dR30VBShRUIZ5/cQHU25upNgKS0AMQ7537J6LCIUEFwwXrKORZKJ8ceR+s3Sr/4jempWVvMdjEpFDE4HYww==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@napi-rs/simple-git-linux-s390x-gnu@0.1.22':
|
'@napi-rs/simple-git-linux-s390x-gnu@0.1.22':
|
||||||
resolution: {integrity: sha512-4FHkPlCSIZUGC6HiADffbe6NVoTBMd65pIwcd40IDbtFKOgFMBA+pWRqKiQ21FERGH16Zed7XHJJoY3jpOqtmQ==}
|
resolution: {integrity: sha512-4FHkPlCSIZUGC6HiADffbe6NVoTBMd65pIwcd40IDbtFKOgFMBA+pWRqKiQ21FERGH16Zed7XHJJoY3jpOqtmQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@napi-rs/simple-git-linux-x64-gnu@0.1.22':
|
'@napi-rs/simple-git-linux-x64-gnu@0.1.22':
|
||||||
resolution: {integrity: sha512-Ei1tM5Ho/dwknF3pOzqkNW9Iv8oFzRxE8uOhrITcdlpxRxVrBVptUF6/0WPdvd7R9747D/q61QG/AVyWsWLFKw==}
|
resolution: {integrity: sha512-Ei1tM5Ho/dwknF3pOzqkNW9Iv8oFzRxE8uOhrITcdlpxRxVrBVptUF6/0WPdvd7R9747D/q61QG/AVyWsWLFKw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@napi-rs/simple-git-linux-x64-musl@0.1.22':
|
'@napi-rs/simple-git-linux-x64-musl@0.1.22':
|
||||||
resolution: {integrity: sha512-zRYxg7it0p3rLyEJYoCoL2PQJNgArVLyNavHW03TFUAYkYi5bxQ/UFNVpgxMaXohr5yu7qCBqeo9j4DWeysalg==}
|
resolution: {integrity: sha512-zRYxg7it0p3rLyEJYoCoL2PQJNgArVLyNavHW03TFUAYkYi5bxQ/UFNVpgxMaXohr5yu7qCBqeo9j4DWeysalg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@napi-rs/simple-git-win32-arm64-msvc@0.1.22':
|
'@napi-rs/simple-git-win32-arm64-msvc@0.1.22':
|
||||||
resolution: {integrity: sha512-XGFR1fj+Y9cWACcovV2Ey/R2xQOZKs8t+7KHPerYdJ4PtjVzGznI4c2EBHXtdOIYvkw7tL5rZ7FN1HJKdD5Quw==}
|
resolution: {integrity: sha512-XGFR1fj+Y9cWACcovV2Ey/R2xQOZKs8t+7KHPerYdJ4PtjVzGznI4c2EBHXtdOIYvkw7tL5rZ7FN1HJKdD5Quw==}
|
||||||
@ -1057,24 +1083,28 @@ packages:
|
|||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@next/swc-linux-arm64-musl@16.1.7':
|
'@next/swc-linux-arm64-musl@16.1.7':
|
||||||
resolution: {integrity: sha512-uufcze7LYv0FQg9GnNeZ3/whYfo+1Q3HnQpm16o6Uyi0OVzLlk2ZWoY7j07KADZFY8qwDbsmFnMQP3p3+Ftprw==}
|
resolution: {integrity: sha512-uufcze7LYv0FQg9GnNeZ3/whYfo+1Q3HnQpm16o6Uyi0OVzLlk2ZWoY7j07KADZFY8qwDbsmFnMQP3p3+Ftprw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@next/swc-linux-x64-gnu@16.1.7':
|
'@next/swc-linux-x64-gnu@16.1.7':
|
||||||
resolution: {integrity: sha512-KWVf2gxYvHtvuT+c4MBOGxuse5TD7DsMFYSxVxRBnOzok/xryNeQSjXgxSv9QpIVlaGzEn/pIuI6Koosx8CGWA==}
|
resolution: {integrity: sha512-KWVf2gxYvHtvuT+c4MBOGxuse5TD7DsMFYSxVxRBnOzok/xryNeQSjXgxSv9QpIVlaGzEn/pIuI6Koosx8CGWA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@next/swc-linux-x64-musl@16.1.7':
|
'@next/swc-linux-x64-musl@16.1.7':
|
||||||
resolution: {integrity: sha512-HguhaGwsGr1YAGs68uRKc4aGWxLET+NevJskOcCAwXbwj0fYX0RgZW2gsOCzr9S11CSQPIkxmoSbuVaBp4Z3dA==}
|
resolution: {integrity: sha512-HguhaGwsGr1YAGs68uRKc4aGWxLET+NevJskOcCAwXbwj0fYX0RgZW2gsOCzr9S11CSQPIkxmoSbuVaBp4Z3dA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@next/swc-win32-arm64-msvc@16.1.7':
|
'@next/swc-win32-arm64-msvc@16.1.7':
|
||||||
resolution: {integrity: sha512-S0n3KrDJokKTeFyM/vGGGR8+pCmXYrjNTk2ZozOL1C/JFdfUIL9O1ATaJOl5r2POe56iRChbsszrjMAdWSv7kQ==}
|
resolution: {integrity: sha512-S0n3KrDJokKTeFyM/vGGGR8+pCmXYrjNTk2ZozOL1C/JFdfUIL9O1ATaJOl5r2POe56iRChbsszrjMAdWSv7kQ==}
|
||||||
@ -2088,24 +2118,28 @@ packages:
|
|||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-arm64-musl@4.1.18':
|
'@tailwindcss/oxide-linux-arm64-musl@4.1.18':
|
||||||
resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
|
resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-x64-gnu@4.1.18':
|
'@tailwindcss/oxide-linux-x64-gnu@4.1.18':
|
||||||
resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
|
resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@tailwindcss/oxide-linux-x64-musl@4.1.18':
|
'@tailwindcss/oxide-linux-x64-musl@4.1.18':
|
||||||
resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
|
resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@tailwindcss/oxide-wasm32-wasi@4.1.18':
|
'@tailwindcss/oxide-wasm32-wasi@4.1.18':
|
||||||
resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
|
resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
|
||||||
@ -2507,41 +2541,49 @@ packages:
|
|||||||
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
|
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
|
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
|
||||||
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
|
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
|
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
|
||||||
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
|
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
|
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
|
||||||
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
|
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
|
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
|
||||||
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
|
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
|
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
|
||||||
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
|
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
|
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
|
||||||
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
|
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
|
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
|
||||||
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
|
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
|
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
|
||||||
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
|
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
|
||||||
@ -4159,24 +4201,28 @@ packages:
|
|||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
lightningcss-linux-arm64-musl@1.30.2:
|
lightningcss-linux-arm64-musl@1.30.2:
|
||||||
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
|
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
lightningcss-linux-x64-gnu@1.30.2:
|
lightningcss-linux-x64-gnu@1.30.2:
|
||||||
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
|
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
lightningcss-linux-x64-musl@1.30.2:
|
lightningcss-linux-x64-musl@1.30.2:
|
||||||
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
|
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
|
||||||
engines: {node: '>= 12.0.0'}
|
engines: {node: '>= 12.0.0'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
lightningcss-win32-arm64-msvc@1.30.2:
|
lightningcss-win32-arm64-msvc@1.30.2:
|
||||||
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
|
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
|
||||||
|
|||||||
@ -579,7 +579,7 @@ export default function ChatPage() {
|
|||||||
className={cn(
|
className={cn(
|
||||||
"pointer-events-auto relative w-full max-w-[720px]",
|
"pointer-events-auto relative w-full max-w-[720px]",
|
||||||
showWelcomeStyle && "-translate-y-[calc(50vh-96px)]",
|
showWelcomeStyle && "-translate-y-[calc(50vh-96px)]",
|
||||||
brand === "sxwz" && "-translate-x-[172px]"
|
brand === "sxwz"&& artifactsOpen ===false && "-translate-x-[172px]"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!(showWelcomeStyle && thread.isThreadLoading) ? (
|
{!(showWelcomeStyle && thread.isThreadLoading) ? (
|
||||||
|
|||||||
@ -85,7 +85,10 @@ export function ThreadMemoryPanel({ threadId }: ThreadMemoryPanelProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-[380px] space-y-2 rounded-lg border border-ws-divider bg-ws-surface-elevated p-3 shadow-lg">
|
<div
|
||||||
|
className="w-[380px] space-y-2 rounded-lg border border-ws-divider bg-ws-surface-elevated p-3 shadow-lg"
|
||||||
|
data-testid="thread-memory-panel"
|
||||||
|
>
|
||||||
<div className="text-sm font-semibold">
|
<div className="text-sm font-semibold">
|
||||||
<span className="hidden sm:inline">{t.threadMemoryPanel.title}</span>
|
<span className="hidden sm:inline">{t.threadMemoryPanel.title}</span>
|
||||||
</div>
|
</div>
|
||||||
@ -98,6 +101,7 @@ export function ThreadMemoryPanel({ threadId }: ThreadMemoryPanelProps) {
|
|||||||
void handleLoadMemorySummary();
|
void handleLoadMemorySummary();
|
||||||
}}
|
}}
|
||||||
disabled={loadingSummary}
|
disabled={loadingSummary}
|
||||||
|
data-testid="thread-memory-load"
|
||||||
>
|
>
|
||||||
{loadingSummary ? t.threadMemoryPanel.loading : t.threadMemoryPanel.load}
|
{loadingSummary ? t.threadMemoryPanel.loading : t.threadMemoryPanel.load}
|
||||||
</Button>
|
</Button>
|
||||||
@ -131,6 +135,7 @@ export function ThreadMemoryPanel({ threadId }: ThreadMemoryPanelProps) {
|
|||||||
onChange={(e) => setMemorySummary(e.target.value)}
|
onChange={(e) => setMemorySummary(e.target.value)}
|
||||||
placeholder={t.threadMemoryPanel.summaryPlaceholder}
|
placeholder={t.threadMemoryPanel.summaryPlaceholder}
|
||||||
className="min-h-32 bg-white/80"
|
className="min-h-32 bg-white/80"
|
||||||
|
data-testid="thread-memory-summary"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
243
frontend/tests/e2e/thread-memory-log.spec.ts
Normal file
243
frontend/tests/e2e/thread-memory-log.spec.ts
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
import fs from "node:fs/promises";
|
||||||
|
|
||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
|
||||||
|
import {
|
||||||
|
expandComposer,
|
||||||
|
newChatEntry,
|
||||||
|
openChat,
|
||||||
|
} from "./support/chat-helpers";
|
||||||
|
|
||||||
|
const LANGGRAPH_LOG_PATH = "/home/mt/Projects/deerflow2/logs/langgraph.log";
|
||||||
|
const INPUT_TOOLS_TOUR_SEEN_KEY = "workspace.input_tools_tour_seen.v1";
|
||||||
|
const MEMORY_ERROR_PATTERNS = [
|
||||||
|
"Thread memory update failed",
|
||||||
|
"json.decoder.JSONDecodeError",
|
||||||
|
"JSONDecodeError",
|
||||||
|
];
|
||||||
|
|
||||||
|
async function readLogTail(startOffset: number) {
|
||||||
|
const handle = await fs.open(LANGGRAPH_LOG_PATH, "r");
|
||||||
|
try {
|
||||||
|
const stats = await handle.stat();
|
||||||
|
const length = Math.max(0, stats.size - startOffset);
|
||||||
|
if (length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const buffer = Buffer.alloc(length);
|
||||||
|
await handle.read(buffer, 0, length, startOffset);
|
||||||
|
return buffer.toString("utf8");
|
||||||
|
} finally {
|
||||||
|
await handle.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function e2eLog(message: string, extra?: unknown) {
|
||||||
|
if (extra === undefined) {
|
||||||
|
console.log(`[DF-MEM-001] ${message}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(`[DF-MEM-001] ${message}`, extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function completeInputToolsTour(page: Parameters<typeof openChat>[0]) {
|
||||||
|
e2eLog("checking input tools tour");
|
||||||
|
const tourRoot = page.locator(".workspace-input-tools-tour");
|
||||||
|
const nextButton = page
|
||||||
|
.locator(".workspace-input-tools-tour .ant-tour-next-btn")
|
||||||
|
.getByText(/下一步|完成/);
|
||||||
|
|
||||||
|
if (!(await tourRoot.isVisible().catch(() => false))) {
|
||||||
|
e2eLog("input tools tour not visible, skipping");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let step = 0; step < 4; step += 1) {
|
||||||
|
e2eLog(`input tools tour step ${step + 1}`);
|
||||||
|
await expect(nextButton.first()).toBeVisible();
|
||||||
|
await nextButton.first().click();
|
||||||
|
if (!(await tourRoot.isVisible().catch(() => false))) {
|
||||||
|
e2eLog("input tools tour completed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
e2eLog("input tools tour still visible after max steps");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitForResolvedThreadId(
|
||||||
|
page: Parameters<typeof openChat>[0],
|
||||||
|
threadId: string,
|
||||||
|
) {
|
||||||
|
e2eLog("waiting for resolved thread id", threadId);
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
() =>
|
||||||
|
page.evaluate(
|
||||||
|
(storageKey) => window.sessionStorage.getItem(storageKey),
|
||||||
|
"workspace.thread_id",
|
||||||
|
),
|
||||||
|
{
|
||||||
|
timeout: 30_000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toBe(threadId);
|
||||||
|
e2eLog("resolved thread id is ready", threadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe("线程记忆 / 前端加载与日志校验", () => {
|
||||||
|
test.setTimeout(120_000);
|
||||||
|
|
||||||
|
test("DF-MEM-001 发送消息后可从线程记忆面板加载 summary,且新增日志无记忆报错", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const threadId = uuid();
|
||||||
|
const logStats = await fs.stat(LANGGRAPH_LOG_PATH);
|
||||||
|
const message =
|
||||||
|
`请记住:我常用 TypeScript、React 和 Playwright,偏好中文且直接回答重点。本次 e2e 线程标识 ${threadId.slice(0, 8)}。`;
|
||||||
|
e2eLog("test started", { threadId, initialLogSize: logStats.size });
|
||||||
|
|
||||||
|
await page.addInitScript(
|
||||||
|
({ key, currentThreadId }: { key: string; currentThreadId: string }) => {
|
||||||
|
window.localStorage.setItem(
|
||||||
|
key,
|
||||||
|
JSON.stringify({
|
||||||
|
seen: true,
|
||||||
|
threadIds: [currentThreadId],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: INPUT_TOOLS_TOUR_SEEN_KEY,
|
||||||
|
currentThreadId: threadId,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
e2eLog("seeded localStorage for input tools tour");
|
||||||
|
|
||||||
|
await openChat(page, newChatEntry(threadId));
|
||||||
|
e2eLog("opened chat page", await page.url());
|
||||||
|
await completeInputToolsTour(page);
|
||||||
|
await waitForResolvedThreadId(page, threadId);
|
||||||
|
e2eLog("composer page state ready");
|
||||||
|
const observedRequests: string[] = [];
|
||||||
|
page.on("request", (request) => {
|
||||||
|
const url = request.url();
|
||||||
|
if (url.includes("/stream") || url.includes("/threads")) {
|
||||||
|
observedRequests.push(`${request.method()} ${url}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
e2eLog("registered network observer");
|
||||||
|
const textarea = page.locator("textarea[name='message']");
|
||||||
|
const submit = page.locator("button[aria-label='Submit']");
|
||||||
|
await textarea.click();
|
||||||
|
await textarea.pressSequentially(message);
|
||||||
|
e2eLog("filled textarea", {
|
||||||
|
messageLength: message.length,
|
||||||
|
textareaValueLength: (await textarea.inputValue()).length,
|
||||||
|
});
|
||||||
|
await expect(textarea).toHaveValue(message);
|
||||||
|
e2eLog("textarea value confirmed");
|
||||||
|
e2eLog("submit button state before click", {
|
||||||
|
visible: await submit.isVisible(),
|
||||||
|
enabled: await submit.isEnabled(),
|
||||||
|
});
|
||||||
|
await submit.evaluate((button) => {
|
||||||
|
(button as HTMLButtonElement).click();
|
||||||
|
});
|
||||||
|
e2eLog("submit clicked via evaluate");
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => ({
|
||||||
|
url: page.url(),
|
||||||
|
userCount: await page.locator(".is-user").count(),
|
||||||
|
assistantCount: await page.locator(".is-assistant").count(),
|
||||||
|
textareaValue: await textarea.inputValue(),
|
||||||
|
observedRequests: observedRequests.slice(-10),
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
timeout: 30_000,
|
||||||
|
intervals: [500, 1_000, 2_000],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toMatchObject({
|
||||||
|
url: expect.stringMatching(new RegExp(`/workspace/chats/${threadId}\\?is_chatting=true`)),
|
||||||
|
});
|
||||||
|
e2eLog("post-submit state reached", {
|
||||||
|
currentUrl: await page.url(),
|
||||||
|
observedRequests: observedRequests.slice(-10),
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(textarea).toHaveValue("");
|
||||||
|
e2eLog("textarea cleared after submit");
|
||||||
|
await expect(page).toHaveURL(
|
||||||
|
new RegExp(`/workspace/chats/${threadId}\\?is_chatting=true`),
|
||||||
|
{ timeout: 30_000 },
|
||||||
|
);
|
||||||
|
e2eLog("navigated to active thread page", await page.url());
|
||||||
|
await expect
|
||||||
|
.poll(async () => await page.locator(".is-user").count(), {
|
||||||
|
timeout: 30_000,
|
||||||
|
})
|
||||||
|
.toBeGreaterThan(0);
|
||||||
|
e2eLog("user message rendered", await page.locator(".is-user").count());
|
||||||
|
await expect(page.locator(".is-user").last()).toContainText(
|
||||||
|
"TypeScript",
|
||||||
|
{ timeout: 30_000 },
|
||||||
|
);
|
||||||
|
e2eLog("user message content contains TypeScript");
|
||||||
|
await expect
|
||||||
|
.poll(async () => await page.locator(".is-assistant").count(), {
|
||||||
|
timeout: 30_000,
|
||||||
|
})
|
||||||
|
.toBeGreaterThan(0);
|
||||||
|
e2eLog("assistant message rendered", await page.locator(".is-assistant").count());
|
||||||
|
|
||||||
|
await expandComposer(page);
|
||||||
|
e2eLog("composer expanded for memory button");
|
||||||
|
const memoryButton = page.getByTestId("thread-memory-trigger");
|
||||||
|
e2eLog("memory button visibility precheck", {
|
||||||
|
count: await memoryButton.count(),
|
||||||
|
});
|
||||||
|
await expect(memoryButton).toBeVisible();
|
||||||
|
await memoryButton.click();
|
||||||
|
e2eLog("memory button clicked");
|
||||||
|
|
||||||
|
const loadButton = page.getByTestId("thread-memory-load");
|
||||||
|
await expect(loadButton).toBeVisible();
|
||||||
|
e2eLog("memory load button visible");
|
||||||
|
|
||||||
|
let latestSummary = "";
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
async () => {
|
||||||
|
await loadButton.click();
|
||||||
|
latestSummary = await page
|
||||||
|
.getByTestId("thread-memory-summary")
|
||||||
|
.inputValue();
|
||||||
|
e2eLog("memory summary polled", {
|
||||||
|
length: latestSummary.length,
|
||||||
|
preview: latestSummary.slice(0, 80),
|
||||||
|
});
|
||||||
|
return latestSummary;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timeout: 75_000,
|
||||||
|
intervals: [1_000, 2_000, 3_000, 5_000],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.not.toEqual("");
|
||||||
|
e2eLog("memory summary loaded", {
|
||||||
|
length: latestSummary.length,
|
||||||
|
preview: latestSummary.slice(0, 120),
|
||||||
|
});
|
||||||
|
|
||||||
|
const logTail = await readLogTail(logStats.size);
|
||||||
|
e2eLog("new langgraph log tail length", logTail.length);
|
||||||
|
for (const pattern of MEMORY_ERROR_PATTERNS) {
|
||||||
|
e2eLog(`checking log pattern absence: ${pattern}`);
|
||||||
|
expect(logTail).not.toContain(pattern);
|
||||||
|
}
|
||||||
|
e2eLog("test finished successfully");
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,9 +1,11 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
#
|
||||||
# deploy.sh - Build and start (or stop) DeerFlow production services
|
# deploy.sh - Start (or stop) DeerFlow production services
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# deploy.sh [up] — build images and start containers (default)
|
# deploy.sh [up] — start containers using existing images (default)
|
||||||
|
# BUILD=1 deploy.sh [up] — build images then start containers
|
||||||
|
# deploy.sh push — build and push images to registry.xueai.art/deerflow
|
||||||
# deploy.sh down — stop and remove containers
|
# deploy.sh down — stop and remove containers
|
||||||
#
|
#
|
||||||
# Must be run from the repo root directory.
|
# Must be run from the repo root directory.
|
||||||
@ -172,6 +174,52 @@ if [ "$CMD" = "down" ]; then
|
|||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── push ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
if [ "$CMD" = "push" ]; then
|
||||||
|
sandbox_mode="$(detect_sandbox_mode)"
|
||||||
|
echo -e "${BLUE}Sandbox mode: $sandbox_mode${NC}"
|
||||||
|
|
||||||
|
if [ -z "$DEER_FLOW_DOCKER_SOCKET" ]; then
|
||||||
|
export DEER_FLOW_DOCKER_SOCKET="/var/run/docker.sock"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$sandbox_mode" != "local" ]; then
|
||||||
|
if [ ! -S "$DEER_FLOW_DOCKER_SOCKET" ]; then
|
||||||
|
echo -e "${RED}⚠ Docker socket not found at $DEER_FLOW_DOCKER_SOCKET${NC}"
|
||||||
|
echo " AioSandboxProvider (DooD) build context may not work."
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}✓ Docker socket: $DEER_FLOW_DOCKER_SOCKET${NC}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
REGISTRY_BASE="${REGISTRY_BASE:-registry.xueai.art/deerflow}"
|
||||||
|
REGISTRY_TAG="${REGISTRY_TAG:-latest}"
|
||||||
|
PUSH_SERVICES="frontend gateway langgraph"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Building images for push..."
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
"${COMPOSE_CMD[@]}" build $PUSH_SERVICES
|
||||||
|
|
||||||
|
for service in frontend gateway langgraph; do
|
||||||
|
# local_image="deer-flow-${service}:latest"
|
||||||
|
remote_image="${REGISTRY_BASE}/deer-flow-${service}:${REGISTRY_TAG}"
|
||||||
|
# echo "Tagging ${local_image} -> ${remote_image}"
|
||||||
|
# docker tag "$local_image" "$remote_image"
|
||||||
|
echo "Pushing ${remote_image}"
|
||||||
|
docker push "$remote_image"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}✓ Push complete${NC}"
|
||||||
|
echo " Registry: ${REGISTRY_BASE}"
|
||||||
|
echo " Tag: ${REGISTRY_TAG}"
|
||||||
|
echo ""
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Banner ────────────────────────────────────────────────────────────────────
|
# ── Banner ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
@ -211,13 +259,19 @@ fi
|
|||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# ── Step 2: Build and start ───────────────────────────────────────────────────
|
# ── Step 2: Start (optionally build) ─────────────────────────────────────────
|
||||||
|
|
||||||
echo "Building images and starting containers..."
|
if [ "${BUILD:-0}" = "1" ]; then
|
||||||
|
echo "BUILD=1 detected: building images and starting containers..."
|
||||||
|
up_build_flag="--build"
|
||||||
|
else
|
||||||
|
echo "Starting containers from existing local images (no build)..."
|
||||||
|
up_build_flag=""
|
||||||
|
fi
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# shellcheck disable=SC2086
|
# shellcheck disable=SC2086
|
||||||
"${COMPOSE_CMD[@]}" $extra_args up --build -d --remove-orphans $services
|
"${COMPOSE_CMD[@]}" $extra_args up $up_build_flag -d --remove-orphans $services
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=========================================="
|
echo "=========================================="
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user