release: bump version to 0.45.0
This commit is contained in:
13
.dockerignore
Normal file
13
.dockerignore
Normal file
@@ -0,0 +1,13 @@
|
||||
**
|
||||
|
||||
!pyproject.toml
|
||||
!uv.lock
|
||||
!aiprovider/
|
||||
!aiprovider/**
|
||||
|
||||
aiprovider/.env
|
||||
aiprovider/.env.*
|
||||
!aiprovider/.env.example
|
||||
**/__pycache__/
|
||||
**/*.pyc
|
||||
**/*.pyo
|
||||
@@ -1,3 +1,5 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
ARG PYTHON_IMAGE=python:3.14-slim
|
||||
ARG UV_IMAGE=ghcr.io/astral-sh/uv:latest
|
||||
|
||||
@@ -18,9 +20,10 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY pyproject.toml uv.lock /app/
|
||||
RUN uv sync --frozen --no-dev
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --frozen --no-dev
|
||||
|
||||
COPY . /app
|
||||
COPY aiprovider /app/aiprovider
|
||||
|
||||
EXPOSE 8010
|
||||
|
||||
|
||||
@@ -398,6 +398,11 @@ async def list_datasources(
|
||||
"task_id": running_task.id if running_task else None,
|
||||
"progress": running_task.progress if running_task else None,
|
||||
"phase": running_task.phase if running_task else None,
|
||||
"phase_progress": running_task.phase_progress if running_task else None,
|
||||
"phase_message": running_task.phase_message if running_task else None,
|
||||
"phase_current": running_task.phase_current if running_task else None,
|
||||
"phase_total": running_task.phase_total if running_task else None,
|
||||
"phase_unit": running_task.phase_unit if running_task else None,
|
||||
"records_processed": running_task.records_processed if running_task else None,
|
||||
"total_records": running_task.total_records if running_task else None,
|
||||
}
|
||||
@@ -626,6 +631,11 @@ async def trigger_datasource(
|
||||
"message": "当前采集任务尚未完成,重新触发会丢失本次未完成进度。是否强制重新采集?",
|
||||
"task_id": running_task.id,
|
||||
"phase": running_task.phase,
|
||||
"phase_progress": running_task.phase_progress,
|
||||
"phase_message": running_task.phase_message,
|
||||
"phase_current": running_task.phase_current,
|
||||
"phase_total": running_task.phase_total,
|
||||
"phase_unit": running_task.phase_unit,
|
||||
"progress": running_task.progress,
|
||||
"records_processed": running_task.records_processed,
|
||||
"total_records": running_task.total_records,
|
||||
@@ -709,13 +719,29 @@ async def get_task_status(
|
||||
task = await get_running_task(db, datasource.id)
|
||||
|
||||
if not task:
|
||||
return {"is_running": False, "task_id": None, "progress": None, "phase": None, "status": "idle"}
|
||||
return {
|
||||
"is_running": False,
|
||||
"task_id": None,
|
||||
"progress": None,
|
||||
"phase": None,
|
||||
"phase_progress": None,
|
||||
"phase_message": None,
|
||||
"phase_current": None,
|
||||
"phase_total": None,
|
||||
"phase_unit": None,
|
||||
"status": "idle",
|
||||
}
|
||||
|
||||
return {
|
||||
"is_running": task.status == "running",
|
||||
"task_id": task.id,
|
||||
"progress": task.progress,
|
||||
"phase": task.phase,
|
||||
"phase_progress": task.phase_progress,
|
||||
"phase_message": task.phase_message,
|
||||
"phase_current": task.phase_current,
|
||||
"phase_total": task.phase_total,
|
||||
"phase_unit": task.phase_unit,
|
||||
"records_processed": task.records_processed,
|
||||
"total_records": task.total_records,
|
||||
"status": task.status,
|
||||
|
||||
@@ -27,7 +27,9 @@ async def list_tasks(
|
||||
offset = (page - 1) * page_size
|
||||
query = """
|
||||
SELECT ct.id, ct.datasource_id, ds.name as datasource_name, ct.status,
|
||||
ct.started_at, ct.completed_at, ct.records_processed, ct.error_message
|
||||
ct.started_at, ct.completed_at, ct.records_processed, ct.error_message,
|
||||
ct.phase, ct.phase_progress, ct.phase_message, ct.phase_current,
|
||||
ct.phase_total, ct.phase_unit, ct.total_records, ct.progress
|
||||
FROM collection_tasks ct
|
||||
JOIN data_sources ds ON ct.datasource_id = ds.id
|
||||
WHERE 1=1
|
||||
@@ -66,6 +68,14 @@ async def list_tasks(
|
||||
"completed_at": to_iso8601_utc(t[5]),
|
||||
"records_processed": t[6],
|
||||
"error_message": t[7],
|
||||
"phase": t[8],
|
||||
"phase_progress": t[9],
|
||||
"phase_message": t[10],
|
||||
"phase_current": t[11],
|
||||
"phase_total": t[12],
|
||||
"phase_unit": t[13],
|
||||
"total_records": t[14],
|
||||
"progress": t[15],
|
||||
}
|
||||
for t in tasks
|
||||
],
|
||||
@@ -81,7 +91,9 @@ async def get_task(
|
||||
result = await db.execute(
|
||||
text("""
|
||||
SELECT ct.id, ct.datasource_id, ds.name as datasource_name, ct.status,
|
||||
ct.started_at, ct.completed_at, ct.records_processed, ct.error_message
|
||||
ct.started_at, ct.completed_at, ct.records_processed, ct.error_message,
|
||||
ct.phase, ct.phase_progress, ct.phase_message, ct.phase_current,
|
||||
ct.phase_total, ct.phase_unit, ct.total_records, ct.progress
|
||||
FROM collection_tasks ct
|
||||
JOIN data_sources ds ON ct.datasource_id = ds.id
|
||||
WHERE ct.id = :id
|
||||
@@ -105,6 +117,14 @@ async def get_task(
|
||||
"completed_at": to_iso8601_utc(task[5]),
|
||||
"records_processed": task[6],
|
||||
"error_message": task[7],
|
||||
"phase": task[8],
|
||||
"phase_progress": task[9],
|
||||
"phase_message": task[10],
|
||||
"phase_current": task[11],
|
||||
"phase_total": task[12],
|
||||
"phase_unit": task[13],
|
||||
"total_records": task[14],
|
||||
"progress": task[15],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -146,7 +146,12 @@ async def init_db():
|
||||
text(
|
||||
"""
|
||||
ALTER TABLE collection_tasks
|
||||
ADD COLUMN IF NOT EXISTS phase VARCHAR(30) DEFAULT 'queued'
|
||||
ADD COLUMN IF NOT EXISTS phase VARCHAR(30) DEFAULT 'queued',
|
||||
ADD COLUMN IF NOT EXISTS phase_progress DOUBLE PRECISION,
|
||||
ADD COLUMN IF NOT EXISTS phase_message VARCHAR(255),
|
||||
ADD COLUMN IF NOT EXISTS phase_current BIGINT,
|
||||
ADD COLUMN IF NOT EXISTS phase_total BIGINT,
|
||||
ADD COLUMN IF NOT EXISTS phase_unit VARCHAR(30)
|
||||
"""
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Collection Task model"""
|
||||
|
||||
from sqlalchemy import Column, DateTime, Integer, String, Text, Float
|
||||
from sqlalchemy import BigInteger, Column, DateTime, Integer, String, Text, Float
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
from app.db.session import Base
|
||||
@@ -13,6 +13,11 @@ class CollectionTask(Base):
|
||||
datasource_id = Column(Integer, nullable=False, index=True)
|
||||
status = Column(String(20), nullable=False) # pending, running, success, failed, cancelled
|
||||
phase = Column(String(30), default="queued")
|
||||
phase_progress = Column(Float)
|
||||
phase_message = Column(String(255))
|
||||
phase_current = Column(BigInteger)
|
||||
phase_total = Column(BigInteger)
|
||||
phase_unit = Column(String(30))
|
||||
started_at = Column(DateTime(timezone=True))
|
||||
completed_at = Column(DateTime(timezone=True))
|
||||
records_processed = Column(Integer, default=0)
|
||||
|
||||
@@ -54,6 +54,11 @@ class BaseCollector(ABC):
|
||||
"task_id": self._current_task.id,
|
||||
"status": self._current_task.status,
|
||||
"phase": self._current_task.phase,
|
||||
"phase_progress": self._current_task.phase_progress,
|
||||
"phase_message": self._current_task.phase_message,
|
||||
"phase_current": self._current_task.phase_current,
|
||||
"phase_total": self._current_task.phase_total,
|
||||
"phase_unit": self._current_task.phase_unit,
|
||||
"progress": progress,
|
||||
"records_processed": self._current_task.records_processed,
|
||||
"total_records": self._current_task.total_records,
|
||||
@@ -80,12 +85,52 @@ class BaseCollector(ABC):
|
||||
|
||||
await self._publish_task_update(force=force)
|
||||
|
||||
async def set_phase(self, phase: str):
|
||||
async def set_phase(self, phase: str, *, message: str | None = None, reset_progress: bool = True):
|
||||
if self._current_task and self._db_session:
|
||||
self._current_task.phase = phase
|
||||
self._current_task.phase_message = message
|
||||
if reset_progress:
|
||||
self._current_task.phase_progress = None
|
||||
self._current_task.phase_current = None
|
||||
self._current_task.phase_total = None
|
||||
self._current_task.phase_unit = None
|
||||
await self._db_session.commit()
|
||||
await self._publish_task_update(force=True)
|
||||
|
||||
async def update_phase_progress(
|
||||
self,
|
||||
*,
|
||||
current: int | None = None,
|
||||
total: int | None = None,
|
||||
unit: str | None = None,
|
||||
message: str | None = None,
|
||||
progress: float | None = None,
|
||||
commit: bool = False,
|
||||
force: bool = False,
|
||||
):
|
||||
"""Update progress for the current phase without changing task totals."""
|
||||
if not self._current_task or not self._db_session:
|
||||
return
|
||||
|
||||
if progress is None and current is not None and total and total > 0:
|
||||
progress = (current / total) * 100
|
||||
|
||||
if progress is not None:
|
||||
self._current_task.phase_progress = max(0.0, min(float(progress), 100.0))
|
||||
if current is not None:
|
||||
self._current_task.phase_current = max(0, int(current))
|
||||
if total is not None:
|
||||
self._current_task.phase_total = max(0, int(total))
|
||||
if unit is not None:
|
||||
self._current_task.phase_unit = unit
|
||||
if message is not None:
|
||||
self._current_task.phase_message = message
|
||||
|
||||
if commit:
|
||||
await self._db_session.commit()
|
||||
|
||||
await self._publish_task_update(force=force)
|
||||
|
||||
@abstractmethod
|
||||
async def fetch(self) -> List[Dict[str, Any]]:
|
||||
"""Fetch raw data from source"""
|
||||
@@ -251,7 +296,7 @@ class BaseCollector(ABC):
|
||||
await self._publish_task_update(force=True)
|
||||
|
||||
try:
|
||||
await self.set_phase("fetching")
|
||||
await self.set_phase("fetching", message="正在拉取原始数据")
|
||||
raw_data = await self.fetch()
|
||||
task.total_records = len(raw_data)
|
||||
await db.commit()
|
||||
@@ -260,15 +305,20 @@ class BaseCollector(ABC):
|
||||
if self.fail_on_empty and not raw_data:
|
||||
raise RuntimeError(f"Collector {self.name} returned no data")
|
||||
|
||||
await self.set_phase("transforming")
|
||||
await self.set_phase("transforming", message="正在转换采集数据")
|
||||
data = self.transform(raw_data)
|
||||
snapshot_id = await self._create_snapshot(db, task_id, data, start_time)
|
||||
|
||||
await self.set_phase("saving")
|
||||
await self.set_phase("saving", message="正在保存采集数据")
|
||||
records_count = await self._save_data(db, data, task_id=task_id, snapshot_id=snapshot_id)
|
||||
|
||||
task.status = "success"
|
||||
task.phase = "completed"
|
||||
task.phase_progress = 100.0
|
||||
task.phase_message = "采集完成"
|
||||
task.phase_current = records_count
|
||||
task.phase_total = records_count
|
||||
task.phase_unit = "records"
|
||||
task.records_processed = records_count
|
||||
task.progress = 100.0
|
||||
task.completed_at = datetime.now(UTC)
|
||||
@@ -285,6 +335,7 @@ class BaseCollector(ABC):
|
||||
await db.rollback()
|
||||
task.status = "cancelled"
|
||||
task.phase = "cancelled"
|
||||
task.phase_message = "采集已取消"
|
||||
task.error_message = "Collection cancelled by operator and rolled back"
|
||||
task.completed_at = datetime.now(UTC)
|
||||
if snapshot_id is not None:
|
||||
@@ -301,6 +352,7 @@ class BaseCollector(ABC):
|
||||
await db.rollback()
|
||||
task.status = "failed"
|
||||
task.phase = "failed"
|
||||
task.phase_message = str(e)
|
||||
task.error_message = str(e)
|
||||
task.completed_at = datetime.now(UTC)
|
||||
if snapshot_id is not None:
|
||||
|
||||
@@ -108,6 +108,11 @@ class IPtoASNPrefixGeoCollector(BaseCollector):
|
||||
self._current_task.total_records = total_expected
|
||||
self._current_task.records_processed = 0
|
||||
self._current_task.progress = 0.0
|
||||
self._current_task.phase_progress = 0.0
|
||||
self._current_task.phase_message = "正在下载 IPtoASN 数据"
|
||||
self._current_task.phase_current = 0
|
||||
self._current_task.phase_total = total_expected
|
||||
self._current_task.phase_unit = "bytes"
|
||||
await self._db_session.commit()
|
||||
await self._publish_task_update(force=True)
|
||||
|
||||
@@ -135,7 +140,14 @@ class IPtoASNPrefixGeoCollector(BaseCollector):
|
||||
return
|
||||
last_emit["value"] = aggregated
|
||||
last_emit["t"] = now
|
||||
await self.update_progress(min(aggregated, total_expected), commit=True)
|
||||
current = min(aggregated, total_expected)
|
||||
await self.update_phase_progress(
|
||||
current=current,
|
||||
total=total_expected,
|
||||
unit="bytes",
|
||||
message="正在下载 IPtoASN 数据",
|
||||
)
|
||||
await self.update_progress(current, commit=True)
|
||||
|
||||
batches = await asyncio.gather(
|
||||
*(
|
||||
@@ -148,6 +160,12 @@ class IPtoASNPrefixGeoCollector(BaseCollector):
|
||||
)
|
||||
)
|
||||
if total_expected > 0:
|
||||
await self.update_phase_progress(
|
||||
current=total_expected,
|
||||
total=total_expected,
|
||||
unit="bytes",
|
||||
message="IPtoASN 数据下载完成",
|
||||
)
|
||||
await self.update_progress(total_expected, commit=True, force=True)
|
||||
|
||||
rows: list[dict[str, Any]] = []
|
||||
|
||||
@@ -39,12 +39,23 @@ class NRODelegatedPrefixGeoCollector(BaseCollector):
|
||||
self._current_task.total_records = total_expected
|
||||
self._current_task.records_processed = 0
|
||||
self._current_task.progress = 0.0
|
||||
self._current_task.phase_progress = 0.0
|
||||
self._current_task.phase_message = "正在下载 NRO delegated 数据"
|
||||
self._current_task.phase_current = 0
|
||||
self._current_task.phase_total = total_expected
|
||||
self._current_task.phase_unit = "bytes"
|
||||
await self._db_session.commit()
|
||||
await self._publish_task_update(force=True)
|
||||
|
||||
async def on_progress(downloaded: int, total: int | None) -> None:
|
||||
if not total or total <= 0:
|
||||
return
|
||||
await self.update_phase_progress(
|
||||
current=min(downloaded, total),
|
||||
total=total,
|
||||
unit="bytes",
|
||||
message="正在下载 NRO delegated 数据",
|
||||
)
|
||||
await self.update_progress(min(downloaded, total), commit=True)
|
||||
|
||||
body_path = await self._downloader.download_file(
|
||||
|
||||
@@ -40,12 +40,23 @@ class OpenGeoFeedPrefixGeoCollector(BaseCollector):
|
||||
self._current_task.total_records = total_expected
|
||||
self._current_task.records_processed = 0
|
||||
self._current_task.progress = 0.0
|
||||
self._current_task.phase_progress = 0.0
|
||||
self._current_task.phase_message = "正在下载 OpenGeoFeed 数据"
|
||||
self._current_task.phase_current = 0
|
||||
self._current_task.phase_total = total_expected
|
||||
self._current_task.phase_unit = "bytes"
|
||||
await self._db_session.commit()
|
||||
await self._publish_task_update(force=True)
|
||||
|
||||
async def on_progress(downloaded: int, total: int | None) -> None:
|
||||
if not total or total <= 0:
|
||||
return
|
||||
await self.update_phase_progress(
|
||||
current=min(downloaded, total),
|
||||
total=total,
|
||||
unit="bytes",
|
||||
message="正在下载 OpenGeoFeed 数据",
|
||||
)
|
||||
await self.update_progress(min(downloaded, total), commit=True)
|
||||
|
||||
body_path = await self._downloader.download_file(
|
||||
|
||||
@@ -6,6 +6,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from app.services.collectors.top500 import TOP500Collector
|
||||
from app.services.collectors.base import BaseCollector, HTTPCollector
|
||||
from app.models.task import CollectionTask
|
||||
|
||||
|
||||
class TestBaseCollector:
|
||||
@@ -19,6 +20,31 @@ class TestBaseCollector:
|
||||
assert collector.module == "L1"
|
||||
assert collector.frequency_hours == 4
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_phase_progress_tracks_phase_fields(self, mock_db_session):
|
||||
"""Test phase-level progress updates independently from record totals"""
|
||||
collector = TOP500Collector()
|
||||
task = CollectionTask(datasource_id=1, status="running", phase="fetching")
|
||||
collector._current_task = task
|
||||
collector._db_session = mock_db_session
|
||||
|
||||
with patch.object(collector, "_publish_task_update", new=AsyncMock()) as publish:
|
||||
await collector.update_phase_progress(
|
||||
current=512,
|
||||
total=1024,
|
||||
unit="bytes",
|
||||
message="Downloading dataset",
|
||||
commit=True,
|
||||
)
|
||||
|
||||
assert task.phase_progress == 50.0
|
||||
assert task.phase_current == 512
|
||||
assert task.phase_total == 1024
|
||||
assert task.phase_unit == "bytes"
|
||||
assert task.phase_message == "Downloading dataset"
|
||||
mock_db_session.commit.assert_awaited_once()
|
||||
publish.assert_awaited_once()
|
||||
|
||||
|
||||
class TestTOP500Collector:
|
||||
"""Tests for TOP500Collector"""
|
||||
|
||||
@@ -121,6 +121,25 @@ class TestCollectionTaskModel:
|
||||
)
|
||||
assert task.records_processed == 100
|
||||
|
||||
def test_task_with_phase_progress(self):
|
||||
"""Test collection task phase-level progress fields"""
|
||||
task = CollectionTask(
|
||||
datasource_id=1,
|
||||
status="running",
|
||||
phase="fetching",
|
||||
phase_progress=42.5,
|
||||
phase_message="Downloading dataset",
|
||||
phase_current=1024,
|
||||
phase_total=4096,
|
||||
phase_unit="bytes",
|
||||
)
|
||||
assert task.phase == "fetching"
|
||||
assert task.phase_progress == 42.5
|
||||
assert task.phase_message == "Downloading dataset"
|
||||
assert task.phase_current == 1024
|
||||
assert task.phase_total == 4096
|
||||
assert task.phase_unit == "bytes"
|
||||
|
||||
def test_task_error_message(self):
|
||||
"""Test collection task with error message"""
|
||||
task = CollectionTask(
|
||||
|
||||
@@ -86,6 +86,12 @@ CREATE TABLE collection_tasks (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
datasource_id INTEGER NOT NULL REFERENCES data_sources(id) ON DELETE CASCADE,
|
||||
status task_status NOT NULL DEFAULT 'pending',
|
||||
phase VARCHAR(30) DEFAULT 'queued',
|
||||
phase_progress FLOAT,
|
||||
phase_message VARCHAR(255),
|
||||
phase_current BIGINT,
|
||||
phase_total BIGINT,
|
||||
phase_unit VARCHAR(30),
|
||||
started_at TIMESTAMP WITH TIME ZONE,
|
||||
completed_at TIMESTAMP WITH TIME ZONE,
|
||||
records_processed INTEGER DEFAULT 0,
|
||||
|
||||
@@ -8,6 +8,9 @@ services:
|
||||
args:
|
||||
PYTHON_IMAGE: ${PYTHON_IMAGE:-python:3.14-slim}
|
||||
UV_IMAGE: ${UV_IMAGE:-ghcr.io/astral-sh/uv:latest}
|
||||
env_file:
|
||||
- ./aiprovider/.env
|
||||
- ${PLANET_AI_PROVIDER_RUNTIME_ENV_FILE:-./aiprovider/.env}
|
||||
container_name: planet_aiprovider
|
||||
ports:
|
||||
- "8010:8010"
|
||||
|
||||
@@ -10,6 +10,7 @@ services:
|
||||
UV_IMAGE: ${UV_IMAGE:-ghcr.io/astral-sh/uv:latest}
|
||||
env_file:
|
||||
- ./aiprovider/.env
|
||||
- ${PLANET_AI_PROVIDER_RUNTIME_ENV_FILE:-./aiprovider/.env}
|
||||
container_name: planet_aiprovider
|
||||
ports:
|
||||
- "8010:8010"
|
||||
|
||||
@@ -8,6 +8,20 @@ This project follows the repository versioning rule:
|
||||
- `improvement` -> `+0.0.1`(bugfix + 小功能混合)
|
||||
- `bugfix` -> `+0.0.1`
|
||||
|
||||
## [0.45.0] — 2026-04-29
|
||||
|
||||
### ✨ Highlights
|
||||
- 采集任务新增阶段级量化进度,`fetching` 可展示百分比、阶段说明和字节下载量。
|
||||
- AI Provider 启动链路支持从 `aiprovider/.env` 与 `~/.zshrc` 注入运行期配置,并避免密钥/模型变化触发镜像重建。
|
||||
- AI Provider Docker build context 收敛到服务必需文件,`uv sync` 接入 BuildKit 缓存以减少重复下载。
|
||||
|
||||
### 🔧 Improvements
|
||||
- IPtoASN、OpenGeoFeed、NRO delegated 下载型采集器接入真实字节进度上报。
|
||||
- 数据源页、采集中任务弹窗和任务历史页展示阶段摘要,并在 tooltip 中保留完整进度细节。
|
||||
- 调整 Earth 船只默认高度偏移,进一步贴近地表展示。
|
||||
|
||||
---
|
||||
|
||||
## [0.44.2] — 2026-04-29
|
||||
|
||||
### 📝 Documentation
|
||||
|
||||
@@ -55,6 +55,30 @@ Parameters:
|
||||
| `--allow-lan` | Enable LAN access |
|
||||
| `--verbose` | Show more command output during execution |
|
||||
|
||||
### AI Provider Environment and Builds
|
||||
|
||||
AI Provider runtime configuration can live in `aiprovider/.env` or in matching variables in `~/.zshrc`. `planet.sh` reads simple `export AI_...=...` / `AI_...=...` lines and passes them to the container at startup.
|
||||
|
||||
Changing model, API key, or base URL does not rebuild the image. Restart only AI Provider to pick up runtime configuration changes:
|
||||
|
||||
```bash
|
||||
./planet.sh restart -a
|
||||
```
|
||||
|
||||
For complex shell expansion in `~/.zshrc`, opt in explicitly:
|
||||
|
||||
```bash
|
||||
PLANET_LOAD_ZSHRC_ENV=source ./planet.sh start -a
|
||||
```
|
||||
|
||||
To ignore `~/.zshrc` during troubleshooting:
|
||||
|
||||
```bash
|
||||
PLANET_LOAD_ZSHRC_ENV=0 ./planet.sh start -a
|
||||
```
|
||||
|
||||
The AI Provider Docker build context is intentionally limited to the files required by the service, and `uv sync` uses a BuildKit cache mount so dependency downloads are reused after the first build.
|
||||
|
||||
### Stop
|
||||
|
||||
```bash
|
||||
|
||||
@@ -24,6 +24,12 @@ This script checks and syncs common dependencies, and generates if missing:
|
||||
- `aiprovider/.env`
|
||||
- `frontend/.env.local`
|
||||
|
||||
Personal AI Provider configuration can also live in `~/.zshrc`. `planet.sh` reads simple `export AI_...=...` / `AI_...=...` lines and passes them to the AI Provider container. After changing model, key, or base URL, restart only AI Provider:
|
||||
|
||||
```bash
|
||||
./planet.sh restart -a
|
||||
```
|
||||
|
||||
## 1. Start Services
|
||||
|
||||
From the repository root:
|
||||
|
||||
@@ -190,8 +190,8 @@
|
||||
|
||||
| 正式名称 | 变量名 | 当前值 | 使用位置 / 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| 船只半径偏移 | `VESSEL_CONFIG.altitudeOffset` | `0.56` | 普通 marker 位置 |
|
||||
| 船只轨迹半径偏移 | `VESSEL_CONFIG.track.altitudeOffset` | `0.7` | 选中船只轨迹线 |
|
||||
| 船只半径偏移 | `VESSEL_CONFIG.altitudeOffset` | `0.2` | 普通 marker 位置,贴近真实地形基础层 |
|
||||
| 船只轨迹半径偏移 | `VESSEL_CONFIG.track.altitudeOffset` | `0.22` | 选中船只轨迹线,略高于船只 marker 以保持可见 |
|
||||
| 船只 renderOrder | local `VESSEL_RENDER_ORDER` | `4.4` | 普通 marker 和交互 overlay |
|
||||
| 船只轨迹 renderOrder | `VESSEL_RENDER_ORDER - 0.1` | `4.3` | 低于船只 marker |
|
||||
| 船只点像素尺寸 | local `VESSEL_POINT_SIZE` | `34` | 普通 marker 与 hover / locked overlay 共享尺寸 |
|
||||
|
||||
@@ -55,6 +55,52 @@
|
||||
| `--allow-lan` | 允许局域网访问 |
|
||||
| `--verbose` | 在执行过程中显示更多命令输出 |
|
||||
|
||||
### AI Provider 环境变量和构建
|
||||
|
||||
AI Provider 的运行期配置可以放在两处:
|
||||
|
||||
| 位置 | 适合内容 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `aiprovider/.env` | 团队约定的本地默认配置 | Docker Compose 会作为 `env_file` 读取 |
|
||||
| `~/.zshrc` | 个人机器上的 provider、模型、密钥和代理变量 | `planet.sh` 启动时会读取常见的 `AI_*`、`SERVICE_*`、`PYTHON_IMAGE`、`UV_IMAGE`、代理变量 |
|
||||
|
||||
推荐写法:
|
||||
|
||||
```bash
|
||||
export AI_PROVIDER=minimax
|
||||
export AI_PROVIDER_API=anthropic-messages
|
||||
export AI_BASE_URL=https://api.example.com/anthropic
|
||||
export AI_API_KEY=sk-change-me
|
||||
export AI_MODEL=MiniMax-M2.7
|
||||
export AI_PROVIDER_SERVICE_TOKEN=change_me
|
||||
```
|
||||
|
||||
默认情况下,`planet.sh` 只静态解析 `~/.zshrc` 中简单的 `export KEY=value` 或 `KEY=value` 行,避免 shell 主题、插件或交互初始化拖慢启动。如果变量依赖复杂 shell 展开,可以显式启用 source 模式:
|
||||
|
||||
```bash
|
||||
PLANET_LOAD_ZSHRC_ENV=source ./planet.sh start -a
|
||||
```
|
||||
|
||||
如需完全忽略 `~/.zshrc`:
|
||||
|
||||
```bash
|
||||
PLANET_LOAD_ZSHRC_ENV=0 ./planet.sh start -a
|
||||
```
|
||||
|
||||
AI Provider 镜像只在代码、Dockerfile、Compose 配置或相关 Python 依赖变化时重建。修改 `aiprovider/.env` 或 `~/.zshrc` 中的模型、密钥、Base URL 不会触发镜像重建;重启 AI Provider 即可让容器读取新配置:
|
||||
|
||||
```bash
|
||||
./planet.sh restart -a
|
||||
```
|
||||
|
||||
构建较慢时,优先判断当前卡在哪一层:
|
||||
|
||||
| 现象 | 常见原因 | 处理方式 |
|
||||
| --- | --- | --- |
|
||||
| `transferring context` 很大 | Docker build context 包含前端资源、PDF、数据目录等无关文件 | 当前仓库通过 `.dockerignore` 只发送 AI Provider 必需文件 |
|
||||
| `uv sync` 下载依赖较慢 | 首次构建或缓存为空,网络访问 Python 包较慢 | 等待首次构建完成;后续会复用 BuildKit 的 uv 下载缓存 |
|
||||
| 改密钥后仍显示旧配置 | 容器尚未重启 | 执行 `./planet.sh restart -a` |
|
||||
|
||||
### 停止
|
||||
|
||||
```bash
|
||||
|
||||
@@ -46,6 +46,8 @@ compute_ai_provider_build_fingerprint() {
|
||||
find aiprovider \
|
||||
-type f \
|
||||
! -path '*/__pycache__/*' \
|
||||
! -name '.env' \
|
||||
! -name '.env.*' \
|
||||
! -name '*.pyc' \
|
||||
! -name '*.pyo' \
|
||||
| LC_ALL=C sort \
|
||||
@@ -57,6 +59,58 @@ compute_ai_provider_build_fingerprint() {
|
||||
|
||||
速度提升约 10 倍(大量小文件场景),误报率相同(mtime+size 变化 ≡ 文件被修改)。
|
||||
|
||||
`.env` 和 `.env.*` 被排除在 fingerprint 外。它们属于运行期配置,不应该因为修改模型、密钥或 Base URL 触发镜像重建。
|
||||
|
||||
### Docker build context 收敛
|
||||
|
||||
AI Provider 镜像只需要根目录的 `pyproject.toml`、`uv.lock` 和 `aiprovider/` 代码。仓库中还包含前端静态大图、PDF、历史数据和 Unreal 资料,如果 build context 使用整个仓库,`transferring context` 会浪费大量时间。
|
||||
|
||||
当前通过根目录 `.dockerignore` 收敛上下文:
|
||||
|
||||
```dockerignore
|
||||
**
|
||||
|
||||
!pyproject.toml
|
||||
!uv.lock
|
||||
!aiprovider/
|
||||
!aiprovider/**
|
||||
|
||||
aiprovider/.env
|
||||
aiprovider/.env.*
|
||||
!aiprovider/.env.example
|
||||
```
|
||||
|
||||
Dockerfile 也从全仓复制改为只复制 AI Provider 代码:
|
||||
|
||||
```dockerfile
|
||||
COPY pyproject.toml uv.lock /app/
|
||||
RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
uv sync --frozen --no-dev
|
||||
|
||||
COPY aiprovider /app/aiprovider
|
||||
```
|
||||
|
||||
`uv sync` 使用 BuildKit cache mount 后,首次构建仍可能受网络影响;后续构建会复用 `/root/.cache/uv`,依赖下载不再重复从零开始。
|
||||
|
||||
### 运行期配置来源
|
||||
|
||||
`planet.sh` 启动 AI Provider 前会生成临时 env-file,并把它传给 Compose 或手动 `docker run` fallback。配置优先来自:
|
||||
|
||||
1. `aiprovider/.env`
|
||||
2. `~/.zshrc` 中简单的 `export AI_...=...` 或 `AI_...=...` 行
|
||||
|
||||
默认解析是静态的,只覆盖 AI Provider、镜像、代理相关变量,避免执行交互 shell 初始化。如果确实需要复杂 shell 展开,可以显式启用:
|
||||
|
||||
```bash
|
||||
PLANET_LOAD_ZSHRC_ENV=source ./planet.sh start -a
|
||||
```
|
||||
|
||||
如果排查时需要忽略个人 shell 配置:
|
||||
|
||||
```bash
|
||||
PLANET_LOAD_ZSHRC_ENV=0 ./planet.sh start -a
|
||||
```
|
||||
|
||||
### 跳过重建的原理
|
||||
|
||||
fingerprint 一致时不执行 `docker compose build`,而是:
|
||||
@@ -151,4 +205,7 @@ PY
|
||||
## 相关文件
|
||||
|
||||
- `planet.sh` — 全量修改
|
||||
- `.dockerignore` — 收敛 AI Provider Docker build context
|
||||
- `aiprovider/Dockerfile` — 只复制 AI Provider 代码,并为 `uv sync` 启用 BuildKit cache mount
|
||||
- `docker-compose.yml` / `docker-compose.simple.yml` — 读取 `planet.sh` 生成的运行期 env-file
|
||||
- `scripts/compute_aiprovider_dependency_fingerprint.py` — 依赖 fingerprint(未改动)
|
||||
|
||||
@@ -24,6 +24,12 @@
|
||||
- `aiprovider/.env`
|
||||
- `frontend/.env.local`
|
||||
|
||||
AI Provider 的个人配置也可以放在 `~/.zshrc`。`planet.sh` 会读取简单的 `export AI_...=...` 或 `AI_...=...` 行,并在启动 AI Provider 时传给容器。修改模型、密钥或 Base URL 后,通常只需要重启 AI Provider:
|
||||
|
||||
```bash
|
||||
./planet.sh restart -a
|
||||
```
|
||||
|
||||
## 1. 启动服务
|
||||
|
||||
在仓库根目录执行:
|
||||
|
||||
@@ -16,12 +16,13 @@
|
||||
## Current Version
|
||||
|
||||
- `main` 当前主线历史推导到:`0.16.5`
|
||||
- `dev` 当前开发分支历史推导到:`0.44.2`
|
||||
- `dev` 当前开发分支历史推导到:`0.45.0`
|
||||
|
||||
## Timeline
|
||||
|
||||
| Version | Type | Branch | Commit | Summary |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| `0.45.0` | feature | `dev` | `pending` | 新增采集任务 fetching 阶段量化进度,收敛 AI Provider 运行期环境注入和 Docker build context |
|
||||
| `0.44.2` | bugfix | `dev` | `pending` | 补充 Earth 船只批量渲染、屏幕拾取、图层顺序、样式参考和性能计划状态文档 |
|
||||
| `0.44.1` | bugfix | `dev` | `pending` | 优化 Earth 船只批量渲染性能,修复拖动卡顿、拾取错位、交互态方向/尺寸和地表压盖问题 |
|
||||
| `0.44.0` | feature | `dev` | `pending` | 重构数据源目录与采集器设置,新增 BarentsWatch AIS 连接教程、Earth 船只/缩放体验优化和仪表盘前端重启 |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "planet-frontend",
|
||||
"version": "0.44.2",
|
||||
"version": "0.45.0",
|
||||
"private": true,
|
||||
"packageManager": "bun@1",
|
||||
"dependencies": {
|
||||
|
||||
@@ -209,7 +209,7 @@ export const PATHS = {
|
||||
};
|
||||
|
||||
export const VESSEL_CONFIG = {
|
||||
altitudeOffset: 0.56,
|
||||
altitudeOffset: 0.2,
|
||||
maxRenderedMarkers: 5000,
|
||||
marker: {
|
||||
baseScale: 7.5,
|
||||
@@ -232,7 +232,7 @@ export const VESSEL_CONFIG = {
|
||||
max: 2.4,
|
||||
},
|
||||
track: {
|
||||
altitudeOffset: 0.7,
|
||||
altitudeOffset: 0.22,
|
||||
color: 0x7dd3fc,
|
||||
opacity: 0.82,
|
||||
},
|
||||
|
||||
@@ -30,6 +30,7 @@ import axios, { type AxiosResponse } from 'axios'
|
||||
import AppLayout from '../../components/AppLayout/AppLayout'
|
||||
import ScrollbarOverlay from '../../components/Scrollbar/ScrollbarOverlay'
|
||||
import { formatDateTimeZhCN } from '../../utils/datetime'
|
||||
import { formatPhaseMetric, getPhaseDisplay, getPhaseSummary } from '../../utils/phaseProgress'
|
||||
|
||||
const { Text } = Typography
|
||||
const COLLECTION_REFRESH_DELAY_MS = 800
|
||||
@@ -54,6 +55,11 @@ interface BuiltInDataSource {
|
||||
task_id: number | null
|
||||
progress: number | null
|
||||
phase?: string | null
|
||||
phase_progress?: number | null
|
||||
phase_message?: string | null
|
||||
phase_current?: number | null
|
||||
phase_total?: number | null
|
||||
phase_unit?: string | null
|
||||
records_processed: number | null
|
||||
total_records: number | null
|
||||
is_free?: boolean
|
||||
@@ -107,6 +113,11 @@ interface UnifiedDataSource {
|
||||
is_running?: boolean
|
||||
progress?: number | null
|
||||
phase?: string | null
|
||||
phase_progress?: number | null
|
||||
phase_message?: string | null
|
||||
phase_current?: number | null
|
||||
phase_total?: number | null
|
||||
phase_unit?: string | null
|
||||
task_id?: number | null
|
||||
created_at?: string
|
||||
updated_at?: string | null
|
||||
@@ -129,6 +140,11 @@ type TriggerDatasourceConflict = {
|
||||
message?: string
|
||||
progress?: number | null
|
||||
phase?: string | null
|
||||
phase_progress?: number | null
|
||||
phase_message?: string | null
|
||||
phase_current?: number | null
|
||||
phase_total?: number | null
|
||||
phase_unit?: string | null
|
||||
records_processed?: number | null
|
||||
total_records?: number | null
|
||||
}
|
||||
@@ -142,6 +158,11 @@ type DatasourceTaskStatus = {
|
||||
task_id?: number | null
|
||||
progress?: number | null
|
||||
phase?: string | null
|
||||
phase_progress?: number | null
|
||||
phase_message?: string | null
|
||||
phase_current?: number | null
|
||||
phase_total?: number | null
|
||||
phase_unit?: string | null
|
||||
records_processed?: number | null
|
||||
total_records?: number | null
|
||||
status?: string | null
|
||||
@@ -166,6 +187,11 @@ function normalizeBuiltin(source: BuiltInDataSource): UnifiedDataSource {
|
||||
is_running: source.is_running,
|
||||
progress: source.progress,
|
||||
phase: source.phase,
|
||||
phase_progress: source.phase_progress,
|
||||
phase_message: source.phase_message,
|
||||
phase_current: source.phase_current,
|
||||
phase_total: source.phase_total,
|
||||
phase_unit: source.phase_unit,
|
||||
task_id: source.task_id,
|
||||
is_free: source.is_free,
|
||||
requires_credentials: source.requires_credentials,
|
||||
@@ -291,7 +317,7 @@ function DataSources() {
|
||||
content: (
|
||||
<div>
|
||||
<p>{taskInfo?.message || '当前采集任务仍在运行,重新触发会丢失本次未完成进度。'}</p>
|
||||
<p>当前阶段: {taskInfo?.phase || 'running'}</p>
|
||||
<p>当前阶段: {getPhaseDisplay(taskInfo || {})}</p>
|
||||
<p>当前进度: {typeof taskInfo?.progress === 'number' ? `${Math.round(taskInfo.progress)}%` : '未知'}</p>
|
||||
<p>确认后会强制取消当前采集,并重新开始采集。</p>
|
||||
</div>
|
||||
@@ -474,7 +500,11 @@ function DataSources() {
|
||||
return <Tag color={record.is_active ? 'green' : 'default'}>{record.is_active ? '启用' : '禁用'}</Tag>
|
||||
}
|
||||
if (record.is_running) {
|
||||
return <Tag color="processing">{record.phase || '运行中'}{record.progress ? ` ${Math.round(record.progress)}%` : ''}</Tag>
|
||||
return (
|
||||
<Tooltip title={getPhaseDisplay(record)}>
|
||||
<Tag color="processing">{getPhaseSummary(record)}</Tag>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
if (!record.last_status) return <Tag>未执行</Tag>
|
||||
return <Tag color={record.last_status === 'success' ? 'success' : record.last_status === 'failed' ? 'error' : 'default'}>{record.last_status}</Tag>
|
||||
@@ -603,8 +633,13 @@ function DataSources() {
|
||||
{source.source}{source.task_id ? ` · #${source.task_id}` : ''}
|
||||
</Text>
|
||||
</Space>
|
||||
<Tag color="processing">{source.phase || 'running'}</Tag>
|
||||
<Tooltip title={getPhaseDisplay(source)}>
|
||||
<Tag color="processing">{getPhaseSummary(source)}</Tag>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{source.phase_message ? (
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>{source.phase_message}</Text>
|
||||
) : null}
|
||||
<Progress
|
||||
percent={Math.round(source.progress || 0)}
|
||||
size="small"
|
||||
@@ -612,8 +647,9 @@ function DataSources() {
|
||||
strokeColor="#1677ff"
|
||||
/>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
已处理 {source.records_processed ?? 0}
|
||||
{source.total_records ? ` / ${source.total_records}` : ''}
|
||||
{source.phase_unit === 'bytes'
|
||||
? `下载 ${formatPhaseMetric(source) || '准备中'}`
|
||||
: `已处理 ${source.records_processed ?? 0}${source.total_records ? ` / ${source.total_records}` : ''}`}
|
||||
</Text>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
@@ -1,16 +1,26 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Table, Tag, Card, Row, Col, Statistic, Button } from 'antd'
|
||||
import { Table, Tag, Card, Row, Col, Statistic, Button, Tooltip } from 'antd'
|
||||
import { ReloadOutlined, CheckCircleOutlined, CloseCircleOutlined, SyncOutlined } from '@ant-design/icons'
|
||||
import { useAuthStore } from '../../stores/auth'
|
||||
import AppLayout from '../../components/AppLayout/AppLayout'
|
||||
import TableScrollRegion from '../../components/Scrollbar/TableScrollRegion'
|
||||
import { formatDateTimeZhCN } from '../../utils/datetime'
|
||||
import { getPhaseDisplay, getPhaseSummary } from '../../utils/phaseProgress'
|
||||
|
||||
interface Task {
|
||||
id: number
|
||||
collector: string
|
||||
collector?: string
|
||||
datasource_name?: string
|
||||
status: 'success' | 'failed' | 'running' | 'pending'
|
||||
phase?: string | null
|
||||
phase_progress?: number | null
|
||||
phase_message?: string | null
|
||||
phase_current?: number | null
|
||||
phase_total?: number | null
|
||||
phase_unit?: string | null
|
||||
records_processed: number
|
||||
total_records?: number | null
|
||||
progress?: number | null
|
||||
started_at: string
|
||||
completed_at: string
|
||||
duration_seconds: number
|
||||
@@ -53,7 +63,20 @@ function Tasks() {
|
||||
title: '收集器',
|
||||
dataIndex: 'collector',
|
||||
key: 'collector',
|
||||
render: (c: string) => <Tag color="blue">{c}</Tag>,
|
||||
render: (_: string, task: Task) => <Tag color="blue">{task.collector || task.datasource_name || '-'}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '阶段',
|
||||
dataIndex: 'phase',
|
||||
key: 'phase',
|
||||
render: (_: string, task: Task) => {
|
||||
const detail = task.phase ? getPhaseDisplay(task) : null
|
||||
return detail ? (
|
||||
<Tooltip title={detail}>
|
||||
<Tag color={task.status === 'running' ? 'processing' : 'default'}>{getPhaseSummary(task)}</Tag>
|
||||
</Tooltip>
|
||||
) : '-'
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
|
||||
61
frontend/src/utils/phaseProgress.ts
Normal file
61
frontend/src/utils/phaseProgress.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
export type PhaseProgressLike = {
|
||||
phase?: string | null
|
||||
phase_progress?: number | null
|
||||
phase_message?: string | null
|
||||
phase_current?: number | null
|
||||
phase_total?: number | null
|
||||
phase_unit?: string | null
|
||||
}
|
||||
|
||||
const BYTE_UNIT_BASE = 1024
|
||||
const BYTE_UNITS = ['B', 'KB', 'MB', 'GB'] as const
|
||||
|
||||
export function formatBytes(value: number): string {
|
||||
if (!Number.isFinite(value) || value <= 0) return '0 B'
|
||||
let size = value
|
||||
let unitIndex = 0
|
||||
while (size >= BYTE_UNIT_BASE && unitIndex < BYTE_UNITS.length - 1) {
|
||||
size /= BYTE_UNIT_BASE
|
||||
unitIndex += 1
|
||||
}
|
||||
const digits = unitIndex === 0 ? 0 : size >= 10 ? 1 : 2
|
||||
return `${size.toFixed(digits)} ${BYTE_UNITS[unitIndex]}`
|
||||
}
|
||||
|
||||
export function formatPhaseMetric(source: Pick<PhaseProgressLike, 'phase_current' | 'phase_total' | 'phase_unit'>): string | null {
|
||||
const current = source.phase_current
|
||||
const total = source.phase_total
|
||||
if (typeof current !== 'number') return null
|
||||
if (source.phase_unit === 'bytes') {
|
||||
return typeof total === 'number' && total > 0
|
||||
? `${formatBytes(current)} / ${formatBytes(total)}`
|
||||
: formatBytes(current)
|
||||
}
|
||||
if (typeof total === 'number' && total > 0) {
|
||||
return `${current} / ${total}${source.phase_unit ? ` ${source.phase_unit}` : ''}`
|
||||
}
|
||||
return `${current}${source.phase_unit ? ` ${source.phase_unit}` : ''}`
|
||||
}
|
||||
|
||||
export function getPhaseDisplay(source: PhaseProgressLike, fallback = 'running'): string {
|
||||
const phase = source.phase || fallback
|
||||
const parts = [phase]
|
||||
if (typeof source.phase_progress === 'number') {
|
||||
parts.push(`${Math.round(source.phase_progress)}%`)
|
||||
}
|
||||
if (source.phase_message) {
|
||||
parts.push(source.phase_message)
|
||||
}
|
||||
const metric = formatPhaseMetric(source)
|
||||
if (metric) {
|
||||
parts.push(metric)
|
||||
}
|
||||
return parts.join(' · ')
|
||||
}
|
||||
|
||||
export function getPhaseSummary(source: PhaseProgressLike, fallback = 'running'): string {
|
||||
const phase = source.phase || fallback
|
||||
return typeof source.phase_progress === 'number'
|
||||
? `${phase} ${Math.round(source.phase_progress)}%`
|
||||
: phase
|
||||
}
|
||||
80
planet.sh
80
planet.sh
@@ -6,6 +6,48 @@ SCRIPT_PATH="${(%):-%N}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
load_user_zshrc_exported_env() {
|
||||
local load_mode="${PLANET_LOAD_ZSHRC_ENV:-1}"
|
||||
[ "$load_mode" != "0" ] || return 0
|
||||
[ "$load_mode" != "false" ] || return 0
|
||||
[ -f "$HOME/.zshrc" ] || return 0
|
||||
|
||||
local env_line env_name env_value
|
||||
if [ "$load_mode" = "source" ]; then
|
||||
command -v zsh >/dev/null 2>&1 || return 0
|
||||
while IFS= read -r env_line; do
|
||||
env_name="${env_line%%=*}"
|
||||
env_value="${env_line#*=}"
|
||||
case "$env_name" in
|
||||
AI_*|SERVICE_NAME|SERVICE_VERSION|PYTHON_IMAGE|UV_IMAGE|DOCKER_BUILDKIT|COMPOSE_DOCKER_CLI_BUILD|HTTP_PROXY|HTTPS_PROXY|NO_PROXY|http_proxy|https_proxy|no_proxy)
|
||||
export "${env_name}=${env_value}"
|
||||
;;
|
||||
esac
|
||||
done < <(zsh -fc 'source ~/.zshrc >/dev/null 2>&1; env' 2>/dev/null || true)
|
||||
return 0
|
||||
fi
|
||||
|
||||
while IFS= read -r env_line; do
|
||||
env_line="$(printf "%s" "$env_line" | sed -E 's/^[[:space:]]+//;s/[[:space:]]+$//')"
|
||||
case "$env_line" in
|
||||
""|\#*) continue ;;
|
||||
export\ *) env_line="${env_line#export }" ;;
|
||||
esac
|
||||
env_name="${env_line%%=*}"
|
||||
env_value="${env_line#*=}"
|
||||
[ "$env_name" != "$env_value" ] || continue
|
||||
env_name="$(printf "%s" "$env_name" | sed -E 's/^[[:space:]]+//;s/[[:space:]]+$//')"
|
||||
env_value="$(printf "%s" "$env_value" | sed -E 's/^[[:space:]]+//;s/[[:space:]]+$//;s/^"//;s/"$//;s/^'\''//;s/'\''$//')"
|
||||
case "$env_name" in
|
||||
AI_*|SERVICE_NAME|SERVICE_VERSION|PYTHON_IMAGE|UV_IMAGE|DOCKER_BUILDKIT|COMPOSE_DOCKER_CLI_BUILD|HTTP_PROXY|HTTPS_PROXY|NO_PROXY|http_proxy|https_proxy|no_proxy)
|
||||
export "${env_name}=${env_value}"
|
||||
;;
|
||||
esac
|
||||
done < "$HOME/.zshrc"
|
||||
}
|
||||
|
||||
load_user_zshrc_exported_env
|
||||
|
||||
RED='\033[38;5;203m'
|
||||
GREEN='\033[38;5;114m'
|
||||
YELLOW='\033[38;5;221m'
|
||||
@@ -68,7 +110,23 @@ AI_PROVIDER_BUILD_STAMP_FILE="$HOME/.cache/planet/aiprovider_build.sha256"
|
||||
AI_PROVIDER_BUILD_LOG_FILE="/tmp/planet_aiprovider_build.log"
|
||||
AI_PROVIDER_IMAGE_NAME="${AI_PROVIDER_IMAGE_NAME:-planet_aiprovider:latest}"
|
||||
AI_PROVIDER_CONTAINER_NAME="${AI_PROVIDER_CONTAINER_NAME:-planet_aiprovider}"
|
||||
PLANET_AI_PROVIDER_RUNTIME_ENV_FILE="${PLANET_AI_PROVIDER_RUNTIME_ENV_FILE:-/tmp/planet_aiprovider_runtime.env}"
|
||||
AI_PROVIDER_RECREATE_REQUIRED=0
|
||||
AI_PROVIDER_RUNTIME_ENV_NAMES=(
|
||||
SERVICE_NAME
|
||||
SERVICE_VERSION
|
||||
AI_PROVIDER
|
||||
AI_PROVIDER_API
|
||||
AI_BASE_URL
|
||||
AI_API_KEY
|
||||
AI_MODEL
|
||||
AI_TIMEOUT_SECONDS
|
||||
AI_HTTP_RETRY_ATTEMPTS
|
||||
AI_MAX_TOKENS
|
||||
AI_ANTHROPIC_VERSION
|
||||
AI_ANALYSIS_SYSTEM_PROMPT
|
||||
AI_PROVIDER_SERVICE_TOKEN
|
||||
)
|
||||
|
||||
prepend_path_once() {
|
||||
local path_entry="$1"
|
||||
@@ -590,6 +648,8 @@ compute_ai_provider_build_fingerprint() {
|
||||
find aiprovider \
|
||||
-type f \
|
||||
! -path '*/__pycache__/*' \
|
||||
! -name '.env' \
|
||||
! -name '.env.*' \
|
||||
! -name '*.pyc' \
|
||||
! -name '*.pyo' \
|
||||
| LC_ALL=C sort \
|
||||
@@ -633,6 +693,9 @@ run_ai_provider_container_manually() {
|
||||
if [ -f "$SCRIPT_DIR/aiprovider/.env" ]; then
|
||||
env_file_args=(--env-file "$SCRIPT_DIR/aiprovider/.env")
|
||||
fi
|
||||
if [ -s "$PLANET_AI_PROVIDER_RUNTIME_ENV_FILE" ]; then
|
||||
env_file_args+=(--env-file "$PLANET_AI_PROVIDER_RUNTIME_ENV_FILE")
|
||||
fi
|
||||
|
||||
docker run -d \
|
||||
--name "$AI_PROVIDER_CONTAINER_NAME" \
|
||||
@@ -641,6 +704,22 @@ run_ai_provider_container_manually() {
|
||||
"$AI_PROVIDER_IMAGE_NAME" >/dev/null
|
||||
}
|
||||
|
||||
write_ai_provider_runtime_env_file() {
|
||||
local env_name env_value
|
||||
|
||||
: > "$PLANET_AI_PROVIDER_RUNTIME_ENV_FILE"
|
||||
chmod 600 "$PLANET_AI_PROVIDER_RUNTIME_ENV_FILE" 2>/dev/null || true
|
||||
|
||||
for env_name in "${AI_PROVIDER_RUNTIME_ENV_NAMES[@]}"; do
|
||||
if [ -n "${(P)env_name+x}" ]; then
|
||||
env_value="${(P)env_name}"
|
||||
printf "%s=%s\n" "$env_name" "$env_value" >> "$PLANET_AI_PROVIDER_RUNTIME_ENV_FILE"
|
||||
fi
|
||||
done
|
||||
|
||||
export PLANET_AI_PROVIDER_RUNTIME_ENV_FILE
|
||||
}
|
||||
|
||||
recreate_ai_provider_container() {
|
||||
local ai_provider_port="${1:-$DEFAULT_AI_PROVIDER_PORT}"
|
||||
|
||||
@@ -1156,6 +1235,7 @@ start_ai_provider_service() {
|
||||
local retry=1
|
||||
set_wait_detail "启动 AI Provider"
|
||||
|
||||
write_ai_provider_runtime_env_file
|
||||
ensure_ai_provider_image_current
|
||||
|
||||
while [ "$retry" -le "$AI_PROVIDER_START_MAX_RETRIES" ]; do
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "planet"
|
||||
version = "0.44.2"
|
||||
version = "0.45.0"
|
||||
description = "智能星球计划 - 态势感知系统"
|
||||
requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
|
||||
Reference in New Issue
Block a user