chore: 合并为单仓库,统一部署流程

- 根目录加入 docker-compose、Caddyfile、文档等原本不在仓库里的文件
- 前后端两份 GitHub Actions 合并成根目录的一个 deploy workflow:
  构建前端上传到 build/,再在服务器上 git pull 并重建容器

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-26 16:02:12 +08:00
1 parent 70983a12a5
commit 47d4679984
40 files changed
+13723 -34

No files matched your search

@@ -0,0 +1,267 @@
# Chain Source Indicator Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** In the ChainModal prompt chain, show a small tag on each round indicating whether it originated from AI dialogue or a manual submission.
**Architecture:** The `Message.source` field already exists on the backend ("conversation" or "manual") but is not serialized. We expose it through the existing `list_messages` endpoint, propagate it through the frontend type, and render a tag badge in `ChainModal.vue` alongside the existing L1–L6 level badge.
**Tech Stack:** Django-ninja (Python), Vue 3, TypeScript, Naive UI
---
## File Map
| File | Change |
|------|--------|
| `api/prompt/schemas.py` | Add `source: str` to `MessageOut` |
| `api/prompt/api.py` | Add `"source": m.source` to `list_messages` response |
| `client/src/utils/type.ts` | Add `source?: string` to `PromptMessage` |
| `client/src/components/submissions/ChainModal.vue` | Carry `source` through rounds, render tag |
---
### Task 1: Expose `source` from the backend messages endpoint
**Files:**
- Modify: `api/prompt/schemas.py`
- Modify: `api/prompt/api.py`
- [ ] **Step 1: Add `source` to `MessageOut` schema**
Open `api/prompt/schemas.py`. Change:
```python
class MessageOut(Schema):
id: int
role: str
content: str
code_html: Optional[str] = None
code_css: Optional[str] = None
code_js: Optional[str] = None
prompt_level: Optional[int] = None
created: str
```
to:
```python
class MessageOut(Schema):
id: int
role: str
source: str
content: str
code_html: Optional[str] = None
code_css: Optional[str] = None
code_js: Optional[str] = None
prompt_level: Optional[int] = None
created: str
```
- [ ] **Step 2: Include `source` in the `list_messages` response dict**
Open `api/prompt/api.py`. In `list_messages`, the return list comprehension currently builds dicts manually. Add `"source": m.source`:
```python
return [
{
"id": m.id,
"role": m.role,
"source": m.source,
"content": m.content,
"code_html": m.code_html,
"code_css": m.code_css,
"code_js": m.code_js,
"prompt_level": m.prompt_level,
"created": m.created.isoformat(),
}
for m in messages
]
```
- [ ] **Step 3: Verify the API returns `source` manually**
With the dev server running (`uv run manage.py runserver` from `api/`), open the Django-ninja interactive docs at `http://localhost:8000/api/docs` and call the messages endpoint. Confirm the response includes `"source": "conversation"` or `"source": "manual"` on each message object.
- [ ] **Step 4: Commit**
```bash
git add api/prompt/schemas.py api/prompt/api.py
git commit -m "feat: expose message source field in messages API"
```
---
### Task 2: Add `source` to the frontend type
**Files:**
- Modify: `client/src/utils/type.ts`
- [ ] **Step 1: Add `source` to `PromptMessage`**
Open `client/src/utils/type.ts`. Change:
```typescript
export interface PromptMessage {
id: number
role: string
content: string
code_html: string | null
code_css: string | null
code_js: string | null
prompt_level?: number | null
created: string
}
```
to:
```typescript
export interface PromptMessage {
id: number
role: string
source?: string
content: string
code_html: string | null
code_css: string | null
code_js: string | null
prompt_level?: number | null
created: string
}
```
- [ ] **Step 2: Commit**
```bash
git add client/src/utils/type.ts
git commit -m "feat: add source field to PromptMessage type"
```
---
### Task 3: Render source indicator in ChainModal
**Files:**
- Modify: `client/src/components/submissions/ChainModal.vue`
- [ ] **Step 1: Carry `source` through the rounds computed property**
In `ChainModal.vue`, the `rounds` computed property builds objects with `{ question, prompt_level, html, css, js }`. Add `source` from the user message:
```typescript
const rounds = computed(() => {
const result: {
question: string
source: string | null
prompt_level: number | null
html: string | null
css: string | null
js: string | null
}[] = []
for (const [i, msg] of messages.value.entries()) {
if (msg.role !== "user") continue
let html: string | null = null,
css: string | null = null,
js: string | null = null
for (const reply of messages.value.slice(i + 1)) {
if (reply.role === "user") break
if (reply.role === "assistant" && reply.code_html) {
html = reply.code_html
css = reply.code_css
js = reply.code_js
break
}
}
result.push({
question: msg.content,
source: msg.source ?? null,
prompt_level: msg.prompt_level ?? null,
html,
css,
js,
})
}
return result
})
```
- [ ] **Step 2: Add the source tag to the round card template**
Locate the `<div>` that renders the question and the L-level badge in the round card. It currently looks like:
```html
<div
style="
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 8px;
"
>
<span>{{ round.question }}</span>
<span
v-if="round.prompt_level"
:style="{
flexShrink: 0,
fontSize: '11px',
fontWeight: 'bold',
color: levelColors[round.prompt_level],
marginTop: '2px',
}"
>L{{ round.prompt_level }}</span
>
</div>
```
Replace it with (source tag appears to the right of the L-level badge, in a small flex column):
```html
<div
style="
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 8px;
"
>
<span>{{ round.question }}</span>
<div style="display: flex; flex-direction: column; align-items: flex-end; gap: 3px; flex-shrink: 0">
<span
v-if="round.prompt_level"
:style="{
fontSize: '11px',
fontWeight: 'bold',
color: levelColors[round.prompt_level],
marginTop: '2px',
}"
>L{{ round.prompt_level }}</span
>
<span
v-if="round.source"
:style="{
fontSize: '10px',
padding: '1px 5px',
borderRadius: '4px',
background: round.source === 'conversation' ? '#e8f0fe' : '#f0f0f0',
color: round.source === 'conversation' ? '#2060c0' : '#888',
fontWeight: 500,
whiteSpace: 'nowrap',
}"
>{{ round.source === "conversation" ? "AI" : "手动" }}</span>
</div>
</div>
```
- [ ] **Step 3: Verify visually in the browser**
Start the frontend dev server (`npm run start` from `client/`). Open the Submissions page, expand a row, click 查看 to open ChainModal. Confirm:
- Rounds from AI chat show a blue `AI` badge
- Rounds from manual submissions show a gray `手动` badge
- Rounds with no `source` (old data) show no badge and don't break
- [ ] **Step 4: Commit**
```bash
git add client/src/components/submissions/ChainModal.vue
git commit -m "feat: show AI/manual source indicator on each chain round"
```