release: bump version to 0.25.1
This commit is contained in:
@@ -44,6 +44,56 @@ class _ActiveRun:
|
|||||||
_ACTIVE_RUNS: dict[str, _ActiveRun] = {}
|
_ACTIVE_RUNS: dict[str, _ActiveRun] = {}
|
||||||
|
|
||||||
|
|
||||||
|
async def _get_session_by_key(
|
||||||
|
db: AsyncSession,
|
||||||
|
*,
|
||||||
|
user_id: int,
|
||||||
|
session_key: str,
|
||||||
|
) -> PlaygroundSession | None:
|
||||||
|
result = await db.execute(
|
||||||
|
select(PlaygroundSession).where(
|
||||||
|
PlaygroundSession.user_id == user_id,
|
||||||
|
PlaygroundSession.session_key == session_key,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
||||||
|
async def _require_session(
|
||||||
|
db: AsyncSession,
|
||||||
|
*,
|
||||||
|
user_id: int,
|
||||||
|
session_key: str,
|
||||||
|
) -> PlaygroundSession:
|
||||||
|
session = await _get_session_by_key(db, user_id=user_id, session_key=session_key)
|
||||||
|
if session is None:
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Playground session not found")
|
||||||
|
return session
|
||||||
|
|
||||||
|
|
||||||
|
async def _require_visible_message(
|
||||||
|
db: AsyncSession,
|
||||||
|
*,
|
||||||
|
user_id: int,
|
||||||
|
public_id: str,
|
||||||
|
role: str | None = None,
|
||||||
|
) -> PlaygroundMessage:
|
||||||
|
conditions = [
|
||||||
|
PlaygroundMessage.user_id == user_id,
|
||||||
|
PlaygroundMessage.public_id == public_id,
|
||||||
|
PlaygroundMessage.is_visible.is_(True),
|
||||||
|
]
|
||||||
|
if role is not None:
|
||||||
|
conditions.append(PlaygroundMessage.role == role)
|
||||||
|
|
||||||
|
result = await db.execute(select(PlaygroundMessage).where(*conditions))
|
||||||
|
message = result.scalar_one_or_none()
|
||||||
|
if message is None:
|
||||||
|
detail = "User message not found" if role == "user" else "Playground message not found"
|
||||||
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=detail)
|
||||||
|
return message
|
||||||
|
|
||||||
|
|
||||||
def _message_to_record(message: PlaygroundMessage, parent_public_id: str | None = None) -> PlaygroundMessageRecord:
|
def _message_to_record(message: PlaygroundMessage, parent_public_id: str | None = None) -> PlaygroundMessageRecord:
|
||||||
return PlaygroundMessageRecord(
|
return PlaygroundMessageRecord(
|
||||||
id=message.public_id,
|
id=message.public_id,
|
||||||
@@ -142,18 +192,26 @@ async def get_thread(
|
|||||||
user_id: int,
|
user_id: int,
|
||||||
session_key: str,
|
session_key: str,
|
||||||
) -> PlaygroundThreadResponse | None:
|
) -> PlaygroundThreadResponse | None:
|
||||||
result = await db.execute(
|
session = await _get_session_by_key(db, user_id=user_id, session_key=session_key)
|
||||||
select(PlaygroundSession).where(
|
|
||||||
PlaygroundSession.user_id == user_id,
|
|
||||||
PlaygroundSession.session_key == session_key,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
session = result.scalar_one_or_none()
|
|
||||||
if session is None:
|
if session is None:
|
||||||
return None
|
return None
|
||||||
return await _build_thread_response(db, session=session)
|
return await _build_thread_response(db, session=session)
|
||||||
|
|
||||||
|
|
||||||
|
async def _build_action_response(
|
||||||
|
db: AsyncSession,
|
||||||
|
*,
|
||||||
|
session: PlaygroundSession,
|
||||||
|
active_message_id: str | None = None,
|
||||||
|
) -> PlaygroundMessageActionResponse:
|
||||||
|
thread = await _build_thread_response(db, session=session)
|
||||||
|
return PlaygroundMessageActionResponse(
|
||||||
|
session=thread.session,
|
||||||
|
messages=thread.messages,
|
||||||
|
active_message_id=active_message_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _collect_constraints(raw_constraints: str) -> list[str]:
|
def _collect_constraints(raw_constraints: str) -> list[str]:
|
||||||
return [item.strip() for item in raw_constraints.split("\n") if item.strip()]
|
return [item.strip() for item in raw_constraints.split("\n") if item.strip()]
|
||||||
|
|
||||||
@@ -190,6 +248,31 @@ async def _set_session_state(
|
|||||||
return session
|
return session
|
||||||
|
|
||||||
|
|
||||||
|
def _spawn_assistant_run(
|
||||||
|
*,
|
||||||
|
user_id: int,
|
||||||
|
session_id: int,
|
||||||
|
session_key: str,
|
||||||
|
user_message_id: int,
|
||||||
|
assistant_message_id: int,
|
||||||
|
assistant_public_id: str,
|
||||||
|
payload: PlaygroundMessageCreateRequest,
|
||||||
|
provider_client: AIProviderClient,
|
||||||
|
) -> None:
|
||||||
|
task = asyncio.create_task(
|
||||||
|
_run_assistant_message(
|
||||||
|
user_id=user_id,
|
||||||
|
session_id=session_id,
|
||||||
|
session_key=session_key,
|
||||||
|
user_message_id=user_message_id,
|
||||||
|
assistant_message_id=assistant_message_id,
|
||||||
|
payload=payload,
|
||||||
|
provider_client=provider_client,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_ACTIVE_RUNS[assistant_public_id] = _ActiveRun(task)
|
||||||
|
|
||||||
|
|
||||||
async def create_turn(
|
async def create_turn(
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
*,
|
*,
|
||||||
@@ -252,23 +335,20 @@ async def create_turn(
|
|||||||
await db.refresh(user_message)
|
await db.refresh(user_message)
|
||||||
await db.refresh(assistant_message)
|
await db.refresh(assistant_message)
|
||||||
|
|
||||||
task = asyncio.create_task(
|
_spawn_assistant_run(
|
||||||
_run_assistant_message(
|
user_id=user_id,
|
||||||
user_id=user_id,
|
session_id=session.id,
|
||||||
session_id=session.id,
|
session_key=payload.session_key,
|
||||||
session_key=payload.session_key,
|
user_message_id=user_message.id,
|
||||||
user_message_id=user_message.id,
|
assistant_message_id=assistant_message.id,
|
||||||
assistant_message_id=assistant_message.id,
|
assistant_public_id=assistant_message.public_id,
|
||||||
payload=payload,
|
payload=payload,
|
||||||
provider_client=provider_client,
|
provider_client=provider_client,
|
||||||
)
|
|
||||||
)
|
)
|
||||||
_ACTIVE_RUNS[assistant_message.public_id] = _ActiveRun(task)
|
|
||||||
|
|
||||||
thread = await _build_thread_response(db, session=session)
|
return await _build_action_response(
|
||||||
return PlaygroundMessageActionResponse(
|
db,
|
||||||
session=thread.session,
|
session=session,
|
||||||
messages=thread.messages,
|
|
||||||
active_message_id=assistant_message.public_id,
|
active_message_id=assistant_message.public_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -303,23 +383,20 @@ async def _create_assistant_retry_turn(
|
|||||||
await db.refresh(session)
|
await db.refresh(session)
|
||||||
await db.refresh(assistant_message)
|
await db.refresh(assistant_message)
|
||||||
|
|
||||||
task = asyncio.create_task(
|
_spawn_assistant_run(
|
||||||
_run_assistant_message(
|
user_id=user_id,
|
||||||
user_id=user_id,
|
session_id=session.id,
|
||||||
session_id=session.id,
|
session_key=payload.session_key,
|
||||||
session_key=payload.session_key,
|
user_message_id=user_message.id,
|
||||||
user_message_id=user_message.id,
|
assistant_message_id=assistant_message.id,
|
||||||
assistant_message_id=assistant_message.id,
|
assistant_public_id=assistant_message.public_id,
|
||||||
payload=payload,
|
payload=payload,
|
||||||
provider_client=provider_client,
|
provider_client=provider_client,
|
||||||
)
|
|
||||||
)
|
)
|
||||||
_ACTIVE_RUNS[assistant_message.public_id] = _ActiveRun(task)
|
|
||||||
|
|
||||||
thread = await _build_thread_response(db, session=session)
|
return await _build_action_response(
|
||||||
return PlaygroundMessageActionResponse(
|
db,
|
||||||
session=thread.session,
|
session=session,
|
||||||
messages=thread.messages,
|
|
||||||
active_message_id=assistant_message.public_id,
|
active_message_id=assistant_message.public_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -330,34 +407,11 @@ async def stop_message(
|
|||||||
user_id: int,
|
user_id: int,
|
||||||
payload: PlaygroundMessageStopRequest,
|
payload: PlaygroundMessageStopRequest,
|
||||||
) -> PlaygroundMessageActionResponse:
|
) -> PlaygroundMessageActionResponse:
|
||||||
result = await db.execute(
|
session = await _require_session(db, user_id=user_id, session_key=payload.session_key)
|
||||||
select(PlaygroundSession).where(
|
message = await _require_visible_message(db, user_id=user_id, public_id=payload.message_id)
|
||||||
PlaygroundSession.user_id == user_id,
|
|
||||||
PlaygroundSession.session_key == payload.session_key,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
session = result.scalar_one_or_none()
|
|
||||||
if session is None:
|
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Playground session not found")
|
|
||||||
|
|
||||||
result = await db.execute(
|
|
||||||
select(PlaygroundMessage).where(
|
|
||||||
PlaygroundMessage.user_id == user_id,
|
|
||||||
PlaygroundMessage.public_id == payload.message_id,
|
|
||||||
PlaygroundMessage.is_visible.is_(True),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
message = result.scalar_one_or_none()
|
|
||||||
if message is None:
|
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Playground message not found")
|
|
||||||
|
|
||||||
if message.status not in {"pending", "thinking", "answering"}:
|
if message.status not in {"pending", "thinking", "answering"}:
|
||||||
thread = await _build_thread_response(db, session=session)
|
return await _build_action_response(db, session=session)
|
||||||
return PlaygroundMessageActionResponse(
|
|
||||||
session=thread.session,
|
|
||||||
messages=thread.messages,
|
|
||||||
active_message_id=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
active_run = _ACTIVE_RUNS.get(message.public_id)
|
active_run = _ACTIVE_RUNS.get(message.public_id)
|
||||||
if active_run is not None:
|
if active_run is not None:
|
||||||
@@ -371,12 +425,7 @@ async def stop_message(
|
|||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(message)
|
await db.refresh(message)
|
||||||
|
|
||||||
thread = await _build_thread_response(db, session=session)
|
return await _build_action_response(db, session=session)
|
||||||
return PlaygroundMessageActionResponse(
|
|
||||||
session=thread.session,
|
|
||||||
messages=thread.messages,
|
|
||||||
active_message_id=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def resend_turn(
|
async def resend_turn(
|
||||||
@@ -386,27 +435,13 @@ async def resend_turn(
|
|||||||
payload: PlaygroundMessageResendRequest,
|
payload: PlaygroundMessageResendRequest,
|
||||||
provider_client: AIProviderClient,
|
provider_client: AIProviderClient,
|
||||||
) -> PlaygroundMessageActionResponse:
|
) -> PlaygroundMessageActionResponse:
|
||||||
result = await db.execute(
|
session = await _require_session(db, user_id=user_id, session_key=payload.session_key)
|
||||||
select(PlaygroundSession).where(
|
user_message = await _require_visible_message(
|
||||||
PlaygroundSession.user_id == user_id,
|
db,
|
||||||
PlaygroundSession.session_key == payload.session_key,
|
user_id=user_id,
|
||||||
)
|
public_id=payload.user_message_id,
|
||||||
|
role="user",
|
||||||
)
|
)
|
||||||
session = result.scalar_one_or_none()
|
|
||||||
if session is None:
|
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Playground session not found")
|
|
||||||
|
|
||||||
result = await db.execute(
|
|
||||||
select(PlaygroundMessage).where(
|
|
||||||
PlaygroundMessage.user_id == user_id,
|
|
||||||
PlaygroundMessage.public_id == payload.user_message_id,
|
|
||||||
PlaygroundMessage.role == "user",
|
|
||||||
PlaygroundMessage.is_visible.is_(True),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
user_message = result.scalar_one_or_none()
|
|
||||||
if user_message is None:
|
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User message not found")
|
|
||||||
|
|
||||||
later_messages = await db.execute(
|
later_messages = await db.execute(
|
||||||
select(PlaygroundMessage).where(
|
select(PlaygroundMessage).where(
|
||||||
@@ -451,39 +486,20 @@ async def edit_user_message(
|
|||||||
user_id: int,
|
user_id: int,
|
||||||
payload: PlaygroundMessageEditRequest,
|
payload: PlaygroundMessageEditRequest,
|
||||||
) -> PlaygroundMessageActionResponse:
|
) -> PlaygroundMessageActionResponse:
|
||||||
result = await db.execute(
|
session = await _require_session(db, user_id=user_id, session_key=payload.session_key)
|
||||||
select(PlaygroundSession).where(
|
user_message = await _require_visible_message(
|
||||||
PlaygroundSession.user_id == user_id,
|
db,
|
||||||
PlaygroundSession.session_key == payload.session_key,
|
user_id=user_id,
|
||||||
)
|
public_id=payload.user_message_id,
|
||||||
|
role="user",
|
||||||
)
|
)
|
||||||
session = result.scalar_one_or_none()
|
|
||||||
if session is None:
|
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Playground session not found")
|
|
||||||
|
|
||||||
result = await db.execute(
|
|
||||||
select(PlaygroundMessage).where(
|
|
||||||
PlaygroundMessage.user_id == user_id,
|
|
||||||
PlaygroundMessage.public_id == payload.user_message_id,
|
|
||||||
PlaygroundMessage.role == "user",
|
|
||||||
PlaygroundMessage.is_visible.is_(True),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
user_message = result.scalar_one_or_none()
|
|
||||||
if user_message is None:
|
|
||||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="User message not found")
|
|
||||||
|
|
||||||
user_message.content = payload.content.strip()
|
user_message.content = payload.content.strip()
|
||||||
await db.flush()
|
await db.flush()
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(user_message)
|
await db.refresh(user_message)
|
||||||
|
|
||||||
thread = await _build_thread_response(db, session=session)
|
return await _build_action_response(db, session=session)
|
||||||
return PlaygroundMessageActionResponse(
|
|
||||||
session=thread.session,
|
|
||||||
messages=thread.messages,
|
|
||||||
active_message_id=None,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
async def _append_meta_if_missing(db: AsyncSession, message_id: int, meta_line: str) -> None:
|
async def _append_meta_if_missing(db: AsyncSession, message_id: int, meta_line: str) -> None:
|
||||||
|
|||||||
@@ -7,6 +7,23 @@ This project follows the repository versioning rule:
|
|||||||
- `feature` -> `+0.1.0`
|
- `feature` -> `+0.1.0`
|
||||||
- `bugfix` -> `+0.0.1`
|
- `bugfix` -> `+0.0.1`
|
||||||
|
|
||||||
|
## 0.25.1
|
||||||
|
|
||||||
|
Released: 2026-04-10
|
||||||
|
|
||||||
|
### Highlights
|
||||||
|
|
||||||
|
- Cleaned up the first persistent Playground rollout, fixed sidebar submenu persistence to match the intended operator behavior, and added reusable code-hygiene rules to prevent this class of drift from accumulating again.
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
|
||||||
|
- Improved [backend/app/services/playground_chat_service.py](/home/ray/dev/linkong/planet/backend/app/services/playground_chat_service.py) and [frontend/src/pages/Playground/Playground.tsx](/home/ray/dev/linkong/planet/frontend/src/pages/Playground/Playground.tsx) by extracting repeated lookup, response, and request-action paths into shared helpers, reducing duplicated Playground flow code without changing behavior.
|
||||||
|
- Improved [rules.md](/home/ray/dev/linkong/planet/rules.md) by adding a new `Code Hygiene - MANDATORY` section covering single-source-of-truth state, transitional cleanup, repeated-logic extraction, layout debugging order, and post-feature cleanup expectations.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed [frontend/src/components/AppLayout/AppLayout.tsx](/home/ray/dev/linkong/planet/frontend/src/components/AppLayout/AppLayout.tsx) so first-level menu expansion now behaves correctly across in-app navigation: `采集与数据` remains the default expanded group after refresh, while manually expanded groups stay open when navigating to their own child routes and reset only on page reload.
|
||||||
|
|
||||||
## 0.25.0
|
## 0.25.0
|
||||||
|
|
||||||
Released: 2026-04-10
|
Released: 2026-04-10
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
## Current Version
|
## Current Version
|
||||||
|
|
||||||
- `main` 当前主线历史推导到:`0.16.5`
|
- `main` 当前主线历史推导到:`0.16.5`
|
||||||
- `dev` 当前开发分支历史推导到:`0.25.0`
|
- `dev` 当前开发分支历史推导到:`0.25.1`
|
||||||
|
|
||||||
## Timeline
|
## Timeline
|
||||||
|
|
||||||
@@ -82,6 +82,7 @@
|
|||||||
| `0.24.7` | bugfix | `dev` | `pending` | formalize release workflow and frontend layout constraints with repo rules and a reusable release skill |
|
| `0.24.7` | bugfix | `dev` | `pending` | formalize release workflow and frontend layout constraints with repo rules and a reusable release skill |
|
||||||
| `0.24.8` | bugfix | `dev` | `pending` | move BGP brief markdown into a dedicated modal, keep tab content metadata-focused, and constrain modal scrolling to the viewport |
|
| `0.24.8` | bugfix | `dev` | `pending` | move BGP brief markdown into a dedicated modal, keep tab content metadata-focused, and constrain modal scrolling to the viewport |
|
||||||
| `0.25.0` | feature | `dev` | `89a71e6f` | add persistent backend-backed AI Playground chat state, split alert workspaces into dedicated pages, and establish the situational-awareness foundation for later multi-signal analysis |
|
| `0.25.0` | feature | `dev` | `89a71e6f` | add persistent backend-backed AI Playground chat state, split alert workspaces into dedicated pages, and establish the situational-awareness foundation for later multi-signal analysis |
|
||||||
|
| `0.25.1` | bugfix | `dev` | `pending` | clean duplicated Playground flow code, add reusable code-hygiene rules, and fix first-level sidebar menu expansion behavior across route navigation and refresh |
|
||||||
|
|
||||||
## Maintenance Commits Not Counted as Version Bumps
|
## Maintenance Commits Not Counted as Version Bumps
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "planet-frontend",
|
"name": "planet-frontend",
|
||||||
"version": "0.25.0",
|
"version": "0.25.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "bun@1",
|
"packageManager": "bun@1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import packageJson from '../../../package.json'
|
|||||||
|
|
||||||
const { Sider, Content } = Layout
|
const { Sider, Content } = Layout
|
||||||
const { Text } = Typography
|
const { Text } = Typography
|
||||||
|
const DEFAULT_OPEN_MENU_KEY = 'collection'
|
||||||
|
let cachedOpenKeys: string[] = [DEFAULT_OPEN_MENU_KEY]
|
||||||
|
|
||||||
interface AppLayoutProps {
|
interface AppLayoutProps {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
@@ -33,7 +35,7 @@ function AppLayout({ children }: AppLayoutProps) {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { user, logout } = useAuthStore()
|
const { user, logout } = useAuthStore()
|
||||||
const [collapsed, setCollapsed] = useState(false)
|
const [collapsed, setCollapsed] = useState(false)
|
||||||
const [openKeys, setOpenKeys] = useState<string[]>(['collection'])
|
const [openKeys, setOpenKeys] = useState<string[]>(cachedOpenKeys)
|
||||||
const showBanner = true
|
const showBanner = true
|
||||||
const appVersion = `v${packageJson.version}`
|
const appVersion = `v${packageJson.version}`
|
||||||
|
|
||||||
@@ -91,6 +93,11 @@ function AppLayout({ children }: AppLayoutProps) {
|
|||||||
return location.pathname
|
return location.pathname
|
||||||
}, [location.pathname])
|
}, [location.pathname])
|
||||||
|
|
||||||
|
const updateOpenKeys = (nextKeys: string[]) => {
|
||||||
|
cachedOpenKeys = nextKeys
|
||||||
|
setOpenKeys(nextKeys)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout className="dashboard-layout">
|
<Layout className="dashboard-layout">
|
||||||
<Sider
|
<Sider
|
||||||
@@ -101,7 +108,10 @@ function AppLayout({ children }: AppLayoutProps) {
|
|||||||
onCollapse={(nextCollapsed) => {
|
onCollapse={(nextCollapsed) => {
|
||||||
setCollapsed(nextCollapsed)
|
setCollapsed(nextCollapsed)
|
||||||
if (nextCollapsed) {
|
if (nextCollapsed) {
|
||||||
setOpenKeys([])
|
return
|
||||||
|
}
|
||||||
|
if (!openKeys.length) {
|
||||||
|
updateOpenKeys([DEFAULT_OPEN_MENU_KEY])
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="dashboard-sider"
|
className="dashboard-sider"
|
||||||
@@ -129,7 +139,7 @@ function AppLayout({ children }: AppLayoutProps) {
|
|||||||
openKeys={collapsed ? [] : openKeys}
|
openKeys={collapsed ? [] : openKeys}
|
||||||
items={menuItems}
|
items={menuItems}
|
||||||
onOpenChange={(keys) => {
|
onOpenChange={(keys) => {
|
||||||
setOpenKeys(keys as string[])
|
updateOpenKeys(keys as string[])
|
||||||
}}
|
}}
|
||||||
onClick={({ key }) => {
|
onClick={({ key }) => {
|
||||||
if (key !== selectedKey) {
|
if (key !== selectedKey) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
|
||||||
import { ApiOutlined, ArrowDownOutlined, ArrowUpOutlined, BorderOutlined, CopyOutlined, EditOutlined, InfoCircleOutlined, RedoOutlined, SettingOutlined, SyncOutlined } from '@ant-design/icons'
|
import { ApiOutlined, ArrowDownOutlined, ArrowUpOutlined, BorderOutlined, CopyOutlined, EditOutlined, InfoCircleOutlined, RedoOutlined, RobotOutlined, SettingOutlined, SyncOutlined, UserOutlined } from '@ant-design/icons'
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Avatar,
|
Avatar,
|
||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
message,
|
message,
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { RobotOutlined, UserOutlined } from '@ant-design/icons'
|
|
||||||
|
|
||||||
import AppLayout from '../../components/AppLayout/AppLayout'
|
import AppLayout from '../../components/AppLayout/AppLayout'
|
||||||
import MarkdownRenderer from '../../components/MarkdownRenderer/MarkdownRenderer'
|
import MarkdownRenderer from '../../components/MarkdownRenderer/MarkdownRenderer'
|
||||||
@@ -31,6 +30,17 @@ const PLAYGROUND_PROVIDER_STATUS_STORAGE_KEY = 'playground-provider-status'
|
|||||||
const PLAYGROUND_REMOTE_SESSION_KEY = 'default'
|
const PLAYGROUND_REMOTE_SESSION_KEY = 'default'
|
||||||
const THREAD_POLL_INTERVAL_MS = 1200
|
const THREAD_POLL_INTERVAL_MS = 1200
|
||||||
const SCROLL_BOTTOM_THRESHOLD_PX = 120
|
const SCROLL_BOTTOM_THRESHOLD_PX = 120
|
||||||
|
const PLAYGROUND_SYSTEM_MESSAGE: PlaygroundMessage = {
|
||||||
|
id: 'playground-system-intro',
|
||||||
|
role: 'system',
|
||||||
|
title: 'Playground 已就绪',
|
||||||
|
content: '这里是 AI 测试台。聊天记录与执行状态现在以后台数据库为准,刷新后会恢复到真实进度。',
|
||||||
|
}
|
||||||
|
const EMPTY_ANALYSIS_META: AnalysisRunMeta = {
|
||||||
|
requestId: null,
|
||||||
|
durationMs: null,
|
||||||
|
completedAt: null,
|
||||||
|
}
|
||||||
|
|
||||||
interface AIProviderStatus {
|
interface AIProviderStatus {
|
||||||
provider: string
|
provider: string
|
||||||
@@ -217,22 +227,11 @@ function Playground() {
|
|||||||
const [objective, setObjective] = useState(PLAYGROUND_PRESETS[0].values.objective)
|
const [objective, setObjective] = useState(PLAYGROUND_PRESETS[0].values.objective)
|
||||||
const [constraints, setConstraints] = useState(PLAYGROUND_PRESETS[0].values.constraints || '')
|
const [constraints, setConstraints] = useState(PLAYGROUND_PRESETS[0].values.constraints || '')
|
||||||
const [inputValue, setInputValue] = useState(PLAYGROUND_PRESETS[0].values.message)
|
const [inputValue, setInputValue] = useState(PLAYGROUND_PRESETS[0].values.message)
|
||||||
const [messages, setMessages] = useState<PlaygroundMessage[]>([
|
const [messages, setMessages] = useState<PlaygroundMessage[]>([PLAYGROUND_SYSTEM_MESSAGE])
|
||||||
{
|
|
||||||
id: 'playground-system-intro',
|
|
||||||
role: 'system',
|
|
||||||
title: 'Playground 已就绪',
|
|
||||||
content: '这里是 AI 测试台。选择一个快速预设,补充输入消息,然后发送一次真实后端链路请求。',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
const [analysis, setAnalysis] = useState<SituationalAnalysisResponse | null>(null)
|
const [analysis, setAnalysis] = useState<SituationalAnalysisResponse | null>(null)
|
||||||
const [detailOpen, setDetailOpen] = useState(false)
|
const [detailOpen, setDetailOpen] = useState(false)
|
||||||
const [latestAnalysisMessageId, setLatestAnalysisMessageId] = useState<string | null>(null)
|
const [latestAnalysisMessageId, setLatestAnalysisMessageId] = useState<string | null>(null)
|
||||||
const [analysisMeta, setAnalysisMeta] = useState<AnalysisRunMeta>({
|
const [analysisMeta, setAnalysisMeta] = useState<AnalysisRunMeta>(EMPTY_ANALYSIS_META)
|
||||||
requestId: null,
|
|
||||||
durationMs: null,
|
|
||||||
completedAt: null,
|
|
||||||
})
|
|
||||||
const [requestPending, setRequestPending] = useState(false)
|
const [requestPending, setRequestPending] = useState(false)
|
||||||
const [editingMessageId, setEditingMessageId] = useState<string | null>(null)
|
const [editingMessageId, setEditingMessageId] = useState<string | null>(null)
|
||||||
const [editingContent, setEditingContent] = useState('')
|
const [editingContent, setEditingContent] = useState('')
|
||||||
@@ -266,24 +265,46 @@ function Playground() {
|
|||||||
setInputValue(sessionState.inputValue || '')
|
setInputValue(sessionState.inputValue || '')
|
||||||
setHelpExpanded(sessionState.helpExpanded ?? true)
|
setHelpExpanded(sessionState.helpExpanded ?? true)
|
||||||
const remoteMessages = thread.messages.map(toPlaygroundMessage)
|
const remoteMessages = thread.messages.map(toPlaygroundMessage)
|
||||||
setMessages([
|
setMessages([PLAYGROUND_SYSTEM_MESSAGE, ...remoteMessages])
|
||||||
{
|
|
||||||
id: 'playground-system-intro',
|
|
||||||
role: 'system',
|
|
||||||
title: 'Playground 已就绪',
|
|
||||||
content: '这里是 AI 测试台。聊天记录与执行状态现在以后台数据库为准,刷新后会恢复到真实进度。',
|
|
||||||
},
|
|
||||||
...remoteMessages,
|
|
||||||
])
|
|
||||||
setLatestAnalysisMessageId(sessionState.latestAnalysisMessageId || null)
|
setLatestAnalysisMessageId(sessionState.latestAnalysisMessageId || null)
|
||||||
setAnalysis((sessionState.analysis as SituationalAnalysisResponse | null) || null)
|
setAnalysis((sessionState.analysis as SituationalAnalysisResponse | null) || null)
|
||||||
setAnalysisMeta(sessionState.analysisMeta || {
|
setAnalysisMeta(sessionState.analysisMeta || EMPTY_ANALYSIS_META)
|
||||||
requestId: null,
|
}
|
||||||
durationMs: null,
|
|
||||||
completedAt: null,
|
const authHeaders = (includeJson = false): HeadersInit => ({
|
||||||
|
...(includeJson ? { 'Content-Type': 'application/json' } : {}),
|
||||||
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||||
|
})
|
||||||
|
|
||||||
|
const applyActionResponse = (data: { messages: PlaygroundApiMessage[]; session: PlaygroundSessionRecord }) => {
|
||||||
|
applyThreadSnapshot({
|
||||||
|
session: data.session,
|
||||||
|
messages: data.messages,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchPlaygroundThread = async () => {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/ai/playground/thread?session_key=${encodeURIComponent(PLAYGROUND_REMOTE_SESSION_KEY)}`, {
|
||||||
|
headers: authHeaders(),
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error('THREAD_FETCH_FAILED')
|
||||||
|
}
|
||||||
|
return (await res.json()) as PlaygroundThreadRecord | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const postPlaygroundAction = async <TBody,>(path: string, body: TBody) => {
|
||||||
|
const res = await fetch(`${API_BASE_URL}${path}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: authHeaders(true),
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error(`PLAYGROUND_ACTION_FAILED:${path}`)
|
||||||
|
}
|
||||||
|
return (await res.json()) as { messages: PlaygroundApiMessage[]; session: PlaygroundSessionRecord }
|
||||||
|
}
|
||||||
|
|
||||||
const scrollToBottom = (behavior: ScrollBehavior = 'smooth') => {
|
const scrollToBottom = (behavior: ScrollBehavior = 'smooth') => {
|
||||||
const container = messagesContainerRef.current
|
const container = messagesContainerRef.current
|
||||||
if (!container) {
|
if (!container) {
|
||||||
@@ -327,20 +348,12 @@ function Playground() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE_URL}/ai/playground/thread?session_key=${encodeURIComponent(PLAYGROUND_REMOTE_SESSION_KEY)}`, {
|
const thread = await fetchPlaygroundThread()
|
||||||
headers: {
|
applyThreadSnapshot(thread)
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
if (res.ok) {
|
|
||||||
const thread = (await res.json()) as PlaygroundThreadRecord | null
|
|
||||||
applyThreadSnapshot(thread)
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
if (!options?.silent) {
|
if (!options?.silent) {
|
||||||
messageApi.error('Playground 会话加载失败')
|
messageApi.error('Playground 会话加载失败')
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -417,25 +430,11 @@ function Playground() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE_URL}/ai/playground/messages/stop`, {
|
const data = await postPlaygroundAction('/ai/playground/messages/stop', {
|
||||||
method: 'POST',
|
session_key: PLAYGROUND_REMOTE_SESSION_KEY,
|
||||||
headers: {
|
message_id: activeAssistantMessage.id,
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
session_key: PLAYGROUND_REMOTE_SESSION_KEY,
|
|
||||||
message_id: activeAssistantMessage.id,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error('STOP_FAILED')
|
|
||||||
}
|
|
||||||
const data = (await res.json()) as { messages: PlaygroundApiMessage[], session: PlaygroundSessionRecord }
|
|
||||||
applyThreadSnapshot({
|
|
||||||
session: data.session,
|
|
||||||
messages: data.messages,
|
|
||||||
})
|
})
|
||||||
|
applyActionResponse(data)
|
||||||
messageApi.warning('已停止生成,已输出内容会保留')
|
messageApi.warning('已停止生成,已输出内容会保留')
|
||||||
} catch {
|
} catch {
|
||||||
messageApi.error('停止失败,请稍后再试')
|
messageApi.error('停止失败,请稍后再试')
|
||||||
@@ -455,30 +454,16 @@ function Playground() {
|
|||||||
setRequestPending(true)
|
setRequestPending(true)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE_URL}/ai/playground/messages`, {
|
const data = await postPlaygroundAction('/ai/playground/messages', {
|
||||||
method: 'POST',
|
session_key: PLAYGROUND_REMOTE_SESSION_KEY,
|
||||||
headers: {
|
title: trimmedTitle,
|
||||||
'Content-Type': 'application/json',
|
objective: trimmedObjective,
|
||||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
constraints,
|
||||||
},
|
input: trimmedInput,
|
||||||
body: JSON.stringify({
|
selected_preset_key: selectedPreset.key,
|
||||||
session_key: PLAYGROUND_REMOTE_SESSION_KEY,
|
help_expanded: helpExpanded,
|
||||||
title: trimmedTitle,
|
|
||||||
objective: trimmedObjective,
|
|
||||||
constraints,
|
|
||||||
input: trimmedInput,
|
|
||||||
selected_preset_key: selectedPreset.key,
|
|
||||||
help_expanded: helpExpanded,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error('SEND_FAILED')
|
|
||||||
}
|
|
||||||
const data = (await res.json()) as { messages: PlaygroundApiMessage[], session: PlaygroundSessionRecord }
|
|
||||||
applyThreadSnapshot({
|
|
||||||
session: data.session,
|
|
||||||
messages: data.messages,
|
|
||||||
})
|
})
|
||||||
|
applyActionResponse(data)
|
||||||
setRequestPending(false)
|
setRequestPending(false)
|
||||||
void refreshThread({ silent: true })
|
void refreshThread({ silent: true })
|
||||||
} catch {
|
} catch {
|
||||||
@@ -521,25 +506,11 @@ function Playground() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE_URL}/ai/playground/messages/resend`, {
|
const data = await postPlaygroundAction('/ai/playground/messages/resend', {
|
||||||
method: 'POST',
|
session_key: PLAYGROUND_REMOTE_SESSION_KEY,
|
||||||
headers: {
|
user_message_id: entry.id,
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
session_key: PLAYGROUND_REMOTE_SESSION_KEY,
|
|
||||||
user_message_id: entry.id,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error('RESEND_FAILED')
|
|
||||||
}
|
|
||||||
const data = (await res.json()) as { messages: PlaygroundApiMessage[], session: PlaygroundSessionRecord }
|
|
||||||
applyThreadSnapshot({
|
|
||||||
session: data.session,
|
|
||||||
messages: data.messages,
|
|
||||||
})
|
})
|
||||||
|
applyActionResponse(data)
|
||||||
messageApi.success('已重试,并清理该消息之后的旧分支')
|
messageApi.success('已重试,并清理该消息之后的旧分支')
|
||||||
} catch {
|
} catch {
|
||||||
messageApi.error('重试失败,请稍后再试')
|
messageApi.error('重试失败,请稍后再试')
|
||||||
@@ -569,40 +540,16 @@ function Playground() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
setEditSaving(true)
|
setEditSaving(true)
|
||||||
const editRes = await fetch(`${API_BASE_URL}/ai/playground/messages/edit`, {
|
await postPlaygroundAction('/ai/playground/messages/edit', {
|
||||||
method: 'POST',
|
session_key: PLAYGROUND_REMOTE_SESSION_KEY,
|
||||||
headers: {
|
user_message_id: entry.id,
|
||||||
'Content-Type': 'application/json',
|
content: trimmed,
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
session_key: PLAYGROUND_REMOTE_SESSION_KEY,
|
|
||||||
user_message_id: entry.id,
|
|
||||||
content: trimmed,
|
|
||||||
}),
|
|
||||||
})
|
})
|
||||||
if (!editRes.ok) {
|
const data = await postPlaygroundAction('/ai/playground/messages/resend', {
|
||||||
throw new Error('EDIT_FAILED')
|
session_key: PLAYGROUND_REMOTE_SESSION_KEY,
|
||||||
}
|
user_message_id: entry.id,
|
||||||
const resendRes = await fetch(`${API_BASE_URL}/ai/playground/messages/resend`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: `Bearer ${token}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
session_key: PLAYGROUND_REMOTE_SESSION_KEY,
|
|
||||||
user_message_id: entry.id,
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
if (!resendRes.ok) {
|
|
||||||
throw new Error('RESEND_AFTER_EDIT_FAILED')
|
|
||||||
}
|
|
||||||
const data = (await resendRes.json()) as { messages: PlaygroundApiMessage[], session: PlaygroundSessionRecord }
|
|
||||||
applyThreadSnapshot({
|
|
||||||
session: data.session,
|
|
||||||
messages: data.messages,
|
|
||||||
})
|
})
|
||||||
|
applyActionResponse(data)
|
||||||
setEditingMessageId(null)
|
setEditingMessageId(null)
|
||||||
setEditingContent('')
|
setEditingContent('')
|
||||||
setEditSaving(false)
|
setEditSaving(false)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "planet"
|
name = "planet"
|
||||||
version = "0.25.0"
|
version = "0.25.1"
|
||||||
description = "智能星球计划 - 态势感知系统"
|
description = "智能星球计划 - 态势感知系统"
|
||||||
requires-python = ">=3.14"
|
requires-python = ">=3.14"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
15
rules.md
15
rules.md
@@ -225,6 +225,21 @@ class BaseCollector:
|
|||||||
- Feature flags for incomplete features
|
- Feature flags for incomplete features
|
||||||
- Use config files for environment-specific settings
|
- Use config files for environment-specific settings
|
||||||
|
|
||||||
|
## Code Hygiene - MANDATORY
|
||||||
|
|
||||||
|
- Maintain a single source of truth for business state. Frontend temporary state, cached state, and persisted backend state must not evolve into parallel truths.
|
||||||
|
- Transitional paths are temporary. Once a new implementation is stable, remove old branches, old interfaces, old mocks, and compatibility layers instead of letting them linger.
|
||||||
|
- Repeated logic must be extracted. If request flow, response handling, auth/header assembly, validation, or state reconciliation appears more than once or twice, promote it into a helper or shared layer.
|
||||||
|
- Repeated backend resource lookup and response assembly must be centralized. Avoid scattering the same `load -> validate -> transform -> respond` pattern across multiple handlers or services.
|
||||||
|
- Default values, system prompts, placeholder structures, and other fixed constants must be centralized rather than re-declared in multiple states or code paths.
|
||||||
|
- When debugging layout, scrolling, or overflow issues, first inspect structural ownership of height, width, and overflow before applying isolated style patches.
|
||||||
|
- Distinct interaction modes must have explicit structure and state semantics. View, edit, loading, error, stopped, and retry states should not be forced through the exact same markup or logic path.
|
||||||
|
- Presentation state must not pretend to be business state. UI animation, phase labels, and optimistic display layers must defer to real persisted or backend task state when it exists.
|
||||||
|
- Responsive adaptations must preserve the primary action path. Reflow is fine; losing or displacing the main user action is not.
|
||||||
|
- After large feature commits, perform an explicit cleanup pass for dead code, temporary branches, duplicated helpers, stale interfaces, and naming drift before considering the work complete.
|
||||||
|
- If a file or module starts accumulating repeated patterns or mixed responsibilities, stop and refactor before continuing to add more features on top.
|
||||||
|
- Public interfaces, persisted fields, and state structures must have a current owner and caller. If something is no longer used, delete it instead of keeping it “just in case”.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Query Performance - MANDATORY
|
## Query Performance - MANDATORY
|
||||||
|
|||||||
Reference in New Issue
Block a user