From 7c8fe96446df085e7103b4c377947efb20870b3f Mon Sep 17 00:00:00 2001 From: tianma Date: Wed, 1 Jul 2026 12:06:23 +0800 Subject: [PATCH] Replace profile feature with agent prompts --- README.md | 2 +- apps/api/src/index.ts | 389 --------------------- apps/web/src/components/SettingsPane.tsx | 323 ++++++++--------- apps/web/src/lib/api.ts | 13 - docs/openapi.json | 140 -------- migrations/0008_profile_snapshots.sql | 12 - migrations/0009_drop_profile_snapshots.sql | 1 + packages/shared/src/types.ts | 28 -- wrangler.toml | 3 - 9 files changed, 155 insertions(+), 756 deletions(-) delete mode 100644 migrations/0008_profile_snapshots.sql create mode 100644 migrations/0009_drop_profile_snapshots.sql diff --git a/README.md b/README.md index be661fcc..3b87c172 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ packages/shared 共享类型、Zod schema、TipTap / Markdown 内容转换 scripts Wrangler 封装、密码 hash、CLI、MCP stdio bridge、Evernote ENEX 导入 migrations D1 数据库迁移 docs OpenAPI schema、迁移指南等文档 -wrangler.toml Cloudflare Workers、Assets、D1、R2、Workers AI 配置 +wrangler.toml Cloudflare Workers、Assets、D1、R2 配置 ``` ## 内容格式 diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 5d6ce4ee..a4dce8d0 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -26,8 +26,6 @@ import { type Resource, type ResourceListItem, type ResourceStorageSummary, - type ProfileSnapshot, - type ProfileSection, type TagSummary, type TiptapDoc, } from "@edgeever/shared"; @@ -41,12 +39,10 @@ import openApiSpec from "../../../docs/openapi.json"; type Bindings = { DB: D1Database; RESOURCES: R2Bucket; - AI?: Ai; EDGE_EVER_AUTH_USERNAME?: string; EDGE_EVER_AUTH_PASSWORD_HASH?: string; EDGE_EVER_SESSION_TTL_DAYS?: string; EDGE_EVER_R2_BUCKET_NAME?: string; - EDGE_EVER_PROFILE_MODEL?: string; }; type AuthContext = { @@ -188,24 +184,6 @@ type ResourceStatsRow = { attachment_count: number; }; -type ProfileSnapshotRow = { - id: string; - profile_json: string; - source_memo_ids_json: string; - memo_count: number; - model: string; - generated_at: string; - created_at: string; -}; - -type ProfileSourceMemoRow = { - id: string; - title: string | null; - tags_json: string; - updated_at: string; - content_text: string; -}; - type AppContext = Context<{ Bindings: Bindings; Variables: { auth: AuthContext } }>; const SESSION_COOKIE = "edgeever_session"; @@ -216,10 +194,6 @@ const PASSWORD_SALT_BYTES = 16; const SESSION_TOKEN_BYTES = 32; const DEFAULT_SESSION_TTL_DAYS = 30; const DEFAULT_R2_BUCKET_NAME = "edgeever-resources"; -const DEFAULT_PROFILE_MODEL = "@cf/zai-org/glm-5.2"; -const PROFILE_MEMO_LIMIT = 200; -const PROFILE_MEMO_TEXT_LIMIT = 1200; -const PROFILE_CHUNK_CHAR_LIMIT = 12000; const MAX_IMAGE_UPLOAD_BYTES = 100 * 1024 * 1024; const MAX_ATTACHMENT_UPLOAD_BYTES = 100 * 1024 * 1024; const REVISION_SNAPSHOT_INTERVAL_MS = 5 * 60 * 1000; @@ -234,8 +208,6 @@ const ALL_TOKEN_SCOPES = [ "write:resources", "read:tags", "write:tags", - "read:profile", - "write:profile", ] as const; type TokenScope = (typeof ALL_TOKEN_SCOPES)[number]; const SUPPORTED_IMAGE_MIME_TYPES = new Set([ @@ -611,41 +583,6 @@ app.delete("/api/v1/tags/:tag", async (c) => { return c.json({ ok: true, updated }); }); -app.get("/api/v1/profile", async (c) => { - const denied = requireScopes(c, "read:profile"); - - if (denied) { - return denied; - } - - return c.json({ profile: await getLatestProfileSnapshot(c.env.DB) }); -}); - -app.post("/api/v1/profile/generate", async (c) => { - const denied = requireScopes(c, "write:profile"); - - if (denied) { - return denied; - } - - if (!c.env.AI) { - return apiError(c, "ai_not_configured", "Workers AI binding is not configured for this deployment.", 501); - } - - const actor = getAuditActor(c); - - try { - const profile = await generateProfileSnapshot(c.env.DB, c.env.AI, c.env.EDGE_EVER_PROFILE_MODEL, actor); - return c.json({ profile }, 201); - } catch (error) { - if (error instanceof AppError) { - return apiError(c, error.code, error.message, error.status); - } - - throw error; - } -}); - app.get("/api/v1/memos", async (c) => { const denied = requireScopes(c, "read:memos"); @@ -2590,332 +2527,6 @@ const mapTagSummary = (row: TagSummaryRow): TagSummary => ({ updatedAt: row.updated_at, }); -const mapProfileSnapshot = (row: ProfileSnapshotRow): ProfileSnapshot => { - const profile = parseProfileJson(row.profile_json); - - return { - id: row.id, - summary: profile.summary, - sections: profile.sections, - sourceMemoIds: parseJsonArray(row.source_memo_ids_json), - memoCount: row.memo_count, - model: row.model, - generatedAt: row.generated_at, - createdAt: row.created_at, - }; -}; - -const getLatestProfileSnapshot = async (db: D1Database): Promise => { - const row = await db - .prepare( - `SELECT id, profile_json, source_memo_ids_json, memo_count, model, generated_at, created_at - FROM profile_snapshots - ORDER BY created_at DESC - LIMIT 1` - ) - .first(); - - return row ? mapProfileSnapshot(row) : null; -}; - -const generateProfileSnapshot = async ( - db: D1Database, - ai: Ai, - modelOverride: string | undefined, - actor: AuditActor -): Promise => { - const memos = await listProfileSourceMemos(db); - - if (memos.length === 0) { - throw new AppError("no_profile_sources", "At least one non-empty memo is required to generate a profile.", 400); - } - - const model = modelOverride?.trim() || DEFAULT_PROFILE_MODEL; - const chunks = chunkProfileMemos(memos); - const chunkSummaries: string[] = []; - - for (let index = 0; index < chunks.length; index += 1) { - const summary = await runAiText(ai, model, [ - { - role: "system", - content: - "你是一个谨慎的个人知识管理分析助手。只基于用户笔记做事实归纳,不做心理诊断,不使用夸张或确定性人格判断。", - }, - { - role: "user", - content: - "请把下面这组笔记压缩成画像证据摘要。保留主题、项目、偏好、能力、反复出现的问题和可引用的 memo_id。输出中文,控制在 900 字以内。\n\n" + - chunks[index], - }, - ]); - chunkSummaries.push(summary); - } - - const profileText = await runAiText(ai, model, [ - { - role: "system", - content: - "你是 EdgeEver 的人物画像生成器。必须输出严格 JSON,不要 Markdown。画像应可追溯、克制、尊重用户隐私。避免医学、心理诊断、职业定性和无法从笔记支持的结论。", - }, - { - role: "user", - content: - `请基于这些证据摘要生成人物画像 JSON。可引用的 memo_id 只能来自证据摘要。JSON schema: -{ - "summary": "一段 80-160 字中文总览", - "sections": [ - { - "title": "关注主题", - "insights": [ - { - "label": "短标签", - "description": "基于笔记的克制描述", - "confidence": "low|medium|high", - "evidence": { "memoIds": ["memo_x"], "note": "为什么引用这些笔记" } - } - ] - } - ] -} -请包含 4-6 个 section,优先使用这些 section 名称:关注主题、项目脉络、能力画像、偏好与原则、行为模式、近期动向。每个 section 2-5 条 insight。不要虚构 memo_id。\n\n证据摘要:\n` + - chunkSummaries.map((summary, index) => `【证据摘要 ${index + 1}】\n${summary}`).join("\n\n"), - }, - ]); - - const profile = normalizeProfileOutput(profileText, memos.map((memo) => memo.id)); - const id = createId("profile"); - const now = isoNow(); - const sourceMemoIds = Array.from(new Set(profile.sections.flatMap((section) => section.insights.flatMap((insight) => insight.evidence.memoIds)))); - const finalSourceMemoIds = sourceMemoIds.length > 0 ? sourceMemoIds : memos.map((memo) => memo.id); - - await db.batch([ - db - .prepare( - `INSERT INTO profile_snapshots ( - id, profile_json, source_memo_ids_json, memo_count, model, generated_at, created_at - ) VALUES (?, ?, ?, ?, ?, ?, ?)` - ) - .bind(id, JSON.stringify(profile), JSON.stringify(finalSourceMemoIds), memos.length, model, now, now), - auditStatement(db, actor.actorType, actor.actorId, "profile.generate", "profile", id, { - memoCount: memos.length, - model, - }), - ]); - - const row = await db - .prepare( - `SELECT id, profile_json, source_memo_ids_json, memo_count, model, generated_at, created_at - FROM profile_snapshots - WHERE id = ?` - ) - .bind(id) - .first(); - - if (!row) { - throw new AppError("not_found", "Profile snapshot not found after generation.", 404); - } - - return mapProfileSnapshot(row); -}; - -const listProfileSourceMemos = async (db: D1Database): Promise => { - const rows = await db - .prepare( - `SELECT m.id, m.title, m.tags_json, m.updated_at, c.content_text - FROM memos m - INNER JOIN memo_contents c ON c.memo_id = m.id - WHERE m.is_deleted = 0 - AND length(trim(c.content_text)) >= 20 - AND m.created_by <> 'system' - ORDER BY m.updated_at DESC - LIMIT ?` - ) - .bind(PROFILE_MEMO_LIMIT) - .all(); - - return rows.results; -}; - -const chunkProfileMemos = (memos: ProfileSourceMemoRow[]) => { - const chunks: string[] = []; - let current = ""; - - for (const memo of memos) { - const tags = parseJsonArray(memo.tags_json); - const text = memo.content_text.replace(/\s+/g, " ").trim().slice(0, PROFILE_MEMO_TEXT_LIMIT); - const entry = [ - `memo_id: ${memo.id}`, - `title: ${memo.title || DEFAULT_MEMO_TITLE}`, - `updated_at: ${memo.updated_at}`, - `tags: ${tags.join(", ") || "无"}`, - `content: ${text}`, - ].join("\n"); - const next = current ? `${current}\n\n---\n\n${entry}` : entry; - - if (next.length > PROFILE_CHUNK_CHAR_LIMIT && current) { - chunks.push(current); - current = entry; - } else { - current = next; - } - } - - if (current) { - chunks.push(current); - } - - return chunks; -}; - -const runAiText = async ( - ai: Ai, - model: string, - messages: { role: "system" | "user"; content: string }[] -): Promise => { - let result: unknown; - - try { - result = await ai.run(model, { - messages, - max_tokens: 1800, - temperature: 0.2, - }); - } catch (error) { - throw new AppError( - "ai_request_failed", - error instanceof Error ? `Workers AI request failed: ${error.message}` : "Workers AI request failed.", - 502 - ); - } - - const response = extractAiText(result); - - if (!response) { - throw new AppError("ai_response_invalid", "Workers AI returned an empty response.", 502); - } - - return response; -}; - -const extractAiText = (value: unknown) => { - if (typeof value === "string") { - return value.trim(); - } - - if (!value || typeof value !== "object") { - return ""; - } - - const record = value as Record; - - if (typeof record.response === "string") { - return record.response.trim(); - } - - if (typeof record.result === "string") { - return record.result.trim(); - } - - return ""; -}; - -const normalizeProfileOutput = (text: string, allowedMemoIds: string[]) => { - const parsed = parseJsonObject(extractJsonObject(text)); - const allowed = new Set(allowedMemoIds); - const summary = typeof parsed.summary === "string" ? parsed.summary.trim().slice(0, 360) : ""; - const rawSections = Array.isArray(parsed.sections) ? parsed.sections : []; - const sections: ProfileSection[] = rawSections - .map((section) => normalizeProfileSection(section, allowed)) - .filter((section): section is ProfileSection => Boolean(section)); - - if (!summary || sections.length === 0) { - throw new AppError("ai_response_invalid", "Workers AI returned a profile that could not be parsed.", 502); - } - - return { - summary, - sections: sections.slice(0, 6), - }; -}; - -const normalizeProfileSection = (value: unknown, allowedMemoIds: Set): ProfileSection | null => { - if (!value || typeof value !== "object") { - return null; - } - - const record = value as Record; - const title = typeof record.title === "string" ? record.title.trim().slice(0, 40) : ""; - const rawInsights = Array.isArray(record.insights) ? record.insights : []; - const insights = rawInsights - .map((insight) => normalizeProfileInsight(insight, allowedMemoIds)) - .filter((insight): insight is ProfileSection["insights"][number] => Boolean(insight)) - .slice(0, 5); - - if (!title || insights.length === 0) { - return null; - } - - return { title, insights }; -}; - -const normalizeProfileInsight = (value: unknown, allowedMemoIds: Set): ProfileSection["insights"][number] | null => { - if (!value || typeof value !== "object") { - return null; - } - - const record = value as Record; - const evidence = record.evidence && typeof record.evidence === "object" ? (record.evidence as Record) : {}; - const memoIds = (Array.isArray(evidence.memoIds) ? evidence.memoIds : []) - .filter((memoId): memoId is string => typeof memoId === "string" && allowedMemoIds.has(memoId)) - .slice(0, 8); - const confidence = record.confidence === "low" || record.confidence === "high" ? record.confidence : "medium"; - const label = typeof record.label === "string" ? record.label.trim().slice(0, 60) : ""; - const description = typeof record.description === "string" ? record.description.trim().slice(0, 360) : ""; - - if (!label || !description) { - return null; - } - - return { - label, - description, - confidence, - evidence: { - memoIds, - note: typeof evidence.note === "string" ? evidence.note.trim().slice(0, 160) : undefined, - }, - }; -}; - -const parseProfileJson = (json: string): Pick => { - const value = parseJsonObject(json); - const summary = typeof value.summary === "string" ? value.summary : ""; - const sections = Array.isArray(value.sections) ? (value.sections as ProfileSection[]) : []; - return { summary, sections }; -}; - -const extractJsonObject = (text: string) => { - const fenced = /```(?:json)?\s*([\s\S]*?)```/i.exec(text); - const candidate = fenced?.[1] ?? text; - const start = candidate.indexOf("{"); - const end = candidate.lastIndexOf("}"); - - if (start === -1 || end === -1 || end <= start) { - return candidate; - } - - return candidate.slice(start, end + 1); -}; - -const parseJsonObject = (json: string): Record => { - try { - const value = JSON.parse(json); - return value && typeof value === "object" && !Array.isArray(value) ? (value as Record) : {}; - } catch { - return {}; - } -}; - const getApiTokenRow = async (db: D1Database, id: string): Promise => db .prepare( diff --git a/apps/web/src/components/SettingsPane.tsx b/apps/web/src/components/SettingsPane.tsx index ac36d54b..cdc052dd 100644 --- a/apps/web/src/components/SettingsPane.tsx +++ b/apps/web/src/components/SettingsPane.tsx @@ -1,14 +1,14 @@ import { useEffect, useState, type FormEvent } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { Brain, ChevronLeft, Copy, HelpCircle, Image, KeyRound, LogOut, Plus, RefreshCw, ShieldCheck, Sparkles, Trash2, UploadCloud, User } from "lucide-react"; -import type { ApiToken, AuthUser, ProfileSnapshot } from "@edgeever/shared"; +import { ChevronDown, ChevronLeft, Copy, HelpCircle, Image, KeyRound, LogOut, Plus, ShieldCheck, Sparkles, Trash2, UploadCloud, User } from "lucide-react"; +import type { ApiToken, AuthUser } from "@edgeever/shared"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Checkbox } from "@/components/ui/checkbox"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; -import { ApiRequestError, api } from "@/lib/api"; +import { api } from "@/lib/api"; import { EVERNOTE_MIGRATION_PATH } from "@/lib/routes"; import { cn, formatDateTime } from "@/lib/utils"; import { AppConfirmDialog } from "./dialogs/ConfirmDialogs"; @@ -22,8 +22,6 @@ const ALL_TOKEN_SCOPES = [ "write:resources", "read:tags", "write:tags", - "read:profile", - "write:profile", ]; const TOKEN_SCOPE_LABELS: Record = { @@ -35,8 +33,6 @@ const TOKEN_SCOPE_LABELS: Record = { "write:resources": "上传与修改附件", "read:tags": "读取标签", "write:tags": "创建与修改标签", - "read:profile": "读取人物画像", - "write:profile": "生成人物画像", }; const getTokenScopeLabel = (scope: string) => TOKEN_SCOPE_LABELS[scope] ?? scope; @@ -134,125 +130,100 @@ const PreferenceCard = ({ imageCompressionEnabled, onImageCompressionChange }: P ); -interface PersonaProfileCardProps { - profile: ProfileSnapshot | null; - isLoading: boolean; - isGenerating: boolean; - errorMessage?: string; - onGenerate: () => void; -} +const ADVANCED_PROMPTS = [ + { + title: "人物画像", + prompt: + "请通过 EdgeEver MCP 读取我的笔记,基于真实笔记内容为我整理一份人物画像。请只根据笔记中的证据判断,不要做心理诊断,不要夸张定性。输出包括:长期关注的主题、做事偏好、能力线索、反复出现的问题、近期动向,并在每条结论后列出相关笔记标题或 memo id。", + }, + { + title: "近期复盘", + prompt: + "请通过 EdgeEver MCP 读取我最近更新的笔记,帮我做一次近期复盘。请总结我最近在推进什么、卡在哪里、有哪些反复出现的想法、哪些事情值得继续跟进,最后给出一份简短的下一步行动清单。", + }, + { + title: "项目整理", + prompt: + "请通过 EdgeEver MCP 读取我的笔记,找出其中和项目、产品、代码、计划相关的内容。请按项目归类,整理每个项目的目标、当前进展、待办事项、风险和相关笔记来源。不要改动我的笔记,只输出整理结果。", + }, + { + title: "知识地图", + prompt: + "请通过 EdgeEver MCP 读取我的笔记,为我整理一份知识地图。请找出主要知识领域、每个领域下的关键概念、相关笔记、我已经掌握的部分和还需要补齐的问题。输出结构要适合后续继续学习和写作。", + }, + { + title: "标签建议", + prompt: + "请通过 EdgeEver MCP 读取我的笔记和现有标签,帮我设计一套更清晰的标签体系。请指出重复、过细、过宽或命名不一致的标签,并给出合并、重命名和新增标签建议。先不要修改笔记,等我确认后再执行。", + }, +] as const; -const confidenceLabel: Record = { - low: "线索较弱", - medium: "有一定依据", - high: "依据较多", -}; +const AdvancedPlayCard = () => { + const [copiedPrompt, setCopiedPrompt] = useState(null); + const [expanded, setExpanded] = useState(false); -const getProfileErrorMessage = (error: unknown) => { - if (!error) { - return undefined; - } - - if (error instanceof ApiRequestError) { - if (error.code === "ai_request_failed") { - return "生成失败了,请稍后再试。"; + const handleCopyPrompt = async (title: string, prompt: string) => { + if (!(await copyTextToClipboard(prompt))) { + return; } - if (error.code === "ai_not_configured") { - return "生成功能暂时不可用。"; - } + setCopiedPrompt(title); + window.setTimeout(() => { + setCopiedPrompt((current) => (current === title ? null : current)); + }, 1600); + }; - if (error.code === "no_profile_sources") { - return "还没有足够的有效笔记用于生成人物画像。"; - } - } - - return error instanceof Error ? error.message : "人物画像请求失败。"; -}; - -const PersonaProfileCard = ({ profile, isLoading, isGenerating, errorMessage, onGenerate }: PersonaProfileCardProps) => ( - - -
-
- - - 人物画像 - - - 基于现有笔记手动生成,结果会缓存为快照。 - -
- -
-
- - {errorMessage && ( -
- {errorMessage} -
- )} - - {isLoading ? ( -
- 正在读取画像快照... -
- ) : profile ? ( -
-
-

{profile.summary}

-
- 分析笔记 {profile.memoCount} 条 - {formatDateTime(profile.generatedAt)} - - {profile.model} - -
-
-
- {profile.sections.map((section) => ( -
-

{section.title}

-
- {section.insights.map((insight) => ( -
-
-
{insight.label}
- - {confidenceLabel[insight.confidence]} - -
-

{insight.description}

- {insight.evidence.memoIds.length > 0 && ( -
- 来源 {insight.evidence.memoIds.length} 条笔记 -
- )} -
- ))} -
+ + + + 进阶玩法 + + + 搭配AI Agent的进阶玩法。 + + + + + + {expanded && ( + + {ADVANCED_PROMPTS.map((item) => ( +
+
+
{item.title}
+
- ))} -
-
- ) : ( -
-
还没有人物画像快照
-
+

{item.prompt}

+
+ ))} + )} - - -); + + ); +}; interface EvernoteImportGuideCardProps { onShowGuide?: () => void; @@ -454,40 +425,72 @@ interface ScopePickerProps { } const ScopePicker = ({ availableScopes, selectedScopes, onToggleScope }: ScopePickerProps) => ( -
- Token 权限范围 -
- {availableScopes.map((scope) => { - const checked = selectedScopes.has(scope); - const checkboxId = `token-scope-${scope.replace(/[^a-z0-9]+/gi, "-")}`; - - return ( - - ); - })} -
-
+ ); +const ScopePickerContent = ({ availableScopes, selectedScopes, onToggleScope }: ScopePickerProps) => { + const [expanded, setExpanded] = useState(false); + + return ( +
+ + {expanded && ( +
+ {availableScopes.map((scope) => { + const checked = selectedScopes.has(scope); + const checkboxId = `token-scope-${scope.replace(/[^a-z0-9]+/gi, "-")}`; + + return ( + + ); + })} +
+ )} +
+ ); +}; + interface TokenListProps { tokens: ApiToken[]; isLoading: boolean; @@ -732,14 +735,8 @@ export const SettingsPane = ({ queryFn: () => api.listApiTokens(), }); - const profileQuery = useQuery({ - queryKey: ["profile"], - queryFn: () => api.getProfile(), - }); - const availableScopes = tokensQuery.data?.availableScopes ?? ALL_TOKEN_SCOPES; const tokens = tokensQuery.data?.apiTokens ?? []; - const profile = profileQuery.data?.profile ?? null; useEffect(() => { if (scopeDefaultsSynced || !tokensQuery.data?.availableScopes) { @@ -767,14 +764,6 @@ export const SettingsPane = ({ }, }); - const generateProfileMutation = useMutation({ - mutationFn: api.generateProfile, - onSuccess: async (data) => { - queryClient.setQueryData(["profile"], data); - await queryClient.invalidateQueries({ queryKey: ["profile"] }); - }, - }); - const toggleScope = (scope: string) => { setSelectedScopes((current) => { const next = new Set(current); @@ -829,13 +818,7 @@ export const SettingsPane = ({ imageCompressionEnabled={imageCompressionEnabled} onImageCompressionChange={onImageCompressionChange} /> - generateProfileMutation.mutate()} - /> + request("/api/v1/profile"), - - generateProfile: () => - request("/api/v1/profile/generate", { - method: "POST", - body: JSON.stringify({}), - }), - listApiTokens: () => request("/api/v1/api-tokens"), createApiToken: (payload: { name: string; scopes: string[]; expiresAt?: string | null }) => diff --git a/docs/openapi.json b/docs/openapi.json index 4198323a..66551e80 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -273,43 +273,6 @@ } } }, - "/api/v1/profile": { - "get": { - "summary": "Read latest generated profile snapshot", - "responses": { - "200": { - "description": "Latest profile snapshot", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProfileResponse" - } - } - } - } - } - } - }, - "/api/v1/profile/generate": { - "post": { - "summary": "Generate a profile snapshot with Workers AI", - "responses": { - "201": { - "description": "Generated profile snapshot", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProfileResponse" - } - } - } - }, - "501": { - "$ref": "#/components/responses/Error" - } - } - } - }, "/api/v1/api-tokens": { "get": { "summary": "List API tokens", @@ -1396,109 +1359,6 @@ }, "required": ["name"] }, - "ProfileResponse": { - "type": "object", - "properties": { - "profile": { - "oneOf": [ - { - "$ref": "#/components/schemas/ProfileSnapshot" - }, - { - "type": "null" - } - ] - } - }, - "required": ["profile"] - }, - "ProfileSnapshot": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "summary": { - "type": "string" - }, - "sections": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ProfileSection" - } - }, - "sourceMemoIds": { - "type": "array", - "items": { - "type": "string" - } - }, - "memoCount": { - "type": "integer" - }, - "model": { - "type": "string" - }, - "generatedAt": { - "type": "string", - "format": "date-time" - }, - "createdAt": { - "type": "string", - "format": "date-time" - } - }, - "required": ["id", "summary", "sections", "sourceMemoIds", "memoCount", "model", "generatedAt", "createdAt"] - }, - "ProfileSection": { - "type": "object", - "properties": { - "title": { - "type": "string" - }, - "insights": { - "type": "array", - "items": { - "$ref": "#/components/schemas/ProfileInsight" - } - } - }, - "required": ["title", "insights"] - }, - "ProfileInsight": { - "type": "object", - "properties": { - "label": { - "type": "string" - }, - "description": { - "type": "string" - }, - "confidence": { - "type": "string", - "enum": ["low", "medium", "high"] - }, - "evidence": { - "$ref": "#/components/schemas/ProfileEvidence" - } - }, - "required": ["label", "description", "confidence", "evidence"] - }, - "ProfileEvidence": { - "type": "object", - "properties": { - "memoIds": { - "type": "array", - "items": { - "type": "string" - } - }, - "note": { - "type": "string" - } - }, - "required": ["memoIds"] - }, "ApiToken": { "type": "object", "properties": { diff --git a/migrations/0008_profile_snapshots.sql b/migrations/0008_profile_snapshots.sql deleted file mode 100644 index d6673f68..00000000 --- a/migrations/0008_profile_snapshots.sql +++ /dev/null @@ -1,12 +0,0 @@ -CREATE TABLE profile_snapshots ( - id TEXT PRIMARY KEY, - profile_json TEXT NOT NULL CHECK (json_valid(profile_json)), - source_memo_ids_json TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(source_memo_ids_json)), - memo_count INTEGER NOT NULL DEFAULT 0 CHECK (memo_count >= 0), - model TEXT NOT NULL, - generated_at TEXT NOT NULL, - created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) -); - -CREATE INDEX idx_profile_snapshots_created - ON profile_snapshots(created_at DESC); diff --git a/migrations/0009_drop_profile_snapshots.sql b/migrations/0009_drop_profile_snapshots.sql new file mode 100644 index 00000000..e8aa9234 --- /dev/null +++ b/migrations/0009_drop_profile_snapshots.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS profile_snapshots; diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index 6b309c76..b0cac089 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -104,34 +104,6 @@ export type TagSummary = { updatedAt: string | null; }; -export type ProfileEvidence = { - memoIds: string[]; - note?: string; -}; - -export type ProfileInsight = { - label: string; - description: string; - confidence: "low" | "medium" | "high"; - evidence: ProfileEvidence; -}; - -export type ProfileSection = { - title: string; - insights: ProfileInsight[]; -}; - -export type ProfileSnapshot = { - id: string; - summary: string; - sections: ProfileSection[]; - sourceMemoIds: string[]; - memoCount: number; - model: string; - generatedAt: string; - createdAt: string; -}; - export type AuthUser = { id: string; username: string; diff --git a/wrangler.toml b/wrangler.toml index b98d0f31..2c059b57 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -20,8 +20,5 @@ binding = "RESOURCES" bucket_name = "edgeever-resources" preview_bucket_name = "edgeever-resources-preview" -[ai] -binding = "AI" - [observability] enabled = true