chore: 保存当前架构收口与 bug 修复快照
归档本轮 P0/P1 bug 修复、设计审查迁移、AI selection scope 收口与 stream contract 调整,并保留当前 05 主线迁移起点。
This commit is contained in:
Vendored
+49
@@ -0,0 +1,49 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Generated `api` utility.
|
||||
*
|
||||
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||
*
|
||||
* To regenerate, run `npx convex dev`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type * as aiSessions from "../aiSessions.js";
|
||||
|
||||
import type {
|
||||
ApiFromModules,
|
||||
FilterApi,
|
||||
FunctionReference,
|
||||
} from "convex/server";
|
||||
|
||||
declare const fullApi: ApiFromModules<{
|
||||
aiSessions: typeof aiSessions;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* A utility for referencing Convex functions in your app's public API.
|
||||
*
|
||||
* Usage:
|
||||
* ```js
|
||||
* const myFunctionReference = api.myModule.myFunction;
|
||||
* ```
|
||||
*/
|
||||
export declare const api: FilterApi<
|
||||
typeof fullApi,
|
||||
FunctionReference<any, "public">
|
||||
>;
|
||||
|
||||
/**
|
||||
* A utility for referencing Convex functions in your app's internal API.
|
||||
*
|
||||
* Usage:
|
||||
* ```js
|
||||
* const myFunctionReference = internal.myModule.myFunction;
|
||||
* ```
|
||||
*/
|
||||
export declare const internal: FilterApi<
|
||||
typeof fullApi,
|
||||
FunctionReference<any, "internal">
|
||||
>;
|
||||
|
||||
export declare const components: {};
|
||||
@@ -0,0 +1,23 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Generated `api` utility.
|
||||
*
|
||||
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||
*
|
||||
* To regenerate, run `npx convex dev`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { anyApi, componentsGeneric } from "convex/server";
|
||||
|
||||
/**
|
||||
* A utility for referencing Convex functions in your app's API.
|
||||
*
|
||||
* Usage:
|
||||
* ```js
|
||||
* const myFunctionReference = api.myModule.myFunction;
|
||||
* ```
|
||||
*/
|
||||
export const api = anyApi;
|
||||
export const internal = anyApi;
|
||||
export const components = componentsGeneric();
|
||||
Vendored
+60
@@ -0,0 +1,60 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Generated data model types.
|
||||
*
|
||||
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||
*
|
||||
* To regenerate, run `npx convex dev`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type {
|
||||
DataModelFromSchemaDefinition,
|
||||
DocumentByName,
|
||||
TableNamesInDataModel,
|
||||
SystemTableNames,
|
||||
} from "convex/server";
|
||||
import type { GenericId } from "convex/values";
|
||||
import schema from "../schema.js";
|
||||
|
||||
/**
|
||||
* The names of all of your Convex tables.
|
||||
*/
|
||||
export type TableNames = TableNamesInDataModel<DataModel>;
|
||||
|
||||
/**
|
||||
* The type of a document stored in Convex.
|
||||
*
|
||||
* @typeParam TableName - A string literal type of the table name (like "users").
|
||||
*/
|
||||
export type Doc<TableName extends TableNames> = DocumentByName<
|
||||
DataModel,
|
||||
TableName
|
||||
>;
|
||||
|
||||
/**
|
||||
* An identifier for a document in Convex.
|
||||
*
|
||||
* Convex documents are uniquely identified by their `Id`, which is accessible
|
||||
* on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
|
||||
*
|
||||
* Documents can be loaded using `db.get(tableName, id)` in query and mutation functions.
|
||||
*
|
||||
* IDs are just strings at runtime, but this type can be used to distinguish them from other
|
||||
* strings when type checking.
|
||||
*
|
||||
* @typeParam TableName - A string literal type of the table name (like "users").
|
||||
*/
|
||||
export type Id<TableName extends TableNames | SystemTableNames> =
|
||||
GenericId<TableName>;
|
||||
|
||||
/**
|
||||
* A type describing your Convex data model.
|
||||
*
|
||||
* This type includes information about what tables you have, the type of
|
||||
* documents stored in those tables, and the indexes defined on them.
|
||||
*
|
||||
* This type is used to parameterize methods like `queryGeneric` and
|
||||
* `mutationGeneric` to make them type-safe.
|
||||
*/
|
||||
export type DataModel = DataModelFromSchemaDefinition<typeof schema>;
|
||||
Vendored
+143
@@ -0,0 +1,143 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Generated utilities for implementing server-side Convex query and mutation functions.
|
||||
*
|
||||
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||
*
|
||||
* To regenerate, run `npx convex dev`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import {
|
||||
ActionBuilder,
|
||||
HttpActionBuilder,
|
||||
MutationBuilder,
|
||||
QueryBuilder,
|
||||
GenericActionCtx,
|
||||
GenericMutationCtx,
|
||||
GenericQueryCtx,
|
||||
GenericDatabaseReader,
|
||||
GenericDatabaseWriter,
|
||||
} from "convex/server";
|
||||
import type { DataModel } from "./dataModel.js";
|
||||
|
||||
/**
|
||||
* Define a query in this Convex app's public API.
|
||||
*
|
||||
* This function will be allowed to read your Convex database and will be accessible from the client.
|
||||
*
|
||||
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const query: QueryBuilder<DataModel, "public">;
|
||||
|
||||
/**
|
||||
* Define a query that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* This function will be allowed to read from your Convex database. It will not be accessible from the client.
|
||||
*
|
||||
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const internalQuery: QueryBuilder<DataModel, "internal">;
|
||||
|
||||
/**
|
||||
* Define a mutation in this Convex app's public API.
|
||||
*
|
||||
* This function will be allowed to modify your Convex database and will be accessible from the client.
|
||||
*
|
||||
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const mutation: MutationBuilder<DataModel, "public">;
|
||||
|
||||
/**
|
||||
* Define a mutation that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* This function will be allowed to modify your Convex database. It will not be accessible from the client.
|
||||
*
|
||||
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const internalMutation: MutationBuilder<DataModel, "internal">;
|
||||
|
||||
/**
|
||||
* Define an action in this Convex app's public API.
|
||||
*
|
||||
* An action is a function which can execute any JavaScript code, including non-deterministic
|
||||
* code and code with side-effects, like calling third-party services.
|
||||
* They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
|
||||
* They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
|
||||
*
|
||||
* @param func - The action. It receives an {@link ActionCtx} as its first argument.
|
||||
* @returns The wrapped action. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const action: ActionBuilder<DataModel, "public">;
|
||||
|
||||
/**
|
||||
* Define an action that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
|
||||
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export declare const internalAction: ActionBuilder<DataModel, "internal">;
|
||||
|
||||
/**
|
||||
* Define an HTTP action.
|
||||
*
|
||||
* The wrapped function will be used to respond to HTTP requests received
|
||||
* by a Convex deployment if the requests matches the path and method where
|
||||
* this action is routed. Be sure to route your httpAction in `convex/http.js`.
|
||||
*
|
||||
* @param func - The function. It receives an {@link ActionCtx} as its first argument
|
||||
* and a Fetch API `Request` object as its second.
|
||||
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
|
||||
*/
|
||||
export declare const httpAction: HttpActionBuilder;
|
||||
|
||||
/**
|
||||
* A set of services for use within Convex query functions.
|
||||
*
|
||||
* The query context is passed as the first argument to any Convex query
|
||||
* function run on the server.
|
||||
*
|
||||
* This differs from the {@link MutationCtx} because all of the services are
|
||||
* read-only.
|
||||
*/
|
||||
export type QueryCtx = GenericQueryCtx<DataModel>;
|
||||
|
||||
/**
|
||||
* A set of services for use within Convex mutation functions.
|
||||
*
|
||||
* The mutation context is passed as the first argument to any Convex mutation
|
||||
* function run on the server.
|
||||
*/
|
||||
export type MutationCtx = GenericMutationCtx<DataModel>;
|
||||
|
||||
/**
|
||||
* A set of services for use within Convex action functions.
|
||||
*
|
||||
* The action context is passed as the first argument to any Convex action
|
||||
* function run on the server.
|
||||
*/
|
||||
export type ActionCtx = GenericActionCtx<DataModel>;
|
||||
|
||||
/**
|
||||
* An interface to read from the database within Convex query functions.
|
||||
*
|
||||
* The two entry points are {@link DatabaseReader.get}, which fetches a single
|
||||
* document by its {@link Id}, or {@link DatabaseReader.query}, which starts
|
||||
* building a query.
|
||||
*/
|
||||
export type DatabaseReader = GenericDatabaseReader<DataModel>;
|
||||
|
||||
/**
|
||||
* An interface to read from and write to the database within Convex mutation
|
||||
* functions.
|
||||
*
|
||||
* Convex guarantees that all writes within a single mutation are
|
||||
* executed atomically, so you never have to worry about partial writes leaving
|
||||
* your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control)
|
||||
* for the guarantees Convex provides your functions.
|
||||
*/
|
||||
export type DatabaseWriter = GenericDatabaseWriter<DataModel>;
|
||||
@@ -0,0 +1,93 @@
|
||||
/* eslint-disable */
|
||||
/**
|
||||
* Generated utilities for implementing server-side Convex query and mutation functions.
|
||||
*
|
||||
* THIS CODE IS AUTOMATICALLY GENERATED.
|
||||
*
|
||||
* To regenerate, run `npx convex dev`.
|
||||
* @module
|
||||
*/
|
||||
|
||||
import {
|
||||
actionGeneric,
|
||||
httpActionGeneric,
|
||||
queryGeneric,
|
||||
mutationGeneric,
|
||||
internalActionGeneric,
|
||||
internalMutationGeneric,
|
||||
internalQueryGeneric,
|
||||
} from "convex/server";
|
||||
|
||||
/**
|
||||
* Define a query in this Convex app's public API.
|
||||
*
|
||||
* This function will be allowed to read your Convex database and will be accessible from the client.
|
||||
*
|
||||
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const query = queryGeneric;
|
||||
|
||||
/**
|
||||
* Define a query that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* This function will be allowed to read from your Convex database. It will not be accessible from the client.
|
||||
*
|
||||
* @param func - The query function. It receives a {@link QueryCtx} as its first argument.
|
||||
* @returns The wrapped query. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const internalQuery = internalQueryGeneric;
|
||||
|
||||
/**
|
||||
* Define a mutation in this Convex app's public API.
|
||||
*
|
||||
* This function will be allowed to modify your Convex database and will be accessible from the client.
|
||||
*
|
||||
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const mutation = mutationGeneric;
|
||||
|
||||
/**
|
||||
* Define a mutation that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* This function will be allowed to modify your Convex database. It will not be accessible from the client.
|
||||
*
|
||||
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
|
||||
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const internalMutation = internalMutationGeneric;
|
||||
|
||||
/**
|
||||
* Define an action in this Convex app's public API.
|
||||
*
|
||||
* An action is a function which can execute any JavaScript code, including non-deterministic
|
||||
* code and code with side-effects, like calling third-party services.
|
||||
* They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
|
||||
* They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
|
||||
*
|
||||
* @param func - The action. It receives an {@link ActionCtx} as its first argument.
|
||||
* @returns The wrapped action. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const action = actionGeneric;
|
||||
|
||||
/**
|
||||
* Define an action that is only accessible from other Convex functions (but not from the client).
|
||||
*
|
||||
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
|
||||
* @returns The wrapped function. Include this as an `export` to name it and make it accessible.
|
||||
*/
|
||||
export const internalAction = internalActionGeneric;
|
||||
|
||||
/**
|
||||
* Define an HTTP action.
|
||||
*
|
||||
* The wrapped function will be used to respond to HTTP requests received
|
||||
* by a Convex deployment if the requests matches the path and method where
|
||||
* this action is routed. Be sure to route your httpAction in `convex/http.js`.
|
||||
*
|
||||
* @param func - The function. It receives an {@link ActionCtx} as its first argument
|
||||
* and a Fetch API `Request` object as its second.
|
||||
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
|
||||
*/
|
||||
export const httpAction = httpActionGeneric;
|
||||
@@ -0,0 +1,433 @@
|
||||
import { mutation, query } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
const runtimeRunArgs = {
|
||||
schema: v.literal("mnote.acp_runtime_run.v1"),
|
||||
source: v.literal("acp"),
|
||||
userId: v.string(),
|
||||
workspaceId: v.optional(v.union(v.string(), v.null())),
|
||||
documentId: v.string(),
|
||||
sessionId: v.string(),
|
||||
runId: v.string(),
|
||||
title: v.optional(v.union(v.string(), v.null())),
|
||||
profile: v.string(),
|
||||
acpRuntime: v.string(),
|
||||
traceId: v.string(),
|
||||
status: v.string(),
|
||||
runtime: v.any(),
|
||||
payload: v.any(),
|
||||
retention: v.any(),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
};
|
||||
|
||||
const runtimeEventArgs = {
|
||||
schema: v.literal("mnote.acp_runtime_event.v1"),
|
||||
source: v.literal("acp"),
|
||||
eventId: v.string(),
|
||||
userId: v.string(),
|
||||
workspaceId: v.optional(v.union(v.string(), v.null())),
|
||||
documentId: v.string(),
|
||||
sessionId: v.string(),
|
||||
runId: v.string(),
|
||||
profile: v.string(),
|
||||
acpRuntime: v.string(),
|
||||
eventType: v.string(),
|
||||
payload: v.any(),
|
||||
createdAt: v.number(),
|
||||
};
|
||||
|
||||
function isoFromMillis(value: number) {
|
||||
return new Date(value).toISOString();
|
||||
}
|
||||
|
||||
async function requireScopedUser(ctx: any, expectedUserId: string) {
|
||||
const identity = await ctx.auth.getUserIdentity();
|
||||
const subject = String(identity?.subject ?? "");
|
||||
const identityUserId = subject.split("|")[0] || "";
|
||||
if (identityUserId && identityUserId !== expectedUserId) {
|
||||
throw new Error("无权限");
|
||||
}
|
||||
if (!identityUserId && !expectedUserId.trim()) {
|
||||
throw new Error("未登录");
|
||||
}
|
||||
return expectedUserId;
|
||||
}
|
||||
|
||||
function rowToClient(row: any) {
|
||||
return {
|
||||
id: row.id,
|
||||
userId: row.user_id,
|
||||
workspaceId: row.workspace_id,
|
||||
documentId: row.document_id,
|
||||
sessionId: row.session_id,
|
||||
runId: row.run_id,
|
||||
profile: row.profile,
|
||||
title: row.title,
|
||||
acpRuntime: row.acp_runtime,
|
||||
source: row.source,
|
||||
status: row.status,
|
||||
traceId: row.trace_id,
|
||||
runtime: row.runtime,
|
||||
usage: row.usage ?? null,
|
||||
payload: row.payload,
|
||||
retention: row.retention,
|
||||
createdAt: row.created_at,
|
||||
updatedAt: row.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
export const upsertRuntimeRun = mutation({
|
||||
args: runtimeRunArgs,
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireScopedUser(ctx, args.userId);
|
||||
const workspaceId = args.workspaceId ?? null;
|
||||
const nowIso = isoFromMillis(args.updatedAt);
|
||||
const existing = await ctx.db
|
||||
.query("acp_runtime_runs")
|
||||
.withIndex("by_run_id", (q: any) => q.eq("run_id", args.runId))
|
||||
.first();
|
||||
|
||||
const patch = {
|
||||
id: `acp_run:${args.runId}`,
|
||||
user_id: userId,
|
||||
workspace_id: workspaceId,
|
||||
document_id: args.documentId,
|
||||
session_id: args.sessionId,
|
||||
run_id: args.runId,
|
||||
title: args.title ?? null,
|
||||
profile: args.profile,
|
||||
acp_runtime: args.acpRuntime,
|
||||
source: args.source,
|
||||
status: args.status,
|
||||
trace_id: args.traceId,
|
||||
runtime: args.runtime,
|
||||
usage: null,
|
||||
payload: args.payload,
|
||||
retention: args.retention,
|
||||
updated_at: nowIso,
|
||||
deleted_at: null,
|
||||
};
|
||||
|
||||
if (existing) {
|
||||
if (String(existing.user_id) !== userId) {
|
||||
throw new Error("无权限");
|
||||
}
|
||||
await ctx.db.patch(existing._id, patch);
|
||||
return { ok: true, id: existing.id, runId: args.runId, updated: true };
|
||||
}
|
||||
|
||||
await ctx.db.insert("acp_runtime_runs", {
|
||||
...patch,
|
||||
created_at: isoFromMillis(args.createdAt),
|
||||
});
|
||||
return { ok: true, id: patch.id, runId: args.runId, created: true };
|
||||
},
|
||||
});
|
||||
|
||||
export const getRuntimeRun = query({
|
||||
args: {
|
||||
userId: v.string(),
|
||||
runId: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireScopedUser(ctx, args.userId);
|
||||
const row = await ctx.db
|
||||
.query("acp_runtime_runs")
|
||||
.withIndex("by_run_id", (q: any) => q.eq("run_id", args.runId))
|
||||
.first();
|
||||
if (!row || row.deleted_at !== null || String(row.user_id) !== userId) {
|
||||
return null;
|
||||
}
|
||||
return rowToClient(row);
|
||||
},
|
||||
});
|
||||
|
||||
export const listRuntimeRuns = query({
|
||||
args: {
|
||||
userId: v.string(),
|
||||
workspaceId: v.optional(v.union(v.string(), v.null())),
|
||||
documentId: v.optional(v.union(v.string(), v.null())),
|
||||
sessionId: v.optional(v.union(v.string(), v.null())),
|
||||
limit: v.optional(v.number()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireScopedUser(ctx, args.userId);
|
||||
const workspaceId = args.workspaceId ?? null;
|
||||
const limit = Math.min(Math.max(args.limit ?? 50, 1), 100);
|
||||
let rows;
|
||||
if (args.sessionId) {
|
||||
rows = await ctx.db
|
||||
.query("acp_runtime_runs")
|
||||
.withIndex("by_user_workspace_session", (q: any) =>
|
||||
q.eq("user_id", userId).eq("workspace_id", workspaceId).eq("session_id", args.sessionId),
|
||||
)
|
||||
.order("desc")
|
||||
.take(limit);
|
||||
} else if (args.documentId) {
|
||||
rows = await ctx.db
|
||||
.query("acp_runtime_runs")
|
||||
.withIndex("by_user_workspace_document", (q: any) =>
|
||||
q.eq("user_id", userId).eq("workspace_id", workspaceId).eq("document_id", args.documentId),
|
||||
)
|
||||
.order("desc")
|
||||
.take(limit);
|
||||
} else {
|
||||
rows = await ctx.db
|
||||
.query("acp_runtime_runs")
|
||||
.withIndex("by_user_created_at", (q: any) => q.eq("user_id", userId))
|
||||
.order("desc")
|
||||
.take(limit);
|
||||
}
|
||||
|
||||
return rows
|
||||
.filter((row: any) => row.deleted_at === null)
|
||||
.map(rowToClient);
|
||||
},
|
||||
});
|
||||
|
||||
export const appendRuntimeEvent = mutation({
|
||||
args: runtimeEventArgs,
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireScopedUser(ctx, args.userId);
|
||||
const workspaceId = args.workspaceId ?? null;
|
||||
const existingRun = await ctx.db
|
||||
.query("acp_runtime_runs")
|
||||
.withIndex("by_run_id", (q: any) => q.eq("run_id", args.runId))
|
||||
.first();
|
||||
if (existingRun && String(existingRun.user_id) !== userId) {
|
||||
throw new Error("无权限");
|
||||
}
|
||||
|
||||
await ctx.db.insert("acp_runtime_events", {
|
||||
id: args.eventId,
|
||||
user_id: userId,
|
||||
workspace_id: workspaceId,
|
||||
document_id: args.documentId,
|
||||
session_id: args.sessionId,
|
||||
run_id: args.runId,
|
||||
profile: args.profile,
|
||||
acp_runtime: args.acpRuntime,
|
||||
source: args.source,
|
||||
event_type: args.eventType,
|
||||
payload: args.payload,
|
||||
created_at: isoFromMillis(args.createdAt),
|
||||
});
|
||||
const usage = usageFromRuntimeEvent(args.eventType, args.payload);
|
||||
if (existingRun && usage) {
|
||||
await ctx.db.patch(existingRun._id, {
|
||||
usage,
|
||||
updated_at: isoFromMillis(args.createdAt),
|
||||
});
|
||||
}
|
||||
return { ok: true, id: args.eventId, runId: args.runId };
|
||||
},
|
||||
});
|
||||
|
||||
function usageFromRuntimeEvent(eventType: string, payload: any) {
|
||||
if (eventType === "usage.updated") {
|
||||
return {
|
||||
source: "usage_update",
|
||||
contextUsed: Number(payload?.used ?? payload?.contextUsed ?? 0),
|
||||
contextSize: Number(payload?.size ?? payload?.contextSize ?? 0),
|
||||
};
|
||||
}
|
||||
if (eventType === "run.completed" && payload?.usage && typeof payload.usage === "object") {
|
||||
return {
|
||||
source: "run_completed",
|
||||
...payload.usage,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const listRuntimeEvents = query({
|
||||
args: {
|
||||
userId: v.string(),
|
||||
runId: v.string(),
|
||||
limit: v.optional(v.number()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireScopedUser(ctx, args.userId);
|
||||
const limit = Math.min(Math.max(args.limit ?? 100, 1), 500);
|
||||
const rows = await ctx.db
|
||||
.query("acp_runtime_events")
|
||||
.withIndex("by_run_created_at", (q: any) => q.eq("run_id", args.runId))
|
||||
.order("asc")
|
||||
.take(limit);
|
||||
return rows
|
||||
.filter((row: any) => String(row.user_id) === userId)
|
||||
.map((row: any) => ({
|
||||
id: row.id,
|
||||
userId: row.user_id,
|
||||
workspaceId: row.workspace_id,
|
||||
documentId: row.document_id,
|
||||
sessionId: row.session_id,
|
||||
runId: row.run_id,
|
||||
profile: row.profile,
|
||||
acpRuntime: row.acp_runtime,
|
||||
source: row.source,
|
||||
eventType: row.event_type,
|
||||
payload: row.payload,
|
||||
createdAt: row.created_at,
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
function titleFromRun(row: any) {
|
||||
const payload = row?.payload ?? {};
|
||||
const raw = String(payload.message ?? payload.input ?? payload.title ?? "").trim();
|
||||
if (!raw) return "当前页问答";
|
||||
return raw.length > 32 ? `${raw.slice(0, 32)}...` : raw;
|
||||
}
|
||||
|
||||
async function sessionRows(ctx: any, userId: string, workspaceId: string | null, sessionId: string) {
|
||||
return await ctx.db
|
||||
.query("acp_runtime_runs")
|
||||
.withIndex("by_user_workspace_session", (q: any) =>
|
||||
q.eq("user_id", userId).eq("workspace_id", workspaceId).eq("session_id", sessionId),
|
||||
)
|
||||
.collect();
|
||||
}
|
||||
|
||||
export const renameRuntimeSession = mutation({
|
||||
args: {
|
||||
userId: v.string(),
|
||||
workspaceId: v.optional(v.union(v.string(), v.null())),
|
||||
sessionId: v.string(),
|
||||
title: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireScopedUser(ctx, args.userId);
|
||||
const workspaceId = args.workspaceId ?? null;
|
||||
const title = args.title.trim();
|
||||
if (!title) throw new Error("标题不能为空");
|
||||
const rows = await sessionRows(ctx, userId, workspaceId, args.sessionId);
|
||||
for (const row of rows) {
|
||||
if (row.deleted_at === null) {
|
||||
await ctx.db.patch(row._id, {
|
||||
title,
|
||||
updated_at: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
return { ok: true, sessionId: args.sessionId, title, updated: rows.length };
|
||||
},
|
||||
});
|
||||
|
||||
export const autoTitleRuntimeSession = mutation({
|
||||
args: {
|
||||
userId: v.string(),
|
||||
workspaceId: v.optional(v.union(v.string(), v.null())),
|
||||
sessionId: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireScopedUser(ctx, args.userId);
|
||||
const workspaceId = args.workspaceId ?? null;
|
||||
const rows = await sessionRows(ctx, userId, workspaceId, args.sessionId);
|
||||
const title = titleFromRun(rows.find((row: any) => row.deleted_at === null));
|
||||
for (const row of rows) {
|
||||
if (row.deleted_at === null) {
|
||||
await ctx.db.patch(row._id, {
|
||||
title,
|
||||
updated_at: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
return { ok: true, sessionId: args.sessionId, title, updated: rows.length };
|
||||
},
|
||||
});
|
||||
|
||||
export const deleteRuntimeSession = mutation({
|
||||
args: {
|
||||
userId: v.string(),
|
||||
workspaceId: v.optional(v.union(v.string(), v.null())),
|
||||
sessionId: v.string(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireScopedUser(ctx, args.userId);
|
||||
const workspaceId = args.workspaceId ?? null;
|
||||
const rows = await sessionRows(ctx, userId, workspaceId, args.sessionId);
|
||||
const deletedAt = new Date().toISOString();
|
||||
for (const row of rows) {
|
||||
if (row.deleted_at === null) {
|
||||
await ctx.db.patch(row._id, {
|
||||
deleted_at: deletedAt,
|
||||
updated_at: deletedAt,
|
||||
});
|
||||
}
|
||||
}
|
||||
return { ok: true, sessionId: args.sessionId, deleted: rows.length };
|
||||
},
|
||||
});
|
||||
|
||||
function searchText(row: any) {
|
||||
const payload = row?.payload ?? {};
|
||||
return [
|
||||
row.title,
|
||||
row.session_id,
|
||||
row.run_id,
|
||||
row.profile,
|
||||
row.status,
|
||||
payload.message,
|
||||
payload.input,
|
||||
payload.title,
|
||||
]
|
||||
.map((value) => String(value ?? ""))
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function snippetFor(row: any, q: string) {
|
||||
const payload = row?.payload ?? {};
|
||||
const raw = String(row.title ?? payload.message ?? payload.input ?? row.session_id ?? "").trim();
|
||||
if (!raw) return "";
|
||||
const index = raw.toLowerCase().indexOf(q.toLowerCase());
|
||||
if (index < 0) return raw.length > 80 ? `${raw.slice(0, 80)}...` : raw;
|
||||
const start = Math.max(0, index - 24);
|
||||
const end = Math.min(raw.length, index + q.length + 56);
|
||||
return `${start > 0 ? "..." : ""}${raw.slice(start, end)}${end < raw.length ? "..." : ""}`;
|
||||
}
|
||||
|
||||
export const searchRuntimeSessions = query({
|
||||
args: {
|
||||
userId: v.string(),
|
||||
workspaceId: v.optional(v.union(v.string(), v.null())),
|
||||
q: v.string(),
|
||||
limit: v.optional(v.number()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await requireScopedUser(ctx, args.userId);
|
||||
const workspaceId = args.workspaceId ?? null;
|
||||
const q = args.q.trim().toLowerCase();
|
||||
if (!q) return [];
|
||||
const limit = Math.min(Math.max(args.limit ?? 20, 1), 50);
|
||||
const rows = await ctx.db
|
||||
.query("acp_runtime_runs")
|
||||
.withIndex("by_user_created_at", (query: any) => query.eq("user_id", userId))
|
||||
.order("desc")
|
||||
.take(500);
|
||||
const seen = new Set<string>();
|
||||
const results = [];
|
||||
for (const row of rows) {
|
||||
if (row.deleted_at !== null) continue;
|
||||
if ((row.workspace_id ?? null) !== workspaceId) continue;
|
||||
if (!searchText(row).includes(q)) continue;
|
||||
if (seen.has(row.session_id)) continue;
|
||||
seen.add(row.session_id);
|
||||
results.push({
|
||||
sessionId: row.session_id,
|
||||
runId: row.run_id,
|
||||
workspaceId: row.workspace_id,
|
||||
documentId: row.document_id,
|
||||
title: row.title ?? titleFromRun(row),
|
||||
profile: row.profile,
|
||||
status: row.status,
|
||||
snippet: snippetFor(row, args.q),
|
||||
score: 1,
|
||||
});
|
||||
if (results.length >= limit) break;
|
||||
}
|
||||
return results;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import { defineSchema, defineTable } from "convex/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
export default defineSchema({
|
||||
acp_runtime_runs: defineTable({
|
||||
id: v.string(),
|
||||
user_id: v.string(),
|
||||
workspace_id: v.union(v.string(), v.null()),
|
||||
document_id: v.string(),
|
||||
session_id: v.string(),
|
||||
run_id: v.string(),
|
||||
title: v.union(v.string(), v.null()),
|
||||
profile: v.string(),
|
||||
acp_runtime: v.string(),
|
||||
source: v.literal("acp"),
|
||||
status: v.string(),
|
||||
trace_id: v.string(),
|
||||
runtime: v.any(),
|
||||
usage: v.any(),
|
||||
payload: v.any(),
|
||||
retention: v.any(),
|
||||
created_at: v.string(),
|
||||
updated_at: v.string(),
|
||||
deleted_at: v.union(v.string(), v.null()),
|
||||
})
|
||||
.index("by_run_id", ["run_id"])
|
||||
.index("by_user_workspace_document", ["user_id", "workspace_id", "document_id", "created_at"])
|
||||
.index("by_user_workspace_session", ["user_id", "workspace_id", "session_id", "created_at"])
|
||||
.index("by_user_created_at", ["user_id", "created_at"]),
|
||||
|
||||
acp_runtime_events: defineTable({
|
||||
id: v.string(),
|
||||
user_id: v.string(),
|
||||
workspace_id: v.union(v.string(), v.null()),
|
||||
document_id: v.string(),
|
||||
session_id: v.string(),
|
||||
run_id: v.string(),
|
||||
profile: v.string(),
|
||||
acp_runtime: v.string(),
|
||||
source: v.literal("acp"),
|
||||
event_type: v.string(),
|
||||
payload: v.any(),
|
||||
created_at: v.string(),
|
||||
})
|
||||
.index("by_run_created_at", ["run_id", "created_at"])
|
||||
.index("by_user_workspace_session_created_at", [
|
||||
"user_id",
|
||||
"workspace_id",
|
||||
"session_id",
|
||||
"created_at",
|
||||
]),
|
||||
});
|
||||
Reference in New Issue
Block a user