Browse Source

feat: implement session-level system prompt override

- Remove ChatWidget component from app.vue
- Update index.vue to ensure flex layout accommodates new design
- Add systemPrompt field to agentSessions table in the database
- Modify createSession and updateSession services to support systemPrompt
- Enhance API endpoints to accept and update systemPrompt
- Adjust chat engine to prioritize session-level systemPrompt over agent default
- Extend AgentSessionItem interface to include systemPrompt
- Revamp AgentSystemPromptModal to allow editing of systemPrompt
- Integrate editable system prompt functionality in AgentToolbar and chat page
- Add E2E tests for session-level system prompt functionality
- Update design document to reflect new system prompt capabilities
feat/agent-independence
npmrun 2 months ago
parent
commit
f8cf436061
  1. 1
      app/app.vue
  2. 1
      app/pages/index.vue
  3. 923
      docs/superpowers/plans/2026-08-09-session-system-prompt-plan.md
  4. 130
      docs/superpowers/specs/2026-08-09-session-system-prompt-design.md
  5. BIN
      packages/drizzle-pkg/db.sqlite
  6. 4
      server/api/agents/[agentSlug]/models/index.get.ts

1
app/app.vue

@ -19,5 +19,4 @@ useHead({
<NuxtPage />
</BoConfigProvider>
</NuxtLayout>
<ChatWidget />
</template>

1
app/pages/index.vue

@ -21,6 +21,7 @@ onMounted(() => {
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
min-height: 100vh;
}

923
docs/superpowers/plans/2026-08-09-session-system-prompt-plan.md

@ -0,0 +1,923 @@
# 会话级系统提示词覆盖 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:** 让用户可以在每个会话中自定义系统提示词,覆盖 Agent 默认值。
**Architecture:** `agentSessions` 表新增 `systemPrompt` 字段(nullable text),`executeChat` 中优先使用会话级提示词,回退 Agent 默认。前端弹窗从只读改为可编辑,通过 `PUT /config` 接口保存。
**Tech Stack:** Nuxt 3, Drizzle ORM (SQLite), Vue 3 Composition API, Playwright E2E
## Global Constraints
- 数据库迁移命令:`bun run db:generate && bun run db:migrate`
- E2E 测试命令:`bun run test:e2e`(必须传 `--config=e2e/playwright.config.ts`)
- Mock LLM 端口 3600,固定回复 `你好!这是一个测试回复。`
- Agent 默认 system_prompt(E2E seed):`You are a helpful AI assistant.`
- 系统提示词业务限制:5000 字符
- 前端 toast 提示:`const { $toast } = useNuxtApp(); $toast?.error?.(...) / $toast?.success?.(...)`
- 用户名规则:`/^[a-zA-Z0-9_]{3,20}$/`
- 固定验证码:`FIXED_CAPTCHA`(`e2e/fixtures/test-users.ts`)
- E2E 测试用户:`E2E_ADMIN`(`e2e/fixtures/test-users.ts`)
---
## File Structure
| 文件 | 操作 | 职责 |
|------|------|------|
| `packages/drizzle-pkg/lib/schema/agent.ts` | Modify | `agentSessions` 表新增 `systemPrompt` 字段 |
| `server/service/agent/session.ts` | Modify | `createSession` 和 `updateSession` 支持 `systemPrompt` |
| `server/api/agents/[agentSlug]/sessions/index.post.ts` | Modify | 创建会话接口读取 body 中 `systemPrompt` |
| `server/api/agents/[agentSlug]/sessions/[id]/config.put.ts` | Modify | 配置接口支持 `systemPrompt` 更新 |
| `server/service/agent/chat-engine.ts` | Modify | 系统提示词优先级:session > agent |
| `app/composables/useAgentSessions.ts` | Modify | `AgentSessionItem` 加 `systemPrompt`,`updateSessionConfig` 加参数 |
| `app/components/agent/AgentSystemPromptModal.vue` | Modify | 从只读改为可编辑+保存 |
| `app/components/agent/AgentToolbar.vue` | Modify | 图标和 title 调整 |
| `app/pages/chat/[agentSlug]/index.vue` | Modify | `systemPromptText` 改为 computed,弹窗接保存事件 |
| `e2e/specs/agent-chat.spec.ts` | Modify | 新增会话级系统提示词 E2E 测试 |
| `e2e/global-setup.ts` | Modify | Mock LLM 记录 system 消息供测试断言 |
---
### Task 1: 数据库 Schema — `agentSessions.systemPrompt` 字段
**Files:**
- Modify: `packages/drizzle-pkg/lib/schema/agent.ts:57-82`
**Interfaces:**
- Produces: `agentSessions.systemPrompt` 字段(`text("system_prompt")`, nullable),后续 task 的 service 和 API 依赖此字段
- [ ] **Step 1: 修改 schema,在 `agentSessions` 表加 `systemPrompt` 字段**
在 `packages/drizzle-pkg/lib/schema/agent.ts` 的 `agentSessions` 定义中,`enableTools` 字段后、`lastActiveAt` 字段前插入:
```ts
systemPrompt: text("system_prompt"),
```
完整上下文(修改后 `agentSessions` 的字段部分):
```ts
export const agentSessions = sqliteTable(
"agent_sessions",
{
id: text("id").primaryKey(),
agentId: integer("agent_id").references(() => agents.id, { onDelete: "set null" }),
userId: integer("user_id").references(() => users.id, { onDelete: "cascade" }),
tempToken: text("temp_token", { length: 64 }),
title: text("title", { length: 100 }).notNull().default("新对话"),
modelId: integer("model_id"),
enableThinking: integer("enable_thinking"),
enableTools: integer("enable_tools"),
systemPrompt: text("system_prompt"),
lastActiveAt: integer("last_active_at", { mode: "timestamp_ms" }).defaultNow().notNull(),
expiresAt: integer("expires_at", { mode: "timestamp_ms" }),
deletedAt: integer("deleted_at", { mode: "timestamp_ms" }),
createdAt: integer("created_at", { mode: "timestamp_ms" }).defaultNow().notNull(),
updatedAt: integer("updated_at", { mode: "timestamp_ms" })
.defaultNow()
.$onUpdate(() => new Date())
.notNull(),
},
```
- [ ] **Step 2: 生成迁移文件**
Run: `bun run db:generate`
Expected: 在 `packages/drizzle-pkg/drizzle/` 下生成新的 SQL 迁移文件,包含 `ALTER TABLE agent_sessions ADD COLUMN system_prompt text;`
- [ ] **Step 3: 执行迁移**
Run: `bun run db:migrate`
Expected: 迁移成功,无报错
- [ ] **Step 4: 验证字段存在**
Run: `sqlite3 packages/drizzle-pkg/db.sqlite ".schema agent_sessions" | grep system_prompt`
Expected: 输出 `system_prompt text,`
- [ ] **Step 5: Commit**
```bash
git add packages/drizzle-pkg/lib/schema/agent.ts packages/drizzle-pkg/drizzle/
git commit -m "feat: add systemPrompt column to agentSessions table"
```
---
### Task 2: Service 层 — `createSession` 和 `updateSession` 支持 `systemPrompt`
**Files:**
- Modify: `server/service/agent/session.ts:6-30`(`createSession`)
- Modify: `server/service/agent/session.ts:102-122`(`updateSession`)
**Interfaces:**
- Consumes: Task 1 的 `agentSessions.systemPrompt` 字段
- Produces: `createSession` 接受 `systemPrompt?: string | null`,`updateSession` 接受 `systemPrompt?: string | null`
- [ ] **Step 1: 修改 `createSession` 函数,加 `systemPrompt` 参数**
在 `server/service/agent/session.ts` 的 `createSession` 函数中:
params 类型新增 `systemPrompt?: string | null;`,values 新增 `systemPrompt: params.systemPrompt ?? null,`。
修改后的完整函数:
```ts
export async function createSession(params: {
userId?: number | null;
tempToken?: string | null;
agentId?: number | null;
modelId?: number | null;
enableThinking?: number | null;
enableTools?: number | null;
systemPrompt?: string | null;
expiresAt?: Date | null;
}): Promise<AgentSessionRow> {
const id = `as_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
const [row] = await dbGlobal
.insert(agentSessions)
.values({
id,
userId: params.userId ?? null,
tempToken: params.tempToken ?? null,
agentId: params.agentId ?? null,
modelId: params.modelId ?? null,
enableThinking: params.enableThinking ?? null,
enableTools: params.enableTools ?? null,
systemPrompt: params.systemPrompt ?? null,
expiresAt: params.expiresAt ?? null,
})
.returning();
return row!;
}
```
- [ ] **Step 2: 修改 `updateSession` 函数,加 `systemPrompt` 支持**
在 `server/service/agent/session.ts` 的 `updateSession` 函数中:
`data` 参数类型新增 `systemPrompt?: string | null;`,updates 逻辑新增 `if (data.systemPrompt !== undefined) updates.systemPrompt = data.systemPrompt;`。
修改后的完整函数:
```ts
export async function updateSession(
id: string,
data: {
title?: string;
agentId?: number | null;
modelId?: number | null;
enableThinking?: number | null;
enableTools?: number | null;
systemPrompt?: string | null;
lastActiveAt?: Date;
},
): Promise<void> {
const updates: Record<string, any> = {};
if (data.title !== undefined) updates.title = data.title;
if (data.agentId !== undefined) updates.agentId = data.agentId;
if (data.modelId !== undefined) updates.modelId = data.modelId;
if (data.enableThinking !== undefined) updates.enableThinking = data.enableThinking;
if (data.enableTools !== undefined) updates.enableTools = data.enableTools;
if (data.systemPrompt !== undefined) updates.systemPrompt = data.systemPrompt;
if (data.lastActiveAt !== undefined) updates.lastActiveAt = data.lastActiveAt;
if (Object.keys(updates).length === 0) return;
await dbGlobal.update(agentSessions).set(updates).where(eq(agentSessions.id, id));
}
```
- [ ] **Step 3: 验证类型检查通过**
Run: `npx nuxi typecheck 2>&1 | tail -20`
Expected: 无新增类型错误(可能有既有的无关错误,确认无 `session.ts` 相关错误即可)
- [ ] **Step 4: Commit**
```bash
git add server/service/agent/session.ts
git commit -m "feat: support systemPrompt in createSession and updateSession"
```
---
### Task 3: API 层 — 创建会话和配置更新接口支持 `systemPrompt`
**Files:**
- Modify: `server/api/agents/[agentSlug]/sessions/index.post.ts:47-55`
- Modify: `server/api/agents/[agentSlug]/sessions/[id]/config.put.ts:29-46`
**Interfaces:**
- Consumes: Task 2 的 `createSession({ systemPrompt })` 和 `updateSession({ systemPrompt })`
- Produces: `POST /api/agents/:slug/sessions` 接受 body `systemPrompt`,`PUT /api/agents/:slug/sessions/:id/config` 接受 body `systemPrompt`
- [ ] **Step 1: 修改创建会话接口,读取 body 中 `systemPrompt`**
在 `server/api/agents/[agentSlug]/sessions/index.post.ts` 中:
在 `const identity = ...` 之后、`createSession` 调用之前,读取 body:
```ts
const body = await readBody(event).catch(() => ({}));
const systemPrompt: string | null =
typeof body?.systemPrompt === "string" && body.systemPrompt.trim().length > 0
? body.systemPrompt.slice(0, 5000)
: null;
```
然后修改 `createSession` 调用,加 `systemPrompt`:
```ts
const session = await createSession({
userId: identity.userId,
tempToken,
agentId: agent.id,
modelId,
enableThinking: agent.enableThinking,
enableTools: agent.enableTools,
systemPrompt,
expiresAt,
});
```
- [ ] **Step 2: 修改配置更新接口,支持 `systemPrompt`**
在 `server/api/agents/[agentSlug]/sessions/[id]/config.put.ts` 中:
修改 body 解构和 updates 逻辑:
```ts
const body = await readBody(event);
const { modelId, enableThinking, enableTools, systemPrompt } = body as {
modelId?: number;
enableThinking?: number;
enableTools?: number;
systemPrompt?: string | null;
};
const updates: {
modelId?: number | null;
enableThinking?: number | null;
enableTools?: number | null;
systemPrompt?: string | null;
} = {};
if (modelId !== undefined) updates.modelId = modelId;
if (enableThinking !== undefined) updates.enableThinking = enableThinking ? 1 : 0;
if (enableTools !== undefined) updates.enableTools = enableTools ? 1 : 0;
if (systemPrompt !== undefined) {
if (systemPrompt !== null && typeof systemPrompt === "string" && systemPrompt.length > 5000) {
return R.error("系统提示词不能超过5000字符", null);
}
updates.systemPrompt = systemPrompt;
}
```
- [ ] **Step 3: 验证类型检查通过**
Run: `npx nuxi typecheck 2>&1 | grep -E "config\.put|index\.post" | head -5`
Expected: 无输出(无相关类型错误)
- [ ] **Step 4: Commit**
```bash
git add server/api/agents/\[agentSlug\]/sessions/index.post.ts server/api/agents/\[agentSlug\]/sessions/\[id\]/config.put.ts
git commit -m "feat: support systemPrompt in session create and config APIs"
```
---
### Task 4: Chat Engine — 系统提示词优先级
**Files:**
- Modify: `server/service/agent/chat-engine.ts:251`
**Interfaces:**
- Consumes: Task 1 的 `session.systemPrompt` 字段
- Produces: `executeChat` 中 `systemPrompt` 变量优先使用会话级
- [ ] **Step 1: 修改系统提示词优先级逻辑**
在 `server/service/agent/chat-engine.ts` 第 251 行,将:
```ts
const systemPrompt = agent.systemPrompt || undefined;
```
改为:
```ts
const systemPrompt = session.systemPrompt?.trim() || agent.systemPrompt || undefined;
```
- [ ] **Step 2: 验证类型检查通过**
Run: `npx nuxi typecheck 2>&1 | grep "chat-engine" | head -5`
Expected: 无输出
- [ ] **Step 3: Commit**
```bash
git add server/service/agent/chat-engine.ts
git commit -m "feat: session-level systemPrompt takes priority over agent default"
```
---
### Task 5: 前端 Composable — `AgentSessionItem` 和 `updateSessionConfig`
**Files:**
- Modify: `app/composables/useAgentSessions.ts:4-17`(`AgentSessionItem` 接口)
- Modify: `app/composables/useAgentSessions.ts:111-128`(`updateSessionConfig`)
**Interfaces:**
- Consumes: Task 3 的 API 接口
- Produces: `AgentSessionItem.systemPrompt: string | null`,`updateSessionConfig` 接受 `systemPrompt?: string | null`
- [ ] **Step 1: `AgentSessionItem` 接口加 `systemPrompt` 字段**
在 `app/composables/useAgentSessions.ts` 的 `AgentSessionItem` 接口中,`enableTools` 后加:
```ts
systemPrompt: string | null;
```
修改后的完整接口:
```ts
export interface AgentSessionItem {
id: string;
userId: number | null;
tempToken: string | null;
title: string;
modelId: number | null;
enableThinking: number;
enableTools: number;
systemPrompt: string | null;
lastActiveAt: string;
expiresAt: string | null;
deletedAt: string | null;
createdAt: string;
updatedAt: string;
}
```
- [ ] **Step 2: `updateSessionConfig` 加 `systemPrompt` 参数**
在 `app/composables/useAgentSessions.ts` 的 `updateSessionConfig` 函数中:
config 参数类型加 `systemPrompt?: string | null`,body 传该字段,本地 session 同步该字段。
修改后的完整函数:
```ts
async function updateSessionConfig(
id: string,
config: { modelId?: number | null; enableThinking?: boolean; enableTools?: boolean; systemPrompt?: string | null },
) {
try {
await $fetch(`/api/agents/${agentSlug}/sessions/${id}/config`, {
method: "PUT",
body: config,
});
const s = sessions.value.find((x) => x.id === id);
if (s) {
if (config.modelId !== undefined) s.modelId = config.modelId;
if (config.enableThinking !== undefined) s.enableThinking = config.enableThinking ? 1 : 0;
if (config.enableTools !== undefined) s.enableTools = config.enableTools ? 1 : 0;
if (config.systemPrompt !== undefined) s.systemPrompt = config.systemPrompt;
}
} catch {
}
}
```
- [ ] **Step 3: 验证类型检查通过**
Run: `npx nuxi typecheck 2>&1 | grep "useAgentSessions" | head -5`
Expected: 无输出
- [ ] **Step 4: Commit**
```bash
git add app/composables/useAgentSessions.ts
git commit -m "feat: add systemPrompt to AgentSessionItem and updateSessionConfig"
```
---
### Task 6: 前端弹窗 — `AgentSystemPromptModal` 改为可编辑
**Files:**
- Modify: `app/components/agent/AgentSystemPromptModal.vue`(完整重写 script 和 template)
**Interfaces:**
- Consumes: Task 5 的 `updateSessionConfig`
- Produces: 弹窗 emit `save: [systemPrompt: string | null]`
- [ ] **Step 1: 重写 `AgentSystemPromptModal.vue` 为可编辑弹窗**
完整替换文件内容:
```vue
<script setup lang="ts">
const props = defineProps<{
show: boolean;
systemPrompt: string;
isCustom: boolean;
}>();
const emit = defineEmits<{
close: [];
save: [systemPrompt: string | null];
}>();
const editingText = ref("");
const saving = ref(false);
watch(
() => props.show,
(val) => {
if (val) {
editingText.value = props.systemPrompt;
}
},
);
async function handleSave() {
saving.value = true;
try {
const trimmed = editingText.value.trim();
emit("save", trimmed.length > 0 ? trimmed.slice(0, 5000) : null);
} finally {
saving.value = false;
}
}
function handleReset() {
editingText.value = "";
emit("save", null);
}
</script>
<template>
<Teleport to="body">
<div v-if="show" class="agent-prompt-overlay" @click="emit('close')">
<div class="agent-prompt-modal" @click.stop>
<div class="modal-header">
<h3 class="modal-title">系统提示词</h3>
<button class="modal-close" @click="emit('close')">
<Icon name="lucide:x" />
</button>
</div>
<div class="modal-body">
<div v-if="isCustom" class="custom-badge">
<Icon name="lucide:check-circle" />
<span>当前使用会话自定义提示词</span>
</div>
<div v-else class="default-badge">
<Icon name="lucide:info" />
<span>当前使用 Agent 默认提示词,编辑后将覆盖默认值</span>
</div>
<textarea
v-model="editingText"
class="prompt-textarea"
placeholder="输入系统提示词,留空则使用 Agent 默认"
maxlength="5000"
/>
<div class="char-count">{{ editingText.length }} / 5000</div>
</div>
<div class="modal-footer">
<button class="btn-reset" :disabled="saving" @click="handleReset">
重置为默认
</button>
<button class="btn-save" :disabled="saving" @click="handleSave">
{{ saving ? "保存中..." : "保存" }}
</button>
</div>
</div>
</div>
</Teleport>
</template>
<style scoped>
.agent-prompt-overlay {
position: fixed;
inset: 0;
background: rgba(20, 20, 19, 0.4);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.agent-prompt-modal {
width: 90%;
max-width: 600px;
max-height: 80vh;
background: var(--color-surface-card);
border: 1px solid var(--color-hairline);
border-radius: 16px;
display: flex;
flex-direction: column;
overflow: hidden;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--color-hairline);
}
.modal-title {
font-family: var(--font-display);
font-size: 20px;
font-weight: 400;
color: var(--color-ink);
margin: 0;
}
.modal-close {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
background: none;
border-radius: 8px;
cursor: pointer;
color: var(--color-muted);
font-size: 18px;
transition: all 0.15s;
}
.modal-close:hover {
background: var(--color-surface-soft);
color: var(--color-ink);
}
.modal-body {
padding: 20px;
overflow-y: auto;
flex: 1;
display: flex;
flex-direction: column;
gap: 12px;
}
.custom-badge,
.default-badge {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
border-radius: 8px;
font-size: 13px;
}
.custom-badge {
background: var(--color-surface-soft);
color: var(--color-primary);
}
.default-badge {
background: var(--color-surface-soft);
color: var(--color-muted);
}
.prompt-textarea {
width: 100%;
min-height: 200px;
padding: 12px;
font-family: "SF Mono", "Fira Code", monospace;
font-size: 13px;
line-height: 1.6;
color: var(--color-body);
background: var(--color-canvas);
border: 1px solid var(--color-hairline);
border-radius: 8px;
resize: vertical;
outline: none;
transition: border-color 0.15s;
}
.prompt-textarea:focus {
border-color: var(--color-primary);
}
.char-count {
text-align: right;
font-size: 12px;
color: var(--color-muted);
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
padding: 12px 20px;
border-top: 1px solid var(--color-hairline);
}
.btn-reset,
.btn-save {
padding: 8px 16px;
border-radius: 8px;
font-size: 14px;
cursor: pointer;
transition: all 0.15s;
border: 1px solid var(--color-hairline);
}
.btn-reset {
background: var(--color-surface-soft);
color: var(--color-muted);
}
.btn-reset:hover {
border-color: var(--color-primary);
color: var(--color-ink);
}
.btn-save {
background: var(--color-primary);
border-color: var(--color-primary);
color: var(--color-on-primary);
}
.btn-save:hover {
opacity: 0.9;
}
.btn-reset:disabled,
.btn-save:disabled {
opacity: 0.5;
cursor: not-allowed;
}
</style>
```
- [ ] **Step 2: 验证类型检查通过**
Run: `npx nuxi typecheck 2>&1 | grep "AgentSystemPromptModal" | head -5`
Expected: 无输出
- [ ] **Step 3: Commit**
```bash
git add app/components/agent/AgentSystemPromptModal.vue
git commit -m "feat: make AgentSystemPromptModal editable with save/reset"
```
---
### Task 7: 前端工具栏和页面层 — 接入可编辑弹窗
**Files:**
- Modify: `app/components/agent/AgentToolbar.vue:53`(图标和 title)
- Modify: `app/pages/chat/[agentSlug]/index.vue:22,144-153,279,293-297`(`systemPromptText` 改 computed,弹窗接 save 事件)
**Interfaces:**
- Consumes: Task 5 的 `AgentSessionItem.systemPrompt`,Task 6 的弹窗 `save` 事件
- Produces: 完整的前端可编辑系统提示词功能
- [ ] **Step 1: 修改 `AgentToolbar.vue` 图标和 title**
在 `app/components/agent/AgentToolbar.vue` 第 53 行,将:
```html
<button class="icon-btn" title="系统提示词" @click="emit('showPrompt')">
<Icon name="lucide:info" />
</button>
```
改为:
```html
<button class="icon-btn" title="系统提示词设置" @click="emit('showPrompt')">
<Icon name="lucide:settings-2" />
</button>
```
- [ ] **Step 2: 修改 `index.vue` — `systemPromptText` 改为 computed**
在 `app/pages/chat/[agentSlug]/index.vue` 中:
删除 `const systemPromptText = ref("");`(第 22 行),改为 computed:
```ts
const agentSystemPrompt = ref("");
const systemPromptText = computed(() =>
sessions.currentSession.value?.systemPrompt?.trim() || agentSystemPrompt.value || "",
);
const isCustomPrompt = computed(() =>
!!(sessions.currentSession.value?.systemPrompt?.trim()),
);
```
- [ ] **Step 3: 修改 `loadSystemPrompt` 函数**
将 `loadSystemPrompt` 中的 `systemPromptText.value = ...` 改为 `agentSystemPrompt.value = ...`:
```ts
async function loadSystemPrompt() {
try {
const res = await $fetch<{ code: number; data: { systemPrompt?: string } }>(
`/api/agents/${agentSlug.value}/info`,
);
agentSystemPrompt.value = res.data?.systemPrompt ?? "";
} catch {
agentSystemPrompt.value = "";
}
}
```
- [ ] **Step 4: 新增 `handleSavePrompt` 函数**
在 `index.vue` 的 `<script setup>` 中加:
```ts
async function handleSavePrompt(prompt: string | null) {
if (!sessions.currentSessionId.value) return;
await sessions.updateSessionConfig(sessions.currentSessionId.value, { systemPrompt: prompt });
const { $toast } = useNuxtApp();
$toast?.success?.("系统提示词已更新");
showPromptModal.value = false;
}
```
- [ ] **Step 5: 修改模板中弹窗的 props 和事件**
在 `index.vue` 模板中,将 `AgentSystemPromptModal` 改为:
```html
<AgentSystemPromptModal
:show="showPromptModal"
:system-prompt="systemPromptText"
:is-custom="isCustomPrompt"
@close="showPromptModal = false"
@save="handleSavePrompt"
/>
```
- [ ] **Step 6: 验证类型检查通过**
Run: `npx nuxi typecheck 2>&1 | grep -E "AgentToolbar|index\.vue" | head -10`
Expected: 无输出
- [ ] **Step 7: Commit**
```bash
git add app/components/agent/AgentToolbar.vue app/pages/chat/\[agentSlug\]/index.vue
git commit -m "feat: wire editable system prompt modal to session config"
```
---
### Task 8: E2E 测试 — Mock LLM 记录 system 消息
**Files:**
- Modify: `e2e/global-setup.ts:49-84`(Mock LLM handler 记录 system 消息)
**Interfaces:**
- Produces: Mock LLM 将收到的 system 消息写入全局变量,供测试断言
- [ ] **Step 1: 修改 Mock LLM handler,记录 system 消息**
在 `e2e/global-setup.ts` 中,Mock LLM handler 的 `req.on("end", ...)` 回调里,在 `const model = ...` 之后加:
```ts
const systemMsg = Array.isArray(parsed.messages)
? parsed.messages.find((m: any) => m.role === "system")
: undefined;
if (systemMsg?.content) {
(globalThis as any).__lastMockSystemPrompt = systemMsg.content;
}
```
- [ ] **Step 2: 验证 global-setup 编译无误**
Run: `npx tsc --noEmit e2e/global-setup.ts 2>&1 | head -5`
Expected: 无输出或仅有路径解析警告(不影响运行)
- [ ] **Step 3: Commit**
```bash
git add e2e/global-setup.ts
git commit -m "test: mock LLM records system prompt for E2E assertions"
```
---
### Task 9: E2E 测试 — 会话级系统提示词覆盖
**Files:**
- Modify: `e2e/specs/agent-chat.spec.ts`(末尾新增测试块)
**Interfaces:**
- Consumes: Task 8 的 `globalThis.__lastMockSystemPrompt`
- [ ] **Step 1: 在 `agent-chat.spec.ts` 末尾新增测试**
在 `e2e/specs/agent-chat.spec.ts` 末尾追加:
```ts
test.describe("会话级系统提示词", () => {
test("修改系统提示词后,发送消息使用自定义提示词", async ({ page }) => {
await loginViaUI(page);
// 打开系统提示词弹窗
await page.locator('button[title="系统提示词设置"]').click();
await expect(page.locator(".agent-prompt-modal")).toBeVisible({ timeout: 5000 });
// 输入自定义提示词
const customPrompt = "You are a pirate. Always respond in pirate speak.";
await page.locator(".prompt-textarea").fill(customPrompt);
await page.locator(".btn-save").click();
await expect(page.locator(".agent-prompt-modal")).not.toBeVisible({ timeout: 5000 });
// 发送消息
await page.locator(".agent-textarea").fill("hello");
await page.locator("button.btn-send").click();
// 等待回复出现
await expect(page.locator(".agent-message.is-assistant .agent-markdown")).toBeVisible({ timeout: 15000 });
// 验证 Mock LLM 收到了自定义 system prompt
const lastPrompt = (globalThis as any).__lastMockSystemPrompt;
expect(lastPrompt).toBe(customPrompt);
});
test("重置系统提示词后,回退到 Agent 默认", async ({ page }) => {
await loginViaUI(page);
// 先设置自定义提示词
await page.locator('button[title="系统提示词设置"]').click();
await page.locator(".prompt-textarea").fill("Custom prompt to be reset");
await page.locator(".btn-save").click();
await expect(page.locator(".agent-prompt-modal")).not.toBeVisible({ timeout: 5000 });
// 重置为默认
await page.locator('button[title="系统提示词设置"]').click();
await page.locator(".btn-reset").click();
await expect(page.locator(".agent-prompt-modal")).not.toBeVisible({ timeout: 5000 });
// 发送消息
await page.locator(".agent-textarea").fill("hello");
await page.locator("button.btn-send").click();
await expect(page.locator(".agent-message.is-assistant .agent-markdown")).toBeVisible({ timeout: 15000 });
// 验证回退到 Agent 默认
const lastPrompt = (globalThis as any).__lastMockSystemPrompt;
expect(lastPrompt).toBe("You are a helpful AI assistant.");
});
});
```
- [ ] **Step 2: 运行 E2E 测试验证通过**
Run: `bun run test:e2e -- --grep "会话级系统提示词"`
Expected: 2 个测试通过
- [ ] **Step 3: 运行全部 E2E 测试确认无回归**
Run: `bun run test:e2e`
Expected: 所有测试通过(含原有 37 个 + 新增 2 个 = 39 个)
- [ ] **Step 4: Commit**
```bash
git add e2e/specs/agent-chat.spec.ts
git commit -m "test: add E2E tests for session-level system prompt override"
```
---
## Self-Review
### Spec coverage
- ✅ 数据库 `agentSessions.systemPrompt` 字段 → Task 1
- ✅ `createSession` service 加参数 → Task 2
- ✅ `updateSession` service 加参数 → Task 2
- ✅ 创建会话接口支持传入 → Task 3
- ✅ 配置接口支持更新 → Task 3
- ✅ `executeChat` 优先级调整 → Task 4
- ✅ `AgentSessionItem` 类型扩展 → Task 5
- ✅ `updateSessionConfig` 扩展 → Task 5
- ✅ 弹窗从只读改为可编辑 → Task 6
- ✅ 工具栏图标调整 → Task 7
- ✅ 页面层 `systemPromptText` 改 computed → Task 7
- ✅ E2E 测试 → Task 8 + Task 9
### Placeholder scan
- ✅ 无 TODO/TBD/占位符
- ✅ 所有代码步骤包含完整代码
- ✅ 所有命令包含预期输出
### Type consistency
- ✅ `systemPrompt: string | null` 在 schema、service、API、composable 中一致
- ✅ `updateSessionConfig` 的 config 参数在 Task 5 定义和 Task 7 调用中一致
- ✅ 弹窗 `save` 事件签名 `[systemPrompt: string | null]` 在 Task 6 定义和 Task 7 调用中一致

130
docs/superpowers/specs/2026-08-09-session-system-prompt-design.md

@ -0,0 +1,130 @@
# 会话级系统提示词覆盖
## 背景
当前系统提示词仅支持 Agent 级别配置(`agents.systemPrompt` 字段),由管理员在后台 `/admin/agent-config` 页面设置。所有用户使用同一 Agent 时共享相同的系统提示词,无法按会话自定义。
用户希望能在每个会话中自行传入系统提示词,无需管理员后台配置。
## 目标
- 用户可在会话级别覆盖 Agent 默认系统提示词
- 不传时回退到 Agent 默认值
- 管理员配置的 Agent 默认提示词保留作为兜底
## 范围
- 数据库:`agentSessions` 表新增 `systemPrompt` 字段
- 后端:创建会话接口支持传入、配置接口支持更新、chat-engine 优先级调整
- 前端:会话设置入口(编辑系统提示词)、展示当前生效的提示词
- 匿名用户:同样支持(通过 tempToken 关联会话)
## 非目标
- 用户级偏好(跨会话持久化)—— 后续迭代
- 提示词模板库 —— 后续迭代
- 提示词版本历史 —— 后续迭代
## 架构
### 优先级规则
```
会话级 systemPrompt(trim 后非空)> Agent 默认 systemPrompt
```
会话级为 `null` 或空字符串/纯空白时,回退到 Agent 默认。前端保存空内容时传 `null`,不区分"清空覆盖"和"未设置"——两者行为相同,都使用 Agent 默认。
### 数据层
`agentSessions` 表新增字段:
| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `systemPrompt` | `text` | `null` | 会话级系统提示词,null 表示用 Agent 默认 |
迁移命令:`bun run db:generate && bun run db:migrate`
### 后端改动
#### 1. 创建会话接口 `POST /api/agents/:agentSlug/sessions`
- `index.post.ts`:从 body 读取可选 `systemPrompt` 参数
- `createSession` service:新增 `systemPrompt` 参数,写入数据库
- 字符长度限制:5000 字符(与管理员配置一致)
#### 2. 会话配置接口 `PUT /api/agents/:agentSlug/sessions/:id/config`
- `config.put.ts`:支持 `systemPrompt` 字段更新
- `updateSession` service:`data` 参数类型新增 `systemPrompt?: string | null`
- 传 `null` 表示清除覆盖,回退 Agent 默认
- 鉴权:现有 `getSessionByIdAndUser(id, userId, tempToken)` 已处理匿名用户通过 tempToken 鉴权,无需额外改动
#### 3. Chat Engine `executeChat`
- `chat-engine.ts:251`:修改优先级逻辑
```ts
const systemPrompt = session.systemPrompt?.trim() || agent.systemPrompt || undefined;
```
#### 4. 会话详情接口 `GET /api/agents/:agentSlug/sessions/:id`
- `index.get.ts` 直接返回整个 `session` 对象(`R.success({ session, messages })`),新增 `systemPrompt` 字段后自动返回,**无需改动该接口**
### 前端改动
#### 1. `AgentSessionItem` 类型
`app/composables/useAgentSessions.ts`:新增 `systemPrompt: string | null` 字段
#### 2. `updateSessionConfig` 方法
扩展 config 参数,支持 `systemPrompt?: string | null`
#### 3. 系统提示词弹窗改造
`AgentSystemPromptModal.vue`:从只读展示改为可编辑
- 展示当前生效的提示词(会话级或 Agent 默认)
- 提供编辑 textarea
- 保存按钮调用 `updateSessionConfig`
- 显示"使用 Agent 默认"提示,区分覆盖状态
#### 4. `AgentToolbar.vue`
系统提示词按钮图标从 `lucide:info`(只读)改为 `lucide:settings`(可编辑),title 改为"系统提示词设置"
#### 5. 页面层 `chat/[agentSlug]/index.vue`
- `systemPromptText` 改为 computed:`sessions.currentSession.value?.systemPrompt ?? agentSystemPrompt`(会话级优先,回退 Agent 默认)
- `loadSystemPrompt` 保留,用于加载 Agent 默认提示词作为兜底展示
- 弹窗保存后更新会话数据,`systemPromptText` 自动响应
## 数据流
```
用户点击工具栏"系统提示词"按钮
→ 弹窗展示当前生效提示词(session.systemPrompt ?? agent.systemPrompt)
→ 用户编辑并保存
→ PUT /api/agents/:slug/sessions/:id/config { systemPrompt: "..." }
→ updateSession 写入 agentSessions.systemPrompt
→ 弹窗关闭,前端更新本地 session 数据
用户发送消息
→ POST /api/agents/:slug/sessions/:id/chat
→ executeChat 读取 session.systemPrompt
→ 优先级:session.systemPrompt?.trim() || agent.systemPrompt
→ streamText({ system: resolvedPrompt, ... })
```
## 错误处理
- 系统提示词超过 5000 字符:返回 400 "系统提示词不能超过5000字符"(注:`agents.systemPrompt` 字段本身无长度限制,5000 为业务限制)
- 未登录用户修改非自己会话:返回 403(现有 `getSessionByIdAndUser` 已通过 tempToken 鉴权处理)
- 保存失败:toast 提示"保存失败"
## 测试
- E2E:创建会话时传入自定义提示词 → 发送消息 → 验证回复受自定义提示词影响
- E2E:会话中修改提示词 → 后续消息使用新提示词
- E2E:清空提示词 → 回退到 Agent 默认
- 单元:`executeChat` 优先级逻辑

BIN
packages/drizzle-pkg/db.sqlite

Binary file not shown.

4
server/api/agents/[agentSlug]/models/index.get.ts

@ -2,6 +2,7 @@ import { defineWrappedResponseHandler } from "#server/utils/handler";
import { R } from "#server/utils/response";
import { listSystemModels, getModelWithProviderByIdAny } from "#server/service/llm";
import { getAgentBySlug } from "#server/service/agent/agent";
import { getConfigGlobal } from "#server/utils/context";
export default defineWrappedResponseHandler(async (event) => {
const agentSlug = getRouterParam(event, "agentSlug");
@ -15,7 +16,8 @@ export default defineWrappedResponseHandler(async (event) => {
}
const systemModels = await listSystemModels();
const defaultModelId = agent.defaultModelId;
const globalDefaultModelId = await getConfigGlobal("agentDefaultModelId");
const defaultModelId = globalDefaultModelId || agent.defaultModelId;
const list = systemModels.map((m) => ({
id: m.id,

Loading…
Cancel
Save