```
feat(home): 添加首页卡通形象卡片入口 - 新增 onTapCompanion 回调属性用于处理点击卡通形象卡片事件 - 在首页添加 companionCard 视图组件,显示"和康康聊聊"功能入口 - 实现卡通形象卡片样式,包含头像、标题和描述文本 - 集成 HealthChatView 全屏覆盖页面用于健康记录问答功能 - 添加相关国际化字符串支持 BREAKING CHANGE: 新增首页功能模块,改变原有界面布局结构 ```
This commit is contained in:
661
.agents/skills/vibe-submission-collector/scripts/submission_tool.py
Executable file
661
.agents/skills/vibe-submission-collector/scripts/submission_tool.py
Executable file
@@ -0,0 +1,661 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Initialize, render, and validate Vibe-a-thon submission packages."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from urllib.parse import urlparse
|
||||
|
||||
SCHEMA = "vibe-a-thon-submission/v3"
|
||||
LEGACY_SCHEMAS = {"vibe-a-thon-submission/v1", "vibe-a-thon-submission/v2"}
|
||||
REQUIRED_MARKDOWN = [
|
||||
"01-overview.md",
|
||||
"02-user-problem.md",
|
||||
"03-product-demo.md",
|
||||
"04-google-technology.md",
|
||||
"05-market-localization.md",
|
||||
"06-innovation.md",
|
||||
"07-declarations.md",
|
||||
]
|
||||
|
||||
|
||||
def now_iso() -> str:
|
||||
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
||||
|
||||
|
||||
def valid_url(value: str) -> bool:
|
||||
parsed = urlparse(value or "")
|
||||
return parsed.scheme in {"http", "https"} and bool(parsed.netloc)
|
||||
|
||||
|
||||
def blank_submission(submission_id: str, title: str) -> dict:
|
||||
return {
|
||||
"schema_version": SCHEMA,
|
||||
"submission_id": submission_id,
|
||||
"updated_at": now_iso(),
|
||||
"basic": {
|
||||
"title": title,
|
||||
"team_name": "",
|
||||
"track": "",
|
||||
"one_liner": "",
|
||||
"contestant_name_certificate": "",
|
||||
"has_team": None,
|
||||
"team_member_names_certificate": [],
|
||||
"contact": {"name": "", "email": "", "phone": "", "wechat": ""},
|
||||
"members": [],
|
||||
"target_markets": [],
|
||||
"languages": [],
|
||||
"status": "",
|
||||
"xiaohongshu_url": "",
|
||||
},
|
||||
"problem": {
|
||||
"target_users": "",
|
||||
"scenario": "",
|
||||
"current_workaround": "",
|
||||
"pain_points": [],
|
||||
"evidence_summary": "",
|
||||
"evidence_links": [],
|
||||
"research_count": 0,
|
||||
"representative_feedback": "",
|
||||
"unvalidated_assumptions": [],
|
||||
},
|
||||
"solution": {"summary": "", "user_value": "", "user_flow": [], "core_features": []},
|
||||
"demo": {
|
||||
"url": "",
|
||||
"access_method": "",
|
||||
"recommended_environment": "",
|
||||
"experience_steps": [],
|
||||
"expected_duration_minutes": None,
|
||||
"known_issues": [],
|
||||
"fallback_download_url": "",
|
||||
"published_channel": "",
|
||||
"published_product_name": "",
|
||||
"support_contact": "",
|
||||
"availability_confirmed": False,
|
||||
"automated_testing_consent": False,
|
||||
"access_restricted": False,
|
||||
"private_access_file": "private/08-test-access.md",
|
||||
},
|
||||
"video": {
|
||||
"url": "",
|
||||
"duration_minutes": None,
|
||||
"contents_confirmed": {"demo": False, "user_problem": False, "google_technology": False, "innovation": False, "localization": False},
|
||||
"timecodes": {"problem": "", "demo": "", "google_technology": "", "innovation": "", "localization": ""},
|
||||
},
|
||||
"google_technology": [],
|
||||
"technical": {
|
||||
"architecture_file": "",
|
||||
"repository_url": "",
|
||||
"commit_id": "",
|
||||
"code_evidence_paths": [],
|
||||
"deployment_notes": "",
|
||||
"dependencies": [],
|
||||
"confidentiality_notes": "",
|
||||
},
|
||||
"innovation": {"technical": [], "product_experience": []},
|
||||
"market": {
|
||||
"first_market": "",
|
||||
"market_rationale": "",
|
||||
"user_acquisition": "",
|
||||
"usage_conditions": "",
|
||||
"barriers": [],
|
||||
"cost_range": "",
|
||||
"social_impact_path": "",
|
||||
"impact_metrics": [],
|
||||
"six_month_plan": "",
|
||||
},
|
||||
"localization": {
|
||||
"target_culture": "",
|
||||
"habits": [],
|
||||
"user_concerns": [],
|
||||
"adaptations": [],
|
||||
"evidence": [],
|
||||
"tested_with_local_users": False,
|
||||
"test_count": 0,
|
||||
"changes_after_testing": "",
|
||||
},
|
||||
"declarations": {
|
||||
"existed_before_contest": None,
|
||||
"new_work_during_contest": "",
|
||||
"originality_confirmed": False,
|
||||
"review_access_consent": False,
|
||||
},
|
||||
"attachments": [],
|
||||
}
|
||||
|
||||
|
||||
def load_submission(folder: Path) -> dict:
|
||||
path = folder / "00-submission.json"
|
||||
if not path.is_file():
|
||||
raise ValueError(f"missing {path}")
|
||||
data = json.loads(path.read_text(encoding="utf-8"))
|
||||
if data.get("schema_version") in LEGACY_SCHEMAS | {SCHEMA}:
|
||||
basic = data.setdefault("basic", {})
|
||||
basic.setdefault("contestant_name_certificate", basic.get("contact", {}).get("name", ""))
|
||||
basic.setdefault("team_member_names_certificate", [
|
||||
member.get("name", "") for member in basic.get("members", []) if isinstance(member, dict) and member.get("name")
|
||||
])
|
||||
basic.setdefault("has_team", bool(basic.get("team_member_names_certificate")))
|
||||
demo = data.setdefault("demo", {})
|
||||
demo.setdefault("published_channel", "")
|
||||
demo.setdefault("published_product_name", "")
|
||||
video = data.setdefault("video", {})
|
||||
video.setdefault("contents_confirmed", {"demo": False, "user_problem": False, "google_technology": False, "innovation": False, "localization": False})
|
||||
if data.get("schema_version") in LEGACY_SCHEMAS:
|
||||
data["schema_version"] = SCHEMA
|
||||
return data
|
||||
|
||||
|
||||
def bullets(values, empty="未提供") -> str:
|
||||
if not values:
|
||||
return f"- {empty}"
|
||||
result = []
|
||||
for value in values:
|
||||
if isinstance(value, dict):
|
||||
result.append(f"- {json.dumps(value, ensure_ascii=False)}")
|
||||
else:
|
||||
result.append(f"- {value}")
|
||||
return "\n".join(result)
|
||||
|
||||
|
||||
def text(value, empty="未提供") -> str:
|
||||
if value is None or value == "":
|
||||
return empty
|
||||
if isinstance(value, bool):
|
||||
return "是" if value else "否"
|
||||
return str(value)
|
||||
|
||||
|
||||
def render(folder: Path, data: dict) -> None:
|
||||
basic, problem, solution = data["basic"], data["problem"], data["solution"]
|
||||
demo, video = data["demo"], data["video"]
|
||||
tech, market, loc = data["technical"], data["market"], data["localization"]
|
||||
decl = data["declarations"]
|
||||
|
||||
overview = f"""# 作品概览
|
||||
|
||||
- 作品编号:{text(data['submission_id'])}
|
||||
- 作品名称:{text(basic['title'])}
|
||||
- 团队名称:{text(basic['team_name'])}
|
||||
- 选手姓名(获奖证书):{text(basic['contestant_name_certificate'])}
|
||||
- 参赛形式:{'团队参赛' if basic['has_team'] is True else '个人参赛' if basic['has_team'] is False else '未提供'}
|
||||
- 团队成员姓名(获奖证书):{', '.join(basic['team_member_names_certificate']) or '未提供'}
|
||||
- 赛道:{text(basic['track'])}
|
||||
- 当前状态:{text(basic['status'])}
|
||||
- 一句话介绍:{text(basic['one_liner'])}
|
||||
- 目标市场:{', '.join(basic['target_markets']) or '未提供'}
|
||||
- 产品语言:{', '.join(basic['languages']) or '未提供'}
|
||||
- 小红书链接:{text(basic['xiaohongshu_url'])}
|
||||
|
||||
## 团队成员
|
||||
|
||||
{bullets(basic['members'])}
|
||||
|
||||
## 联系人
|
||||
|
||||
- 姓名:{text(basic['contact']['name'])}
|
||||
- 邮箱:{text(basic['contact']['email'])}
|
||||
- 手机:{text(basic['contact']['phone'])}
|
||||
- 微信:{text(basic['contact']['wechat'])}
|
||||
"""
|
||||
user_problem = f"""# 用户问题
|
||||
|
||||
## 目标用户
|
||||
|
||||
{text(problem['target_users'])}
|
||||
|
||||
## 使用场景
|
||||
|
||||
{text(problem['scenario'])}
|
||||
|
||||
## 当前解决方式
|
||||
|
||||
{text(problem['current_workaround'])}
|
||||
|
||||
## 具体痛点
|
||||
|
||||
{bullets(problem['pain_points'])}
|
||||
|
||||
## 问题证据
|
||||
|
||||
{text(problem['evidence_summary'])}
|
||||
|
||||
{bullets(problem['evidence_links'], '未提供证据链接')}
|
||||
|
||||
- 访谈或测试人数:{text(problem['research_count'], '0')}
|
||||
- 代表性反馈:{text(problem['representative_feedback'])}
|
||||
|
||||
## 尚未验证的假设
|
||||
|
||||
{bullets(problem['unvalidated_assumptions'])}
|
||||
"""
|
||||
feature_lines = []
|
||||
for index, feature in enumerate(solution["core_features"], 1):
|
||||
feature_lines.append(
|
||||
f"### {index}. {text(feature.get('name'))}\n\n"
|
||||
f"- 解决的问题:{text(feature.get('problem'))}\n"
|
||||
f"- 输入:{text(feature.get('input'))}\n"
|
||||
f"- 系统处理:{text(feature.get('processing'))}\n"
|
||||
f"- 输出:{text(feature.get('output'))}\n"
|
||||
f"- 当前状态:{text(feature.get('runnable_status'))}\n"
|
||||
f"- 证据位置:{text(feature.get('evidence_location'))}"
|
||||
)
|
||||
step_lines = []
|
||||
for index, step in enumerate(demo["experience_steps"], 1):
|
||||
step_lines.append(f"{index}. {text(step.get('action'))}\n - 预期结果:{text(step.get('expected_result'))}")
|
||||
timecodes = video["timecodes"]
|
||||
product_demo = f"""# 产品与 Demo
|
||||
|
||||
## 解决方案
|
||||
|
||||
{text(solution['summary'])}
|
||||
|
||||
## 用户价值
|
||||
|
||||
{text(solution['user_value'])}
|
||||
|
||||
## 用户流程
|
||||
|
||||
{bullets(solution['user_flow'])}
|
||||
|
||||
## 核心功能
|
||||
|
||||
{chr(10).join(feature_lines) or '未提供'}
|
||||
|
||||
## Demo
|
||||
|
||||
- Demo链接:{text(demo['url'])}
|
||||
- 访问方式:{text(demo['access_method'])}
|
||||
- 推荐环境:{text(demo['recommended_environment'])}
|
||||
- 预计体验时长:{text(demo['expected_duration_minutes'])} 分钟
|
||||
- 备用下载链接:{text(demo['fallback_download_url'])}
|
||||
- 已发布渠道:{text(demo['published_channel'])}
|
||||
- 已发布产品名称:{text(demo['published_product_name'])}
|
||||
- 技术支持联系人:{text(demo['support_contact'])}
|
||||
- 受限访问:{text(demo['access_restricted'])}
|
||||
- 测试账号文件:{text(demo['private_access_file'])}
|
||||
|
||||
### 体验步骤
|
||||
|
||||
{chr(10).join(step_lines) or '未提供'}
|
||||
|
||||
### 已知问题
|
||||
|
||||
{bullets(demo['known_issues'])}
|
||||
|
||||
## 演示视频
|
||||
|
||||
- 视频链接:{text(video['url'])}
|
||||
- 时长:{text(video['duration_minutes'])} 分钟
|
||||
- 包含 Demo 演示:{text(video['contents_confirmed']['demo'])}
|
||||
- 包含用户问题:{text(video['contents_confirmed']['user_problem'])}
|
||||
- 包含 Google 技术说明:{text(video['contents_confirmed']['google_technology'])}
|
||||
- 包含创新点说明:{text(video['contents_confirmed']['innovation'])}
|
||||
- 包含本地化说明:{text(video['contents_confirmed']['localization'])}
|
||||
- 用户问题:{text(timecodes['problem'])}
|
||||
- Demo:{text(timecodes['demo'])}
|
||||
- Google技术:{text(timecodes['google_technology'])}
|
||||
- 创新点:{text(timecodes['innovation'])}
|
||||
- 本地化:{text(timecodes['localization'])}
|
||||
"""
|
||||
google_lines = []
|
||||
for index, item in enumerate(data["google_technology"], 1):
|
||||
google_lines.append(
|
||||
f"## {index}. {text(item.get('name'))}\n\n"
|
||||
f"- 具体能力或模型:{text(item.get('capability'))}\n"
|
||||
f"- 对应功能:{text(item.get('feature'))}\n"
|
||||
f"- 输入:{text(item.get('input'))}\n"
|
||||
f"- 处理:{text(item.get('processing'))}\n"
|
||||
f"- 输出:{text(item.get('output'))}\n"
|
||||
f"- 核心程度:{text(item.get('role'))}\n"
|
||||
f"- 选型原因:{text(item.get('selection_reason'))}\n"
|
||||
f"- 移除后的影响:{text(item.get('removal_impact'))}\n"
|
||||
f"- 证据位置:{text(item.get('evidence_location'))}"
|
||||
)
|
||||
google_technology = f"""# Google 技术与实现证据
|
||||
|
||||
{chr(10).join(google_lines) or '未提供 Google 技术记录'}
|
||||
|
||||
## 技术材料
|
||||
|
||||
- 架构图:{text(tech['architecture_file'])}
|
||||
- 代码仓库:{text(tech['repository_url'])}
|
||||
- 评审版本:{text(tech['commit_id'])}
|
||||
- 部署说明:{text(tech['deployment_notes'])}
|
||||
- 保密限制:{text(tech['confidentiality_notes'])}
|
||||
|
||||
### 核心代码或证据位置
|
||||
|
||||
{bullets(tech['code_evidence_paths'])}
|
||||
|
||||
### 外部依赖
|
||||
|
||||
{bullets(tech['dependencies'])}
|
||||
"""
|
||||
market_localization = f"""# 出海市场、社会价值与本地化
|
||||
|
||||
## 出海落地
|
||||
|
||||
- 首个市场:{text(market['first_market'])}
|
||||
- 市场选择原因:{text(market['market_rationale'])}
|
||||
- 首批用户获取方式:{text(market['user_acquisition'])}
|
||||
- 使用条件:{text(market['usage_conditions'])}
|
||||
- 成本范围:{text(market['cost_range'])}
|
||||
- 六个月计划:{text(market['six_month_plan'])}
|
||||
|
||||
### 主要障碍
|
||||
|
||||
{bullets(market['barriers'])}
|
||||
|
||||
## 社会价值
|
||||
|
||||
{text(market['social_impact_path'])}
|
||||
|
||||
### 影响指标
|
||||
|
||||
{bullets(market['impact_metrics'])}
|
||||
|
||||
## 本地化
|
||||
|
||||
- 目标文化或地区:{text(loc['target_culture'])}
|
||||
- 是否经过当地用户测试:{text(loc['tested_with_local_users'])}
|
||||
- 测试人数:{text(loc['test_count'], '0')}
|
||||
- 测试后的修改:{text(loc['changes_after_testing'])}
|
||||
|
||||
### 当地习惯
|
||||
|
||||
{bullets(loc['habits'])}
|
||||
|
||||
### 用户顾虑
|
||||
|
||||
{bullets(loc['user_concerns'])}
|
||||
|
||||
### 非翻译型适配
|
||||
|
||||
{bullets(loc['adaptations'])}
|
||||
|
||||
### 本地化证据
|
||||
|
||||
{bullets(loc['evidence'])}
|
||||
"""
|
||||
innovation_lines = []
|
||||
for label, values in [("技术创新", data["innovation"]["technical"]), ("产品与体验创新", data["innovation"]["product_experience"])]:
|
||||
innovation_lines.append(f"## {label}")
|
||||
if not values:
|
||||
innovation_lines.append("\n未提供")
|
||||
for index, item in enumerate(values, 1):
|
||||
innovation_lines.append(
|
||||
f"\n### {index}. {text(item.get('title'))}\n\n"
|
||||
f"- 常见做法:{text(item.get('common_approach'))}\n"
|
||||
f"- 本作品的不同:{text(item.get('difference'))}\n"
|
||||
f"- 实际效果:{text(item.get('effect'))}\n"
|
||||
f"- 证据位置:{text(item.get('evidence_location'))}"
|
||||
)
|
||||
declarations = f"""# 原创性与合规声明
|
||||
|
||||
- 赛前已存在:{text(decl['existed_before_contest'])}
|
||||
- 比赛期间新增:{text(decl['new_work_during_contest'])}
|
||||
- 原创性确认:{text(decl['originality_confirmed'])}
|
||||
- 评委访问和测试授权:{text(decl['review_access_consent'])}
|
||||
|
||||
## 小型附件索引
|
||||
|
||||
{bullets(data['attachments'])}
|
||||
"""
|
||||
outputs = {
|
||||
"01-overview.md": overview,
|
||||
"02-user-problem.md": user_problem,
|
||||
"03-product-demo.md": product_demo,
|
||||
"04-google-technology.md": google_technology,
|
||||
"05-market-localization.md": market_localization,
|
||||
"06-innovation.md": "# 创新说明\n\n" + "\n".join(innovation_lines) + "\n",
|
||||
"07-declarations.md": declarations,
|
||||
}
|
||||
for filename, content in outputs.items():
|
||||
(folder / filename).write_text(content.strip() + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def validate(folder: Path, data: dict) -> dict:
|
||||
errors, warnings = [], []
|
||||
|
||||
def missing(value) -> bool:
|
||||
return value is None or value == "" or value == []
|
||||
|
||||
def warn_missing(value, label):
|
||||
if missing(value):
|
||||
warnings.append(f"missing scoring information: {label}")
|
||||
|
||||
if missing(data.get("schema_version")):
|
||||
errors.append("missing schema_version")
|
||||
if data.get("schema_version") != SCHEMA:
|
||||
errors.append(f"unsupported schema_version: {data.get('schema_version')}")
|
||||
if missing(data.get("submission_id")):
|
||||
errors.append("missing submission_id")
|
||||
|
||||
basic = data.get("basic", {})
|
||||
warn_missing(basic.get("contact", {}).get("wechat"), "微信号")
|
||||
if missing(basic.get("contestant_name_certificate")):
|
||||
errors.append("missing basic.contestant_name_certificate")
|
||||
if missing(basic.get("contact", {}).get("phone")):
|
||||
errors.append("missing basic.contact.phone")
|
||||
if not isinstance(basic.get("has_team"), bool):
|
||||
errors.append("basic.has_team must be answered true or false")
|
||||
elif basic.get("has_team") and missing(basic.get("team_member_names_certificate")):
|
||||
errors.append("missing basic.team_member_names_certificate for team entry")
|
||||
elif not basic.get("has_team") and basic.get("team_member_names_certificate"):
|
||||
warnings.append("inconsistent administrative information: 个人参赛不应填写团队成员证书姓名")
|
||||
warn_missing(basic.get("track"), "参赛赛道")
|
||||
if basic.get("track") and basic["track"] not in {"真", "善", "美"}:
|
||||
warnings.append("invalid scoring information: 参赛赛道应为真、善或美")
|
||||
warn_missing(basic.get("status"), "当前状态")
|
||||
warn_missing(basic.get("one_liner"), "一句话介绍")
|
||||
warn_missing(basic.get("contact", {}).get("email"), "邮箱")
|
||||
warn_missing(basic.get("target_markets"), "目标国家或地区")
|
||||
warn_missing(basic.get("xiaohongshu_url"), "小红书作品链接")
|
||||
if basic.get("xiaohongshu_url") and not valid_url(basic["xiaohongshu_url"]):
|
||||
warnings.append("invalid optional URL: 小红书作品链接")
|
||||
|
||||
contact = basic.get("contact", {})
|
||||
if not contact.get("email") and not contact.get("wechat"):
|
||||
warnings.append("missing administrative information: 邮箱或微信号")
|
||||
|
||||
problem = data.get("problem", {})
|
||||
warn_missing(problem.get("target_users"), "具体目标用户")
|
||||
warn_missing(problem.get("scenario"), "典型使用场景")
|
||||
warn_missing(problem.get("current_workaround"), "用户当前解决方式")
|
||||
warn_missing(problem.get("pain_points"), "核心用户痛点和现有方式不足")
|
||||
if not problem.get("evidence_summary") and not problem.get("evidence_links"):
|
||||
warnings.append("scoring evidence missing: 用户问题证据")
|
||||
|
||||
solution = data.get("solution", {})
|
||||
warn_missing(solution.get("summary"), "解决方案概述")
|
||||
warn_missing(solution.get("user_value"), "用户价值")
|
||||
warn_missing(solution.get("user_flow"), "完整用户流程")
|
||||
warn_missing(solution.get("core_features"), "核心功能")
|
||||
|
||||
demo = data.get("demo", {})
|
||||
warn_missing(demo.get("url"), "Demo或可运行文件链接")
|
||||
if demo.get("url") and not valid_url(demo["url"]):
|
||||
warnings.append("invalid optional URL: Demo链接")
|
||||
if demo.get("published_channel") and not demo.get("published_product_name"):
|
||||
warnings.append("incomplete scoring information: 已发布渠道缺少产品名称")
|
||||
if demo.get("published_product_name") and not demo.get("published_channel"):
|
||||
warnings.append("incomplete scoring information: 已发布产品缺少发布渠道")
|
||||
warn_missing(demo.get("access_method"), "Demo访问方式")
|
||||
warn_missing(demo.get("recommended_environment"), "推荐运行环境")
|
||||
warn_missing(demo.get("expected_duration_minutes"), "预计体验时间")
|
||||
steps = demo.get("experience_steps", [])
|
||||
if not steps:
|
||||
warnings.append("missing scoring information: Demo体验步骤")
|
||||
elif not 3 <= len(steps) <= 5:
|
||||
warnings.append("incomplete scoring information: Demo体验步骤建议为3至5步")
|
||||
for index, step in enumerate(steps, 1):
|
||||
if missing(step.get("action")):
|
||||
warnings.append(f"incomplete scoring information: Demo第{index}步缺少操作")
|
||||
if missing(step.get("expected_result")):
|
||||
warnings.append(f"incomplete scoring information: Demo第{index}步缺少预期结果")
|
||||
if not demo.get("availability_confirmed"):
|
||||
warnings.append("missing scoring information: Demo评审期可用确认")
|
||||
if demo.get("access_restricted") and not (folder / demo.get("private_access_file", "")).is_file():
|
||||
warnings.append("restricted Demo has no private test-access file")
|
||||
if not demo.get("fallback_download_url"):
|
||||
warnings.append("missing scoring information: 备用Demo或下载链接")
|
||||
elif not valid_url(demo["fallback_download_url"]):
|
||||
warnings.append("invalid optional URL: 备用Demo或下载链接")
|
||||
|
||||
video = data.get("video", {})
|
||||
warn_missing(video.get("url"), "2至5分钟演示视频链接")
|
||||
if video.get("url") and not valid_url(video["url"]):
|
||||
warnings.append("invalid optional URL: 演示视频链接")
|
||||
duration = video.get("duration_minutes")
|
||||
if duration is None:
|
||||
warnings.append("missing scoring information: 演示视频时长")
|
||||
elif not 2 <= duration <= 5:
|
||||
warnings.append("incomplete scoring information: 演示视频应为2至5分钟")
|
||||
video_contents = video.get("contents_confirmed", {})
|
||||
for key, label in [("demo", "Demo演示"), ("user_problem", "用户问题"), ("google_technology", "Google技术说明"), ("innovation", "创新点说明"), ("localization", "本地化说明")]:
|
||||
if not video_contents.get(key):
|
||||
warnings.append(f"missing scoring information: 视频未确认包含{label}")
|
||||
|
||||
google_items = data.get("google_technology", [])
|
||||
if not google_items:
|
||||
warnings.append("missing scoring information: Google技术应用")
|
||||
required_google = ["name", "capability", "feature", "input", "processing", "output", "role", "selection_reason", "removal_impact", "evidence_location"]
|
||||
for index, item in enumerate(google_items, 1):
|
||||
for key in required_google:
|
||||
if missing(item.get(key)):
|
||||
warnings.append(f"incomplete scoring information: Google技术第{index}项缺少{key}")
|
||||
if item.get("role") and item["role"] not in {"核心", "支撑"}:
|
||||
warnings.append(f"invalid scoring information: Google技术第{index}项角色应为核心或支撑")
|
||||
|
||||
innovation = data.get("innovation", {})
|
||||
warn_missing(innovation.get("technical"), "技术创新")
|
||||
warn_missing(innovation.get("product_experience"), "产品与体验创新")
|
||||
|
||||
market = data.get("market", {})
|
||||
warn_missing(market.get("first_market"), "首个计划进入的市场")
|
||||
warn_missing(market.get("market_rationale"), "市场选择原因")
|
||||
warn_missing(market.get("user_acquisition"), "首批用户获取方式")
|
||||
warn_missing(market.get("usage_conditions"), "用户使用条件")
|
||||
warn_missing(market.get("barriers"), "主要落地障碍")
|
||||
|
||||
localization = data.get("localization", {})
|
||||
warn_missing(localization.get("target_culture"), "目标文化或地区")
|
||||
warn_missing(localization.get("habits"), "当地用户习惯")
|
||||
warn_missing(localization.get("user_concerns"), "当地用户情绪、顾虑或限制")
|
||||
warn_missing(localization.get("adaptations"), "语言翻译之外的产品适配")
|
||||
if not localization.get("tested_with_local_users"):
|
||||
warnings.append("scoring evidence missing: 未经过当地用户测试")
|
||||
if not localization.get("evidence"):
|
||||
warnings.append("scoring evidence missing: 本地化研究或测试证据")
|
||||
|
||||
declarations = data.get("declarations", {})
|
||||
if not isinstance(declarations.get("existed_before_contest"), bool):
|
||||
errors.append("declarations.existed_before_contest must be answered true or false")
|
||||
if missing(declarations.get("new_work_during_contest")):
|
||||
errors.append("missing declarations.new_work_during_contest")
|
||||
if declarations.get("originality_confirmed") is not True:
|
||||
errors.append("declarations.originality_confirmed must be true")
|
||||
if declarations.get("review_access_consent") is not True:
|
||||
errors.append("declarations.review_access_consent must be true")
|
||||
|
||||
for path in folder.rglob("*"):
|
||||
if path.is_file() and path.stat().st_size > 10 * 1024 * 1024:
|
||||
warnings.append(f"large local file should be a cloud link: {path.relative_to(folder)}")
|
||||
|
||||
shipping_path = folder / "private/09-gift-shipping.json"
|
||||
if not shipping_path.is_file():
|
||||
warnings.append("missing administrative information: 礼品邮寄信息文件")
|
||||
else:
|
||||
try:
|
||||
shipping = json.loads(shipping_path.read_text(encoding="utf-8"))
|
||||
for key, label in [("recipient_name", "礼品收件人姓名"), ("phone", "礼品收件电话"), ("address", "礼品邮寄地址")]:
|
||||
if missing(shipping.get(key)):
|
||||
warnings.append(f"missing administrative information: {label}")
|
||||
except json.JSONDecodeError:
|
||||
warnings.append("invalid administrative information: 礼品邮寄信息文件不是有效JSON")
|
||||
return {"schema_version": SCHEMA, "submission_id": data.get("submission_id"), "valid": not errors, "errors": errors, "warnings": warnings}
|
||||
|
||||
|
||||
def command_init(args) -> int:
|
||||
if not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]{1,63}", args.id):
|
||||
raise ValueError("submission ID must be 2-64 filesystem-safe characters")
|
||||
folder = Path(args.root).expanduser().resolve() / args.id
|
||||
if folder.exists() and any(folder.iterdir()):
|
||||
raise ValueError(f"refusing to overwrite non-empty folder: {folder}")
|
||||
folder.mkdir(parents=True, exist_ok=True)
|
||||
for subdir in ["private", "evidence/user-research", "evidence/technical", "evidence/localization"]:
|
||||
(folder / subdir).mkdir(parents=True, exist_ok=True)
|
||||
data = blank_submission(args.id, args.title)
|
||||
(folder / "00-submission.json").write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
(folder / "private/08-test-access.md").write_text(
|
||||
"# 评审测试账号(敏感)\n\n"
|
||||
"> 仅填写专用临时测试账号。禁止填写 API Key、个人密码、生产凭据或真实用户数据。\n\n"
|
||||
"- Demo:\n- 测试账号:\n- 临时密码:\n- 有效期:\n- 额外说明:\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(folder / "private/09-gift-shipping.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"schema_version": "vibe-a-thon-gift-shipping/v1",
|
||||
"submission_id": args.id,
|
||||
"recipient_name": "",
|
||||
"phone": "",
|
||||
"address": "",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
render(folder, data)
|
||||
print(folder)
|
||||
return 0
|
||||
|
||||
|
||||
def command_render(args) -> int:
|
||||
folder = Path(args.folder).expanduser().resolve()
|
||||
data = load_submission(folder)
|
||||
data["updated_at"] = now_iso()
|
||||
(folder / "00-submission.json").write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
render(folder, data)
|
||||
print(f"rendered {folder}")
|
||||
return 0
|
||||
|
||||
|
||||
def command_validate(args) -> int:
|
||||
folder = Path(args.folder).expanduser().resolve()
|
||||
result = validate(folder, load_submission(folder))
|
||||
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||
return 0 if result["valid"] else 1
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
subparsers = parser.add_subparsers(dest="command", required=True)
|
||||
init_parser = subparsers.add_parser("init")
|
||||
init_parser.add_argument("--root", required=True)
|
||||
init_parser.add_argument("--id", required=True)
|
||||
init_parser.add_argument("--title", default="")
|
||||
init_parser.set_defaults(func=command_init)
|
||||
render_parser = subparsers.add_parser("render")
|
||||
render_parser.add_argument("folder")
|
||||
render_parser.set_defaults(func=command_render)
|
||||
validate_parser = subparsers.add_parser("validate")
|
||||
validate_parser.add_argument("folder")
|
||||
validate_parser.set_defaults(func=command_validate)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
return args.func(args)
|
||||
except (ValueError, KeyError, json.JSONDecodeError) as exc:
|
||||
print(f"ERROR: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user