diff --git a/.env.example b/.env.example index d9a0930..6599cc9 100644 --- a/.env.example +++ b/.env.example @@ -7,24 +7,11 @@ LOG_LEVEL=debug # development | production | test OPERATING_ENV=production # Resource file storage location -STORAGE_DIR=/public/uploads +STORAGE_DIR=./public/uploads # Maximum upload size for attachments MAX_UPLOAD_SIZE_MB=5 -# The number of segments when reaching the maximum Tokens count -MAX_RESPONSE_SEGMENTS=5 -# Default token usage for a single chat -MAX_TOKENS=8000 - -# Example Context Values for qwen2.5-coder:32b -# -# DEFAULT_NUM_CTX=32768 # Consumes 36GB of VRAM -# DEFAULT_NUM_CTX=24576 # Consumes 32GB of VRAM -# DEFAULT_NUM_CTX=12288 # Consumes 26GB of VRAM -# DEFAULT_NUM_CTX=6144 # Consumes 24GB of VRAM -DEFAULT_NUM_CTX= - # Enabled model providers, currently supporting Anthropic, Cohere, Deepseek, DouBao, Ernie, Google, Groq, -# HuggingFace, Hyperbolic, Kimi, Mistral, Ollama, OpenAI, OpenRouter, OpenAILike, Perplexity, Qwen, xAI, +# HuggingFace, Hyperbolic, Kimi, Mistral, Ollama, OpenAI, OpenRouter, Perplexity, Qwen, xAI, # ZhiPu, Together, LMStudio, AmazonBedrock, Github LLM_PROVIDER= diff --git a/CLAUDE.md b/CLAUDE.md index 623aa12..9cd2c7f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -85,8 +85,8 @@ API routes are grouped under `/api/*` with prefixes for domains: `chat`, `projec The core AI logic lives here: -- **`chat-stream-text.ts`** — Main streaming text entry point. Builds system prompts, calls `streamText` from the AI SDK, and attaches tools. -- **`tools/`** — AI SDK tools exposed to the model (Serper search, weather). +- **`agents/page-builder.ts`** — Main agent runtime entry point. Prepares steps, controls the tool loop, and coordinates the page builder session. +- **`agents/page-builder-tools.ts`** — Registers the page-builder toolset used by the main agent, including optional tools such as Serper search and weather. - **`prompts/prompts.ts`** — System prompts for page generation. - **`select-context.ts`** — Selects relevant context from chat history for the current request. - **`structured-page-snapshot.ts`** — Parses and structures the LLM's page output. diff --git a/app/.server/llm/agents/page-generation.spec.ts b/app/.server/llm/agents/page-generation.spec.ts index f7ea176..484d970 100644 --- a/app/.server/llm/agents/page-generation.spec.ts +++ b/app/.server/llm/agents/page-generation.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { appendPageSummaryContext, createElementEditPrompt } from './page-generation'; +import { appendPageSummaryContext, buildPageGenerationSystemPrompt, createElementEditPrompt } from './page-generation'; describe('createElementEditPrompt', () => { it('should pin update scope to the selected domId', () => { @@ -31,3 +31,19 @@ describe('appendPageSummaryContext', () => { expect(prompt).toContain('detailed'); }); }); + +describe('buildPageGenerationSystemPrompt', () => { + it('should require primary content to stay visible without script', () => { + const prompt = buildPageGenerationSystemPrompt({ + summary: '', + pageSummaryOutline: '', + pageSummaryDetailed: '', + context: {}, + designMd: '# Design System', + }); + + expect(prompt).toContain('页面在没有任何 Script 执行时也必须可正常预览'); + expect(prompt).toContain('不能依赖脚本在稍后把主要内容从隐藏切换为显示'); + expect(prompt).toContain('不要把首屏、正文主体、关键信息卡片、主要 CTA 做成'); + }); +}); diff --git a/app/.server/llm/chat-stream-text.ts b/app/.server/llm/chat-stream-text.ts deleted file mode 100644 index 9b7531e..0000000 --- a/app/.server/llm/chat-stream-text.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { - streamText as _streamText, - type CallSettings, - convertToModelMessages, - type LanguageModel, - type LanguageModelUsage, - type StreamTextOnFinishCallback, - stepCountIs, - type UIMessageStreamWriter, -} from 'ai'; -import { getSystemPrompt } from '~/.server/prompts/prompts'; -import { approximatePromptTokenCount, encode } from '~/.server/utils/token'; -import type { ElementInfo } from '~/routes/api/chat/chat'; -import type { ChatUIMessage } from '~/types/message'; -import { appendPageSummaryContext, createElementEditPrompt } from './agents/page-generation'; -import { MAX_TOKENS } from './constants'; -import type { SelectContextResult } from './select-context'; -import { tools } from './tools'; - -export type ChatStreamTextProps = CallSettings & { - messages: ChatUIMessage[]; - summary: string; - pageSummaryOutline: string; - pageSummaryDetailed: string; - context?: Record; - model: LanguageModel; - maxTokens?: number; - elementInfo?: ElementInfo; - designMd: string; - userPageContext?: string; - writer?: UIMessageStreamWriter; - onFinish?: StreamTextOnFinishCallback; - onAbort?: (params: { event: any; totalUsage: LanguageModelUsage }) => void; -}; - -export async function chatStreamText({ - messages, - summary, - pageSummaryOutline, - pageSummaryDetailed, - context, - model, - maxTokens, - elementInfo, - designMd, - userPageContext, - writer, - abortSignal, - onFinish, - onAbort, -}: ChatStreamTextProps) { - const modelMessages = await convertToModelMessages(messages); - let systemPrompt = getSystemPrompt(); - systemPrompt = appendPageSummaryContext(systemPrompt, { - pageSummaryOutline, - pageSummaryDetailed, - }); - - if (summary) { - systemPrompt = `${systemPrompt} -以下是截至目前为止的聊天记录摘要: -CHAT SUMMARY: ---- -${summary} ---- - `; - } - - if (context) { - systemPrompt = `${systemPrompt} -以下是根据用户的聊天记录和任务分析出的可能对此次任务有帮助的页面及其代码片段,按页面名称区分,多个页面使用 ------ 分割 -CONTEXT: ---- -${Object.entries(context) - .map( - ([key, value]) => ` - - 页面名称: ${value.pageName} - - 页面标题: ${value.pageTitle} - - 页面内容: ${value.sections.join('\n')} - `, - ) - .join('------')} ---- - `; - } - - if (userPageContext) { - systemPrompt = `${systemPrompt} -以下是用户当前本地尚未保存、但与你这次任务直接相关的页面快照。你必须在理解这些内容后再决定如何修改页面: -LOCAL PAGE SNAPSHOT: ---- -${userPageContext} ---- - `; - } - - systemPrompt = `${systemPrompt} - -以下设计系统规范对所有视觉决策具有最高优先级: - -- 颜色:必须严格使用 colors 中定义的色值,禁止自行引入其他颜色 -- 字体:必须遵循 typography 中定义的字族、字号、字重和行高 -- 圆角:使用 rounded 中定义的圆角值,保持一致的形状语言 -- 间距:使用 spacing 中定义的间距标尺 -- 组件:遵循 components 中定义的按钮、输入框等组件样式 - -${designMd} - - `; - - if (elementInfo) { - systemPrompt = `${systemPrompt} - ${createElementEditPrompt(elementInfo)} - `; - } - - return _streamText({ - model, - tools: tools({ - onPage: (page) => { - writer?.write({ - type: 'data-upage-page', - data: page, - }); - }, - }), - system: systemPrompt, - maxOutputTokens: maxTokens || MAX_TOKENS, - messages: modelMessages, - stopWhen: stepCountIs(3), - prepareStep: async ({ messages }) => { - if (messages.length > 20) { - return { - messages: messages.slice(-10), - }; - } - return {}; - }, - abortSignal, - onFinish, - onAbort(event) { - // 由于 AI SDK 没有提供在 onAbort 中计算 Token 消耗的方法。所以这里手动计算。 - let inoutTokens = 0; - inoutTokens += approximatePromptTokenCount(messages); - inoutTokens += encode(systemPrompt).length; - onAbort?.({ - event, - totalUsage: { - inputTokens: inoutTokens, - inputTokenDetails: { - noCacheTokens: inoutTokens, - cacheReadTokens: 0, - cacheWriteTokens: 0, - }, - outputTokens: 0, - outputTokenDetails: { - textTokens: 0, - reasoningTokens: 0, - }, - totalTokens: inoutTokens, - reasoningTokens: 0, - cachedInputTokens: 0, - }, - }); - }, - }); -} diff --git a/app/.server/llm/constants.ts b/app/.server/llm/constants.ts deleted file mode 100644 index 12acb51..0000000 --- a/app/.server/llm/constants.ts +++ /dev/null @@ -1,7 +0,0 @@ -// see https://docs.anthropic.com/en/docs/about-claude/models -export const MAX_TOKENS = process.env.MAX_TOKENS ? parseInt(process.env.MAX_TOKENS) : 8000; - -// limits the number of model responses that can be returned in a single request -export const MAX_RESPONSE_SEGMENTS = process.env.MAX_RESPONSE_SEGMENTS - ? parseInt(process.env.MAX_RESPONSE_SEGMENTS) - : 5; diff --git a/app/.server/llm/tools/index.ts b/app/.server/llm/tools/index.ts deleted file mode 100644 index ce67012..0000000 --- a/app/.server/llm/tools/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { Tool, ToolSet } from 'ai'; -import type { UPagePagePart } from '~/types/message'; -import { serperTool } from './serper'; -import { createUPageTool } from './upage'; -import { weatherTool } from './weather'; - -export const tools = ({ onPage }: { onPage?: (page: UPagePagePart) => void } = {}): ToolSet => { - const tools: Record = {}; - - if (onPage) { - tools.upage = createUPageTool(onPage); - } - - if (process.env.SERPER_API_KEY) { - tools.serper = serperTool; - } - - if (process.env.WEATHER_API_KEY) { - tools.weather = weatherTool; - } - - return tools; -}; diff --git a/app/.server/modules/llm/providers/ollama.ts b/app/.server/modules/llm/providers/ollama.ts index dcd4921..386e957 100644 --- a/app/.server/modules/llm/providers/ollama.ts +++ b/app/.server/modules/llm/providers/ollama.ts @@ -37,10 +37,6 @@ export default class OllamaProvider extends BaseProvider { staticModels: ModelInfo[] = []; - getDefaultNumCtx(): number { - return process.env.DEFAULT_NUM_CTX ? parseInt(process.env.DEFAULT_NUM_CTX, 10) : 32768; - } - async getDynamicModels(settings?: IProviderSetting): Promise { let { baseUrl } = this.getProviderBaseUrlAndKey(settings); diff --git a/app/.server/prompts/prompts.ts b/app/.server/prompts/prompts.ts index c333e8b..9e3101f 100644 --- a/app/.server/prompts/prompts.ts +++ b/app/.server/prompts/prompts.ts @@ -30,6 +30,7 @@ export const getSystemPrompt = () => ` - 如果有图标,则使用iconify-icon库提供所需的图标。 - 如果需要占位图,则使用 https://picsum.photos 提供占位图。 - 保持移动端的适配性,确保在不同尺寸的设备上能够正常显示。 + - 页面在没有任何 Script 执行时也必须可正常预览,尤其是首屏与主要内容必须默认可见。 - 非常重要:首个页面的 name 一定是 index,title 根据用户要求和页面类型确定。 - 页面结构应根据实际内容需要组织;当用户只描述目标时,自主选择最合适的 section 数量与类型,而不是套用固定官网模板。 @@ -105,11 +106,13 @@ export const getSystemPrompt = () => ` - 设计丰富的交互体验:精致的悬停效果、流畅的动画过渡、视差滚动 - 精心设计微交互,为用户提供愉悦的互动体验 - 实现滚动感知设计:顶部导航区域在滚动时变化(如背景透明度、高度缩小、阴影增强等),创造动态视觉体验 - - 设计滚动触发动画:元素随页面滚动逐渐显现、移动或变化,增强页面生命力 + - 设计滚动触发动画时,元素必须默认可见;只能做轻量增强,不能依赖脚本在稍后把主要内容从隐藏切换为显示 - 精致细节:添加微妙动画、状态转换、视差效果 结构规则(始终生效): - Script 兼容性:在页面无 Script 时,也可以正常预览,Script 用于提升用户体验。 + - 默认可见性:不要把首屏、正文主体、关键信息卡片、主要 CTA 做成 \`hidden\`、\`invisible\`、\`opacity-0\`、\`display:none\`、\`visibility:hidden\` 等初始隐藏状态,再等待 Script 驱动显示。 + - 如需入场动画,初始状态也必须能被用户直接看到;优先使用默认可见的透明度/位移过渡增强,而不是“先隐藏后显现”。 - Header:如果具有导航栏,则滚动时导航栏要跟随滚动,且为用户呈现适当的交互体验。 - 图标语义关联:所有选择的图标需要与当前内容有明确的语义联系,确保图标直观地表达相应的概念或功能 - 优先保证信息层次与内容匹配;该简洁时简洁,该丰富时丰富,不要为了满足固定密度而堆砌无意义元素。 diff --git a/docker-compose-dev.yaml b/docker-compose-dev.yaml index 1bc39f9..b0a8b37 100644 --- a/docker-compose-dev.yaml +++ b/docker-compose-dev.yaml @@ -9,7 +9,6 @@ services: - OPERATING_ENV=${OPERATING_ENV:-production} - NODE_ENV=${NODE_ENV:-production} - LOG_LEVEL=${LOG_LEVEL:-debug} - - DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX:-32768} - LLM_PROVIDER=${LLM_PROVIDER} - PROVIDER_BASE_URL=${PROVIDER_BASE_URL} - PROVIDER_API_KEY=${PROVIDER_API_KEY} diff --git a/docker-compose-prod.yaml b/docker-compose-prod.yaml index f0ecf41..f96143f 100644 --- a/docker-compose-prod.yaml +++ b/docker-compose-prod.yaml @@ -9,7 +9,6 @@ services: - OPERATING_ENV=${OPERATING_ENV:-production} - NODE_ENV=${NODE_ENV:-production} - LOG_LEVEL=${LOG_LEVEL:-debug} - - DEFAULT_NUM_CTX=${DEFAULT_NUM_CTX:-32768} - LLM_PROVIDER=${LLM_PROVIDER} - PROVIDER_BASE_URL=${PROVIDER_BASE_URL} - PROVIDER_API_KEY=${PROVIDER_API_KEY} diff --git a/docs/content/configuration.md b/docs/content/configuration.md index 0eaf4cf..d9e5a6e 100644 --- a/docs/content/configuration.md +++ b/docs/content/configuration.md @@ -23,9 +23,7 @@ UPage 使用环境变量进行配置。您可以通过以下方式设置环境 | `LOG_LEVEL` | 日志级别(debug, info, warn, error) | `info` | 否 | | `USAGE_LOG_FILE` | 是否开启文件日志 | `true` | 否 | | `MAX_UPLOAD_SIZE_MB` | 附件上传的最大大小 (MB) | `5` | 否 | -| `STORAGE_DIR` | 资源文件存储位置 | `/app/storage` | 否 | -| `MAX_RESPONSE_SEGMENTS` | 最大响应分段数 | `5` | 否 | -| `MAX_TOKENS` | 最大 token 数 | `8000` | 否 | +| `STORAGE_DIR` | 资源文件存储位置 | `./public/uploads` | 否 | ## AI 提供商配置 diff --git a/docs/content/deployment/docker-compose.md b/docs/content/deployment/docker-compose.md index ebe6988..f102284 100644 --- a/docs/content/deployment/docker-compose.md +++ b/docs/content/deployment/docker-compose.md @@ -113,7 +113,7 @@ UPage 支持通过环境变量进行配置。以下是一些比较重要的环 | `LOG_LEVEL` | 日志级别 | `debug` | | `USAGE_LOG_FILE` | 是否开启文件日志 | `true` | | `MAX_UPLOAD_SIZE_MB` | 附件上传的最大大小 (MB) | `5` | -| `STORAGE_DIR` | 资源文件存储位置 | `/app/storage` | +| `STORAGE_DIR` | 资源文件存储位置 | `./public/uploads` | ### 模型提供商配置 根据您选择的 AI 提供商,您还需要配置相应的 API 密钥和基础 URL,例如: diff --git a/docs/content/deployment/docker.md b/docs/content/deployment/docker.md index 8613c31..05f5f4f 100644 --- a/docs/content/deployment/docker.md +++ b/docs/content/deployment/docker.md @@ -110,7 +110,7 @@ UPage 支持通过环境变量进行配置。以下是一些比较重要的环 | `LOG_LEVEL` | 日志级别 | `debug` | | `USAGE_LOG_FILE` | 是否开启文件日志 | `true` | | `MAX_UPLOAD_SIZE_MB` | 附件上传的最大大小 (MB) | `5` | -| `STORAGE_DIR` | 资源文件存储位置 | `/app/storage` | +| `STORAGE_DIR` | 资源文件存储位置 | `./public/uploads` | ### 模型提供商配置 根据您选择的 AI 提供商,您还需要配置相应的 API 密钥和基础 URL,例如: