Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6104889633 | |||
| 555a3f64c8 | |||
| 5daec0ef97 | |||
| 0a424bbc46 | |||
| 570815b487 |
@@ -18,7 +18,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -278,9 +277,7 @@ class SmartAssetSelector:
|
||||
# 分桶
|
||||
short_bucket = [d for d in scored if d.duration is not None and d.duration < _SHORT_BUCKET_MAX]
|
||||
medium_bucket = [
|
||||
d
|
||||
for d in scored
|
||||
if d.duration is not None and _SHORT_BUCKET_MAX <= d.duration < _MEDIUM_BUCKET_MAX
|
||||
d for d in scored if d.duration is not None and _SHORT_BUCKET_MAX <= d.duration < _MEDIUM_BUCKET_MAX
|
||||
]
|
||||
long_bucket = [d for d in scored if d.duration is not None and d.duration >= _MEDIUM_BUCKET_MAX]
|
||||
unknown_bucket = [d for d in scored if d.duration is None]
|
||||
@@ -295,7 +292,7 @@ class SmartAssetSelector:
|
||||
selected_ids: set[str] = set()
|
||||
|
||||
# 先按配额从每个桶取
|
||||
for bucket, name in zip(buckets, bucket_names):
|
||||
for bucket, _name in zip(buckets, bucket_names, strict=False):
|
||||
quota = min(base_quota, len(bucket))
|
||||
if quota <= 0:
|
||||
continue
|
||||
|
||||
@@ -1,117 +1,18 @@
|
||||
#!/bin/sh
|
||||
# CI 公共步骤:前端依赖安装(Docker Volume 持久化缓存方案)
|
||||
# 通过 Docker named volume 缓存 node_modules,按 package-lock.json hash 命名
|
||||
# 缓存命中时跳过 npm ci,直接复用已有 volume
|
||||
# CI 公共步骤:前端依赖安装
|
||||
# 直接在 CI 容器内运行(CI 镜像已包含 Node.js),无需 Docker 嵌套
|
||||
set -eu
|
||||
|
||||
MODE="${1:-full}"
|
||||
|
||||
echo "=== 前端依赖安装开始 (模式: $MODE) ==="
|
||||
|
||||
# npm国内镜像源(加速下载,减少网络失败)
|
||||
NPM_REGISTRY="https://registry.npmmirror.com"
|
||||
cd apps/web
|
||||
|
||||
# 缓存配置 — 与 step_frontend_run.sh 保持一致
|
||||
LOCK_FILE="apps/web/package-lock.json"
|
||||
VOLUME_PREFIX="ci-web-nm-"
|
||||
KEEP_CACHE_COUNT=5
|
||||
# 配置国内镜像源加速
|
||||
npm config set registry https://registry.npmmirror.com
|
||||
|
||||
# 计算 package-lock.json 的 md5 hash 作为缓存 key
|
||||
VOLUME_NAME=""
|
||||
if [ -f "$LOCK_FILE" ]; then
|
||||
LOCK_HASH=$(md5sum "$LOCK_FILE" | cut -c1-12)
|
||||
VOLUME_NAME="${VOLUME_PREFIX}${LOCK_HASH}"
|
||||
echo "缓存 key: $LOCK_HASH (volume: $VOLUME_NAME)"
|
||||
else
|
||||
echo "警告: 未找到 $LOCK_FILE,将不使用持久化缓存"
|
||||
fi
|
||||
|
||||
# 检查 volume 是否存在(缓存命中)
|
||||
CACHE_HIT=0
|
||||
if [ -n "$VOLUME_NAME" ]; then
|
||||
if docker volume inspect "$VOLUME_NAME" >/dev/null 2>&1; then
|
||||
CACHE_HIT=1
|
||||
echo "缓存命中!复用 volume: $VOLUME_NAME"
|
||||
else
|
||||
echo "缓存未命中,创建 volume 并安装依赖..."
|
||||
# 创建 volume(失败则降级为无缓存模式)
|
||||
if ! docker volume create "$VOLUME_NAME" >/dev/null 2>&1; then
|
||||
echo "警告: 创建 volume 失败,降级为无缓存模式"
|
||||
VOLUME_NAME=""
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# 构建 docker run 的 volume 挂载参数(空时不挂载)
|
||||
VOLUME_ARGS=""
|
||||
if [ -n "$VOLUME_NAME" ]; then
|
||||
VOLUME_ARGS="-v ${VOLUME_NAME}:/workspace/apps/web/node_modules"
|
||||
fi
|
||||
|
||||
# 缓存未命中时执行 npm ci
|
||||
if [ "$CACHE_HIT" -eq 0 ]; then
|
||||
for i in 1 2 3; do
|
||||
echo "npm ci 尝试 $i/3 (镜像: $NPM_REGISTRY)"
|
||||
docker run --rm -v "$PWD:/workspace" $VOLUME_ARGS -w /workspace/apps/web docker.m.daocloud.io/library/node:20 sh -lc "npm config set registry $NPM_REGISTRY && npm ci --no-audit --no-fund" && break
|
||||
echo "npm ci 失败,重试 $i/3..."
|
||||
[ $i -eq 3 ] && exit 1
|
||||
sleep 10
|
||||
done
|
||||
else
|
||||
echo "缓存命中,验证依赖完整性..."
|
||||
# 验证关键依赖是否存在(防止缓存损坏或版本漂移)
|
||||
DEPS_OK=1
|
||||
if ! docker run --rm $VOLUME_ARGS -v "$PWD:/workspace" -w /workspace/apps/web docker.m.daocloud.io/library/node:20 sh -lc "npx --yes vitest --version > /dev/null 2>&1 && npx --yes vite --version > /dev/null 2>&1" 2>/dev/null; then
|
||||
echo "⚠️ 缓存依赖不完整(vitest/vite缺失),废弃缓存重新安装"
|
||||
DEPS_OK=0
|
||||
docker volume rm "$VOLUME_NAME" > /dev/null 2>&1 || true
|
||||
docker volume create "$VOLUME_NAME" > /dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if [ "$DEPS_OK" -eq 1 ]; then
|
||||
echo "✅ 依赖完整性校验通过,跳过 npm ci"
|
||||
else
|
||||
# 重新安装
|
||||
for i in 1 2 3; do
|
||||
echo "npm ci 重新安装尝试 $i/3 (镜像: $NPM_REGISTRY)"
|
||||
docker run --rm -v "$PWD:/workspace" $VOLUME_ARGS -w /workspace/apps/web docker.m.daocloud.io/library/node:20 sh -lc "npm config set registry $NPM_REGISTRY && npm ci --no-audit --no-fund" && break
|
||||
echo "npm ci 失败,重试 $i/3..."
|
||||
[ $i -eq 3 ] && exit 1
|
||||
sleep 10
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
# 清理旧缓存 volume(保留最近 N 个,防止磁盘占用无限增长)
|
||||
if [ -n "$VOLUME_PREFIX" ]; then
|
||||
echo "清理旧缓存 volume(保留最近 ${KEEP_CACHE_COUNT} 个)..."
|
||||
ALL_VOLUMES=$(docker volume ls -q --filter "name=${VOLUME_PREFIX}" 2>/dev/null || true)
|
||||
if [ -n "$ALL_VOLUMES" ]; then
|
||||
TOTAL=$(echo "$ALL_VOLUMES" | wc -l)
|
||||
if [ "$TOTAL" -gt "$KEEP_CACHE_COUNT" ]; then
|
||||
# 按创建时间排序,保留最新的 N 个
|
||||
SORTED_VOLUMES=$(for v in $ALL_VOLUMES; do
|
||||
CREATED=$(docker volume inspect --format '{{.CreatedAt}}' "$v" 2>/dev/null || echo "0")
|
||||
echo "$CREATED $v"
|
||||
done | sort | awk '{print $2}')
|
||||
|
||||
# 删除超出保留数量的旧 volume
|
||||
REMOVE_COUNT=$((TOTAL - KEEP_CACHE_COUNT))
|
||||
TO_DELETE=$(echo "$SORTED_VOLUMES" | head -n "$REMOVE_COUNT")
|
||||
REMOVED=0
|
||||
for v in $TO_DELETE; do
|
||||
# 跳过当前正在使用的 volume
|
||||
if [ "$v" != "$VOLUME_NAME" ]; then
|
||||
if docker volume rm "$v" >/dev/null 2>&1; then
|
||||
REMOVED=$((REMOVED + 1))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
echo "已清理 $REMOVED 个旧缓存 volume,当前共 $((TOTAL - REMOVED)) 个"
|
||||
else
|
||||
echo "当前缓存 volume 数量: $TOTAL,无需清理"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
# 安装依赖
|
||||
npm ci --no-audit --no-fund
|
||||
|
||||
echo "=== 前端依赖安装完成 ==="
|
||||
|
||||
@@ -1,26 +1,9 @@
|
||||
#!/bin/sh
|
||||
# CI 公共步骤:前端命令执行(在 docker node 容器中运行)
|
||||
# 用法:step_frontend_run.sh "要执行的命令"
|
||||
# 支持 Docker Volume 持久化缓存的 node_modules
|
||||
# CI 公共步骤:前端命令执行
|
||||
# 直接在 CI 容器内运行(CI 镜像已包含 Node.js + pnpm),无需 Docker 嵌套
|
||||
set -eu
|
||||
|
||||
CMD="${1:-echo 'no command'}"
|
||||
|
||||
# 缓存配置 — 与 step_frontend_install.sh 保持一致
|
||||
LOCK_FILE="apps/web/package-lock.json"
|
||||
VOLUME_PREFIX="ci-web-nm-"
|
||||
|
||||
# 计算 package-lock.json 的 hash,挂载对应的 volume
|
||||
VOLUME_ARGS=""
|
||||
if [ -f "$LOCK_FILE" ]; then
|
||||
LOCK_HASH=$(md5sum "$LOCK_FILE" | cut -c1-12)
|
||||
VOLUME_NAME="${VOLUME_PREFIX}${LOCK_HASH}"
|
||||
if docker volume inspect "$VOLUME_NAME" >/dev/null 2>&1; then
|
||||
VOLUME_ARGS="-v ${VOLUME_NAME}:/workspace/apps/web/node_modules"
|
||||
echo "使用缓存 volume: $VOLUME_NAME"
|
||||
else
|
||||
echo "提示: 未找到缓存 volume $VOLUME_NAME,将使用源码目录 node_modules"
|
||||
fi
|
||||
fi
|
||||
|
||||
docker run --rm -v "$PWD:/workspace" $VOLUME_ARGS -w /workspace/apps/web docker.m.daocloud.io/library/node:20 sh -lc "$CMD"
|
||||
cd apps/web
|
||||
sh -lc "$CMD"
|
||||
|
||||
@@ -18,8 +18,8 @@ from unittest.mock import MagicMock, patch
|
||||
sys.path.insert(0, "apps/api")
|
||||
|
||||
from app.services.ai_service import ( # noqa: E402
|
||||
DoubaoAIClient,
|
||||
TITLE_STYLES,
|
||||
DoubaoAIClient,
|
||||
_generate_titles_fallback,
|
||||
_parse_titles_from_response,
|
||||
generate_smart_titles,
|
||||
@@ -88,7 +88,7 @@ class TestTitleParsing(unittest.TestCase):
|
||||
|
||||
def test_parse_markdown_code_block_json(self):
|
||||
"""解析 markdown 代码块包裹的 JSON."""
|
||||
content = "```json\n[\"标题1\", \"标题2\"]\n```"
|
||||
content = '```json\n["标题1", "标题2"]\n```'
|
||||
result = _parse_titles_from_response(content)
|
||||
self.assertEqual(len(result), 2)
|
||||
|
||||
@@ -222,11 +222,7 @@ class TestGenerateSmartTitles(unittest.TestCase):
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"content": json.dumps(["AI标题1", "AI标题2", "AI标题3", "AI标题4", "AI标题5"])
|
||||
}
|
||||
}
|
||||
{"message": {"content": json.dumps(["AI标题1", "AI标题2", "AI标题3", "AI标题4", "AI标题5"])}}
|
||||
]
|
||||
}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
@@ -265,9 +261,7 @@ class TestGenerateSmartTitles(unittest.TestCase):
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
# 返回无法解析的内容(只有一个标题且格式异常)
|
||||
mock_response.json.return_value = {
|
||||
"choices": [{"message": {"content": "一段文字说明,不是标题列表"}}]
|
||||
}
|
||||
mock_response.json.return_value = {"choices": [{"message": {"content": "一段文字说明,不是标题列表"}}]}
|
||||
mock_response.raise_for_status = MagicMock()
|
||||
|
||||
with patch("httpx.post", return_value=mock_response):
|
||||
@@ -281,7 +275,7 @@ class TestTitleStyles(unittest.TestCase):
|
||||
|
||||
def test_all_styles_have_required_fields(self):
|
||||
"""所有风格都有必要字段."""
|
||||
for key, info in TITLE_STYLES.items():
|
||||
for _key, info in TITLE_STYLES.items():
|
||||
self.assertIn("name", info)
|
||||
self.assertIn("description", info)
|
||||
self.assertIn("examples", info)
|
||||
|
||||
Reference in New Issue
Block a user