diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml index 6a87e18..1473325 100644 --- a/.github/workflows/sync.yml +++ b/.github/workflows/sync.yml @@ -10,6 +10,9 @@ on: jobs: sync: name: POST /functions/sync-templates + # Forks provide their own catalog delivery path. Never post a fork commit + # to InsForge's production marketplace sync endpoint. + if: github.repository == 'InsForge/insforge-templates' runs-on: ubuntu-latest steps: - name: Trigger sync diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 04a6dee..7bdd5d6 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -31,13 +31,6 @@ jobs: working-directory: scripts run: npm test - # We deliberately do NOT run `npm install` on each template in CI: - # contributors install locally before opening a PR, package-lock.json - # makes the install reproducible, and the slow per-template install - # would dominate CI time as the registry grows. The static checks below - # (JSON parse, file existence, secret patterns, SQL parse, edge-function - # tsc) cover what install would catch except the rare "package was - # yanked from npm" case — we'll catch that via Dependabot/Snyk later. edge-function-tsc: name: tsc --noEmit on edge functions runs-on: ubuntu-latest @@ -92,3 +85,33 @@ jobs: exit 1 fi done + + publishable-builds: + name: Native template build and artifact admission + runs-on: ubuntu-latest + needs: [validate-registry] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + - name: Enable pinned package managers + run: corepack enable + - name: Build every native template with its controlled profile + run: | + { node -e "for (const item of require('./registry.json')) if (item.publishingCompatibility === 'native') console.log(item.slug + '\t' + item.buildProfile)"; printf 'react\tvite-npm-v1\ntodo\tnext-static-npm-v1\n'; } | + while IFS=$'\t' read -r slug profile; do + case "$profile" in + vite-npm-v1|next-static-npm-v1) + (cd "$slug" && npm ci --ignore-scripts && npm run build) + ;; + vite-pnpm-v1|next-static-pnpm-v1) + (cd "$slug" && pnpm install --frozen-lockfile --ignore-scripts && pnpm run build) + ;; + *) + echo "::error::$slug declares unknown controlled profile $profile" + exit 1 + ;; + esac + node scripts/verify-publishable-template.mjs "$slug" + done diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6339e87..b6cc0ec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -24,6 +24,7 @@ Templates listed on https://insforge.dev/templates live in this repo. Adding one "framework": "nextjs", "features": ["auth", "ai"], "tags": ["my-tag"], + "requiredCapabilities": ["ai.chat", "ai.streaming", "storage"], "cover": "assets/covers/my-template.png", "demo_url": "https://my-demo.us-east.insforge.app", "author": "Your Name", @@ -31,6 +32,9 @@ Templates listed on https://insforge.dev/templates live in this repo. Adding one } ``` + `requiredCapabilities` is optional. Declare only capabilities the template actually uses; + Insight Flow rejects creation before provisioning when the active runtime cannot satisfy them. + 6. Open the PR. CI (`Validate Registry`) must be green. A maintainer will review and merge. ## CI checks diff --git a/README.md b/README.md index e644dd4..d5c839e 100644 --- a/README.md +++ b/README.md @@ -50,9 +50,11 @@ If you prefer to inspect a template manually, clone this repository, move into a | Template | Framework | Best for | Includes | | --- | --- | --- | --- | -| [`chatbot`](./chatbot) | Next.js App Router | Building an AI chat product on top of InsForge | Persisted chat history, file uploads, auth, storage, optional Vercel AI Gateway support | +| [`chatbot`](./chatbot) | Next.js App Router | Building an AI chat product on the Insight Flow runtime | Persisted chat history, streaming AI, file uploads, auth, and Platform-backed object storage | | [`crm`](./crm) | Next.js App Router | Building an authenticated internal tool or CRM | Sales pipeline, lead management, client flows, RLS, seeded defaults | | [`e-commerce`](./e-commerce) | Next.js App Router | Launching a storefront with user accounts and checkout | Seeded catalog, product pages, cart, checkout, account area, analytics | +| [`web-research-agent`](./web-research-agent) | React + Vite | Building evidence-led browser research workflows | Browser-backed investigations, cited synthesis, evidence ledger, auth, and RLS | +| [`website-change-monitor`](./website-change-monitor) | React + Vite | Tracking meaningful changes across important web pages | Scheduled checks, durable snapshots, before/after diffs, AI classification, auth, and RLS | ### Auth Provider Overlays @@ -100,6 +102,8 @@ For full setup details, go directly to the template README you want to use: - [`chatbot/README.md`](./chatbot/README.md) - [`crm/README.md`](./crm/README.md) - [`e-commerce/README.md`](./e-commerce/README.md) +- [`web-research-agent/README.md`](./web-research-agent/README.md) +- [`website-change-monitor/README.md`](./website-change-monitor/README.md) - [`nextjs/README.md`](./nextjs/README.md) - [`react/README.md`](./react/README.md) diff --git a/admin-dashboard/index.html b/admin-dashboard/index.html index d209b78..60d2647 100644 --- a/admin-dashboard/index.html +++ b/admin-dashboard/index.html @@ -8,6 +8,7 @@
+ diff --git a/admin-dashboard/src/lib/env.ts b/admin-dashboard/src/lib/env.ts index 6df6818..c447f44 100644 --- a/admin-dashboard/src/lib/env.ts +++ b/admin-dashboard/src/lib/env.ts @@ -5,7 +5,12 @@ const schema = z.object({ VITE_INSFORGE_ANON_KEY: z.string().min(1), }) -const parsed = schema.safeParse(import.meta.env) +const runtime = (window as Window & { __INSFORGE_RUNTIME_CONFIG__?: { apiBaseURL?: string; anonKey?: string } }).__INSFORGE_RUNTIME_CONFIG__ +const parsed = schema.safeParse({ + ...import.meta.env, + VITE_INSFORGE_URL: runtime?.apiBaseURL ?? import.meta.env.VITE_INSFORGE_URL, + VITE_INSFORGE_ANON_KEY: runtime?.anonKey ?? import.meta.env.VITE_INSFORGE_ANON_KEY, +}) if (!parsed.success) { // Surface a clear message in the browser console so missing config is obvious. diff --git a/assets/covers/chatbot.png b/assets/covers/chatbot.png new file mode 100644 index 0000000..265dcd5 Binary files /dev/null and b/assets/covers/chatbot.png differ diff --git a/chatbot/.env.example b/chatbot/.env.example index e209fe9..97febf3 100644 --- a/chatbot/.env.example +++ b/chatbot/.env.example @@ -1,13 +1,5 @@ NEXT_PUBLIC_INSFORGE_URL=https://your-project.region.insforge.app NEXT_PUBLIC_INSFORGE_ANON_KEY=your-anon-key -NEXT_PUBLIC_APP_URL=https://your-project.insforge.site -# AI provider: "insforge" (default) or "vercel" (Vercel AI Gateway) -# AI_PROVIDER=insforge - -# --- Vercel AI Gateway (only when AI_PROVIDER=vercel) --- -# Gateway auth (optional on Vercel deployments — OIDC is used automatically) -# AI_GATEWAY_API_KEY=your-gateway-api-key - -# Provider API keys — add one for each provider you want to use. -# OPENAI_API_KEY=sk-... +# Optional. Leave unset to use the model selected by the Platform runtime. +# INSFORGE_AI_MODEL=provider/model diff --git a/chatbot/AGENTS.md b/chatbot/AGENTS.md new file mode 100644 index 0000000..e0b4845 --- /dev/null +++ b/chatbot/AGENTS.md @@ -0,0 +1,23 @@ +# Platform-managed AI and Storage contract + +This template declares `ai.chat`, `ai.streaming`, and `storage`. Those runtime integrations are +part of the template contract even when an application adapts the framework, interface, or product +flow. + +## AI + +- Send model requests through the current application's InsForge Model Gateway. Prefer + `client.ai.chat.completions.create(...)` from `@insforge/sdk`. +- Omit `model` by default so the server-owned `AI_DEFAULT_MODEL` selects the configured model. + Pass an explicit model only when the product requirements demand an allowed override. +- Never read a provider API key from application or Edge Function code, and never call OpenRouter, + DeepSeek, LiteLLM, OpenAI, Anthropic, or another provider endpoint directly. +- Exercise the product's real model path before delivery. A fallback-only result does not satisfy + acceptance; verify the real model response and the explicit failure fallback separately. + +## Storage + +- Use InsForge Storage through `@insforge/sdk`. Browser and application source may contain only the + public InsForge endpoint and Anon Key. +- Never read or ship COS/S3 access keys. Bucket routing and the application prefix are owned by the + application's InsForge runtime. diff --git a/chatbot/LICENSE b/chatbot/LICENSE new file mode 100644 index 0000000..45da8d7 --- /dev/null +++ b/chatbot/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 InsForge and Lexmount contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/chatbot/README.md b/chatbot/README.md index f625089..fc00492 100644 --- a/chatbot/README.md +++ b/chatbot/README.md @@ -1,17 +1,15 @@ -

InsForge Chatbot Starter

+

Lexmount AI Chat + Storage Starter

- A Next.js chatbot starter with InsForge auth, database, storage, and optional Vercel AI Gateway support. + A Next.js starter that uses the AI model and object storage already configured by Insight Flow.

Features · - Demo · Quick Launch · Run locally · - Vercel AI Gateway · Deploy to Vercel · First Try

@@ -25,31 +23,23 @@ - [Next.js](https://nextjs.org) App Router - Streaming chat UI with persisted history and file attachments - [InsForge](https://insforge.dev) auth, database, storage, and AI -- Optional routing through [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) -- Multi-provider model selection with `provider/model` IDs +- Platform-owned AI credentials and default model; no provider key in application code +- InsForge Storage backed by the Platform-owned S3-compatible provider (Tencent COS in Lexmount) +- Optional model override with `provider/model` IDs - [shadcn/ui](https://ui.shadcn.com) components - Styling with [Tailwind CSS](https://tailwindcss.com) -## Demo - -Demo: [demochatbot.insforge.site](https://demochatbot.insforge.site) - -The starter includes a simple first-try chat experience, persisted history, file uploads, authentication, and optional routing through the Vercel AI Gateway. - ## Quick Launch -If you want the fastest path, use the InsForge CLI and follow the prompts: - -```bash -npx @insforge/cli create -``` - -From there: +In Insight Flow, choose **AI Chat + Storage Starter** when creating an application. The +Platform checks `ai.chat`, `ai.streaming`, and `storage` before provisioning, installs the +database migration and storage bucket, and generates `.env.local` with only the public InsForge +endpoint and anon key. -1. Choose the chatbot template -2. Create or connect your InsForge project -3. Let the CLI set up the project files -4. Choose to deploy with [InsForge](https://insforge.dev) automatically from the guided flow +The model selector starts at **Platform default**. That option intentionally omits the model from +the request, allowing the server-owned `AI_DEFAULT_MODEL` to select DeepSeek, LiteLLM, or another +configured OpenAI-compatible model. Files are uploaded through InsForge Storage; the application +never receives COS credentials. Use the local setup below if you want to inspect the repo, edit environment variables manually, or control the setup step by step. @@ -58,7 +48,7 @@ Use the local setup below if you want to inspect the repo, edit environment vari 1. Clone the repository and move into the chatbot template: ```bash - git clone https://github.com/InsForge/insforge-templates.git + git clone https://github.com/lexmount/insforge-templates.git cd insforge-templates/chatbot ``` @@ -85,7 +75,6 @@ Use the local setup below if you want to inspect the repo, edit environment vari ```env NEXT_PUBLIC_INSFORGE_URL=https://your-project.region.insforge.app NEXT_PUBLIC_INSFORGE_ANON_KEY=your-public-anon-key - NEXT_PUBLIC_APP_URL=http://localhost:3000 ``` 7. Apply the included schema and seed data to your InsForge project. You can either ask your agent using this prompt: @@ -108,45 +97,16 @@ Use the local setup below if you want to inspect the repo, edit environment vari 9. Open [http://localhost:3000](http://localhost:3000) -## Vercel AI Gateway - -To route AI requests through the [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) instead of InsForge AI: - -1. Enable the provider: - - ```env - AI_PROVIDER=vercel - AI_GATEWAY_API_KEY=your-gateway-api-key - ``` - - On Vercel deployments `AI_GATEWAY_API_KEY` is optional — the gateway authenticates automatically via OIDC. - -2. Add provider credentials for the models you want to use. To get started, `OPENAI_API_KEY` is enough for `openai/*` models: - - ```env - OPENAI_API_KEY=sk-... - ``` - - If you want to use other providers from the model picker, add the matching provider key in your environment or configure it in Vercel AI Gateway settings. Selecting a model whose provider key is missing will return a clear error in the chat UI — there is no silent fallback to another provider. - - Alternatively, if deploying on Vercel, you can configure provider keys in the Vercel dashboard under AI Gateway settings instead of using environment variables. - -**Current limitations:** -- PDF file parsing (`fileParser`) is InsForge-only. PDFs are forwarded as base64 file parts, but results depend on the model. A toast warning is shown when this applies. -- InsForge is still required for auth, database, storage, and file uploads regardless of AI provider. - ## Deploy to Vercel After cloning the repo and running the starter locally, you can deploy it on Vercel: -[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FInsForge%2Finsforge-templates%2Ftree%2Fmain%2Fchatbot&root-directory=chatbot&project-name=insforge-chatbot&repository-name=insforge-chatbot&env=NEXT_PUBLIC_INSFORGE_URL,NEXT_PUBLIC_INSFORGE_ANON_KEY&envDescription=Connect%20your%20InsForge%20project%20URL%20and%20anon%20key.&external-id=https%3A%2F%2Fgithub.com%2FInsForge%2Finsforge-templates%2Ftree%2Fmain%2Fchatbot&demo-title=InsForge%20Chatbot%20Starter&demo-description=A%20Next.js%20chatbot%20starter%20with%20InsForge%20auth%2C%20database%2C%20storage%2C%20and%20optional%20Vercel%20AI%20Gateway%20support.&demo-image=https%3A%2F%2Fraw.githubusercontent.com%2FInsForge%2Finsforge-templates%2Fmain%2Fchatbot%2Fpublic%2Fchatbot-readme-cover.png) +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Flexmount%2Finsforge-templates%2Ftree%2Fmain%2Fchatbot&root-directory=chatbot&project-name=insforge-chatbot&repository-name=insforge-chatbot&env=NEXT_PUBLIC_INSFORGE_URL,NEXT_PUBLIC_INSFORGE_ANON_KEY&envDescription=Connect%20your%20InsForge%20project%20URL%20and%20anon%20key.) 1. Set `NEXT_PUBLIC_INSFORGE_URL` 2. Set `NEXT_PUBLIC_INSFORGE_ANON_KEY` 3. Deploy the project -4. In Vercel, open your project, go to `Settings` → `Environment Variables`, and set `NEXT_PUBLIC_APP_URL` to your deployed app URL -5. Redeploy the project -6. In the InsForge dashboard, open `Authentication` → `General` → `Allowed Redirect URLs`, then add your deployed callback URL (for example `https://your-project.vercel.app/auth/callback`) +4. In the InsForge dashboard, open `Authentication` → `General` → `Allowed Redirect URLs`, then add your deployed callback URL (for example `https://your-project.vercel.app/auth/callback`) ## First Try diff --git a/chatbot/components/chat-shell.tsx b/chatbot/components/chat-shell.tsx index 6da8b53..e32a7ec 100644 --- a/chatbot/components/chat-shell.tsx +++ b/chatbot/components/chat-shell.tsx @@ -35,10 +35,8 @@ import { ChatMarkdown } from '@/components/chat-markdown'; import { Button } from '@/components/ui/button'; import { Textarea } from '@/components/ui/textarea'; import { - DEFAULT_MODEL, FILE_INPUT_ACCEPT, MAX_FILE_SIZE, - MODEL_OPTIONS, SUGGESTED_PROMPTS, isAllowedAttachmentFile, } from '@/lib/constants'; @@ -290,7 +288,6 @@ export function ChatShell({ initialViewer }: { initialViewer: AuthViewer }) { const [activeChatId, setActiveChatId] = useState(null); const [messages, setMessages] = useState([]); const [input, setInput] = useState(''); - const [selectedModel, setSelectedModel] = useState(DEFAULT_MODEL); const [isBootstrapping, setIsBootstrapping] = useState(true); const [isLoadingThread, setIsLoadingThread] = useState(false); const [isSending, setIsSending] = useState(false); @@ -646,7 +643,6 @@ export function ChatShell({ initialViewer }: { initialViewer: AuthViewer }) { ...ownerInfo.bodyField, chatId: activeChatId, input: trimmedInput, - model: selectedModel, attachments: currentAttachments.length > 0 ? currentAttachments : undefined, }), }); @@ -1089,20 +1085,6 @@ export function ChatShell({ initialViewer }: { initialViewer: AuthViewer }) { {isUploading ? : } Attach file -
- - -
+ + {step === 'otp' ? ( + + ) : null} + + + ); +} diff --git a/insight-flow-agent-chat/src/components/ChatPage.tsx b/insight-flow-agent-chat/src/components/ChatPage.tsx new file mode 100644 index 0000000..dd5ed66 --- /dev/null +++ b/insight-flow-agent-chat/src/components/ChatPage.tsx @@ -0,0 +1,251 @@ +import { + Bot, + Check, + Copy, + Menu, + MessageSquarePlus, + PanelLeftClose, + Send, + Settings, + Square, + UserRound, + X, +} from 'lucide-react'; +import { useEffect, useRef, useState } from 'react'; +import type { FormEvent, KeyboardEvent } from 'react'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import { functionErrorMessage, loadAgentConfig } from '../lib/config'; +import type { AgentConfig } from '../lib/config'; +import { streamAgentReply } from '../lib/stream'; + +type ChatMessage = { id: string; role: 'user' | 'assistant'; content: string }; +type ChatPageProps = { + email?: string; + navigate: (path: string) => void; + onSignOut: () => void; +}; + +const starters = [ + '介绍一下你能帮我完成什么', + '帮我分析一个复杂问题', + '先问我几个问题,再开始执行', +]; + +function messageId(role: string) { + return `${role}-${crypto.randomUUID()}`; +} + +export function ChatPage({ email, navigate, onSignOut }: ChatPageProps) { + const [config, setConfig] = useState(null); + const [configLoading, setConfigLoading] = useState(true); + const [messages, setMessages] = useState([]); + const [message, setMessage] = useState(''); + const [sessionKey, setSessionKey] = useState(null); + const [isStreaming, setIsStreaming] = useState(false); + const [error, setError] = useState(''); + const [warning, setWarning] = useState(''); + const [sidebarOpen, setSidebarOpen] = useState(false); + const [copiedId, setCopiedId] = useState(null); + const abortRef = useRef(null); + const endRef = useRef(null); + + useEffect(() => { + let active = true; + loadAgentConfig() + .then((next) => active && setConfig(next)) + .catch((reason) => active && setError(functionErrorMessage(reason, '无法读取 Agent 配置。'))) + .finally(() => active && setConfigLoading(false)); + return () => { active = false; abortRef.current?.abort(); }; + }, []); + + useEffect(() => { + endRef.current?.scrollIntoView({ behavior: isStreaming ? 'auto' : 'smooth' }); + }, [messages, isStreaming]); + + function newChat() { + abortRef.current?.abort(); + setMessages([]); + setMessage(''); + setSessionKey(null); + setError(''); + setWarning(''); + setIsStreaming(false); + setSidebarOpen(false); + } + + async function sendMessage(raw: string) { + const content = raw.trim(); + if (!content || isStreaming || !config?.configured) return; + const assistantId = messageId('assistant'); + setMessages((current) => [ + ...current, + { id: messageId('user'), role: 'user', content }, + { id: assistantId, role: 'assistant', content: '' }, + ]); + setMessage(''); + setError(''); + setWarning(''); + setIsStreaming(true); + const controller = new AbortController(); + abortRef.current = controller; + + try { + const nextSessionKey = await streamAgentReply( + { message: content, ...(sessionKey ? { sessionKey } : {}) }, + controller.signal, + (delta) => setMessages((current) => current.map((item) => ( + item.id === assistantId ? { ...item, content: item.content + delta } : item + ))), + (headerSessionKey) => { + if (headerSessionKey) setSessionKey(headerSessionKey); + }, + ); + if (nextSessionKey) setSessionKey(nextSessionKey); + else setWarning('未收到会话标识,下一条消息可能会开启新会话。'); + } catch (reason) { + if (controller.signal.aborted) { + setMessages((current) => current.map((item) => ( + item.id === assistantId && !item.content ? { ...item, content: '已停止生成。' } : item + ))); + } else { + const detail = reason instanceof Error ? reason.message : 'Agent 请求失败。'; + setError(detail); + setMessages((current) => current.filter((item) => item.id !== assistantId || Boolean(item.content))); + } + } finally { + if (abortRef.current === controller) abortRef.current = null; + setIsStreaming(false); + } + } + + function submit(event: FormEvent) { + event.preventDefault(); + void sendMessage(message); + } + + function onComposerKeyDown(event: KeyboardEvent) { + if (event.key === 'Enter' && !event.shiftKey) { + event.preventDefault(); + void sendMessage(message); + } + } + + async function copyMessage(item: ChatMessage) { + try { + await navigator.clipboard.writeText(item.content); + setCopiedId(item.id); + window.setTimeout(() => setCopiedId(null), 1400); + } catch { + setError('复制失败,请手动选择回复内容。'); + } + } + + const targetLabel = config?.target + ? config.targetMode === 'agent' + ? config.target + : config.target.includes(':') ? config.target : `goclaw:${config.target}` + : 'Insight Flow Agent'; + + return ( +
+ {sidebarOpen ? + + +
+ 今天 + {messages.length > 0 ? ( + + ) :

对话记录仅保留在当前页面。

} +
+
+ + +
+ + +
+
+ +
{targetLabel}{config?.configured ? 已连接 : null}
+ +
+ +
+ {configLoading ? ( +
+ ) : !config?.configured ? ( +
+ +

先连接你的 Agent

+

在独立设置页保存 Insight Flow 连接。API Key 默认隐藏,聊天页面不会显示它。

+ +
+ ) : messages.length === 0 ? ( +
+ +

有什么可以帮忙的?

+
+ {starters.map((starter) => )} +
+
+ ) : ( +
+ {messages.map((item) => ( +
+ {item.role === 'assistant' ? : null} +
+ {item.role === 'assistant' ? ( + item.content ? {item.content} : + ) :

{item.content}

} + {item.role === 'assistant' && item.content ? ( + + ) : null} +
+
+ ))} +
+
+ )} +
+ +
+ {warning ?
{warning}
: null} + {error ?
{error}
: null} +
+