Files
SMANAGE/GVSDServiceServer/apps/venom/views.py
T
TermiNexus aa56281493 feat(订阅式软件提供): 按包名详情接口在 README 为空时合成默认 Markdown
新增 _DefaultReadme:由包名、说明、安装命令、依赖表与版本历史合成 README;仅 PackageByNameView(官网数据源)使用,配置页读到的仍是作者原文,避免合成内容被回写。
2026-10-07 20:16:08 +08:00

236 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""模块:Venom 包管理器 API
远端页面(由本服务托管、在 GERP 宿主内渲染)跨源调用以下接口:
GET /api/venom/packages/ 包列表
POST /api/venom/packages/ 新建包(包名全局唯一)
GET /api/venom/packages/<uuid>/ 包详情(配置页读取)
POST /api/venom/packages/<uuid>/config/ 保存包配置
GET /api/venom/packages/<uuid>/releases/ 发布历史
POST /api/venom/packages/<uuid>/releases/ 发布新版本(写历史)
GET /api/venom/project/<包名>/ 按包名取详情(官网调用:含 README 与依赖列表)
"""
import logging
from django.db.models import Count
from django.utils.dateformat import format as date_format
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import VenomPackage, VenomPackageRelease
logger = logging.getLogger(__name__)
def _ok(data=None, msg='success'):
return Response({'code': 200, 'msg': msg, 'data': data})
def _err(msg, code=400, status=None):
return Response({'code': code, 'msg': msg, 'data': None}, status=status or 200)
def _Time(value):
return date_format(value, 'Y-m-d H:i:s') if value else ''
def _PackageRow(package):
return {
'PackageUUID': str(package.PackageUUID),
'PackageName': package.PackageName,
'PackageDesc': package.PackageDesc or '',
'CreatorName': package.CreatorName or '',
'CreateTime': _Time(package.CreateTime),
'UpdateTime': _Time(package.UpdateTime),
'ReleaseCount': getattr(package, 'release_count', 0),
}
def _ReleaseRow(release):
return {
'ReleaseUUID': str(release.ReleaseUUID),
'Version': release.Version,
'Notes': release.Notes or '',
'PublisherName': release.PublisherName or '',
'CreateTime': _Time(release.CreateTime),
}
def _GetPackage(PackageUUID):
return VenomPackage.objects.annotate(release_count=Count('releases')).filter(
PackageUUID=PackageUUID).first()
def _NormalizeDependencies(value):
"""依赖列表标准化为 [{'Name', 'Version'}];非法项丢弃、同名去重、限 200 条
兼容两种入参:字符串(仅包名)与对象 {'Name','Version'}(Version 可为空 = 无版本约束)。
"""
if not isinstance(value, list):
return []
rows, seen = [], set()
for item in value[:200]:
if isinstance(item, str):
name, version = item.strip(), ''
elif isinstance(item, dict):
name = str(item.get('Name') or '').strip()
version = str(item.get('Version') or '').strip()
else:
continue
if not name or name in seen:
continue
seen.add(name)
rows.append({'Name': name[:128], 'Version': version[:64]})
return rows
def _DetailRow(package):
"""包详情行(含 README/依赖/发布历史;官网与配置页共用)"""
releases = list(package.releases.all())
return {
**_PackageRow(package),
'ConfigText': package.ConfigText or '',
'Readme': package.Readme or '',
'Dependencies': package.Dependencies or [],
'LatestVersion': releases[0].Version if releases else '',
'Releases': [_ReleaseRow(r) for r in releases],
}
def _DefaultReadme(package, releases):
"""包作者未填写 README 时,由包信息合成一份 Markdown 默认 README
仅用于官网展示(`PackageByNameView`);配置页读取到的仍是「作者填写的原文」,
避免把合成内容回写成用户数据。
"""
lines = [f'# {package.PackageName}', '']
lines += [package.PackageDesc.strip() or '该软件包暂未填写说明。', '']
lines += ['## 安装', '', '```bash', f'venom install {package.PackageName}', '```', '']
dependencies = package.Dependencies or []
lines += ['## 依赖', '']
if dependencies:
lines += ['| 依赖包 | 版本约束 |', '| --- | --- |']
lines += [f"| {d.get('Name', '')} | {d.get('Version') or '*'} |" for d in dependencies]
else:
lines += ['无外部依赖。']
lines += ['']
lines += ['## 版本历史', '']
if releases:
lines += [f"- `{r.Version}` · {_Time(r.CreateTime)}" for r in releases]
else:
lines += ['尚未发布任何版本。']
lines += ['']
return '\n'.join(lines)
class PackageListView(APIView):
"""包列表 / 新建包"""
def get(self, request):
keyword = (request.query_params.get('Keyword') or '').strip()
qs = VenomPackage.objects.annotate(release_count=Count('releases'))
if keyword:
qs = qs.filter(PackageName__icontains=keyword)
rows = [_PackageRow(p) for p in qs]
return _ok({'list': rows, 'total': len(rows)})
def post(self, request):
name = (request.data.get('PackageName') or '').strip()
if not name:
return _err('请填写包名')
if len(name) > 128:
return _err('包名过长(最多 128 字)')
if VenomPackage.objects.filter(PackageName=name).exists():
return _err(f'包名「{name}」已存在(包名必须全局唯一)')
package = VenomPackage.objects.create(
PackageName=name,
PackageDesc=(request.data.get('PackageDesc') or '').strip()[:512],
ConfigText=request.data.get('ConfigText') or '',
Dependencies=_NormalizeDependencies(request.data.get('Dependencies')),
Readme=request.data.get('Readme') or '',
CreatorName=(request.data.get('CreatorName') or '').strip()[:128],
)
package = _GetPackage(package.PackageUUID)
return _ok(_DetailRow(package), msg='软件包已创建')
class PackageDetailView(APIView):
"""包详情(配置页读取;含 README / 依赖列表 / 发布历史)"""
def get(self, request, package_uuid):
package = _GetPackage(package_uuid)
if package is None:
return _err('软件包不存在', 404)
return _ok(_DetailRow(package))
class PackageConfigView(APIView):
"""保存包配置(说明 / 配置内容 / 依赖列表 / README)"""
def post(self, request, package_uuid):
package = VenomPackage.objects.filter(PackageUUID=package_uuid).first()
if package is None:
return _err('软件包不存在', 404)
if 'PackageDesc' in request.data:
package.PackageDesc = (request.data.get('PackageDesc') or '').strip()[:512]
if 'ConfigText' in request.data:
package.ConfigText = request.data.get('ConfigText') or ''
if 'Dependencies' in request.data:
package.Dependencies = _NormalizeDependencies(request.data.get('Dependencies'))
if 'Readme' in request.data:
package.Readme = request.data.get('Readme') or ''
package.save(update_fields=[
'PackageDesc', 'ConfigText', 'Dependencies', 'Readme', 'UpdateTime'])
return _ok(_DetailRow(_GetPackage(package.PackageUUID)), msg='配置已保存')
class PackageReleaseView(APIView):
"""发布历史 / 发布新版本"""
def get(self, request, package_uuid):
package = VenomPackage.objects.filter(PackageUUID=package_uuid).first()
if package is None:
return _err('软件包不存在', 404)
rows = [_ReleaseRow(r) for r in package.releases.all()]
return _ok({'PackageName': package.PackageName, 'list': rows, 'total': len(rows)})
def post(self, request, package_uuid):
package = VenomPackage.objects.filter(PackageUUID=package_uuid).first()
if package is None:
return _err('软件包不存在', 404)
version = (request.data.get('Version') or '').strip()
if not version:
return _err('请填写版本号')
if len(version) > 64:
return _err('版本号过长(最多 64 字)')
release = VenomPackageRelease.objects.create(
Package=package,
Version=version,
Notes=request.data.get('Notes') or '',
PublisherName=(request.data.get('PublisherName') or '').strip()[:128],
)
return _ok(_ReleaseRow(release), msg='已发布')
class PackageByNameView(APIView):
"""按包名取包详情——官网 `venom.gvsds.com/project/<包名>/` 的数据源
GET /api/venom/project/<PackageName>/
"""
def get(self, request, package_name):
name = str(package_name or '').strip().strip('/')
package = VenomPackage.objects.annotate(release_count=Count('releases')).filter(
PackageName=name).first()
if package is None:
return _err(f'包「{name}」不存在', 404, status=404)
row = _DetailRow(package)
# 作者未填 README 时给出合成内容,官网不出现「无 README」占位
if not row['Readme']:
row['Readme'] = _DefaultReadme(package, list(package.releases.all()))
return _ok(row)