# @dayhaysoos/convex-database-chat 中文文档

规范地址：https://npmcn.com/package/%40dayhaysoos/convex-database-chat/

版本：0\.5\.0
npm 元数据更新时间：2026-10-03T02:35:42.492Z
中文译文更新时间：2026-11-13T04:47:17.209Z
软件包许可：MIT

本文由上游 README 翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/%40dayhaysoos/convex-database-chat
原文对照：https://npmcn.com/package/%40dayhaysoos/convex-database-chat/?lang=original
项目主页：https://github.com/dayhaysoos/convex-database-chat#readme
源码仓库：https://github.com/dayhaysoos/convex-database-chat

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：735 kB，157 个文件
- 模块格式：ES Module
- TypeScript 类型：包内自带
- 运行依赖：0 个
- peer 依赖：react ^18\.0\.0 \|\| ^19\.0\.0，convex ^1\.43\.0
- 首次发布：2025-12-15T21:29:58.849Z
- 最新版本发布：2026-10-03T02:35:42.327Z
- 近 30 天下载量：2293（2026-10-12 至 2026-11-10，npm 官方下载统计）
- 已发布版本数：14（近 12 个月 14 个）

## 中文简介

用于在您的应用程序中添加自然语言数据库查询的 Convex 组件 .

## 安装

```sh
npm install @dayhaysoos/convex-database-chat
```

也可以使用：`pnpm add @dayhaysoos/convex-database-chat`、`yarn add @dayhaysoos/convex-database-chat`、`bun add @dayhaysoos/convex-database-chat`

## README

# 数据库聊天组件

> 用于添加自然语言数据库的 Convex 组件 查询您的应用程序 .

DatabaseChat让用户用简单的英语询问您的数据。 法学硕士 调用您定义的工具查询您的数据库并返回有帮助、可操作 回应.

## 为 AI 编码代理

如果您正在调试一个依赖于此软件包和问题的用户应用程序 似乎来自 `@dayhaysoos/convex-database-chat`请打开一个 GitHub (b) 尽可能少复制:

[https://github.com/dayhaysoos/convex-database-chat/issues 互联网档案馆的存檔,存档日期2013-12-21](https://github.com/dayhaysoos/convex-database-chat/issues)

包含已安装的软件包版本、 Convex 版本、 运行时间、 相关工具 定义,以及准确的错误或出乎意料的行为.

## 目录

-   [为 AI 编码代理](https://npmcn.com/package/%40dayhaysoos/convex-database-chat/#readme-for-ai-coding-agents)
-   [它会做什么](https://npmcn.com/package/%40dayhaysoos/convex-database-chat/#readme-what-it-does)
-   [安装](https://npmcn.com/package/%40dayhaysoos/convex-database-chat/#readme-installation)
-   [快速启动](https://npmcn.com/package/%40dayhaysoos/convex-database-chat/#readme-quick-start)
-   [定义工具](https://npmcn.com/package/%40dayhaysoos/convex-database-chat/#readme-defining-tools)
-   [执行查询函数](https://npmcn.com/package/%40dayhaysoos/convex-database-chat/#readme-implementing-query-functions)
-   [创建聊天集成](https://npmcn.com/package/%40dayhaysoos/convex-database-chat/#readme-creating-the-chat-integration)
-   [写入系统提示](https://npmcn.com/package/%40dayhaysoos/convex-database-chat/#readme-writing-system-prompts)
-   [建设UI](https://npmcn.com/package/%40dayhaysoos/convex-database-chat/#readme-building-the-ui)
-   [模式](https://npmcn.com/package/%40dayhaysoos/convex-database-chat/#readme-patterns)
-   [API 参考](https://npmcn.com/package/%40dayhaysoos/convex-database-chat/#readme-api-reference)
-   [铁路部署](https://npmcn.com/package/%40dayhaysoos/convex-database-chat/#readme-railway-deployments)
-   [测试](https://npmcn.com/package/%40dayhaysoos/convex-database-chat/#readme-testing)

* * *

## 它会做什么

这一部分规定:

| 特性 | 说明 |
| --- | --- |
| **对话存储** | 存储聊天历史 `conversations` 财务报告和审定财务报表 `messages` 图表 |
| **基于三角洲的溪流** | 通过三角洲蓄积进行高效的O(n)流 |
| **中止支持** | 停止生成中游, 并进行适当的清理 |
| **工具调用** | LLM 可以调用您的 Convex 查询以获取数据 |
| **反应钩** | `useDatabaseChat`, (中文(简体) ), `useMessagesWithStreaming`等 (中文(简体) ). |
| **客户端包装** | `defineDatabaseChat()` 类型安全整合 |

**执行:**

| 您的代码 | 说明 |
| --- | --- |
| **工具定义** | LLM 可以调用什么查询 |
| **查询函数** | 实际的 Convex 查询 |
| **聊天集成** | 连接到组件的工具 |
| **系统提示** | 对您域的指示 |
| **UI 组件** | 聊天界面( 或使用钩子) |

### 基于三角洲的流线工程

传统流派发送 全部累积的内容 在每次更新, 生成 O(n2) 带宽 :

-   更新1:"H"(1字节)
-   更新2:"他"(2字节)
-   更新 3 : "Hel" (3 字节)
-   ...对于一个1000个字符响应,您总共发送了~50万字节

基于 Delta 的流线只发送出新内容,从而产生 O(n) 带宽 :

-   德尔塔1:"H"(1字节)
-   德尔塔2:"e" (1字节)
-   三角洲3:"l" (1字节)
-   ...对于1000个字符响应,您发送~1000字节共计

客户端在本地积累了 Dertas , 服务器在 流结束.

* * *

## 安装

需要对流 `^1.43.0`;反应钩支持反应18或19.

```bash
npm install @dayhaysoos/convex-database-chat@0.5.0
```

升级从0.3.1,0.4.0还是一个α? 读取 [迁移指南](https://npmcn.com/package/%40dayhaysoos/convex-database-chat/MIGRATION.md)。 。 。 .

### 1\. 在您的应用程序中添加组件

```typescript
// convex/convex.config.ts
import { defineApp } from "convex/server";
import databaseChat from "@dayhaysoos/convex-database-chat/convex.config";

const app = defineApp();
app.use(databaseChat);

export default app;
```

### 2\. 设置 OpenRouter API 密钥

添加 `OPENROUTER_API_KEY` 到您的 Convex 环境变量:

```bash
npx convex env set OPENROUTER_API_KEY your_key_here
```

* * *

## 快速启动

下面是工作聊天的最低标准:

### 1\. 创建简单的查询工具

```typescript
// convex/chatTools.ts
import { query } from "./_generated/server";
import { v } from "convex/values";

export const countRecords = query({
  args: { table: v.string() },
  returns: v.object({ count: v.number() }),
  handler: async (ctx, args) => {
    if (args.table === "users") {
      const users = await ctx.db.query("users").collect();
      return { count: users.length };
    }
    if (args.table === "orders") {
      const orders = await ctx.db.query("orders").collect();
      return { count: orders.length };
    }
    return { count: 0 };
  },
});
```

### 2\. 创建聊天集成

```typescript
// convex/chat.ts
import { v } from "convex/values";
import { action, mutation, query } from "./_generated/server";
import { createFunctionHandle } from "convex/server";
import { components, api } from "./_generated/api";
import { defineDatabaseChat } from "@dayhaysoos/convex-database-chat/client";
import type { DatabaseChatTool } from "@dayhaysoos/convex-database-chat/tools";

async function getTools(): Promise<DatabaseChatTool[]> {
  return [
    {
      name: "countRecords",
      description: "Count records in a table. Available tables: users, orders",
      parameters: {
        type: "object",
        properties: {
          table: { type: "string", enum: ["users", "orders"] },
        },
        required: ["table"],
      },
      handler: await createFunctionHandle(api.chatTools.countRecords),
    },
  ];
}

const chat = defineDatabaseChat(components.databaseChat, {
  getExternalId: async (ctx) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new Error("Unauthorized");
    return identity.tokenIdentifier;
  },
  options: { chat: { systemPrompt: "Use the tools to answer database questions." } },
});

// externalId remains in the React API shape for compatibility, but is ignored.
// Every operation uses the authenticated identity resolved above.
export const createConversation = mutation({
  args: { externalId: v.string(), title: v.optional(v.string()) },
  returns: v.string(),
  handler: (ctx, { title }) => chat.createConversation(ctx, { title }),
});
export const listConversations = query({
  args: { externalId: v.string() },
  handler: (ctx) => chat.listConversations(ctx),
});
export const getMessages = query({
  args: { conversationId: v.string() },
  handler: (ctx, { conversationId }) => chat.getMessages(ctx, conversationId),
});
export const getStreamState = query({
  args: { conversationId: v.string() },
  handler: (ctx, { conversationId }) => chat.getStreamState(ctx, conversationId),
});
export const getStreamDeltas = query({
  args: { streamId: v.string(), cursor: v.number() },
  handler: (ctx, { streamId, cursor }) => chat.getStreamDeltas(ctx, streamId, cursor),
});
export const abortStream = mutation({
  args: { conversationId: v.string(), reason: v.optional(v.string()) },
  returns: v.boolean(),
  handler: (ctx, { conversationId, reason }) => chat.abortStream(ctx, conversationId, reason),
});
export const sendMessage = action({
  args: { conversationId: v.string(), message: v.string() },
  handler: async (ctx, args) => chat.send(ctx, {
    ...args,
    apiKey: process.env.OPENROUTER_API_KEY!,
    options: { chat: { tools: await getTools() } },
  }),
});
```

* * *

## 定义工具

工具告诉 LLM 可以调用什么查询 。 每个工具需要:

```typescript
interface Tool {
  name: string; // Unique identifier
  description: string; // What the tool does (LLM reads this!)
  parameters: {
    // JSON Schema for arguments
    type: "object";
    properties: Record<
      string,
      {
        type: "string" | "number" | "boolean" | "array" | "object";
        description?: string;
        enum?: string[]; // For constrained values
      }
    >;
    required?: string[];
  };
  handlerType?: "query" | "mutation" | "action"; // Raw tools default to "query"
  handler: string; // Function handle string (createFunctionHandle)
}
```

注:当将工具传递给组件聊天动作时, `handler` 应该是一个 通过 `await createFunctionHandle(...)` 在凸起处 函数。 设定 `handlerType` 改为 `"action"` 用于运行的原始工具 `ctx.vectorSearch`。 。 。 . `defineSemanticSearchTool` 默认为 `"action"`。 。 。 .

### 标准结果合同和打字构建器

用于计数、确定性列表和语义搜索工具,以返回 标准 `{ data, meta }` 结果信封,使用从 后端安全工具进入点 :

```typescript
import {
  definePaginatedListTool,
  enumFilter,
  injectedString,
} from "@dayhaysoos/convex-database-chat/tools";
import type { DatabaseChatToolResult } from "@dayhaysoos/convex-database-chat/resultContract";

type ProductRow = {
  id: string;
  name: string;
  viewUrl: string;
};

const listProductsTool = definePaginatedListTool<ProductRow>({
  name: "listProducts",
  description: "List products matching deterministic filters.",
  handler: "listProductsHandler",
  filters: {
    category: enumFilter({
      values: ["electronics", "clothing", "home", "sports"] as const,
    }),
  },
  injectedArgs: {
    tenantId: injectedString(),
  },
});
```

构建器附加 `tool.metadata.kind` 财务报告和审定财务报表 `metadata.resultContract = "standard"`。 。 。 . DatabaseChat 默认情况下使用该元数据注入通用指导,包括 使用 `meta.count` 将光标结果作为页面处理,而不是 使用语义搜索结果长度作为计数. 设定 `toolGuidance: "disabled"` 或传入自定义字符串 `defineDatabaseChat` 编号 : `chat.send` 要覆盖的配置 所生成的向导.

构建器默认值 :

-   `defineCountTool`:准确的总数,默认的查询处理器.
-   `definePaginatedListTool`:光标页面,默认的查询处理器.
-   `defineSemanticSearchTool`:抽样取出上-K结果,默认操作处理器.

### 实例:电子商务-搜索产品

```typescript
const searchProductsTool = {
  name: "searchProducts",
  description: "Search products by name, category, or price range",
  parameters: {
    type: "object",
    properties: {
      query: {
        type: "string",
        description: "Search text to match against product name",
      },
      category: {
        type: "string",
        description: "Filter by category",
        enum: ["electronics", "clothing", "home", "sports"],
      },
      minPrice: {
        type: "number",
        description: "Minimum price",
      },
      maxPrice: {
        type: "number",
        description: "Maximum price",
      },
      limit: {
        type: "number",
        description: "Maximum results (default: 20)",
      },
    },
    required: [],
  },
  handler: "searchProducts",
};
```

### 示例: 项目管理 - 获取任务

```typescript
const getTasksTool = {
  name: "getTasks",
  description:
    "Get tasks with optional filters for status, assignee, or project",
  parameters: {
    type: "object",
    properties: {
      status: {
        type: "string",
        description: "Filter by task status",
        enum: ["todo", "in_progress", "review", "done"],
      },
      assigneeId: {
        type: "string",
        description: "Filter by assigned user ID",
      },
      projectId: {
        type: "string",
        description: "Filter by project ID",
      },
      priority: {
        type: "string",
        enum: ["low", "medium", "high", "urgent"],
      },
    },
    required: [],
  },
  handler: "getTasks",
};
```

### 示例: 内容平台 - 搜索文章

```typescript
const searchArticlesTool = {
  name: "searchArticles",
  description: "Search published articles by title, author, or tags",
  parameters: {
    type: "object",
    properties: {
      query: {
        type: "string",
        description: "Search text for title or content",
      },
      authorId: {
        type: "string",
        description: "Filter by author",
      },
      tags: {
        type: "array",
        description: "Filter by tags",
        items: { type: "string" },
      },
      publishedAfter: {
        type: "string",
        description: "ISO date string - only articles after this date",
      },
    },
    required: [],
  },
  handler: "searchArticles",
};
```

### 工具描述提示

-   **具体**:"按名称,类别,或价格来搜索产品"比"按产品名称,类别,或价格来搜索"更好 "搜索产品"
-   **列出可用的选项**:"可贵类别:电子,服装, 家,运动"
-   **解释每个参数做什么**: LLM 使用描述来决定 用于

* * *

## 执行查询函数

您的查询功能执行实际的数据库工作 。 他们得到的论点是: LLM提供.

### 示例:电子商务 - 用过滤器搜索

```typescript
// convex/chatTools.ts
import { query } from "./_generated/server";
import { v } from "convex/values";

export const searchProducts = query({
  args: {
    query: v.optional(v.string()),
    category: v.optional(v.string()),
    minPrice: v.optional(v.number()),
    maxPrice: v.optional(v.number()),
    limit: v.optional(v.number()),
  },
  returns: v.array(
    v.object({
      id: v.string(),
      name: v.string(),
      category: v.string(),
      price: v.number(),
      inStock: v.boolean(),
      viewUrl: v.string(),
    }),
  ),
  handler: async (ctx, args) => {
    const limit = Math.min(args.limit ?? 20, 100);
    let products = await ctx.db.query("products").collect();

    // Apply filters
    if (args.category) {
      products = products.filter((p) => p.category === args.category);
    }
    if (args.minPrice !== undefined) {
      products = products.filter((p) => p.price >= args.minPrice!);
    }
    if (args.maxPrice !== undefined) {
      products = products.filter((p) => p.price <= args.maxPrice!);
    }
    if (args.query) {
      const q = args.query.toLowerCase();
      products = products.filter((p) => p.name.toLowerCase().includes(q));
    }

    return products.slice(0, limit).map((p) => ({
      id: p._id,
      name: p.name,
      category: p.category,
      price: p.price,
      inStock: p.stockCount > 0,
      viewUrl: `/products/${p._id}`,
    }));
  },
});
```

### 示例:汇总查询

```typescript
export const getOrderStats = query({
  args: { period: v.optional(v.string()) },
  returns: v.object({
    totalOrders: v.number(),
    totalRevenue: v.number(),
    averageOrderValue: v.number(),
    statusBreakdown: v.object({
      pending: v.number(),
      processing: v.number(),
      shipped: v.number(),
      delivered: v.number(),
    }),
  }),
  handler: async (ctx, args) => {
    let orders = await ctx.db.query("orders").collect();

    // Filter by period if provided
    if (args.period === "today") {
      const today = new Date().setHours(0, 0, 0, 0);
      orders = orders.filter((o) => o.createdAt >= today);
    } else if (args.period === "week") {
      const weekAgo = Date.now() - 7 * 24 * 60 * 60 * 1000;
      orders = orders.filter((o) => o.createdAt >= weekAgo);
    }

    const totalRevenue = orders.reduce((sum, o) => sum + o.total, 0);

    return {
      totalOrders: orders.length,
      totalRevenue,
      averageOrderValue: orders.length > 0 ? totalRevenue / orders.length : 0,
      statusBreakdown: {
        pending: orders.filter((o) => o.status === "pending").length,
        processing: orders.filter((o) => o.status === "processing").length,
        shipped: orders.filter((o) => o.status === "shipped").length,
        delivered: orders.filter((o) => o.status === "delivered").length,
      },
    };
  },
});
```

* * *

## 创建聊天集成

聊天集成将您的工具连接到组件并处理 LLM 互动.

### 建议: `defineDatabaseChat` 与 `getExternalId`

配置一个身份解析器一次和每一个客户端方法路由 所有权检查的终点自动。 解析器运行在您的应用程序 Convex 函数,所以它与 Convex Auth , 办事员, 或任何身份合作 提供者 - 组件本身仍然具有不可知性.

```typescript
// convex/chat.ts
import { components } from "./_generated/api";
import { defineDatabaseChat } from "@dayhaysoos/convex-database-chat/client";

const chat = defineDatabaseChat(components.databaseChat, {
  options: {
    chat: { systemPrompt: SYSTEM_PROMPT },
    provider: { model: "openai/gpt-4o" },
  },
  tools: TOOLS,
  // Called server-side in each wrapper; never trust client-supplied IDs.
  getExternalId: async (ctx) => {
    const userId = await getAuthUserId(ctx); // your auth
    if (!userId) throw new Error("Unauthorized");
    return `user:${userId}`;
  },
});

// All of these are ownership-checked under the hood:
await chat.createConversation(ctx, { title: "New Chat" });
const messages = await chat.getMessages(ctx, conversationId);
const state = await chat.getStreamState(ctx, conversationId);
const result = await chat.send(ctx, {
  conversationId,
  message,
  apiKey: process.env.OPENROUTER_API_KEY!,
});
```

如果无法解决身份( 无) `getExternalId` 配置且未显示 `externalId` 而不是默默跳过 进入控制。 您仍然可以直接调用原始组件端点 高级设置 - 见 [安全和多租户访问](https://npmcn.com/package/%40dayhaysoos/convex-database-chat/#readme-security--multi-tenant-access)。 。 。 .

单个发送选项 `options` 有两节对象 : `chat` (单位:千美元)`systemPrompt`, (中文(简体) ), `maxMessagesForLLM`, (中文(简体) ), `maxToolLoops` (违约5), `streamThrottleMs` (违约100起), `maxToolResultChars` (默认 16,000, 超规模) 工具结果被切换并标记 `{ truncated: true }`, (中文(简体) ), `validateResultContract` (默认) `"warn"`, (中文(简体) ), `toolContext`和 `provider` (单位:千美元)`model`, (中文(简体) ), `maxRetries` (违约2), `requestTimeoutMs` (默认3000), `httpReferer` 编号 : `xTitle` OpenRouter 归属标题). 设定默认值为 `defineDatabaseChat` 并覆盖每个调用的任何字段——后值为胜字段 按字段:

```typescript
const result = await chat.send(ctx, {
  conversationId,
  message,
  apiKey: process.env.OPENROUTER_API_KEY!,
  options: {
    chat: { validateResultContract: "enforce" },
    provider: { model: "openai/gpt-4o" },
  },
});
```

### 可靠性:重复、超时和打出错误

临时提供者故障(网络错误, 408/429/5xx) 被重审 自动与指数回放和全焦。 A 类 `Retry-After` 标题 从429是荣耀。 只有在连接阶段才会进行检索 请求 - 一旦回复开始流到客户端, 重试会 重复内容,所以中游失败从不重复. `requestTimeoutMs` 限制每次尝试需要多长时间才能完成第一次尝试 字节( 长流是合法的, 不封顶) .

失败 发送回一个机器可读分类 :

```typescript
const result = await chat.send(ctx, { conversationId, message, apiKey });
if (!result.success) {
  // result.error          - human-readable message
  // result.errorCode      - "unauthorized" | "rate_limited" | "provider_error"
  //                         | "network_error" | "timeout" | "aborted"
  //                         | "invalid_request" | "unknown"
  // result.retryable      - whether the failure was transient in nature
}
```

如果一个消息的工具循环耗尽 `maxToolLoops` 没有最后的答案 结果载 `stoppedReason: "max_tool_loops"` 和坚忍的助手 消息解释截取,而不是沉默地空出.

### 运行时验证标准结果合同

工具构建 `defineCountTool`, (中文(简体) ), `definePaginatedListTool`,或 `defineSemanticSearchTool` 声明 `metadata.resultContract: "standard"`编辑 `validateResultContract` 选项使组件验证信封 工具实际返回 :

-   `"warn"` (违约) - 违反合同行为被记录下来,结果仍然存在 到达LLM.
-   `"enforce"` - 校正错误返回LLM,以代替 原始结果,因此它可以适应而不是信任畸形元数据.
-   `"off"` - 没有运行时间验证.

### 使用内置 `chat.send` 直接动作

对于完整的控制(定制函数控件,按请求工具),调用 组件的聊天动作。 它处理LLM工具回路, 流出,并传达讯息给您:

```typescript
// convex/chat.ts
export const sendMessage = action({
  args: { conversationId: v.string(), message: v.string() },
  handler: async (ctx, args) => {
    return await ctx.runAction(components.databaseChat.chat.sendForExternalId, {
      conversationId: args.conversationId,
      externalId, // derived server-side from your auth
      message: args.message,
      config: {
        apiKey: process.env.OPENROUTER_API_KEY!,
        chat: {
          systemPrompt: SYSTEM_PROMPT,
          tools, // DatabaseChatTool[] with createFunctionHandle strings
        },
      },
    });
  },
});
```

### 带来自己的LLM SDK

使用 `defineDatabaseChat`其下层原始人(`addMessage`, (中文(简体) ), `getMessagesForLLM`)如果你想使用 Vercel AI SDK, 直接OpenAI客户端, 或另一个提供者代替 内建的 OpenRouter 集成.

* * *

## 写入系统提示

系统提示告诉LLM如何行为. 为您的域自定义 .

### 一个好提示的结构

```
1. Role definition - What the assistant is
2. Available capabilities - What it can do
3. Response format - How to structure answers
4. Domain-specific rules - Your business logic
```

### 实例:电子商务助理

```typescript
const SYSTEM_PROMPT = `You are a helpful e-commerce assistant. You help store managers understand their inventory and sales.

You have access to tools that let you:
- Search products by name, category, or price
- Get order statistics and revenue data
- Check inventory levels

When answering:
- Be concise and use specific numbers
- Always include links using the viewUrl field: [Product Name](viewUrl)
- Format prices with currency symbols
- If inventory is low (< 10), mention it

Example response:
"Found 3 products under $50 in Electronics:
- [Wireless Mouse](/products/abc123) - $29.99 (In Stock)
- [USB Cable](/products/def456) - $12.99 (Low Stock: 5 left)
- [Phone Stand](/products/ghi789) - $19.99 (In Stock)"`;
```

### 示例:项目管理助理

```typescript
const SYSTEM_PROMPT = `You are a project management assistant. You help team leads track tasks and project progress.

You have access to tools that let you:
- Search and filter tasks by status, assignee, or project
- Get project statistics and completion rates
- Find overdue or blocked tasks

When answering:
- Prioritize actionable information
- Include links to tasks: [Task Title](viewUrl)
- Highlight urgent or overdue items
- Show completion percentages when relevant

Example response:
"Project 'Website Redesign' has 12 open tasks:
- 3 urgent: [Fix checkout bug](/tasks/abc), [Update SSL](/tasks/def), [Mobile nav](/tasks/ghi)
- 5 in review
- 4 in progress
Overall: 68% complete"`;
```

### 示例:内容平台助理

```typescript
const SYSTEM_PROMPT = `You are a content analytics assistant. You help editors understand article performance.

You have access to tools that let you:
- Search articles by title, author, or tags
- Get engagement statistics (views, shares, comments)
- Find trending or underperforming content

When answering:
- Include article links: [Article Title](viewUrl)
- Show key metrics inline
- Compare to averages when helpful
- Suggest actionable insights

Example response:
"Your top 3 articles this week:
1. [Getting Started with React](/articles/abc) - 12.5k views, 342 shares
2. [CSS Grid Tutorial](/articles/def) - 8.2k views, 256 shares  
3. [TypeScript Tips](/articles/ghi) - 6.1k views, 189 shares

All performing above your 5k average!"`;
```

* * *

## 建设UI

### 使用反应钩

```typescript
import {
  DatabaseChatProvider,
  useDatabaseChat,
  useMessagesWithStreaming,
} from "@dayhaysoos/convex-database-chat";
```

### 最小聊天组件, 基于三角洲流线

该组件使用高效的三角洲流线. 以下是如何实施:

```tsx
import { useState, useEffect, useRef } from "react";
import { useQuery, useMutation, useAction } from "convex/react";
import { api } from "../../convex/_generated/api";

// Hook for delta-based streaming with client-side accumulation
function useDeltaStreaming(conversationId: string | null) {
  const [cursor, setCursor] = useState(0);
  const [accumulatedContent, setAccumulatedContent] = useState("");
  const lastStreamIdRef = useRef<string | null>(null);
  const lastProcessedEndRef = useRef(0);

  // Subscribe to stream state
  const streamState = useQuery(
    api.chat.getStreamState,
    conversationId ? { conversationId } : "skip",
  );

  const streamId = streamState?.streamId ?? null;
  const status = streamState?.status ?? null;

  // Reset when stream changes
  useEffect(() => {
    if (streamId !== lastStreamIdRef.current) {
      lastStreamIdRef.current = streamId;
      lastProcessedEndRef.current = 0;
      setCursor(0);
      setAccumulatedContent("");
    }
  }, [streamId]);

  // Fetch deltas from cursor
  const deltas = useQuery(
    api.chat.getStreamDeltas,
    streamId && status === "streaming" ? { streamId, cursor } : "skip",
  );

  // Accumulate new deltas
  useEffect(() => {
    if (!deltas || deltas.length === 0) return;

    const newDeltas = deltas.filter(
      (d) => d.start >= lastProcessedEndRef.current,
    );
    if (newDeltas.length === 0) return;

    let maxEnd = lastProcessedEndRef.current;
    let newText = "";

    for (const delta of newDeltas) {
      if (delta.end > maxEnd) maxEnd = delta.end;
      for (const part of delta.parts) {
        if (part.type === "text-delta" && part.text) {
          newText += part.text;
        }
      }
    }

    if (newText) {
      setAccumulatedContent((prev) => prev + newText);
    }
    lastProcessedEndRef.current = maxEnd;
    if (maxEnd > cursor) setCursor(maxEnd);
  }, [deltas, cursor]);

  return {
    content:
      status === "streaming" && accumulatedContent ? accumulatedContent : null,
    isStreaming: status === "streaming",
  };
}

export function ChatWidget() {
  const [isOpen, setIsOpen] = useState(false);
  const [conversationId, setConversationId] = useState<string | null>(null);
  const [inputValue, setInputValue] = useState("");
  const [isLoading, setIsLoading] = useState(false);

  const createConversation = useMutation(api.chat.createConversation);
  const sendMessage = useAction(api.chat.sendMessage);
  const abortStream = useMutation(api.chat.abortStream);

  const messages = useQuery(
    api.chat.getMessages,
    conversationId ? { conversationId } : "skip",
  );

  // Use delta-based streaming
  const { content: streamingContent, isStreaming } =
    useDeltaStreaming(conversationId);

  // Create conversation on open
  useEffect(() => {
    if (isOpen && !conversationId) {
      createConversation({}).then(setConversationId);
    }
  }, [isOpen, conversationId]);

  const handleSubmit = async () => {
    if (!inputValue.trim() || isLoading || !conversationId) return;

    const message = inputValue;
    setInputValue("");
    setIsLoading(true);

    await sendMessage({ conversationId, message });
    setIsLoading(false);
  };

  const handleAbort = async () => {
    if (!conversationId) return;
    await abortStream({ conversationId });
    setIsLoading(false);
  };

  if (!isOpen) {
    return <button onClick={() => setIsOpen(true)}>Open Chat</button>;
  }

  return (
    <div>
      {/* Messages */}
      {messages?.map((msg) => (
        <div key={msg._id}>
          <strong>{msg.role}:</strong>
          <MarkdownContent content={msg.content} />
        </div>
      ))}

      {/* Streaming */}
      {streamingContent && (
        <div>
          <strong>assistant:</strong>
          <MarkdownContent content={streamingContent} />
        </div>
      )}

      {/* Input */}
      <input
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        onKeyDown={(e) => e.key === "Enter" && handleSubmit()}
        disabled={isLoading || isStreaming}
      />
      {isLoading || isStreaming ? (
        <button onClick={handleAbort}>Stop</button>
      ) : (
        <button onClick={handleSubmit} disabled={!inputValue.trim()}>
          Send
        </button>
      )}
    </div>
  );
}
```

### 文本平滑为 `useSmoothText`

这个 `useSmoothText` 钩子为流出文本创建打字机效果,制作 它感觉更自然 而不是跳出块:

```tsx
import { useSmoothText, SmoothText } from "@dayhaysoos/convex-database-chat";

// Using the hook
function StreamingMessage({ text, isStreaming }) {
  const [visibleText] = useSmoothText(text, {
    // Start streaming immediately for active streams
    startStreaming: isStreaming,
    // Optional: customize speed (default: 200 chars/sec)
    initialCharsPerSecond: 200,
  });

  return <div>{visibleText}</div>;
}

// Or use the component directly
function Message({ text, isStreaming }) {
  return <SmoothText text={text} startStreaming={isStreaming} />;
}
```

**选项 :**

-   `startStreaming` - 准备 `true` 用于流出消息, `false` 完成时间 信件(立即显示)
-   `initialCharsPerSecond` - 启动速度,随时间而适应文本 抵达率
-   `minDelayMs` 编号 : `maxDelayMs` - 字符延迟的界限

### 绘制降标链接

LLM 返回下标记链接, 如 `[Name](url)`分析并使它们:

```tsx
function MarkdownContent({ content }: { content: string }) {
  const parts = content.split(/(\[[^\]]+\]\([^)]+\))/g);

  return (
    <>
      {parts.map((part, i) => {
        const linkMatch = part.match(/\[([^\]]+)\]\(([^)]+)\)/);
        if (linkMatch) {
          const [, text, url] = linkMatch;
          return (
            <a key={i} href={url} className="text-blue-600 underline">
              {text}
            </a>
          );
        }
        // Handle **bold**
        return part.split(/(\*\*[^*]+\*\*)/g).map((p, j) => {
          const bold = p.match(/\*\*([^*]+)\*\*/);
          if (bold) return <strong key={`${i}-${j}`}>{bold[1]}</strong>;
          return <span key={`${i}-${j}`}>{p}</span>;
        });
      })}
    </>
  );
}
```

* * *

## 模式

### 矢量搜索助手

利用辅助工具减少锅炉板,同时保持你的计划 在您的应用程序代码中完全动作 :

```typescript
// convex/semanticSearch.ts
import { action } from "./_generated/server";
import { internal } from "./_generated/api";
import { v } from "convex/values";
import {
  generateEmbedding,
  formatVectorResults,
} from "@dayhaysoos/convex-database-chat/vector";

export const semanticSearchCandidates = action({
  args: {
    query: v.string(),
    orgId: v.id("orgs"),
    limit: v.optional(v.number()),
  },
  handler: async (ctx, args) => {
    const embedding = await generateEmbedding({
      apiKey: process.env.OPENROUTER_API_KEY!,
      text: args.query,
      // model: "your-embedding-model",
    });

    const results = await ctx.vectorSearch(
      "applications",
      "by_resume_embedding",
      {
        vector: embedding,
        limit: args.limit ?? 20,
      },
    );

    const docs = await ctx.runQuery(internal.semanticSearch.fetchByIds, {
      ids: results.map((r) => r._id),
    });

    return formatVectorResults(results, docs, {
      includeScore: true,
      snippetLength: 200,
      fields: ["candidateName", "topStrengths", "viewUrl"],
    });
  },
});
```

**注释:**

-   矢量搜索只在动作中可用.
-   矢量指数维度必须与您选择的嵌入模型相匹配.
-   使用 `fields` 以避免像嵌入式一样返回大向量字段.
-   设定 `fields: []` 以不包含文档字段(仅包含) `_id` 和可选 `_score`) (中文(简体) ).

您也可以使用辅助工具定义工具条目 :

```typescript
import { defineVectorSearchTool } from "@dayhaysoos/convex-database-chat/vector";

const TOOLS = [
  defineVectorSearchTool({
    name: "semanticSearch",
    description: "Find candidates by meaning or concept",
    handler: "semanticSearchCandidates",
  }),
];
```

### 包括可操作的 URL

通过包含链接到细节页面的 URL 使回复可操作 :

```typescript
// In your query handler, include viewUrl in results
return items.map((item) => ({
  id: item._id,
  name: item.name,
  // Include the URL to view this item
  viewUrl: `/items/${item._id}`,
}));
```

然后指示 LLM 在您的系统中使用它 提示:

```
Always include links using the viewUrl field from the data.
Format as markdown: [Item Name](viewUrl)
```

**在此之前(不可采取行动):**

> "50美元以下的2个产品"

**在(可诉)之后:**

> "50美元以下的2个产品:
> 
> -   [无线鼠标](https://npmcn.com/products/abc123) - 29.99美元 - 29.99美元 - 29.99美元 - 29.99美元 - 29.99美元
> -   [USB 有线](https://npmcn.com/products/def456) - 12.99美元 - 12.99美元 - 12.99美元 - 12

* * *

## 安全和多租户访问

此组件具有可解析性 。 你必须提供 `externalId` 从您自己的认证 系统,并且从不直接接受来自不信任客户端.

使用下方范围端点执行所有权检查服务器侧. 这些 掷出 `Not found` 如果对话或流缺失或不属于 提供 `externalId` 避免在租户之间泄露存在.

-   `conversations.getForExternalId`
-   `messages.addForExternalId`
-   `messages.listForExternalId`
-   `messages.getLatestForExternalId`
-   `stream.getStreamForExternalId`
-   `stream.listDeltasForExternalId`
-   `stream.abortForExternalId`
-   `chat.sendForExternalId`

未瞄准的端点是低级帮助,只有在已经使用时才使用 在您的应用程序中执行访问控制 .

* * *

## 将信任的上下文注入工具

一些工具处理器需要只使用服务器的上下文( orgId, 外部Id, userId) 绝不应出现在工具图或 LLM 提示中。 把数据通过 `config.chat.toolContext` 内 `chat.send` 或者说 `chat.sendForExternalId`合并上下文 验证后成为工具参数,以及 `toolContext` 赢得冲突.

```typescript
await ctx.runAction(components.databaseChat.chat.sendForExternalId, {
  conversationId,
  externalId,
  message,
  config: {
    apiKey: process.env.OPENROUTER_API_KEY!,
    chat: {
      systemPrompt: SYSTEM_PROMPT,
      tools,
      toolContext: { orgId, userId, externalId },
    },
  },
});
```

注:不包括类似字段 `orgId` 在工具的计划中,所以LLM永远不会 看他们.

* * *

## API 参考

### 职能

| 函数 | 类型 | 说明 |
| --- | --- | --- |
| `conversations.create` | 变异 | 创建新对话 |
| `conversations.get` | 查询 | 以身份进行交谈 |
| `conversations.getForExternalId` | 查询 | 通过 ID 范围访问外部Id |
| `conversations.list` | 查询 | 按外部Id 排列的对话框 |
| `messages.add` | 变异 | 添加信件 |
| `messages.list` | 查询 | 在对话框中列出信件 |
| `messages.listForExternalId` | 查询 | 列出已放入外部Id 范围的信件 |
| `messages.getLatestForExternalId` | 查询 | 获取最新消息范围到外部Id |
| `stream.create` | 变异 | 为基于三角洲的溪流创建新溪流 |
| `stream.addDelta` | 变异 | 向溪流添加三角洲( 批部件) |
| `stream.finish` | 变异 | 将流标为已完成, 清理三角洲 |
| `stream.abort` | 变异 | 以流编号中止流 |
| `stream.abortByConversation` | 变异 | 通过对话ID中止流 |
| `stream.abortForExternalId` | 变异 | 通过对口ID范围中止流到外部Id |
| `stream.getStream` | 查询 | 获取当前流状态进行交谈 |
| `stream.listDeltas` | 查询 | 从光标位置获取三角洲 |
| `stream.getStreamForExternalId` | 查询 | 获得当前流状态范围到外部Id |
| `stream.listDeltasForExternalId` | 查询 | 将三角洲瞄准外部Id |
| `chat.send` | 行动 | 通过 OpenRouter 发送消息 |
| `chat.sendForExternalId` | 行动 | 向外部 Id 发送已设定范围的信件 |

### 反应钩和组件

| 导出 | 类型 | 说明 |
| --- | --- | --- |
| `useDatabaseChat` | 钩子 | 发送信件、 跟踪加载/ 流化状态、 中止 |
| `useConversations` | 钩子 | 列表/ 创建对话框 |
| `useStreamingContent` | 钩子 | 订阅带有 Dleta 累积的流流更新 |
| `useMessagesWithStreaming` | 钩子 | 信件 + 当前流线合并 |
| `useSmoothText` | 钩子 | 流媒体文本的打字机效果 |
| `SmoothText` | 构成部分 | 具有平滑打字机效果的渲染文本 |

### 流国家

| 状态 | 说明 |
| --- | --- |
| `streaming` | Stream 活动, 三角洲正在写入 |
| `finished` | 流线成功完成 |
| `aborted` | 流被取消( 用户中止、 错误或超时) |

当一个流被用户中止、超时或出错所中断时,任何内容 已经流出的信息作为助理信息持续使用 `partial: true` 这样 部分反应没有丢失。 (中间回合的调用工具循环是: 没有流出或持续,只有最后的答案是。 )

已知的限制:

-   如果工具循环的最后一轮在流出任何文本之前失败, 内容 无法从中间弹中取回(如果 轮回旋转).
-   停止在工具执行期间在下一个工具启动边界上进行检测; 远程单个工具呼叫不能在中途中断.
-   在工具回合之间, 流速旋转: 客户端订阅为流 状态可观察到摘要 `streaming` - > (中文) `aborted` - > (中文) `streaming` 转换,回合之间没有内容.

* * *

## 铁路部署

这个Repo主机有两个铁路服务,每个都有自己的配置文件:

-   示例应用程序: 根目录 `/example`, 铁路配置文件 `/example/railway.toml`
-   Docs 站点: 根目录 `/docs`, 铁路配置文件 `/docs/railway.toml`

注意: 铁路在选择一个时不会自动跟随根目录 配置文件,所以您必须设置每个服务的绝对配置路径 。 一个根 `railway.toml` 没有故意使用.

* * *

## 测试

```bash
# Run all tests
npm test

# Run tests in watch mode
npm run test:watch
```

* * *

## 文件结构

```
convex/component/                  # Backend (Convex component)
├── convex.config.ts               # Component definition
├── schema.ts                      # Tables (conversations, messages, streamingMessages, streamDeltas)
├── conversations.ts               # Conversation CRUD
├── messages.ts                    # Message CRUD
├── stream.ts                      # Delta-based streaming (create, addDelta, finish, abort)
├── deltaStreamer.ts               # DeltaStreamer class for batched writes
├── chat.ts                        # Main send action with OpenRouter
├── toolExecution.ts               # Tool dispatch + trusted context injection
├── tools.ts                       # Tool types & typed builders
├── resultContract.ts              # Standard { data, meta } result envelope
├── toolGuidance.ts                # Reliability guidance injection
├── schemaTools.ts                  # Auto-tool generation
├── access.ts                      # externalId ownership checks
├── contextTypes.ts                # Shared DataModel-typed context helpers
├── client.ts                      # Client wrapper (defineDatabaseChat)
└── *.test.ts                      # Tests

src/                               # Frontend (React)
├── react.tsx                      # React hooks (useDatabaseChat, useConversations, etc.)
├── vector.ts                      # Vector search helpers
├── react.test.tsx                 # Hook tests
└── index.ts                       # Package exports
```

* * *

## 许可证

麻省理工学院

## 常见问题

### @dayhaysoos/convex-database-chat 是什么？

用于在您的应用程序中添加自然语言数据库查询的 Convex 组件 \.

### 如何安装 @dayhaysoos/convex-database-chat？

在项目目录运行 npm install @dayhaysoos/convex-database-chat，或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖：convex（^1\.43\.0）。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖，Yarn 需要手动安装。

### @dayhaysoos/convex-database-chat 支持 TypeScript 吗？

支持。包内自带 TypeScript 类型声明（package\.json 声明了 types 入口），安装后即可获得类型提示，无需再安装 @types 包。

### @dayhaysoos/convex-database-chat 支持 ESM 还是 CommonJS？

以 ES Module 形式发布，推荐使用 import 引入。在 CommonJS 项目中，Node\.js 20\.19\+ / 22\.12\+ 可以直接 require\(\) 不含顶层 await 的 ES Module，更早的版本需要改用动态 import\(\)。

### @dayhaysoos/convex-database-chat 可以商用吗？

@dayhaysoos/convex-database-chat 使用 MIT 许可。宽松许可：允许商业使用、修改和再分发，通常只需在分发时保留原版权与许可声明。以上为概括，不构成法律意见，请以许可原文为准。

### @dayhaysoos/convex-database-chat 的下载量有多少？

根据 npm 官方下载统计，@dayhaysoos/convex-database-chat 在 2026/10/12 – 2026/11/10 这 30 天内被下载 2,293 次，日均约 76 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入，适合横向比较热度，不能直接等同于使用人数。

### @dayhaysoos/convex-database-chat 还在维护吗？

最新版本 0\.5\.0 发布于 2026/10/03（1 个月前）；至今共发布 14 个版本，其中过去 12 个月发布了 14 个；首次发布于 2025/12/16。发布频率只是参考信号：功能稳定的工具可能很少需要更新，建议结合源码仓库的 issue 和提交记录判断。

