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 PYTHON_IMAGE=python:3.14-slim
|
||||||
ARG UV_IMAGE=ghcr.io/astral-sh/uv:latest
|
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/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
COPY pyproject.toml uv.lock /app/
|
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
|
EXPOSE 8010
|
||||||
|
|
||||||
|
|||||||
@@ -398,6 +398,11 @@ async def list_datasources(
|
|||||||
"task_id": running_task.id if running_task else None,
|
"task_id": running_task.id if running_task else None,
|
||||||
"progress": running_task.progress if running_task else None,
|
"progress": running_task.progress if running_task else None,
|
||||||
"phase": running_task.phase 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,
|
"records_processed": running_task.records_processed if running_task else None,
|
||||||
"total_records": running_task.total_records 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": "当前采集任务尚未完成,重新触发会丢失本次未完成进度。是否强制重新采集?",
|
"message": "当前采集任务尚未完成,重新触发会丢失本次未完成进度。是否强制重新采集?",
|
||||||
"task_id": running_task.id,
|
"task_id": running_task.id,
|
||||||
"phase": running_task.phase,
|
"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,
|
"progress": running_task.progress,
|
||||||
"records_processed": running_task.records_processed,
|
"records_processed": running_task.records_processed,
|
||||||
"total_records": running_task.total_records,
|
"total_records": running_task.total_records,
|
||||||
@@ -709,13 +719,29 @@ async def get_task_status(
|
|||||||
task = await get_running_task(db, datasource.id)
|
task = await get_running_task(db, datasource.id)
|
||||||
|
|
||||||
if not task:
|
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 {
|
return {
|
||||||
"is_running": task.status == "running",
|
"is_running": task.status == "running",
|
||||||
"task_id": task.id,
|
"task_id": task.id,
|
||||||
"progress": task.progress,
|
"progress": task.progress,
|
||||||
"phase": task.phase,
|
"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,
|
"records_processed": task.records_processed,
|
||||||
"total_records": task.total_records,
|
"total_records": task.total_records,
|
||||||
"status": task.status,
|
"status": task.status,
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ async def list_tasks(
|
|||||||
offset = (page - 1) * page_size
|
offset = (page - 1) * page_size
|
||||||
query = """
|
query = """
|
||||||
SELECT ct.id, ct.datasource_id, ds.name as datasource_name, ct.status,
|
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
|
FROM collection_tasks ct
|
||||||
JOIN data_sources ds ON ct.datasource_id = ds.id
|
JOIN data_sources ds ON ct.datasource_id = ds.id
|
||||||
WHERE 1=1
|
WHERE 1=1
|
||||||
@@ -66,6 +68,14 @@ async def list_tasks(
|
|||||||
"completed_at": to_iso8601_utc(t[5]),
|
"completed_at": to_iso8601_utc(t[5]),
|
||||||
"records_processed": t[6],
|
"records_processed": t[6],
|
||||||
"error_message": t[7],
|
"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
|
for t in tasks
|
||||||
],
|
],
|
||||||
@@ -81,7 +91,9 @@ async def get_task(
|
|||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
text("""
|
text("""
|
||||||
SELECT ct.id, ct.datasource_id, ds.name as datasource_name, ct.status,
|
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
|
FROM collection_tasks ct
|
||||||
JOIN data_sources ds ON ct.datasource_id = ds.id
|
JOIN data_sources ds ON ct.datasource_id = ds.id
|
||||||
WHERE ct.id = :id
|
WHERE ct.id = :id
|
||||||
@@ -105,6 +117,14 @@ async def get_task(
|
|||||||
"completed_at": to_iso8601_utc(task[5]),
|
"completed_at": to_iso8601_utc(task[5]),
|
||||||
"records_processed": task[6],
|
"records_processed": task[6],
|
||||||
"error_message": task[7],
|
"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(
|
text(
|
||||||
"""
|
"""
|
||||||
ALTER TABLE collection_tasks
|
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"""
|
"""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 sqlalchemy.sql import func
|
||||||
|
|
||||||
from app.db.session import Base
|
from app.db.session import Base
|
||||||
@@ -13,6 +13,11 @@ class CollectionTask(Base):
|
|||||||
datasource_id = Column(Integer, nullable=False, index=True)
|
datasource_id = Column(Integer, nullable=False, index=True)
|
||||||
status = Column(String(20), nullable=False) # pending, running, success, failed, cancelled
|
status = Column(String(20), nullable=False) # pending, running, success, failed, cancelled
|
||||||
phase = Column(String(30), default="queued")
|
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))
|
started_at = Column(DateTime(timezone=True))
|
||||||
completed_at = Column(DateTime(timezone=True))
|
completed_at = Column(DateTime(timezone=True))
|
||||||
records_processed = Column(Integer, default=0)
|
records_processed = Column(Integer, default=0)
|
||||||
|
|||||||
@@ -54,6 +54,11 @@ class BaseCollector(ABC):
|
|||||||
"task_id": self._current_task.id,
|
"task_id": self._current_task.id,
|
||||||
"status": self._current_task.status,
|
"status": self._current_task.status,
|
||||||
"phase": self._current_task.phase,
|
"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,
|
"progress": progress,
|
||||||
"records_processed": self._current_task.records_processed,
|
"records_processed": self._current_task.records_processed,
|
||||||
"total_records": self._current_task.total_records,
|
"total_records": self._current_task.total_records,
|
||||||
@@ -80,12 +85,52 @@ class BaseCollector(ABC):
|
|||||||
|
|
||||||
await self._publish_task_update(force=force)
|
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:
|
if self._current_task and self._db_session:
|
||||||
self._current_task.phase = phase
|
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._db_session.commit()
|
||||||
await self._publish_task_update(force=True)
|
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
|
@abstractmethod
|
||||||
async def fetch(self) -> List[Dict[str, Any]]:
|
async def fetch(self) -> List[Dict[str, Any]]:
|
||||||
"""Fetch raw data from source"""
|
"""Fetch raw data from source"""
|
||||||
@@ -251,7 +296,7 @@ class BaseCollector(ABC):
|
|||||||
await self._publish_task_update(force=True)
|
await self._publish_task_update(force=True)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await self.set_phase("fetching")
|
await self.set_phase("fetching", message="正在拉取原始数据")
|
||||||
raw_data = await self.fetch()
|
raw_data = await self.fetch()
|
||||||
task.total_records = len(raw_data)
|
task.total_records = len(raw_data)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
@@ -260,15 +305,20 @@ class BaseCollector(ABC):
|
|||||||
if self.fail_on_empty and not raw_data:
|
if self.fail_on_empty and not raw_data:
|
||||||
raise RuntimeError(f"Collector {self.name} returned no 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)
|
data = self.transform(raw_data)
|
||||||
snapshot_id = await self._create_snapshot(db, task_id, data, start_time)
|
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)
|
records_count = await self._save_data(db, data, task_id=task_id, snapshot_id=snapshot_id)
|
||||||
|
|
||||||
task.status = "success"
|
task.status = "success"
|
||||||
task.phase = "completed"
|
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.records_processed = records_count
|
||||||
task.progress = 100.0
|
task.progress = 100.0
|
||||||
task.completed_at = datetime.now(UTC)
|
task.completed_at = datetime.now(UTC)
|
||||||
@@ -285,6 +335,7 @@ class BaseCollector(ABC):
|
|||||||
await db.rollback()
|
await db.rollback()
|
||||||
task.status = "cancelled"
|
task.status = "cancelled"
|
||||||
task.phase = "cancelled"
|
task.phase = "cancelled"
|
||||||
|
task.phase_message = "采集已取消"
|
||||||
task.error_message = "Collection cancelled by operator and rolled back"
|
task.error_message = "Collection cancelled by operator and rolled back"
|
||||||
task.completed_at = datetime.now(UTC)
|
task.completed_at = datetime.now(UTC)
|
||||||
if snapshot_id is not None:
|
if snapshot_id is not None:
|
||||||
@@ -301,6 +352,7 @@ class BaseCollector(ABC):
|
|||||||
await db.rollback()
|
await db.rollback()
|
||||||
task.status = "failed"
|
task.status = "failed"
|
||||||
task.phase = "failed"
|
task.phase = "failed"
|
||||||
|
task.phase_message = str(e)
|
||||||
task.error_message = str(e)
|
task.error_message = str(e)
|
||||||
task.completed_at = datetime.now(UTC)
|
task.completed_at = datetime.now(UTC)
|
||||||
if snapshot_id is not None:
|
if snapshot_id is not None:
|
||||||
|
|||||||
@@ -108,6 +108,11 @@ class IPtoASNPrefixGeoCollector(BaseCollector):
|
|||||||
self._current_task.total_records = total_expected
|
self._current_task.total_records = total_expected
|
||||||
self._current_task.records_processed = 0
|
self._current_task.records_processed = 0
|
||||||
self._current_task.progress = 0.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._db_session.commit()
|
||||||
await self._publish_task_update(force=True)
|
await self._publish_task_update(force=True)
|
||||||
|
|
||||||
@@ -135,7 +140,14 @@ class IPtoASNPrefixGeoCollector(BaseCollector):
|
|||||||
return
|
return
|
||||||
last_emit["value"] = aggregated
|
last_emit["value"] = aggregated
|
||||||
last_emit["t"] = now
|
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(
|
batches = await asyncio.gather(
|
||||||
*(
|
*(
|
||||||
@@ -148,6 +160,12 @@ class IPtoASNPrefixGeoCollector(BaseCollector):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
if total_expected > 0:
|
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)
|
await self.update_progress(total_expected, commit=True, force=True)
|
||||||
|
|
||||||
rows: list[dict[str, Any]] = []
|
rows: list[dict[str, Any]] = []
|
||||||
|
|||||||
@@ -39,12 +39,23 @@ class NRODelegatedPrefixGeoCollector(BaseCollector):
|
|||||||
self._current_task.total_records = total_expected
|
self._current_task.total_records = total_expected
|
||||||
self._current_task.records_processed = 0
|
self._current_task.records_processed = 0
|
||||||
self._current_task.progress = 0.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._db_session.commit()
|
||||||
await self._publish_task_update(force=True)
|
await self._publish_task_update(force=True)
|
||||||
|
|
||||||
async def on_progress(downloaded: int, total: int | None) -> None:
|
async def on_progress(downloaded: int, total: int | None) -> None:
|
||||||
if not total or total <= 0:
|
if not total or total <= 0:
|
||||||
return
|
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)
|
await self.update_progress(min(downloaded, total), commit=True)
|
||||||
|
|
||||||
body_path = await self._downloader.download_file(
|
body_path = await self._downloader.download_file(
|
||||||
|
|||||||
@@ -40,12 +40,23 @@ class OpenGeoFeedPrefixGeoCollector(BaseCollector):
|
|||||||
self._current_task.total_records = total_expected
|
self._current_task.total_records = total_expected
|
||||||
self._current_task.records_processed = 0
|
self._current_task.records_processed = 0
|
||||||
self._current_task.progress = 0.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._db_session.commit()
|
||||||
await self._publish_task_update(force=True)
|
await self._publish_task_update(force=True)
|
||||||
|
|
||||||
async def on_progress(downloaded: int, total: int | None) -> None:
|
async def on_progress(downloaded: int, total: int | None) -> None:
|
||||||
if not total or total <= 0:
|
if not total or total <= 0:
|
||||||
return
|
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)
|
await self.update_progress(min(downloaded, total), commit=True)
|
||||||
|
|
||||||
body_path = await self._downloader.download_file(
|
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.top500 import TOP500Collector
|
||||||
from app.services.collectors.base import BaseCollector, HTTPCollector
|
from app.services.collectors.base import BaseCollector, HTTPCollector
|
||||||
|
from app.models.task import CollectionTask
|
||||||
|
|
||||||
|
|
||||||
class TestBaseCollector:
|
class TestBaseCollector:
|
||||||
@@ -19,6 +20,31 @@ class TestBaseCollector:
|
|||||||
assert collector.module == "L1"
|
assert collector.module == "L1"
|
||||||
assert collector.frequency_hours == 4
|
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:
|
class TestTOP500Collector:
|
||||||
"""Tests for TOP500Collector"""
|
"""Tests for TOP500Collector"""
|
||||||
|
|||||||
@@ -121,6 +121,25 @@ class TestCollectionTaskModel:
|
|||||||
)
|
)
|
||||||
assert task.records_processed == 100
|
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):
|
def test_task_error_message(self):
|
||||||
"""Test collection task with error message"""
|
"""Test collection task with error message"""
|
||||||
task = CollectionTask(
|
task = CollectionTask(
|
||||||
|
|||||||
@@ -86,6 +86,12 @@ CREATE TABLE collection_tasks (
|
|||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
datasource_id INTEGER NOT NULL REFERENCES data_sources(id) ON DELETE CASCADE,
|
datasource_id INTEGER NOT NULL REFERENCES data_sources(id) ON DELETE CASCADE,
|
||||||
status task_status NOT NULL DEFAULT 'pending',
|
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,
|
started_at TIMESTAMP WITH TIME ZONE,
|
||||||
completed_at TIMESTAMP WITH TIME ZONE,
|
completed_at TIMESTAMP WITH TIME ZONE,
|
||||||
records_processed INTEGER DEFAULT 0,
|
records_processed INTEGER DEFAULT 0,
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ services:
|
|||||||
args:
|
args:
|
||||||
PYTHON_IMAGE: ${PYTHON_IMAGE:-python:3.14-slim}
|
PYTHON_IMAGE: ${PYTHON_IMAGE:-python:3.14-slim}
|
||||||
UV_IMAGE: ${UV_IMAGE:-ghcr.io/astral-sh/uv:latest}
|
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
|
container_name: planet_aiprovider
|
||||||
ports:
|
ports:
|
||||||
- "8010:8010"
|
- "8010:8010"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ services:
|
|||||||
UV_IMAGE: ${UV_IMAGE:-ghcr.io/astral-sh/uv:latest}
|
UV_IMAGE: ${UV_IMAGE:-ghcr.io/astral-sh/uv:latest}
|
||||||
env_file:
|
env_file:
|
||||||
- ./aiprovider/.env
|
- ./aiprovider/.env
|
||||||
|
- ${PLANET_AI_PROVIDER_RUNTIME_ENV_FILE:-./aiprovider/.env}
|
||||||
container_name: planet_aiprovider
|
container_name: planet_aiprovider
|
||||||
ports:
|
ports:
|
||||||
- "8010:8010"
|
- "8010:8010"
|
||||||
|
|||||||
@@ -8,6 +8,20 @@ This project follows the repository versioning rule:
|
|||||||
- `improvement` -> `+0.0.1`(bugfix + 小功能混合)
|
- `improvement` -> `+0.0.1`(bugfix + 小功能混合)
|
||||||
- `bugfix` -> `+0.0.1`
|
- `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
|
## [0.44.2] — 2026-04-29
|
||||||
|
|
||||||
### 📝 Documentation
|
### 📝 Documentation
|
||||||
|
|||||||
@@ -55,6 +55,30 @@ Parameters:
|
|||||||
| `--allow-lan` | Enable LAN access |
|
| `--allow-lan` | Enable LAN access |
|
||||||
| `--verbose` | Show more command output during execution |
|
| `--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
|
### Stop
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -24,6 +24,12 @@ This script checks and syncs common dependencies, and generates if missing:
|
|||||||
- `aiprovider/.env`
|
- `aiprovider/.env`
|
||||||
- `frontend/.env.local`
|
- `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
|
## 1. Start Services
|
||||||
|
|
||||||
From the repository root:
|
From the repository root:
|
||||||
|
|||||||
@@ -190,8 +190,8 @@
|
|||||||
|
|
||||||
| 正式名称 | 变量名 | 当前值 | 使用位置 / 说明 |
|
| 正式名称 | 变量名 | 当前值 | 使用位置 / 说明 |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| 船只半径偏移 | `VESSEL_CONFIG.altitudeOffset` | `0.56` | 普通 marker 位置 |
|
| 船只半径偏移 | `VESSEL_CONFIG.altitudeOffset` | `0.2` | 普通 marker 位置,贴近真实地形基础层 |
|
||||||
| 船只轨迹半径偏移 | `VESSEL_CONFIG.track.altitudeOffset` | `0.7` | 选中船只轨迹线 |
|
| 船只轨迹半径偏移 | `VESSEL_CONFIG.track.altitudeOffset` | `0.22` | 选中船只轨迹线,略高于船只 marker 以保持可见 |
|
||||||
| 船只 renderOrder | local `VESSEL_RENDER_ORDER` | `4.4` | 普通 marker 和交互 overlay |
|
| 船只 renderOrder | local `VESSEL_RENDER_ORDER` | `4.4` | 普通 marker 和交互 overlay |
|
||||||
| 船只轨迹 renderOrder | `VESSEL_RENDER_ORDER - 0.1` | `4.3` | 低于船只 marker |
|
| 船只轨迹 renderOrder | `VESSEL_RENDER_ORDER - 0.1` | `4.3` | 低于船只 marker |
|
||||||
| 船只点像素尺寸 | local `VESSEL_POINT_SIZE` | `34` | 普通 marker 与 hover / locked overlay 共享尺寸 |
|
| 船只点像素尺寸 | local `VESSEL_POINT_SIZE` | `34` | 普通 marker 与 hover / locked overlay 共享尺寸 |
|
||||||
|
|||||||
@@ -55,6 +55,52 @@
|
|||||||
| `--allow-lan` | 允许局域网访问 |
|
| `--allow-lan` | 允许局域网访问 |
|
||||||
| `--verbose` | 在执行过程中显示更多命令输出 |
|
| `--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
|
```bash
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ compute_ai_provider_build_fingerprint() {
|
|||||||
find aiprovider \
|
find aiprovider \
|
||||||
-type f \
|
-type f \
|
||||||
! -path '*/__pycache__/*' \
|
! -path '*/__pycache__/*' \
|
||||||
|
! -name '.env' \
|
||||||
|
! -name '.env.*' \
|
||||||
! -name '*.pyc' \
|
! -name '*.pyc' \
|
||||||
! -name '*.pyo' \
|
! -name '*.pyo' \
|
||||||
| LC_ALL=C sort \
|
| LC_ALL=C sort \
|
||||||
@@ -57,6 +59,58 @@ compute_ai_provider_build_fingerprint() {
|
|||||||
|
|
||||||
速度提升约 10 倍(大量小文件场景),误报率相同(mtime+size 变化 ≡ 文件被修改)。
|
速度提升约 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`,而是:
|
fingerprint 一致时不执行 `docker compose build`,而是:
|
||||||
@@ -151,4 +205,7 @@ PY
|
|||||||
## 相关文件
|
## 相关文件
|
||||||
|
|
||||||
- `planet.sh` — 全量修改
|
- `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(未改动)
|
- `scripts/compute_aiprovider_dependency_fingerprint.py` — 依赖 fingerprint(未改动)
|
||||||
|
|||||||
@@ -24,6 +24,12 @@
|
|||||||
- `aiprovider/.env`
|
- `aiprovider/.env`
|
||||||
- `frontend/.env.local`
|
- `frontend/.env.local`
|
||||||
|
|
||||||
|
AI Provider 的个人配置也可以放在 `~/.zshrc`。`planet.sh` 会读取简单的 `export AI_...=...` 或 `AI_...=...` 行,并在启动 AI Provider 时传给容器。修改模型、密钥或 Base URL 后,通常只需要重启 AI Provider:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./planet.sh restart -a
|
||||||
|
```
|
||||||
|
|
||||||
## 1. 启动服务
|
## 1. 启动服务
|
||||||
|
|
||||||
在仓库根目录执行:
|
在仓库根目录执行:
|
||||||
|
|||||||
@@ -16,12 +16,13 @@
|
|||||||
## Current Version
|
## Current Version
|
||||||
|
|
||||||
- `main` 当前主线历史推导到:`0.16.5`
|
- `main` 当前主线历史推导到:`0.16.5`
|
||||||
- `dev` 当前开发分支历史推导到:`0.44.2`
|
- `dev` 当前开发分支历史推导到:`0.45.0`
|
||||||
|
|
||||||
## Timeline
|
## Timeline
|
||||||
|
|
||||||
| Version | Type | Branch | Commit | Summary |
|
| 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.2` | bugfix | `dev` | `pending` | 补充 Earth 船只批量渲染、屏幕拾取、图层顺序、样式参考和性能计划状态文档 |
|
||||||
| `0.44.1` | bugfix | `dev` | `pending` | 优化 Earth 船只批量渲染性能,修复拖动卡顿、拾取错位、交互态方向/尺寸和地表压盖问题 |
|
| `0.44.1` | bugfix | `dev` | `pending` | 优化 Earth 船只批量渲染性能,修复拖动卡顿、拾取错位、交互态方向/尺寸和地表压盖问题 |
|
||||||
| `0.44.0` | feature | `dev` | `pending` | 重构数据源目录与采集器设置,新增 BarentsWatch AIS 连接教程、Earth 船只/缩放体验优化和仪表盘前端重启 |
|
| `0.44.0` | feature | `dev` | `pending` | 重构数据源目录与采集器设置,新增 BarentsWatch AIS 连接教程、Earth 船只/缩放体验优化和仪表盘前端重启 |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "planet-frontend",
|
"name": "planet-frontend",
|
||||||
"version": "0.44.2",
|
"version": "0.45.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "bun@1",
|
"packageManager": "bun@1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -209,7 +209,7 @@ export const PATHS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const VESSEL_CONFIG = {
|
export const VESSEL_CONFIG = {
|
||||||
altitudeOffset: 0.56,
|
altitudeOffset: 0.2,
|
||||||
maxRenderedMarkers: 5000,
|
maxRenderedMarkers: 5000,
|
||||||
marker: {
|
marker: {
|
||||||
baseScale: 7.5,
|
baseScale: 7.5,
|
||||||
@@ -232,7 +232,7 @@ export const VESSEL_CONFIG = {
|
|||||||
max: 2.4,
|
max: 2.4,
|
||||||
},
|
},
|
||||||
track: {
|
track: {
|
||||||
altitudeOffset: 0.7,
|
altitudeOffset: 0.22,
|
||||||
color: 0x7dd3fc,
|
color: 0x7dd3fc,
|
||||||
opacity: 0.82,
|
opacity: 0.82,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import axios, { type AxiosResponse } from 'axios'
|
|||||||
import AppLayout from '../../components/AppLayout/AppLayout'
|
import AppLayout from '../../components/AppLayout/AppLayout'
|
||||||
import ScrollbarOverlay from '../../components/Scrollbar/ScrollbarOverlay'
|
import ScrollbarOverlay from '../../components/Scrollbar/ScrollbarOverlay'
|
||||||
import { formatDateTimeZhCN } from '../../utils/datetime'
|
import { formatDateTimeZhCN } from '../../utils/datetime'
|
||||||
|
import { formatPhaseMetric, getPhaseDisplay, getPhaseSummary } from '../../utils/phaseProgress'
|
||||||
|
|
||||||
const { Text } = Typography
|
const { Text } = Typography
|
||||||
const COLLECTION_REFRESH_DELAY_MS = 800
|
const COLLECTION_REFRESH_DELAY_MS = 800
|
||||||
@@ -54,6 +55,11 @@ interface BuiltInDataSource {
|
|||||||
task_id: number | null
|
task_id: number | null
|
||||||
progress: number | null
|
progress: number | null
|
||||||
phase?: string | 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
|
records_processed: number | null
|
||||||
total_records: number | null
|
total_records: number | null
|
||||||
is_free?: boolean
|
is_free?: boolean
|
||||||
@@ -107,6 +113,11 @@ interface UnifiedDataSource {
|
|||||||
is_running?: boolean
|
is_running?: boolean
|
||||||
progress?: number | null
|
progress?: number | null
|
||||||
phase?: string | 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
|
task_id?: number | null
|
||||||
created_at?: string
|
created_at?: string
|
||||||
updated_at?: string | null
|
updated_at?: string | null
|
||||||
@@ -129,6 +140,11 @@ type TriggerDatasourceConflict = {
|
|||||||
message?: string
|
message?: string
|
||||||
progress?: number | null
|
progress?: number | null
|
||||||
phase?: string | 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
|
records_processed?: number | null
|
||||||
total_records?: number | null
|
total_records?: number | null
|
||||||
}
|
}
|
||||||
@@ -142,6 +158,11 @@ type DatasourceTaskStatus = {
|
|||||||
task_id?: number | null
|
task_id?: number | null
|
||||||
progress?: number | null
|
progress?: number | null
|
||||||
phase?: string | 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
|
records_processed?: number | null
|
||||||
total_records?: number | null
|
total_records?: number | null
|
||||||
status?: string | null
|
status?: string | null
|
||||||
@@ -166,6 +187,11 @@ function normalizeBuiltin(source: BuiltInDataSource): UnifiedDataSource {
|
|||||||
is_running: source.is_running,
|
is_running: source.is_running,
|
||||||
progress: source.progress,
|
progress: source.progress,
|
||||||
phase: source.phase,
|
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,
|
task_id: source.task_id,
|
||||||
is_free: source.is_free,
|
is_free: source.is_free,
|
||||||
requires_credentials: source.requires_credentials,
|
requires_credentials: source.requires_credentials,
|
||||||
@@ -291,7 +317,7 @@ function DataSources() {
|
|||||||
content: (
|
content: (
|
||||||
<div>
|
<div>
|
||||||
<p>{taskInfo?.message || '当前采集任务仍在运行,重新触发会丢失本次未完成进度。'}</p>
|
<p>{taskInfo?.message || '当前采集任务仍在运行,重新触发会丢失本次未完成进度。'}</p>
|
||||||
<p>当前阶段: {taskInfo?.phase || 'running'}</p>
|
<p>当前阶段: {getPhaseDisplay(taskInfo || {})}</p>
|
||||||
<p>当前进度: {typeof taskInfo?.progress === 'number' ? `${Math.round(taskInfo.progress)}%` : '未知'}</p>
|
<p>当前进度: {typeof taskInfo?.progress === 'number' ? `${Math.round(taskInfo.progress)}%` : '未知'}</p>
|
||||||
<p>确认后会强制取消当前采集,并重新开始采集。</p>
|
<p>确认后会强制取消当前采集,并重新开始采集。</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -474,7 +500,11 @@ function DataSources() {
|
|||||||
return <Tag color={record.is_active ? 'green' : 'default'}>{record.is_active ? '启用' : '禁用'}</Tag>
|
return <Tag color={record.is_active ? 'green' : 'default'}>{record.is_active ? '启用' : '禁用'}</Tag>
|
||||||
}
|
}
|
||||||
if (record.is_running) {
|
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>
|
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>
|
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}` : ''}
|
{source.source}{source.task_id ? ` · #${source.task_id}` : ''}
|
||||||
</Text>
|
</Text>
|
||||||
</Space>
|
</Space>
|
||||||
<Tag color="processing">{source.phase || 'running'}</Tag>
|
<Tooltip title={getPhaseDisplay(source)}>
|
||||||
|
<Tag color="processing">{getPhaseSummary(source)}</Tag>
|
||||||
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
{source.phase_message ? (
|
||||||
|
<Text type="secondary" style={{ fontSize: 12 }}>{source.phase_message}</Text>
|
||||||
|
) : null}
|
||||||
<Progress
|
<Progress
|
||||||
percent={Math.round(source.progress || 0)}
|
percent={Math.round(source.progress || 0)}
|
||||||
size="small"
|
size="small"
|
||||||
@@ -612,8 +647,9 @@ function DataSources() {
|
|||||||
strokeColor="#1677ff"
|
strokeColor="#1677ff"
|
||||||
/>
|
/>
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
已处理 {source.records_processed ?? 0}
|
{source.phase_unit === 'bytes'
|
||||||
{source.total_records ? ` / ${source.total_records}` : ''}
|
? `下载 ${formatPhaseMetric(source) || '准备中'}`
|
||||||
|
: `已处理 ${source.records_processed ?? 0}${source.total_records ? ` / ${source.total_records}` : ''}`}
|
||||||
</Text>
|
</Text>
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,16 +1,26 @@
|
|||||||
import { useEffect, useState } from 'react'
|
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 { ReloadOutlined, CheckCircleOutlined, CloseCircleOutlined, SyncOutlined } from '@ant-design/icons'
|
||||||
import { useAuthStore } from '../../stores/auth'
|
import { useAuthStore } from '../../stores/auth'
|
||||||
import AppLayout from '../../components/AppLayout/AppLayout'
|
import AppLayout from '../../components/AppLayout/AppLayout'
|
||||||
import TableScrollRegion from '../../components/Scrollbar/TableScrollRegion'
|
import TableScrollRegion from '../../components/Scrollbar/TableScrollRegion'
|
||||||
import { formatDateTimeZhCN } from '../../utils/datetime'
|
import { formatDateTimeZhCN } from '../../utils/datetime'
|
||||||
|
import { getPhaseDisplay, getPhaseSummary } from '../../utils/phaseProgress'
|
||||||
|
|
||||||
interface Task {
|
interface Task {
|
||||||
id: number
|
id: number
|
||||||
collector: string
|
collector?: string
|
||||||
|
datasource_name?: string
|
||||||
status: 'success' | 'failed' | 'running' | 'pending'
|
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
|
records_processed: number
|
||||||
|
total_records?: number | null
|
||||||
|
progress?: number | null
|
||||||
started_at: string
|
started_at: string
|
||||||
completed_at: string
|
completed_at: string
|
||||||
duration_seconds: number
|
duration_seconds: number
|
||||||
@@ -53,7 +63,20 @@ function Tasks() {
|
|||||||
title: '收集器',
|
title: '收集器',
|
||||||
dataIndex: 'collector',
|
dataIndex: 'collector',
|
||||||
key: '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: '状态',
|
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)"
|
SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
|
||||||
cd "$SCRIPT_DIR"
|
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'
|
RED='\033[38;5;203m'
|
||||||
GREEN='\033[38;5;114m'
|
GREEN='\033[38;5;114m'
|
||||||
YELLOW='\033[38;5;221m'
|
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_BUILD_LOG_FILE="/tmp/planet_aiprovider_build.log"
|
||||||
AI_PROVIDER_IMAGE_NAME="${AI_PROVIDER_IMAGE_NAME:-planet_aiprovider:latest}"
|
AI_PROVIDER_IMAGE_NAME="${AI_PROVIDER_IMAGE_NAME:-planet_aiprovider:latest}"
|
||||||
AI_PROVIDER_CONTAINER_NAME="${AI_PROVIDER_CONTAINER_NAME:-planet_aiprovider}"
|
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_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() {
|
prepend_path_once() {
|
||||||
local path_entry="$1"
|
local path_entry="$1"
|
||||||
@@ -590,6 +648,8 @@ compute_ai_provider_build_fingerprint() {
|
|||||||
find aiprovider \
|
find aiprovider \
|
||||||
-type f \
|
-type f \
|
||||||
! -path '*/__pycache__/*' \
|
! -path '*/__pycache__/*' \
|
||||||
|
! -name '.env' \
|
||||||
|
! -name '.env.*' \
|
||||||
! -name '*.pyc' \
|
! -name '*.pyc' \
|
||||||
! -name '*.pyo' \
|
! -name '*.pyo' \
|
||||||
| LC_ALL=C sort \
|
| LC_ALL=C sort \
|
||||||
@@ -633,6 +693,9 @@ run_ai_provider_container_manually() {
|
|||||||
if [ -f "$SCRIPT_DIR/aiprovider/.env" ]; then
|
if [ -f "$SCRIPT_DIR/aiprovider/.env" ]; then
|
||||||
env_file_args=(--env-file "$SCRIPT_DIR/aiprovider/.env")
|
env_file_args=(--env-file "$SCRIPT_DIR/aiprovider/.env")
|
||||||
fi
|
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 \
|
docker run -d \
|
||||||
--name "$AI_PROVIDER_CONTAINER_NAME" \
|
--name "$AI_PROVIDER_CONTAINER_NAME" \
|
||||||
@@ -641,6 +704,22 @@ run_ai_provider_container_manually() {
|
|||||||
"$AI_PROVIDER_IMAGE_NAME" >/dev/null
|
"$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() {
|
recreate_ai_provider_container() {
|
||||||
local ai_provider_port="${1:-$DEFAULT_AI_PROVIDER_PORT}"
|
local ai_provider_port="${1:-$DEFAULT_AI_PROVIDER_PORT}"
|
||||||
|
|
||||||
@@ -1156,6 +1235,7 @@ start_ai_provider_service() {
|
|||||||
local retry=1
|
local retry=1
|
||||||
set_wait_detail "启动 AI Provider"
|
set_wait_detail "启动 AI Provider"
|
||||||
|
|
||||||
|
write_ai_provider_runtime_env_file
|
||||||
ensure_ai_provider_image_current
|
ensure_ai_provider_image_current
|
||||||
|
|
||||||
while [ "$retry" -le "$AI_PROVIDER_START_MAX_RETRIES" ]; do
|
while [ "$retry" -le "$AI_PROVIDER_START_MAX_RETRIES" ]; do
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "planet"
|
name = "planet"
|
||||||
version = "0.44.2"
|
version = "0.45.0"
|
||||||
description = "智能星球计划 - 态势感知系统"
|
description = "智能星球计划 - 态势感知系统"
|
||||||
requires-python = ">=3.14"
|
requires-python = ">=3.14"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
Reference in New Issue
Block a user