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`
|
- `feature` -> `+0.1.0`
|
||||||
- `bugfix` -> `+0.0.1`
|
- `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
|
## 0.24.2
|
||||||
|
|
||||||
Released: 2026-04-09
|
Released: 2026-04-09
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
## Current Version
|
## Current Version
|
||||||
|
|
||||||
- `main` 当前主线历史推导到:`0.16.5`
|
- `main` 当前主线历史推导到:`0.16.5`
|
||||||
- `dev` 当前开发分支历史推导到:`0.24.2`
|
- `dev` 当前开发分支历史推导到:`0.24.3`
|
||||||
|
|
||||||
## Timeline
|
## Timeline
|
||||||
|
|
||||||
@@ -75,6 +75,7 @@
|
|||||||
| `0.24.0` | feature | `dev` | `pending` | add AI Playground entry, provider diagnostics, and frontend layout guidance |
|
| `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.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.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
|
## Maintenance Commits Not Counted as Version Bumps
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "planet-frontend",
|
"name": "planet-frontend",
|
||||||
"version": "0.24.2",
|
"version": "0.24.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "bun@1",
|
"packageManager": "bun@1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -380,6 +380,23 @@ body {
|
|||||||
padding-bottom: 4px;
|
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 {
|
.playground-form__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -555,10 +572,23 @@ body {
|
|||||||
|
|
||||||
.playground-result__meta {
|
.playground-result__meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
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__content,
|
||||||
.playground-result__blocks .ant-card {
|
.playground-result__blocks .ant-card {
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
@@ -591,6 +621,13 @@ body {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.playground-result__blocks-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.playground-result__blocks-scroll {
|
.playground-result__blocks-scroll {
|
||||||
max-height: 180px;
|
max-height: 180px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
@@ -619,6 +656,12 @@ body {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
max-width: none;
|
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 { useEffect, useState } from 'react'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { SyncOutlined } from '@ant-design/icons'
|
import { CopyOutlined, ReloadOutlined, SyncOutlined } from '@ant-design/icons'
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Collapse,
|
Collapse,
|
||||||
|
Descriptions,
|
||||||
Form,
|
Form,
|
||||||
Input,
|
Input,
|
||||||
Space,
|
Space,
|
||||||
@@ -53,6 +54,51 @@ interface PlaygroundFormValues {
|
|||||||
constraints?: string
|
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) {
|
function splitLines(value?: string) {
|
||||||
return (value || '')
|
return (value || '')
|
||||||
.split('\n')
|
.split('\n')
|
||||||
@@ -67,6 +113,12 @@ function Playground() {
|
|||||||
const [analyzing, setAnalyzing] = useState(false)
|
const [analyzing, setAnalyzing] = useState(false)
|
||||||
const [providerStatus, setProviderStatus] = useState<AIProviderStatus | null>(null)
|
const [providerStatus, setProviderStatus] = useState<AIProviderStatus | null>(null)
|
||||||
const [analysis, setAnalysis] = useState<SituationalAnalysisResponse | 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 [activeTab, setActiveTab] = useState<'request' | 'result'>('request')
|
||||||
const [helpExpanded, setHelpExpanded] = useState(true)
|
const [helpExpanded, setHelpExpanded] = useState(true)
|
||||||
|
|
||||||
@@ -87,6 +139,7 @@ function Playground() {
|
|||||||
const res = await axios.get<AIProviderStatus>(`${API_BASE_URL}/ai/provider/status`)
|
const res = await axios.get<AIProviderStatus>(`${API_BASE_URL}/ai/provider/status`)
|
||||||
setProviderStatus(res.data)
|
setProviderStatus(res.data)
|
||||||
sessionStorage.setItem(PLAYGROUND_PROVIDER_STATUS_STORAGE_KEY, JSON.stringify(res.data))
|
sessionStorage.setItem(PLAYGROUND_PROVIDER_STATUS_STORAGE_KEY, JSON.stringify(res.data))
|
||||||
|
setProviderStatusUpdatedAt(new Date().toLocaleString())
|
||||||
} catch {
|
} catch {
|
||||||
messageApi.error('AI Provider 状态获取失败')
|
messageApi.error('AI Provider 状态获取失败')
|
||||||
} finally {
|
} finally {
|
||||||
@@ -99,6 +152,7 @@ function Playground() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleAnalyze = async (values: PlaygroundFormValues) => {
|
const handleAnalyze = async (values: PlaygroundFormValues) => {
|
||||||
|
const startedAt = performance.now()
|
||||||
setAnalyzing(true)
|
setAnalyzing(true)
|
||||||
setActiveTab('result')
|
setActiveTab('result')
|
||||||
try {
|
try {
|
||||||
@@ -115,6 +169,11 @@ function Playground() {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
setAnalysis(res.data)
|
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('分析已完成')
|
messageApi.success('分析已完成')
|
||||||
} catch {
|
} catch {
|
||||||
messageApi.error('分析失败,请检查 AI Provider 配置或稍后再试')
|
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 (
|
return (
|
||||||
<AppLayout>
|
<AppLayout>
|
||||||
{contextHolder}
|
{contextHolder}
|
||||||
@@ -188,6 +272,10 @@ function Playground() {
|
|||||||
{providerStatus.base_url || '-'}
|
{providerStatus.base_url || '-'}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="playground-kv">
|
||||||
|
<Text type="secondary">最后同步</Text>
|
||||||
|
<Text>{providerStatusUpdatedAt || '-'}</Text>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Alert type="warning" showIcon message="尚未获取到 AI Provider 状态" />
|
<Alert type="warning" showIcon message="尚未获取到 AI Provider 状态" />
|
||||||
@@ -223,6 +311,13 @@ function Playground() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<Alert
|
||||||
|
type="success"
|
||||||
|
showIcon
|
||||||
|
className="playground-note"
|
||||||
|
message="推荐使用左侧预设场景快速联调。"
|
||||||
|
description="先看 Provider 状态,再选择一个预设模版发起请求,最后在结果区检查 request id、thinking blocks 和 raw response。"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -241,15 +336,20 @@ function Playground() {
|
|||||||
label: '请求',
|
label: '请求',
|
||||||
children: (
|
children: (
|
||||||
<div className="playground-tabpane">
|
<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={form}
|
form={form}
|
||||||
layout="vertical"
|
layout="vertical"
|
||||||
initialValues={{
|
initialValues={PLAYGROUND_PRESETS[0].values}
|
||||||
title: 'BGP 告警态势简报',
|
|
||||||
objective: '总结当前告警的主要风险、优先级与建议动作。',
|
|
||||||
observations: '出现新的高危告警\n部分观测站最近 24h 事件增多',
|
|
||||||
constraints: '结论要简洁\n优先给出操作建议',
|
|
||||||
}}
|
|
||||||
onFinish={handleAnalyze}
|
onFinish={handleAnalyze}
|
||||||
>
|
>
|
||||||
<Form.Item
|
<Form.Item
|
||||||
@@ -284,11 +384,16 @@ function Playground() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
form.resetFields()
|
form.setFieldsValue(PLAYGROUND_PRESETS[0].values)
|
||||||
setAnalysis(null)
|
setAnalysis(null)
|
||||||
|
setAnalysisMeta({
|
||||||
|
requestId: null,
|
||||||
|
durationMs: null,
|
||||||
|
completedAt: null,
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
清空
|
重置为默认模板
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
@@ -308,9 +413,30 @@ function Playground() {
|
|||||||
) : analysis ? (
|
) : analysis ? (
|
||||||
<Space direction="vertical" size={16} className="playground-result__stack">
|
<Space direction="vertical" size={16} className="playground-result__stack">
|
||||||
<div className="playground-result__meta">
|
<div className="playground-result__meta">
|
||||||
<Tag color="blue">{analysis.provider}</Tag>
|
<div className="playground-result__meta-tags">
|
||||||
<Tag>{analysis.model}</Tag>
|
<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>
|
</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">
|
<div className="playground-result__content">
|
||||||
<pre>{analysis.content}</pre>
|
<pre>{analysis.content}</pre>
|
||||||
</div>
|
</div>
|
||||||
@@ -326,6 +452,31 @@ function Playground() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : 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>
|
</Space>
|
||||||
) : (
|
) : (
|
||||||
<Alert
|
<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_BIN="${FRONTEND_RUNTIME_BIN:-}"
|
||||||
FRONTEND_RUNTIME_SOURCE="${FRONTEND_RUNTIME_SOURCE:-}"
|
FRONTEND_RUNTIME_SOURCE="${FRONTEND_RUNTIME_SOURCE:-}"
|
||||||
FRONTEND_PID_FILE="/tmp/planet_frontend.pid"
|
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() {
|
prepend_path_once() {
|
||||||
local path_entry="$1"
|
local path_entry="$1"
|
||||||
@@ -79,12 +83,55 @@ ensure_local_runtime_bins_on_path() {
|
|||||||
ensure_local_runtime_bins_on_path
|
ensure_local_runtime_bins_on_path
|
||||||
|
|
||||||
# Shell / Docker helpers
|
# 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() {
|
compose_up() {
|
||||||
if docker compose version >/dev/null 2>&1; then
|
local args=("$@")
|
||||||
docker compose "$@"
|
local arg_text="$*"
|
||||||
else
|
|
||||||
docker-compose "$@"
|
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
|
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
|
# Terminal rendering helpers
|
||||||
@@ -338,6 +385,102 @@ run_with_retry() {
|
|||||||
return 1
|
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() {
|
install_uv_if_needed() {
|
||||||
if command -v uv >/dev/null 2>&1; then
|
if command -v uv >/dev/null 2>&1; then
|
||||||
return 0
|
return 0
|
||||||
@@ -753,9 +896,19 @@ start_ai_provider_service() {
|
|||||||
local retry=1
|
local retry=1
|
||||||
set_wait_detail "启动 AI Provider"
|
set_wait_detail "启动 AI Provider"
|
||||||
|
|
||||||
|
ensure_ai_provider_image_current
|
||||||
|
|
||||||
while [ "$retry" -le "$AI_PROVIDER_START_MAX_RETRIES" ]; do
|
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 [ "$AI_PROVIDER_RECREATE_REQUIRED" -eq 1 ]; then
|
||||||
if wait_for_container_health "planet_aiprovider" "$AI_PROVIDER_HEALTH_CHECK_ATTEMPTS" "$AI_PROVIDER_HEALTH_CHECK_INTERVAL" "AI Provider" &&
|
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
|
wait_for_http "http://localhost:${ai_provider_port}/health" "$AI_PROVIDER_HEALTH_CHECK_ATTEMPTS" "$AI_PROVIDER_HEALTH_CHECK_INTERVAL" "AI Provider"; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
@@ -763,17 +916,18 @@ start_ai_provider_service() {
|
|||||||
|
|
||||||
if [ "$retry" -eq "$AI_PROVIDER_START_MAX_RETRIES" ]; then
|
if [ "$retry" -eq "$AI_PROVIDER_START_MAX_RETRIES" ]; then
|
||||||
log_error "AI Provider 启动失败,已重试 ${AI_PROVIDER_START_MAX_RETRIES} 次"
|
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
|
exit 1
|
||||||
fi
|
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"
|
animate_wait_spinner "AI Provider 第 ${retry}/${AI_PROVIDER_START_MAX_RETRIES} 次启动后仍未健康,正在重启容器" "$AI_PROVIDER_RETRY_INTERVAL"
|
||||||
retry=$((retry + 1))
|
retry=$((retry + 1))
|
||||||
done
|
done
|
||||||
|
|
||||||
log_error "AI Provider 启动失败"
|
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
|
exit 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1179,7 +1333,7 @@ stop_backend_service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stop_ai_provider_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() {
|
stop_frontend_service() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "planet"
|
name = "planet"
|
||||||
version = "0.24.2"
|
version = "0.24.3"
|
||||||
description = "智能星球计划 - 态势感知系统"
|
description = "智能星球计划 - 态势感知系统"
|
||||||
requires-python = ">=3.14"
|
requires-python = ">=3.14"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
Reference in New Issue
Block a user