From f8cf43606141b40138f8dbbbbbf0349ef6057c5d Mon Sep 17 00:00:00 2001 From: npmrun <1549469775@qq.com> Date: Sun, 9 Aug 2026 12:01:54 +0800 Subject: [PATCH] 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 --- app/app.vue | 1 - app/pages/index.vue | 1 + .../plans/2026-08-09-session-system-prompt-plan.md | 923 +++++++++++++++++++++ .../2026-08-09-session-system-prompt-design.md | 130 +++ packages/drizzle-pkg/db.sqlite | Bin 561152 -> 581632 bytes server/api/agents/[agentSlug]/models/index.get.ts | 4 +- 6 files changed, 1057 insertions(+), 2 deletions(-) create mode 100644 docs/superpowers/plans/2026-08-09-session-system-prompt-plan.md create mode 100644 docs/superpowers/specs/2026-08-09-session-system-prompt-design.md diff --git a/app/app.vue b/app/app.vue index 3ff6bb7..d6f878e 100644 --- a/app/app.vue +++ b/app/app.vue @@ -19,5 +19,4 @@ useHead({ - diff --git a/app/pages/index.vue b/app/pages/index.vue index 6695bd0..11fb0de 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -21,6 +21,7 @@ onMounted(() => { flex-direction: column; align-items: center; justify-content: center; + flex: 1; min-height: 100vh; } diff --git a/docs/superpowers/plans/2026-08-09-session-system-prompt-plan.md b/docs/superpowers/plans/2026-08-09-session-system-prompt-plan.md new file mode 100644 index 0000000..283065b --- /dev/null +++ b/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 { + 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 { + const updates: Record = {}; + 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 + + + + + + + + 系统提示词 + + + + + + + + 当前使用会话自定义提示词 + + + + 当前使用 Agent 默认提示词,编辑后将覆盖默认值 + + + {{ editingText.length }} / 5000 + + + + + + + + +``` + +- [ ] **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 + + + +``` + +改为: + +```html + + + +``` + +- [ ] **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` 的 `