fix: expand playground diagnostics and compose fallback
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "planet-frontend",
|
||||
"version": "0.24.2",
|
||||
"version": "0.24.3",
|
||||
"private": true,
|
||||
"packageManager": "bun@1",
|
||||
"dependencies": {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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<AIProviderStatus | null>(null)
|
||||
const [analysis, setAnalysis] = useState<SituationalAnalysisResponse | null>(null)
|
||||
const [analysisMeta, setAnalysisMeta] = useState<AnalysisRunMeta>({
|
||||
requestId: null,
|
||||
durationMs: null,
|
||||
completedAt: null,
|
||||
})
|
||||
const [providerStatusUpdatedAt, setProviderStatusUpdatedAt] = useState<string | null>(null)
|
||||
const [activeTab, setActiveTab] = useState<'request' | 'result'>('request')
|
||||
const [helpExpanded, setHelpExpanded] = useState(true)
|
||||
|
||||
@@ -87,6 +139,7 @@ function Playground() {
|
||||
const res = await axios.get<AIProviderStatus>(`${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 (
|
||||
<AppLayout>
|
||||
{contextHolder}
|
||||
@@ -188,6 +272,10 @@ function Playground() {
|
||||
{providerStatus.base_url || '-'}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="playground-kv">
|
||||
<Text type="secondary">最后同步</Text>
|
||||
<Text>{providerStatusUpdatedAt || '-'}</Text>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<Alert type="warning" showIcon message="尚未获取到 AI Provider 状态" />
|
||||
@@ -223,6 +311,13 @@ function Playground() {
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
<Alert
|
||||
type="success"
|
||||
showIcon
|
||||
className="playground-note"
|
||||
message="推荐使用左侧预设场景快速联调。"
|
||||
description="先看 Provider 状态,再选择一个预设模版发起请求,最后在结果区检查 request id、thinking blocks 和 raw response。"
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -241,15 +336,20 @@ function Playground() {
|
||||
label: '请求',
|
||||
children: (
|
||||
<div className="playground-tabpane">
|
||||
<div className="playground-preset-strip">
|
||||
<Text type="secondary">快速预设</Text>
|
||||
<div className="playground-preset-strip__actions">
|
||||
{PLAYGROUND_PRESETS.map((preset) => (
|
||||
<Button key={preset.key} onClick={() => handleApplyPreset(preset)}>
|
||||
{preset.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
title: 'BGP 告警态势简报',
|
||||
objective: '总结当前告警的主要风险、优先级与建议动作。',
|
||||
observations: '出现新的高危告警\n部分观测站最近 24h 事件增多',
|
||||
constraints: '结论要简洁\n优先给出操作建议',
|
||||
}}
|
||||
initialValues={PLAYGROUND_PRESETS[0].values}
|
||||
onFinish={handleAnalyze}
|
||||
>
|
||||
<Form.Item
|
||||
@@ -284,11 +384,16 @@ function Playground() {
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
form.resetFields()
|
||||
form.setFieldsValue(PLAYGROUND_PRESETS[0].values)
|
||||
setAnalysis(null)
|
||||
setAnalysisMeta({
|
||||
requestId: null,
|
||||
durationMs: null,
|
||||
completedAt: null,
|
||||
})
|
||||
}}
|
||||
>
|
||||
清空
|
||||
重置为默认模板
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
@@ -308,9 +413,30 @@ function Playground() {
|
||||
) : analysis ? (
|
||||
<Space direction="vertical" size={16} className="playground-result__stack">
|
||||
<div className="playground-result__meta">
|
||||
<Tag color="blue">{analysis.provider}</Tag>
|
||||
<Tag>{analysis.model}</Tag>
|
||||
<div className="playground-result__meta-tags">
|
||||
<Tag color="blue">{analysis.provider}</Tag>
|
||||
<Tag>{analysis.model}</Tag>
|
||||
</div>
|
||||
<Space wrap>
|
||||
<Button icon={<CopyOutlined />} onClick={() => void handleCopyResult()}>
|
||||
复制结果
|
||||
</Button>
|
||||
<Button icon={<ReloadOutlined />} onClick={() => setActiveTab('request')}>
|
||||
返回请求
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
<Descriptions
|
||||
size="small"
|
||||
bordered
|
||||
className="playground-result__descriptions"
|
||||
column={1}
|
||||
items={[
|
||||
{ key: 'requestId', label: 'Request ID', children: analysisMeta.requestId || '-' },
|
||||
{ key: 'duration', label: '耗时', children: analysisMeta.durationMs ? `${analysisMeta.durationMs} ms` : '-' },
|
||||
{ key: 'completedAt', label: '完成时间', children: analysisMeta.completedAt || '-' },
|
||||
]}
|
||||
/>
|
||||
<div className="playground-result__content">
|
||||
<pre>{analysis.content}</pre>
|
||||
</div>
|
||||
@@ -326,6 +452,31 @@ function Playground() {
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{analysis.thinking_blocks.length ? (
|
||||
<div className="playground-result__blocks">
|
||||
<Text strong>Thinking Blocks</Text>
|
||||
<div className="playground-result__blocks-scroll">
|
||||
{analysis.thinking_blocks.map((block, index) => (
|
||||
<Card key={`${index}-${block.slice(0, 12)}`} size="small">
|
||||
<pre>{block}</pre>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="playground-result__blocks">
|
||||
<div className="playground-result__blocks-head">
|
||||
<Text strong>Raw Response</Text>
|
||||
<Button size="small" icon={<CopyOutlined />} onClick={() => void handleCopyRawResponse()}>
|
||||
复制 JSON
|
||||
</Button>
|
||||
</div>
|
||||
<div className="playground-result__blocks-scroll">
|
||||
<Card size="small">
|
||||
<pre>{JSON.stringify(analysis.raw_response, null, 2)}</pre>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</Space>
|
||||
) : (
|
||||
<Alert
|
||||
|
||||
174
planet.sh
174
planet.sh
@@ -59,6 +59,10 @@ DEFAULT_AI_PROVIDER_PORT="${DEFAULT_AI_PROVIDER_PORT:-8010}"
|
||||
FRONTEND_RUNTIME_BIN="${FRONTEND_RUNTIME_BIN:-}"
|
||||
FRONTEND_RUNTIME_SOURCE="${FRONTEND_RUNTIME_SOURCE:-}"
|
||||
FRONTEND_PID_FILE="/tmp/planet_frontend.pid"
|
||||
AI_PROVIDER_BUILD_STAMP_FILE="/tmp/planet_aiprovider_build.sha256"
|
||||
AI_PROVIDER_IMAGE_NAME="${AI_PROVIDER_IMAGE_NAME:-planet_aiprovider:latest}"
|
||||
AI_PROVIDER_CONTAINER_NAME="${AI_PROVIDER_CONTAINER_NAME:-planet_aiprovider}"
|
||||
AI_PROVIDER_RECREATE_REQUIRED=0
|
||||
|
||||
prepend_path_once() {
|
||||
local path_entry="$1"
|
||||
@@ -79,12 +83,55 @@ ensure_local_runtime_bins_on_path() {
|
||||
ensure_local_runtime_bins_on_path
|
||||
|
||||
# Shell / Docker helpers
|
||||
compose_v2_available() {
|
||||
docker compose version >/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() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "planet"
|
||||
version = "0.24.2"
|
||||
version = "0.24.3"
|
||||
description = "智能星球计划 - 态势感知系统"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
|
||||
Reference in New Issue
Block a user