diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..63ce98d
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,39 @@
+# Django project
+/media/
+/static/
+*.sqlite3
+
+# Python and others
+__pycache__
+*.pyc
+.DS_Store
+*.swp
+/venv/
+/tmp/
+/.vagrant/
+/Vagrantfile.local
+node_modules/
+/npm-debug.log
+/.idea/
+.vscode
+coverage
+.python-version
+
+# Distribution / packaging
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
diff --git a/core/__init__.py b/core/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/core/admin.py b/core/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/core/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/core/apps.py b/core/apps.py
new file mode 100644
index 0000000..26f78a8
--- /dev/null
+++ b/core/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class CoreConfig(AppConfig):
+ name = 'core'
diff --git a/core/migrations/__init__.py b/core/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/core/models.py b/core/models.py
new file mode 100644
index 0000000..71a8362
--- /dev/null
+++ b/core/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/core/tests.py b/core/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/core/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/core/views.py b/core/views.py
new file mode 100644
index 0000000..91ea44a
--- /dev/null
+++ b/core/views.py
@@ -0,0 +1,3 @@
+from django.shortcuts import render
+
+# Create your views here.
diff --git a/docs/03-tech-stack.md b/docs/03-tech-stack.md
index 8e8f251..fe89cf6 100644
--- a/docs/03-tech-stack.md
+++ b/docs/03-tech-stack.md
@@ -8,7 +8,7 @@
| --- | --- | --- | --- |
| CMS / Web 框架 | Wagtail + Django | 已定 | 内容模型、后台、页面、SEO、搜索和后续扩展都适合目录评测站。 |
| Python | Python 3.12 | 已定 | Wagtail 当前支持 Python 3.12;部署环境更稳。 |
-| 版本钉死 | `requirements.txt` 按实际安装版本 pin | T-001 落实 | 初始化时安装 pip 当前稳定版 Wagtail,用 `pip show wagtail` 确认版本后写成 `wagtail==X.Y.Z`,并回写本表。 |
+| 版本钉死 | `requirements.txt` 按实际安装版本 pin | 已落实 | Wagtail 7.4.2 / Django 6.0.6,已 pin 全部依赖版本。 |
| 数据库 | SQLite with JSON1 | 已定 | 开发和第一版上线成本低;MVP 写入少。 |
| 后续数据库 | PostgreSQL | 条件触发 | 出现用户写入、高并发、后台多人编辑、锁冲突或会员功能后迁移。 |
| 前端模板 | Django Templates / Wagtail Templates | 已定 | 第一版以内容站为主,避免引入前后端分离复杂度。 |
@@ -17,7 +17,7 @@
| 鉴权方式 | Wagtail Admin Session | 已定 | 第一版只有后台编辑账号。 |
| 媒体存储 | 本地 media 目录 | 已定 | 2 核 2G VPS 第一版足够;后续可迁移 S3/R2/OSS。 |
| 部署方式 | 单 VPS,Gunicorn + Nginx | 待实现 | 面向 2 核 2G VPS;先不引入 Docker 作为必需项。 |
-| 测试 | Django test / pytest 待定 | 待定 | T-001 初始化后以项目实际生成结构为准。 |
+| 测试 | Django test | 已定 | T-001 初始化后以各 app `tests.py` 为准。 |
| 开发环境 | WSL2 / Linux + bash | 已定 | 仓库根目录 `/mnt/d/opc_project/skelet`;`init.sh` 为标准入口,`init.ps1` 可选。 |
| 访问统计 | Plausible / Umami 或等价轻量方案 | 待实现 | 上线前接入(任务 T-305);自然搜索和外链点击是 M3/M4 商业验证的前置数据。 |
@@ -32,9 +32,7 @@
## 三、构建与运行命令
-生产代码尚未初始化。完成 `T-001` 后必须把本节替换为真实命令。
-
-目标命令形态:
+已完成初始化,以下为真实可用命令:
| 用途 | 命令 |
| --- | --- |
diff --git a/docs/04-architecture.md b/docs/04-architecture.md
index 46b8c46..3f62444 100644
--- a/docs/04-architecture.md
+++ b/docs/04-architecture.md
@@ -216,9 +216,9 @@ skelet/
└── README.md
```
-**结构决策**:保留 `wagtail start` 生成的 `home` 和 `search` app,不重命名(手改默认迁移容易出错);业务模型全部放在新建的 `core` app;测试放各 app 的 `tests.py`,暂不建独立 `tests/` 目录;生成的 `Dockerfile` 删除(第一版不用 Docker,见 `03-tech-stack.md`)。
+**结构决策**:保留 `wagtail start` 生成的 `home` 和 `search` app;业务模型全部放在新建的 `core` app;测试放各 app 的 `tests.py`,暂不建独立 `tests/` 目录;`Dockerfile` 已删除。
-实际路径以初始化后的 Wagtail 项目为准;初始化完成后必须更新本文和 `current-state.md`。
+已初始化:Wagtail 7.4.2 + Django 6.0.6,SQLite 可用,superuser `admin` 已创建。
## 七、架构纪律
diff --git a/docs/06-tasks.md b/docs/06-tasks.md
index 8e7a7bb..2be7e96 100644
--- a/docs/06-tasks.md
+++ b/docs/06-tasks.md
@@ -22,7 +22,7 @@
| ID | 任务 | 依赖 | 验收要点 | 状态 |
| --- | --- | --- | --- | --- |
| T-000 | 初始化 harness 文档 | - | 根目录入口、需求、技术栈、架构、规则、任务和当前状态文档已建立 | DONE |
-| T-001 | 初始化 Wagtail 项目骨架 | T-000 | Python 3.12 虚拟环境可用;Wagtail/Django 项目生成;SQLite 配置可 migrate;首页或默认 Wagtail 页面可访问;`init.ps1` / `init.sh` 替换为真实命令 | TODO |
+| T-001 | 初始化 Wagtail 项目骨架 | T-000 | Python 3.12 虚拟环境可用;Wagtail/Django 项目生成;SQLite 配置可 migrate;首页或默认 Wagtail 页面可访问;`init.ps1` / `init.sh` 替换为真实命令 | DONE |
| T-002 | 建立基础配置与环境样例 | T-001 | 存在 `requirements.txt`、`.env.example` 或等价配置说明;不包含真实密钥;`DEBUG`、`ALLOWED_HOSTS`、media/static 配置清楚 | TODO |
| T-003 | 建立最小验证基线 | T-001 | `manage.py check` 和基础测试命令可运行;验证结果写入 `progress.md` | TODO |
diff --git a/docs/current-state.md b/docs/current-state.md
index d31071e..19dfa85 100644
--- a/docs/current-state.md
+++ b/docs/current-state.md
@@ -11,16 +11,16 @@
## 当前快照
- 日期:2026-07-06
-- 阶段:MVP 起步前,harness 文档已初始化并完成瘦身(元文档合并为 `90-harness-reference.md`,入口统一为 `AGENTS.md`);任务看板已加固(任务详单、验证命令、筛选参数契约、字段必填/默认附注)
-- 开发环境:WSL2 / Linux,仓库根目录 `/mnt/d/opc_project/skelet`,bash 为标准命令形态;Python 命令统一使用 `python3.12`(系统已安装 3.12.12)
-- git:已初始化,主分支 `main`;`.gitignore` 已生效(`deploy/` 及凭证类文件不入库),`.gitattributes` 统一 LF 行尾
-- 技术栈:目标为 Wagtail + Django + Python 3.12 + SQLite;第一版英文单语言站点
-- 生产代码:尚未初始化
-- 测试:尚未初始化
-- 数据:尚未建立 seed 数据;未来通过 Wagtail 后台录入首批项目
-- 标准启动路径:`init.sh` 尚未配置真实命令(`init.ps1` 为可选辅助)
-- 标准验证路径:生产代码初始化后补齐
-- 当前 blocker:未初始化 Wagtail 项目骨架;下一步执行 `T-001`
+- 阶段:T-001 已完成,Wagtail 7.4.2 + Django 6.0.6 项目已初始化;生产代码可运行
+- 开发环境:MSYS2/MinGW Python 3.12.12,venv 使用 `--system-site-packages`(Pillow 由 MSYS2 预编译包提供)
+- git:分支 `ds`;`.gitignore` 已生效(`.venv/`、`db.sqlite3`、`media/` 等被忽略)
+- 技术栈:Wagtail 7.4.2 + Django 6.0.6 + Python 3.12 + SQLite;第一版英文单语言站点
+- 生产代码:已初始化,`manage.py`、`skelet/`、`home/`、`search/`、`core/` 已建
+- 测试:尚未初始化(T-003 待建)
+- 数据:已创建 superuser `admin`;尚未建立 seed 数据
+- 标准启动路径:`init.sh` 已配置 `pip install` / `manage.py check` / `manage.py runserver` 命令
+- 标准验证路径:`.venv/bin/python manage.py check` 通过(3 个 treebeard 兼容警告非阻塞)
+- 当前 blocker:无;下一步执行 `T-002`
## 当前目录要点
@@ -34,31 +34,24 @@
| `deploy/` | 已有,不入库 | 本机部署凭证(SSH 私钥等),被 `.gitignore` 整目录忽略。 |
| `.gitignore` / `.gitattributes` | 已有 | 忽略凭证与运行产物;统一 LF 行尾。 |
| `init.ps1` / `init.sh` | 已有,占位 | T-001 初始化生产代码后替换真实命令。 |
-| `manage.py` | 待建 | Wagtail/Django 入口。 |
-| `requirements.txt` | 待建 | Python 依赖。 |
-| `skelet/` | 待建 | Django 配置包(`wagtail start` 生成 base/dev/production settings)。 |
-| `home/` / `search/` | 待建 | `wagtail start` 默认 app,保留不改名;`HomePage` 在 `home`。 |
-| `core/` | 待建 | 业务 app:Snippet、场景页、项目页、文章模型、模板和静态资源。 |
+| `manage.py` | 已有 | Wagtail/Django 入口。 |
+| `requirements.txt` | 已有 | Python 依赖,全部 pin 精确版本。 |
+| `skelet/` | 已有 | Django 配置包(`wagtail start` 生成 base/dev/production settings)。 |
+| `home/` / `search/` | 已有 | `wagtail start` 默认 app,保留不改名;`HomePage` 在 `home`。 |
+| `core/` | 已有 | 业务 app,已加入 `INSTALLED_APPS`;模型、模板、测试待建。 |
+| `.venv/` | 已有(不入库) | Python 3.12 虚拟环境,`--system-site-packages`(Pillow 12.0 来自 MSYS2)。 |
| 各 app `tests.py` | 待建 | smoke 与模型测试(T-003 起),暂不建独立 `tests/` 目录。 |
## 任务看板状态
-- 已完成:`T-000 初始化 harness 文档`。
+- 已完成:`T-000 初始化 harness 文档`、`T-001 初始化 Wagtail 项目骨架`。
- 正在进行:无。
-- 下一个可领取任务:`T-001 初始化 Wagtail 项目骨架`。
+- 下一个可领取任务:`T-002 建立基础配置与环境样例`。
## 当前可运行内容
```bash
-# 当前只能做文档检查
-git status
-find . -type f -not -path "./.git/*"
-```
-
-生产应用命令待 T-001 初始化后补齐:
-
-```bash
-# 目标形态
+# 标准开发命令(在 MSYS2 bash 中运行)
.venv/bin/python manage.py check
.venv/bin/python manage.py test
.venv/bin/python manage.py runserver
diff --git a/home/__init__.py b/home/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/home/apps.py b/home/apps.py
new file mode 100644
index 0000000..e7d1c7e
--- /dev/null
+++ b/home/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class HomeConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "home"
diff --git a/home/migrations/0001_initial.py b/home/migrations/0001_initial.py
new file mode 100644
index 0000000..67f201d
--- /dev/null
+++ b/home/migrations/0001_initial.py
@@ -0,0 +1,31 @@
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("wagtailcore", "0040_page_draft_title"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="HomePage",
+ fields=[
+ (
+ "page_ptr",
+ models.OneToOneField(
+ on_delete=models.CASCADE,
+ parent_link=True,
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ to="wagtailcore.Page",
+ ),
+ ),
+ ],
+ options={
+ "abstract": False,
+ },
+ bases=("wagtailcore.page",),
+ ),
+ ]
diff --git a/home/migrations/0002_create_homepage.py b/home/migrations/0002_create_homepage.py
new file mode 100644
index 0000000..a3e918e
--- /dev/null
+++ b/home/migrations/0002_create_homepage.py
@@ -0,0 +1,66 @@
+from django.db import migrations
+
+
+def create_homepage(apps, schema_editor):
+ # Get models
+ ContentType = apps.get_model("contenttypes.ContentType")
+ Page = apps.get_model("wagtailcore.Page")
+ Site = apps.get_model("wagtailcore.Site")
+ HomePage = apps.get_model("home.HomePage")
+
+ # Delete the default homepage (of type Page) as created by wagtailcore.0002_initial_data,
+ # if it exists
+ page_content_type = ContentType.objects.get(
+ model="page", app_label="wagtailcore"
+ )
+ Page.objects.filter(
+ content_type=page_content_type, slug="home", depth=2
+ ).delete()
+
+ # Create content type for homepage model
+ homepage_content_type, __ = ContentType.objects.get_or_create(
+ model="homepage", app_label="home"
+ )
+
+ # Create a new homepage
+ homepage = HomePage.objects.create(
+ title="Home",
+ draft_title="Home",
+ slug="home",
+ content_type=homepage_content_type,
+ path="00010001",
+ depth=2,
+ numchild=0,
+ url_path="/home/",
+ )
+
+ # Create a site with the new homepage set as the root
+ Site.objects.create(hostname="localhost", root_page=homepage, is_default_site=True)
+
+
+def remove_homepage(apps, schema_editor):
+ # Get models
+ ContentType = apps.get_model("contenttypes.ContentType")
+ HomePage = apps.get_model("home.HomePage")
+
+ # Delete the default homepage
+ # Page and Site objects CASCADE
+ HomePage.objects.filter(slug="home", depth=2).delete()
+
+ # Delete content type for homepage model
+ ContentType.objects.filter(model="homepage", app_label="home").delete()
+
+
+class Migration(migrations.Migration):
+
+ run_before = [
+ ("wagtailcore", "0053_locale_model"),
+ ]
+
+ dependencies = [
+ ("home", "0001_initial"),
+ ]
+
+ operations = [
+ migrations.RunPython(create_homepage, remove_homepage),
+ ]
diff --git a/home/migrations/__init__.py b/home/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/home/models.py b/home/models.py
new file mode 100644
index 0000000..5076f57
--- /dev/null
+++ b/home/models.py
@@ -0,0 +1,7 @@
+from django.db import models
+
+from wagtail.models import Page
+
+
+class HomePage(Page):
+ pass
diff --git a/home/static/css/welcome_page.css b/home/static/css/welcome_page.css
new file mode 100644
index 0000000..bad2933
--- /dev/null
+++ b/home/static/css/welcome_page.css
@@ -0,0 +1,184 @@
+html {
+ box-sizing: border-box;
+}
+
+*,
+*:before,
+*:after {
+ box-sizing: inherit;
+}
+
+body {
+ max-width: 960px;
+ min-height: 100vh;
+ margin: 0 auto;
+ padding: 0 15px;
+ color: #231f20;
+ font-family: 'Helvetica Neue', 'Segoe UI', Arial, sans-serif;
+ line-height: 1.25;
+}
+
+a {
+ background-color: transparent;
+ color: #308282;
+ text-decoration: underline;
+}
+
+a:hover {
+ color: #ea1b10;
+}
+
+h1,
+h2,
+h3,
+h4,
+h5,
+p,
+ul {
+ padding: 0;
+ margin: 0;
+ font-weight: 400;
+}
+
+svg:not(:root) {
+ overflow: hidden;
+}
+
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding-top: 20px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid #e6e6e6;
+}
+
+.logo {
+ width: 150px;
+ margin-inline-end: 20px;
+}
+
+.logo a {
+ display: block;
+}
+
+.figure-logo {
+ max-width: 150px;
+ max-height: 55.1px;
+}
+
+.release-notes {
+ font-size: 14px;
+}
+
+.main {
+ padding: 40px 0;
+ margin: 0 auto;
+ text-align: center;
+}
+
+.figure-space {
+ max-width: 265px;
+}
+
+@keyframes pos {
+ 0%, 100% {
+ transform: rotate(-6deg);
+ }
+ 50% {
+ transform: rotate(6deg);
+ }
+}
+
+.egg {
+ fill: #43b1b0;
+ animation: pos 3s ease infinite;
+ transform: translateY(50px);
+ transform-origin: 50% 80%;
+}
+
+.main-text {
+ max-width: 400px;
+ margin: 5px auto;
+}
+
+.main-text h1 {
+ font-size: 22px;
+}
+
+.main-text p {
+ margin: 15px auto 0;
+}
+
+.footer {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ border-top: 1px solid #e6e6e6;
+ padding: 10px;
+}
+
+.option {
+ display: block;
+ padding: 10px 10px 10px 34px;
+ position: relative;
+ text-decoration: none;
+}
+
+.option svg {
+ width: 24px;
+ height: 24px;
+ fill: gray;
+ border: 1px solid #d9d9d9;
+ padding: 5px;
+ border-radius: 100%;
+ top: 10px;
+ inset-inline-start: 0;
+ position: absolute;
+}
+
+.option h2 {
+ font-size: 19px;
+ text-decoration: underline;
+}
+
+.option p {
+ padding-top: 3px;
+ color: #231f20;
+ font-size: 15px;
+ font-weight: 300;
+}
+
+@media (max-width: 996px) {
+ body {
+ max-width: 780px;
+ }
+}
+
+@media (max-width: 767px) {
+ .option {
+ flex: 0 0 50%;
+ }
+}
+
+@media (max-width: 599px) {
+ .main {
+ padding: 20px 0;
+ }
+
+ .figure-space {
+ max-width: 200px;
+ }
+
+ .footer {
+ display: block;
+ width: 300px;
+ margin: 0 auto;
+ }
+}
+
+@media (max-width: 360px) {
+ .header-link {
+ max-width: 100px;
+ }
+}
diff --git a/home/templates/home/home_page.html b/home/templates/home/home_page.html
new file mode 100644
index 0000000..db9e9b0
--- /dev/null
+++ b/home/templates/home/home_page.html
@@ -0,0 +1,21 @@
+{% extends "base.html" %}
+{% load static %}
+
+{% block body_class %}template-homepage{% endblock %}
+
+{% block extra_css %}
+
+{% comment %}
+Delete the line below if you're just getting started and want to remove the welcome screen!
+{% endcomment %}
+
+{% endblock extra_css %}
+
+{% block content %}
+
+{% comment %}
+Delete the line below if you're just getting started and want to remove the welcome screen!
+{% endcomment %}
+{% include 'home/welcome_page.html' %}
+
+{% endblock content %}
diff --git a/home/templates/home/welcome_page.html b/home/templates/home/welcome_page.html
new file mode 100644
index 0000000..dcacaf3
--- /dev/null
+++ b/home/templates/home/welcome_page.html
@@ -0,0 +1,52 @@
+{% load i18n wagtailcore_tags %}
+
+
+
+
+
+
{% trans "Welcome to your new Wagtail site!" %}
+
{% trans 'Please feel free to join our community on Slack, or get started with one of the links below.' %}
+
+
+
diff --git a/home/tests.py b/home/tests.py
new file mode 100644
index 0000000..2a737de
--- /dev/null
+++ b/home/tests.py
@@ -0,0 +1,42 @@
+from home.models import HomePage
+
+from wagtail.models import Page, Site
+from wagtail.test.utils import WagtailPageTestCase
+
+
+class HomeSetUpTests(WagtailPageTestCase):
+ """
+ Tests for basic page structure setup and HomePage creation.
+ """
+
+ def test_root_create(self):
+ root_page = Page.objects.get(pk=1)
+ self.assertIsNotNone(root_page)
+
+ def test_homepage_create(self):
+ root_page = Page.objects.get(pk=1)
+ homepage = HomePage(title="Home")
+ root_page.add_child(instance=homepage)
+ self.assertTrue(HomePage.objects.filter(title="Home").exists())
+
+
+class HomeTests(WagtailPageTestCase):
+ """
+ Tests for homepage functionality and rendering.
+ """
+
+ def setUp(self):
+ """
+ Create a homepage instance for testing.
+ """
+ root_page = Page.get_first_root_node()
+ Site.objects.create(hostname="testsite", root_page=root_page, is_default_site=True)
+ self.homepage = HomePage(title="Home")
+ root_page.add_child(instance=self.homepage)
+
+ def test_homepage_is_renderable(self):
+ self.assertPageIsRenderable(self.homepage)
+
+ def test_homepage_template_used(self):
+ response = self.client.get(self.homepage.url)
+ self.assertTemplateUsed(response, "home/home_page.html")
diff --git a/init.ps1 b/init.ps1
index dd73d88..6e335de 100644
--- a/init.ps1
+++ b/init.ps1
@@ -4,32 +4,13 @@
# - Windows 原生 PowerShell:用本文件 ./init.ps1
# - WSL / Git Bash / macOS / Linux:用 ./init.sh
# 一条命令完成:依赖安装 -> 基础验证 -> 打印启动命令。
-# 复制到新项目后,必须先替换下面三个命令,让每轮会话用同一条路径启动,不靠记忆。
-# 本文件不绑定任何技术栈;换技术栈时只替换这三个命令,脚本结构不用动。
$ErrorActionPreference = "Stop"
Set-Location -Path $PSScriptRoot
-# 按你的项目实际情况替换这三个命令。未替换前脚本会主动失败。
-$InstallCmd = "__REPLACE_INSTALL_CMD__" # 依赖安装,如 uv sync / poetry install / npm install
-$VerifyCmd = "__REPLACE_VERIFY_CMD__" # 基础验证 / smoke,如 python -m pytest / go test ./...
-$StartCmd = "__REPLACE_START_CMD__" # 开发启动,如 uvicorn app:app --reload / npm run dev
-
-function Assert-Configured {
- param(
- [string]$Name,
- [string]$Value
- )
-
- if ($Value -like "__REPLACE_*") {
- Write-Error "请先在 init.ps1 中替换 $Name。同步更新 docs/03-tech-stack.md、docs/00-ai-start-here.md 和 docs/current-state.md 中的命令。"
- exit 2
- }
-}
-
-Assert-Configured -Name "InstallCmd" -Value $InstallCmd
-Assert-Configured -Name "VerifyCmd" -Value $VerifyCmd
-Assert-Configured -Name "StartCmd" -Value $StartCmd
+$InstallCmd = ".venv/bin/pip install -r requirements.txt"
+$VerifyCmd = ".venv/bin/python manage.py check"
+$StartCmd = ".venv/bin/python manage.py runserver"
Write-Host "==> 当前目录: $($PWD.Path)"
diff --git a/init.sh b/init.sh
index 02a381d..fa8b456 100644
--- a/init.sh
+++ b/init.sh
@@ -4,32 +4,15 @@
# - WSL / Git Bash / macOS / Linux:用本文件 ./init.sh
# - Windows 原生 PowerShell:用 ./init.ps1
# 一条命令完成:依赖安装 -> 基础验证 -> 打印启动命令。
-# 复制到新项目后,必须先替换下面三个变量,让每轮会话用同一条路径启动,不靠记忆。
-# 本文件不绑定任何技术栈;换技术栈时只替换这三个变量,脚本结构不用动。
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT_DIR"
-# 按你的项目实际情况替换这三个命令。未替换前脚本会主动失败。
-INSTALL_CMD=(__REPLACE_INSTALL_CMD__) # 依赖安装,如 uv sync、poetry install、npm install
-VERIFY_CMD=(__REPLACE_VERIFY_CMD__) # 基础验证 / smoke,如 python -m pytest、go test ./...
-START_CMD=(__REPLACE_START_CMD__) # 开发启动,如 uvicorn app:app --reload、npm run dev
-
-ensure_configured() {
- local name="$1"
- local first="$2"
- if [[ "$first" == __REPLACE_* ]]; then
- echo "ERROR: 请先在 init.sh 中替换 ${name}。"
- echo " 同步更新 docs/03-tech-stack.md、docs/00-ai-start-here.md 和 docs/current-state.md 中的命令。"
- exit 2
- fi
-}
-
-ensure_configured "INSTALL_CMD" "${INSTALL_CMD[0]}"
-ensure_configured "VERIFY_CMD" "${VERIFY_CMD[0]}"
-ensure_configured "START_CMD" "${START_CMD[0]}"
+INSTALL_CMD=(.venv/bin/pip install -r requirements.txt)
+VERIFY_CMD=(.venv/bin/python manage.py check)
+START_CMD=(.venv/bin/python manage.py runserver)
echo "==> 当前目录: $PWD"
diff --git a/manage.py b/manage.py
new file mode 100644
index 0000000..3d44a47
--- /dev/null
+++ b/manage.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python
+"""Django's command-line utility for administrative tasks."""
+import os
+import sys
+
+
+def main():
+ """Run administrative tasks."""
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "skelet.settings.dev")
+ try:
+ from django.core.management import execute_from_command_line
+ except ImportError as exc:
+ raise ImportError(
+ "Couldn't import Django. Are you sure it's installed and "
+ "available on your PYTHONPATH environment variable? Did you "
+ "forget to activate a virtual environment?"
+ ) from exc
+ execute_from_command_line(sys.argv)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/progress.md b/progress.md
index ec96fe6..b891729 100644
--- a/progress.md
+++ b/progress.md
@@ -101,3 +101,26 @@
- 决策:开发统一使用 `python3.12` 命令(系统已安装 3.12.12),不依赖 `python3` 别名(指向 3.8.10)。
- 下一步:T-001 初始化 Wagtail 项目骨架。
+## 2026-07-06 T-001 初始化 Wagtail 项目骨架
+
+- 状态:DONE
+- 变更:
+ - 创建 Python 3.12 venv(`--system-site-packages`,Pillow 由 MSYS2 `mingw-w64-x86_64-python-pillow` 提供)
+ - 安装 Wagtail 7.4.2 + Django 6.0.6 + 全部依赖(通过 MSYS2 bash 环境)
+ - `wagtail start skelet .` 生成项目骨架(`manage.py`、`skelet/`、`home/`、`search/`)
+ - `manage.py startapp core` 并加入 `INSTALLED_APPS`
+ - 删除 `Dockerfile`;`requirements.txt` 替换为 `pip freeze` 精确版本
+ - `manage.py migrate` 成功;`createsuperuser`(admin/admin123)
+ - `init.sh` / `init.ps1` 替换为真实命令(`pip install`、`manage.py check`、`manage.py runserver`)
+ - 回写 `03-tech-stack.md`(版本钉死状态、测试方案、§三命令)、`04-architecture.md` §六(结构决策、已初始化标记)、`current-state.md`(快照、目录表、任务看板状态、可运行命令)、`06-tasks.md`(T-001→DONE)
+- 验证:
+ - `manage.py check`:0 errors, 3 treebeard 兼容警告(非阻塞)
+ - `curl http://127.0.0.1:8000/`:HTTP 200
+ - `curl http://127.0.0.1:8000/admin/login/`:HTTP 200
+- 阻塞:无。
+- 决策:
+ - Python 3.12 来自 MSYS2/MinGW(`C:\msys64\mingw64\bin\python3.12.exe`),创建 Unix 风格 venv(`bin/` 非 `Scripts/`)
+ - 因 MinGW 工具链版本冲突(GCC 8.1 vs Python 3.12),采用 `--system-site-packages` 复用 MSYS2 预编译 Pillow 12.0,非纯 Python 依赖(pillow-heif)在 MSYS2 bash 中编译
+ - 开发命令需在 MSYS2 bash shell 中运行;`init.ps1` 在 PowerShell 中直接调用 `.venv/bin/` 路径可工作
+- 下一步:T-002 建立基础配置与环境样例。
+
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..c01f82d
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,34 @@
+Django==6.0.6
+wagtail==7.4.2
+anyascii==0.3.3
+asgiref==3.11.1
+beautifulsoup4==4.15.0
+certifi==2026.6.17
+charset-normalizer==3.4.7
+defusedxml==0.7.1
+django-filter==25.2
+django-modelcluster==6.5
+django-permissionedforms==0.1
+django-stubs-ext==6.0.6
+django-taggit==6.1.0
+django-tasks==0.12.0
+django-treebeard==5.3.0
+djangorestframework==3.17.1
+draftjs_exporter==5.2.0
+et_xmlfile==2.0.0
+filetype==1.2.0
+idna==3.18
+laces==0.1.2
+modelsearch==1.3.1
+openpyxl==3.1.5
+packaging==25.0
+pillow==12.0.0
+pillow_heif==1.4.0
+requests==2.34.2
+soupsieve==2.8.4
+sqlparse==0.5.5
+telepath==0.3.1
+typing_extensions==4.16.0
+tzdata==2026.2
+urllib3==2.7.0
+Willow==1.12.0
diff --git a/search/__init__.py b/search/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/search/templates/search/search.html b/search/templates/search/search.html
new file mode 100644
index 0000000..476427f
--- /dev/null
+++ b/search/templates/search/search.html
@@ -0,0 +1,38 @@
+{% extends "base.html" %}
+{% load static wagtailcore_tags %}
+
+{% block body_class %}template-searchresults{% endblock %}
+
+{% block title %}Search{% endblock %}
+
+{% block content %}
+
Search
+
+
+
+{% if search_results %}
+
+ {% for result in search_results %}
+ -
+
+ {% if result.search_description %}
+ {{ result.search_description }}
+ {% endif %}
+
+ {% endfor %}
+
+
+{% if search_results.has_previous %}
+Previous
+{% endif %}
+
+{% if search_results.has_next %}
+Next
+{% endif %}
+{% elif search_query %}
+No results found
+{% endif %}
+{% endblock %}
diff --git a/search/views.py b/search/views.py
new file mode 100644
index 0000000..678bb7e
--- /dev/null
+++ b/search/views.py
@@ -0,0 +1,46 @@
+from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
+from django.template.response import TemplateResponse
+
+from wagtail.models import Page
+
+# To enable logging of search queries for use with the "Promoted search results" module
+#
+# uncomment the following line and the lines indicated in the search function
+# (after adding wagtail.contrib.search_promotions to INSTALLED_APPS):
+
+# from wagtail.contrib.search_promotions.models import Query
+
+
+def search(request):
+ search_query = request.GET.get("query", None)
+ page = request.GET.get("page", 1)
+
+ # Search
+ if search_query:
+ search_results = Page.objects.live().search(search_query)
+
+ # To log this query for use with the "Promoted search results" module:
+
+ # query = Query.get(search_query)
+ # query.add_hit()
+
+ else:
+ search_results = Page.objects.none()
+
+ # Pagination
+ paginator = Paginator(search_results, 10)
+ try:
+ search_results = paginator.page(page)
+ except PageNotAnInteger:
+ search_results = paginator.page(1)
+ except EmptyPage:
+ search_results = paginator.page(paginator.num_pages)
+
+ return TemplateResponse(
+ request,
+ "search/search.html",
+ {
+ "search_query": search_query,
+ "search_results": search_results,
+ },
+ )
diff --git a/skelet/__init__.py b/skelet/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/skelet/settings/__init__.py b/skelet/settings/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/skelet/settings/base.py b/skelet/settings/base.py
new file mode 100644
index 0000000..aab4879
--- /dev/null
+++ b/skelet/settings/base.py
@@ -0,0 +1,185 @@
+"""
+Django settings for skelet project.
+
+Generated by 'django-admin startproject' using Django 6.0.6.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/6.0/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/6.0/ref/settings/
+"""
+
+# Build paths inside the project like this: BASE_DIR / 'subdir'.
+from pathlib import Path
+
+PROJECT_DIR = Path(__file__).resolve().parent.parent
+BASE_DIR = PROJECT_DIR.parent
+
+
+# Quick-start development settings - unsuitable for production
+# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
+
+
+# Application definition
+
+INSTALLED_APPS = [
+ "core",
+ "home",
+ "search",
+ "wagtail.contrib.forms",
+ "wagtail.contrib.redirects",
+ "wagtail.embeds",
+ "wagtail.sites",
+ "wagtail.users",
+ "wagtail.snippets",
+ "wagtail.documents",
+ "wagtail.images",
+ "wagtail.search",
+ "wagtail.admin",
+ "wagtail",
+ "modelcluster",
+ "taggit",
+ "django_filters",
+ "django.contrib.admin",
+ "django.contrib.auth",
+ "django.contrib.contenttypes",
+ "django.contrib.sessions",
+ "django.contrib.messages",
+ "django.contrib.staticfiles",
+]
+
+MIDDLEWARE = [
+ "django.middleware.security.SecurityMiddleware",
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "django.middleware.common.CommonMiddleware",
+ "django.middleware.csrf.CsrfViewMiddleware",
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
+ "django.contrib.messages.middleware.MessageMiddleware",
+ "django.middleware.clickjacking.XFrameOptionsMiddleware",
+ "wagtail.contrib.redirects.middleware.RedirectMiddleware",
+]
+
+ROOT_URLCONF = "skelet.urls"
+
+TEMPLATES = [
+ {
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
+ "DIRS": [
+ PROJECT_DIR / "templates",
+ ],
+ "APP_DIRS": True,
+ "OPTIONS": {
+ "context_processors": [
+ "django.template.context_processors.debug",
+ "django.template.context_processors.request",
+ "django.contrib.auth.context_processors.auth",
+ "django.contrib.messages.context_processors.messages",
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = "skelet.wsgi.application"
+
+
+# Database
+# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
+
+DATABASES = {
+ "default": {
+ "ENGINE": "django.db.backends.sqlite3",
+ "NAME": BASE_DIR / "db.sqlite3",
+ }
+}
+
+
+# Password validation
+# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
+ },
+ {
+ "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
+ },
+]
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/6.0/topics/i18n/
+
+LANGUAGE_CODE = "en-us"
+
+TIME_ZONE = "UTC"
+
+USE_I18N = True
+
+USE_TZ = True
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/6.0/howto/static-files/
+
+STATICFILES_FINDERS = [
+ "django.contrib.staticfiles.finders.FileSystemFinder",
+ "django.contrib.staticfiles.finders.AppDirectoriesFinder",
+]
+
+STATICFILES_DIRS = [
+ PROJECT_DIR / "static",
+]
+
+STATIC_ROOT = BASE_DIR / "static"
+STATIC_URL = "/static/"
+
+MEDIA_ROOT = BASE_DIR / "media"
+MEDIA_URL = "/media/"
+
+# Default storage settings
+# See https://docs.djangoproject.com/en/6.0/ref/settings/#std-setting-STORAGES
+STORAGES = {
+ "default": {
+ "BACKEND": "django.core.files.storage.FileSystemStorage",
+ },
+ "staticfiles": {
+ "BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
+ },
+}
+
+# Django sets a maximum of 1000 fields per form by default, but particularly complex page models
+# can exceed this limit within Wagtail's page editor.
+DATA_UPLOAD_MAX_NUMBER_FIELDS = 10_000
+
+
+# Wagtail settings
+
+WAGTAIL_SITE_NAME = "skelet"
+
+# Search
+# https://docs.wagtail.org/en/stable/topics/search/backends.html
+WAGTAILSEARCH_BACKENDS = {
+ "default": {
+ "BACKEND": "wagtail.search.backends.database",
+ }
+}
+
+# Base URL to use when referring to full URLs within the Wagtail admin backend -
+# e.g. in notification emails. Don't include '/admin' or a trailing slash
+WAGTAILADMIN_BASE_URL = "http://example.com"
+
+# Allowed file extensions for documents in the document library.
+# This can be omitted to allow all files, but note that this may present a security risk
+# if untrusted users are allowed to upload files -
+# see https://docs.wagtail.org/en/stable/advanced_topics/deploying.html#user-uploaded-files
+WAGTAILDOCS_EXTENSIONS = ['csv', 'docx', 'key', 'odt', 'pdf', 'pptx', 'rtf', 'txt', 'xlsx', 'zip']
+
+# Maximum upload size for documents in bytes.
+WAGTAILDOCS_MAX_UPLOAD_SIZE = 10 * 1024 * 1024 # 10MB
diff --git a/skelet/settings/dev.py b/skelet/settings/dev.py
new file mode 100644
index 0000000..e0b655d
--- /dev/null
+++ b/skelet/settings/dev.py
@@ -0,0 +1,18 @@
+from .base import *
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = True
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = "django-insecure-a5vf@_(tk2i0e*srydb+d3mnzz1)h23%+z5^53+gq8p#0vz+x-"
+
+# SECURITY WARNING: define the correct hosts in production!
+ALLOWED_HOSTS = ["*"]
+
+EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
+
+
+try:
+ from .local import *
+except ImportError:
+ pass
diff --git a/skelet/settings/production.py b/skelet/settings/production.py
new file mode 100644
index 0000000..936a45f
--- /dev/null
+++ b/skelet/settings/production.py
@@ -0,0 +1,14 @@
+from .base import *
+
+DEBUG = False
+
+# ManifestStaticFilesStorage is recommended in production, to prevent
+# outdated JavaScript / CSS assets being served from cache
+# (e.g. after a Wagtail upgrade).
+# See https://docs.djangoproject.com/en/6.0/ref/contrib/staticfiles/#manifeststaticfilesstorage
+STORAGES["staticfiles"]["BACKEND"] = "django.contrib.staticfiles.storage.ManifestStaticFilesStorage"
+
+try:
+ from .local import *
+except ImportError:
+ pass
diff --git a/skelet/static/css/skelet.css b/skelet/static/css/skelet.css
new file mode 100644
index 0000000..e69de29
diff --git a/skelet/static/js/skelet.js b/skelet/static/js/skelet.js
new file mode 100644
index 0000000..e69de29
diff --git a/skelet/templates/404.html b/skelet/templates/404.html
new file mode 100644
index 0000000..f19ab95
--- /dev/null
+++ b/skelet/templates/404.html
@@ -0,0 +1,11 @@
+{% extends "base.html" %}
+
+{% block title %}Page not found{% endblock %}
+
+{% block body_class %}template-404{% endblock %}
+
+{% block content %}
+Page not found
+
+Sorry, this page could not be found.
+{% endblock %}
diff --git a/skelet/templates/500.html b/skelet/templates/500.html
new file mode 100644
index 0000000..77379e5
--- /dev/null
+++ b/skelet/templates/500.html
@@ -0,0 +1,13 @@
+
+
+
+
+ Internal server error
+
+
+
+ Internal server error
+
+ Sorry, there seems to be an error. Please try again soon.
+
+
diff --git a/skelet/templates/base.html b/skelet/templates/base.html
new file mode 100644
index 0000000..7c5d20f
--- /dev/null
+++ b/skelet/templates/base.html
@@ -0,0 +1,46 @@
+{% load static wagtailcore_tags wagtailuserbar %}
+
+
+
+
+
+
+ {% block title %}
+ {% if page.seo_title %}{{ page.seo_title }}{% else %}{{ page.title }}{% endif %}
+ {% endblock %}
+ {% block title_suffix %}
+ {% wagtail_site as current_site %}
+ {% if current_site and current_site.site_name %}- {{ current_site.site_name }}{% endif %}
+ {% endblock %}
+
+ {% if page.search_description %}
+
+ {% endif %}
+
+
+ {# Force all links in the live preview panel to be opened in a new tab #}
+ {% if request.in_preview_panel %}
+
+ {% endif %}
+
+ {# Global stylesheets #}
+
+
+ {% block extra_css %}
+ {# Override this in templates to add extra stylesheets #}
+ {% endblock %}
+
+
+
+ {% wagtailuserbar %}
+
+ {% block content %}{% endblock %}
+
+ {# Global javascript #}
+
+
+ {% block extra_js %}
+ {# Override this in templates to add extra javascript #}
+ {% endblock %}
+
+
diff --git a/skelet/urls.py b/skelet/urls.py
new file mode 100644
index 0000000..c2e8a0c
--- /dev/null
+++ b/skelet/urls.py
@@ -0,0 +1,35 @@
+from django.conf import settings
+from django.urls import include, path
+from django.contrib import admin
+
+from wagtail.admin import urls as wagtailadmin_urls
+from wagtail import urls as wagtail_urls
+from wagtail.documents import urls as wagtaildocs_urls
+
+from search import views as search_views
+
+urlpatterns = [
+ path("django-admin/", admin.site.urls),
+ path("admin/", include(wagtailadmin_urls)),
+ path("documents/", include(wagtaildocs_urls)),
+ path("search/", search_views.search, name="search"),
+]
+
+
+if settings.DEBUG:
+ from django.conf.urls.static import static
+ from django.contrib.staticfiles.urls import staticfiles_urlpatterns
+
+ # Serve static and media files from development server
+ urlpatterns += staticfiles_urlpatterns()
+ urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
+
+urlpatterns = urlpatterns + [
+ # For anything not caught by a more specific rule above, hand over to
+ # Wagtail's page serving mechanism. This should be the last pattern in
+ # the list:
+ path("", include(wagtail_urls)),
+ # Alternatively, if you want Wagtail pages to be served from a subpath
+ # of your site, rather than the site root:
+ # path("pages/", include(wagtail_urls)),
+]
diff --git a/skelet/wsgi.py b/skelet/wsgi.py
new file mode 100644
index 0000000..bd1a9bf
--- /dev/null
+++ b/skelet/wsgi.py
@@ -0,0 +1,16 @@
+"""
+WSGI config for skelet project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
+"""
+
+import os
+
+from django.core.wsgi import get_wsgi_application
+
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "skelet.settings.dev")
+
+application = get_wsgi_application()