diff --git a/VERSION b/VERSION index 8b95abd9..63082344 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.24.2 +0.24.3 diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b050dcb3..ae56525a 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -7,6 +7,25 @@ This project follows the repository versioning rule: - `feature` -> `+0.1.0` - `bugfix` -> `+0.0.1` +## 0.24.3 + +Released: 2026-04-09 + +### Highlights + +- Extended `AI Playground` from a minimal prompt form into a more repeatable diagnostics workspace, and hardened `planet.sh` so AI Provider restarts can rebuild changed images and expose clearer Compose fallback behavior. + +### Improved + +- Improved [frontend/src/pages/Playground/Playground.tsx](/home/ray/dev/linkong/planet/frontend/src/pages/Playground/Playground.tsx) by adding preset scenarios, response metadata, `thinking` block visibility, raw JSON inspection, and copy actions so the page works more like a proper AI diagnostics console. +- Improved [frontend/src/index.css](/home/ray/dev/linkong/planet/frontend/src/index.css) by styling Playground presets, result metadata, raw response sections, and responsive action groups without breaking the single-screen workspace layout. +- Improved [planet.sh](/home/ray/dev/linkong/planet/planet.sh) so AI Provider restarts detect code/config changes, rebuild the image when needed, and surface which Compose path is being used during image and container operations. + +### Fixed + +- Fixed the AI Provider restart path in [planet.sh](/home/ray/dev/linkong/planet/planet.sh) so code changes inside `aiprovider/` no longer stay hidden behind an old container image after `restart -a`. +- Fixed Compose error reporting in [planet.sh](/home/ray/dev/linkong/planet/planet.sh) by making the script explicitly show `docker compose v2` first, then `docker-compose v1`, and only fail after both execution paths are exhausted. + ## 0.24.2 Released: 2026-04-09 diff --git a/docs/version-history.md b/docs/version-history.md index 922dded0..4d86120a 100644 --- a/docs/version-history.md +++ b/docs/version-history.md @@ -16,7 +16,7 @@ ## Current Version - `main` 当前主线历史推导到:`0.16.5` -- `dev` 当前开发分支历史推导到:`0.24.2` +- `dev` 当前开发分支历史推导到:`0.24.3` ## Timeline @@ -75,6 +75,7 @@ | `0.24.0` | feature | `dev` | `pending` | add AI Playground entry, provider diagnostics, and frontend layout guidance | | `0.24.1` | bugfix | `dev` | `pending` | refactor Earth HUD into class-first CSS layers, unify Bun-only frontend tooling guidance, and auto-bootstrap Bun/uv in `planet.sh` | | `0.24.2` | bugfix | `dev` | `pending` | restore public Earth entry, refresh Playground provider diagnostics correctly, fit help-card content, and split frontend bundles by route/vendor | +| `0.24.3` | bugfix | `dev` | `pending` | expand Playground diagnostics presets and result inspection, and make `planet.sh` rebuild changed AI Provider images with explicit Compose fallback reporting | ## Maintenance Commits Not Counted as Version Bumps diff --git a/frontend/package.json b/frontend/package.json index 95656b0d..7c56e834 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "planet-frontend", - "version": "0.24.2", + "version": "0.24.3", "private": true, "packageManager": "bun@1", "dependencies": { diff --git a/frontend/src/index.css b/frontend/src/index.css index bb495c92..a390ee7b 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -380,6 +380,23 @@ body { padding-bottom: 4px; } +.playground-preset-strip { + display: flex; + flex-direction: column; + gap: 10px; + margin-bottom: 16px; + padding: 12px; + border: 1px solid rgba(148, 163, 184, 0.22); + border-radius: 14px; + background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); +} + +.playground-preset-strip__actions { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + .playground-form__actions { display: flex; gap: 12px; @@ -555,10 +572,23 @@ body { .playground-result__meta { display: flex; + align-items: center; + justify-content: space-between; flex-wrap: wrap; gap: 8px; } +.playground-result__meta-tags { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.playground-result__descriptions .ant-descriptions-view { + border-radius: 14px; + overflow: hidden; +} + .playground-result__content, .playground-result__blocks .ant-card { border-radius: 14px; @@ -591,6 +621,13 @@ body { gap: 12px; } +.playground-result__blocks-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + .playground-result__blocks-scroll { max-height: 180px; overflow: auto; @@ -619,6 +656,12 @@ body { min-width: 0; max-width: none; } + + .playground-result__meta, + .playground-result__blocks-head { + align-items: stretch; + flex-direction: column; + } } diff --git a/frontend/src/pages/Playground/Playground.tsx b/frontend/src/pages/Playground/Playground.tsx index 38c22231..17660e2a 100644 --- a/frontend/src/pages/Playground/Playground.tsx +++ b/frontend/src/pages/Playground/Playground.tsx @@ -1,11 +1,12 @@ import { useEffect, useState } from 'react' import axios from 'axios' -import { SyncOutlined } from '@ant-design/icons' +import { CopyOutlined, ReloadOutlined, SyncOutlined } from '@ant-design/icons' import { Alert, Button, Card, Collapse, + Descriptions, Form, Input, Space, @@ -53,6 +54,51 @@ interface PlaygroundFormValues { constraints?: string } +interface PlaygroundPreset { + key: string + label: string + values: PlaygroundFormValues +} + +interface AnalysisRunMeta { + requestId: string | null + durationMs: number | null + completedAt: string | null +} + +const PLAYGROUND_PRESETS: PlaygroundPreset[] = [ + { + key: 'bgp-brief', + label: 'BGP 简报', + values: { + title: 'BGP 告警态势简报', + objective: '总结当前告警的主要风险、优先级与建议动作。', + observations: '出现新的高危告警\n部分观测站最近 24h 事件增多', + constraints: '结论要简洁\n优先给出操作建议', + }, + }, + { + key: 'datasource-health', + label: '数据源健康', + values: { + title: '采集器健康检查说明', + objective: '判断当前采集器失败是否属于上游接口失效、限流、结构变更或临时波动。', + observations: '最近 3 次任务失败\n部分数据源响应时间抬升\n个别接口返回结构不稳定', + constraints: '区分事实与推断\n先给排障优先级', + }, + }, + { + key: 'link-smoke', + label: '链路探测', + values: { + title: 'AI 链路探测', + objective: '验证 backend -> aiprovider -> model provider 调用链路是否正常。', + observations: '当前从 Playground 发起测试\n希望确认 provider 配置和返回结构正常', + constraints: '输出简洁\n包含一段明确结论', + }, + }, +] + function splitLines(value?: string) { return (value || '') .split('\n') @@ -67,6 +113,12 @@ function Playground() { const [analyzing, setAnalyzing] = useState(false) const [providerStatus, setProviderStatus] = useState(null) const [analysis, setAnalysis] = useState(null) + const [analysisMeta, setAnalysisMeta] = useState({ + requestId: null, + durationMs: null, + completedAt: null, + }) + const [providerStatusUpdatedAt, setProviderStatusUpdatedAt] = useState(null) const [activeTab, setActiveTab] = useState<'request' | 'result'>('request') const [helpExpanded, setHelpExpanded] = useState(true) @@ -87,6 +139,7 @@ function Playground() { const res = await axios.get(`${API_BASE_URL}/ai/provider/status`) setProviderStatus(res.data) sessionStorage.setItem(PLAYGROUND_PROVIDER_STATUS_STORAGE_KEY, JSON.stringify(res.data)) + setProviderStatusUpdatedAt(new Date().toLocaleString()) } catch { messageApi.error('AI Provider 状态获取失败') } finally { @@ -99,6 +152,7 @@ function Playground() { }, []) const handleAnalyze = async (values: PlaygroundFormValues) => { + const startedAt = performance.now() setAnalyzing(true) setActiveTab('result') try { @@ -115,6 +169,11 @@ function Playground() { }, ) setAnalysis(res.data) + setAnalysisMeta({ + requestId: typeof res.headers['x-request-id'] === 'string' ? res.headers['x-request-id'] : null, + durationMs: Math.round(performance.now() - startedAt), + completedAt: new Date().toLocaleString(), + }) messageApi.success('分析已完成') } catch { messageApi.error('分析失败,请检查 AI Provider 配置或稍后再试') @@ -124,6 +183,31 @@ function Playground() { } } + const handleApplyPreset = (preset: PlaygroundPreset) => { + form.setFieldsValue(preset.values) + setActiveTab('request') + } + + const handleCopyResult = async () => { + if (!analysis?.content) return + try { + await navigator.clipboard.writeText(analysis.content) + messageApi.success('结果已复制') + } catch { + messageApi.error('复制失败,请稍后再试') + } + } + + const handleCopyRawResponse = async () => { + if (!analysis) return + try { + await navigator.clipboard.writeText(JSON.stringify(analysis.raw_response, null, 2)) + messageApi.success('原始响应已复制') + } catch { + messageApi.error('复制失败,请稍后再试') + } + } + return ( {contextHolder} @@ -188,6 +272,10 @@ function Playground() { {providerStatus.base_url || '-'} +
+ 最后同步 + {providerStatusUpdatedAt || '-'} +
) : ( @@ -223,6 +311,13 @@ function Playground() { )} /> + ), }, @@ -241,15 +336,20 @@ function Playground() { label: '请求', children: (
+
+ 快速预设 +
+ {PLAYGROUND_PRESETS.map((preset) => ( + + ))} +
+
@@ -308,9 +413,30 @@ function Playground() { ) : analysis ? (
- {analysis.provider} - {analysis.model} +
+ {analysis.provider} + {analysis.model} +
+ + + +
+
{analysis.content}
@@ -326,6 +452,31 @@ function Playground() { ) : null} + {analysis.thinking_blocks.length ? ( +
+ Thinking Blocks +
+ {analysis.thinking_blocks.map((block, index) => ( + +
{block}
+
+ ))} +
+
+ ) : null} +
+
+ Raw Response + +
+
+ +
{JSON.stringify(analysis.raw_response, null, 2)}
+
+
+
) : ( /dev/null 2>&1 +} + +compose_v1_available() { + command -v docker-compose >/dev/null 2>&1 +} + +report_missing_compose() { + clear_wait_spinner + log_error "未检测到可用的 Docker Compose" + log_note "优先需要 Docker Compose v2 (`docker compose`)" + log_note "如果当前机器仍只装有旧版 `docker-compose`,也请确认该命令可执行" + log_note "推荐安装 Docker Compose v2 插件:" + log_note ' mkdir -p ~/.docker/cli-plugins' + log_note ' curl -SL https://github.com/docker/compose/releases/download/v2.40.3/docker-compose-linux-x86_64 -o ~/.docker/cli-plugins/docker-compose' + log_note ' chmod +x ~/.docker/cli-plugins/docker-compose' + log_note ' docker compose version' + return 1 +} + compose_up() { - if docker compose version >/dev/null 2>&1; then - docker compose "$@" - else - docker-compose "$@" + local args=("$@") + local arg_text="$*" + + if compose_v2_available; then + set_wait_detail "正在用 docker compose v2 执行: ${arg_text}" + if docker compose "${args[@]}"; then + return 0 + fi + log_warn "docker compose v2 执行失败,回退到 docker-compose v1" fi + + if compose_v1_available; then + set_wait_detail "正在用 docker-compose v1 执行: ${arg_text}" + if docker-compose "${args[@]}"; then + return 0 + fi + clear_wait_spinner + log_error "docker compose v2 与 docker-compose v1 均执行失败" + log_note "最后尝试的命令: docker-compose ${arg_text}" + return 1 + fi + + report_missing_compose +} + +compose_supports_build() { + compose_up build --help >/dev/null 2>&1 } # Terminal rendering helpers @@ -338,6 +385,102 @@ run_with_retry() { return 1 } +compute_ai_provider_build_fingerprint() { + ( + cd "$SCRIPT_DIR" || exit 1 + tar -cf - \ + aiprovider \ + pyproject.toml \ + uv.lock \ + docker-compose.yml \ + docker-compose.simple.yml 2>/dev/null + ) | sha256sum | awk '{print $1}' +} + +read_ai_provider_build_stamp() { + [ -f "$AI_PROVIDER_BUILD_STAMP_FILE" ] || return 1 + tr -d '[:space:]' < "$AI_PROVIDER_BUILD_STAMP_FILE" +} + +write_ai_provider_build_stamp() { + local fingerprint="$1" + printf "%s\n" "$fingerprint" > "$AI_PROVIDER_BUILD_STAMP_FILE" +} + +ai_provider_image_exists() { + docker image inspect "$AI_PROVIDER_IMAGE_NAME" >/dev/null 2>&1 +} + +list_ai_provider_containers() { + docker ps -aq --filter "name=planet_aiprovider" 2>/dev/null || true +} + +remove_ai_provider_containers() { + local container_ids + container_ids="$(list_ai_provider_containers)" + [ -n "$container_ids" ] || return 0 + echo "$container_ids" | xargs -r docker rm -f >/dev/null 2>&1 || true +} + +run_ai_provider_container_manually() { + local ai_provider_port="${1:-$DEFAULT_AI_PROVIDER_PORT}" + local env_file_args=() + + if [ -f "$SCRIPT_DIR/aiprovider/.env" ]; then + env_file_args=(--env-file "$SCRIPT_DIR/aiprovider/.env") + fi + + docker run -d \ + --name "$AI_PROVIDER_CONTAINER_NAME" \ + "${env_file_args[@]}" \ + -p "${ai_provider_port}:8010" \ + "$AI_PROVIDER_IMAGE_NAME" >/dev/null +} + +recreate_ai_provider_container() { + local ai_provider_port="${1:-$DEFAULT_AI_PROVIDER_PORT}" + + remove_ai_provider_containers + + if compose_up up -d aiprovider >/dev/null 2>&1; then + return 0 + fi + + run_ai_provider_container_manually "$ai_provider_port" +} + +ensure_ai_provider_image_current() { + local current_fingerprint="" + local previous_fingerprint="" + + current_fingerprint="$(compute_ai_provider_build_fingerprint)" + previous_fingerprint="$(read_ai_provider_build_stamp || true)" + + if ai_provider_image_exists && [ -n "$previous_fingerprint" ] && [ "$current_fingerprint" = "$previous_fingerprint" ]; then + AI_PROVIDER_RECREATE_REQUIRED=0 + return 0 + fi + + set_wait_detail "检测到 AI Provider 代码或配置变化,重建镜像" + + if ! compose_supports_build; then + log_error "当前 docker compose 不支持 build,请检查 Docker / Compose 环境" + exit 1 + fi + + if ! run_with_retry \ + "$AI_PROVIDER_START_MAX_RETRIES" \ + "$AI_PROVIDER_RETRY_INTERVAL" \ + "AI Provider 镜像构建失败,已重试 ${AI_PROVIDER_START_MAX_RETRIES} 次" \ + "构建 AI Provider 镜像" \ + compose_up build aiprovider; then + exit 1 + fi + + write_ai_provider_build_stamp "$current_fingerprint" + AI_PROVIDER_RECREATE_REQUIRED=1 +} + install_uv_if_needed() { if command -v uv >/dev/null 2>&1; then return 0 @@ -753,9 +896,19 @@ start_ai_provider_service() { local retry=1 set_wait_detail "启动 AI Provider" + ensure_ai_provider_image_current + while [ "$retry" -le "$AI_PROVIDER_START_MAX_RETRIES" ]; do - if docker start planet_aiprovider >/dev/null 2>&1 || compose_up up -d aiprovider >/dev/null 2>&1; then - if wait_for_container_health "planet_aiprovider" "$AI_PROVIDER_HEALTH_CHECK_ATTEMPTS" "$AI_PROVIDER_HEALTH_CHECK_INTERVAL" "AI Provider" && + if [ "$AI_PROVIDER_RECREATE_REQUIRED" -eq 1 ]; then + if recreate_ai_provider_container "$ai_provider_port"; then + AI_PROVIDER_RECREATE_REQUIRED=0 + if wait_for_container_health "$AI_PROVIDER_CONTAINER_NAME" "$AI_PROVIDER_HEALTH_CHECK_ATTEMPTS" "$AI_PROVIDER_HEALTH_CHECK_INTERVAL" "AI Provider" && + wait_for_http "http://localhost:${ai_provider_port}/health" "$AI_PROVIDER_HEALTH_CHECK_ATTEMPTS" "$AI_PROVIDER_HEALTH_CHECK_INTERVAL" "AI Provider"; then + return 0 + fi + fi + elif docker start "$AI_PROVIDER_CONTAINER_NAME" >/dev/null 2>&1 || compose_up up -d aiprovider >/dev/null 2>&1; then + if wait_for_container_health "$AI_PROVIDER_CONTAINER_NAME" "$AI_PROVIDER_HEALTH_CHECK_ATTEMPTS" "$AI_PROVIDER_HEALTH_CHECK_INTERVAL" "AI Provider" && wait_for_http "http://localhost:${ai_provider_port}/health" "$AI_PROVIDER_HEALTH_CHECK_ATTEMPTS" "$AI_PROVIDER_HEALTH_CHECK_INTERVAL" "AI Provider"; then return 0 fi @@ -763,17 +916,18 @@ start_ai_provider_service() { if [ "$retry" -eq "$AI_PROVIDER_START_MAX_RETRIES" ]; then log_error "AI Provider 启动失败,已重试 ${AI_PROVIDER_START_MAX_RETRIES} 次" - docker logs --tail 20 planet_aiprovider 2>/dev/null || true + docker logs --tail 20 "$AI_PROVIDER_CONTAINER_NAME" 2>/dev/null || true exit 1 fi - docker restart planet_aiprovider >/dev/null 2>&1 || true + remove_ai_provider_containers + AI_PROVIDER_RECREATE_REQUIRED=1 animate_wait_spinner "AI Provider 第 ${retry}/${AI_PROVIDER_START_MAX_RETRIES} 次启动后仍未健康,正在重启容器" "$AI_PROVIDER_RETRY_INTERVAL" retry=$((retry + 1)) done log_error "AI Provider 启动失败" - docker logs --tail 20 planet_aiprovider 2>/dev/null || true + docker logs --tail 20 "$AI_PROVIDER_CONTAINER_NAME" 2>/dev/null || true exit 1 } @@ -1179,7 +1333,7 @@ stop_backend_service() { } stop_ai_provider_service() { - stop_container_if_running "planet_aiprovider" "AI Provider" + stop_container_if_running "$AI_PROVIDER_CONTAINER_NAME" "AI Provider" } stop_frontend_service() { diff --git a/pyproject.toml b/pyproject.toml index 94ce09c3..7f7ddc93 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "planet" -version = "0.24.2" +version = "0.24.3" description = "智能星球计划 - 态势感知系统" requires-python = ">=3.14" dependencies = [ diff --git a/uv.lock b/uv.lock index dc11f79c..926b27e4 100644 --- a/uv.lock +++ b/uv.lock @@ -475,7 +475,7 @@ wheels = [ [[package]] name = "planet" -version = "0.24.2" +version = "0.24.3" source = { virtual = "." } dependencies = [ { name = "aiofiles" },