0.3 增加登录模块
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
// 简单的 ID 生成器,用于生成类似 UUID 的字符串
|
||||
export function generateId(): string {
|
||||
// 使用时间戳 + 随机数生成唯一 ID
|
||||
const timestamp = Date.now().toString(36);
|
||||
const random = Math.random().toString(36).substring(2, 15);
|
||||
return `${timestamp}${random}`;
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { internal } from "../_generated/api";
|
||||
import { nowIso } from "./time";
|
||||
import type { MutationCtx } from "../_generated/server";
|
||||
|
||||
type JobStatus = "queued" | "running" | "succeeded" | "failed";
|
||||
|
||||
async function upsertJob(
|
||||
ctx: MutationCtx,
|
||||
args: {
|
||||
id: string;
|
||||
userId: string;
|
||||
type: string;
|
||||
payload: unknown;
|
||||
debounceMs?: number;
|
||||
},
|
||||
): Promise<{ id: string; status: JobStatus }> {
|
||||
const debounceMs = Math.max(0, Math.floor(args.debounceMs ?? 1200));
|
||||
const ts = nowIso();
|
||||
|
||||
const existing = await ctx.db
|
||||
.query("jobs")
|
||||
.withIndex("by_job_id", (q) => q.eq("id", args.id))
|
||||
.first();
|
||||
|
||||
if (existing) {
|
||||
if (existing.user_id !== args.userId) {
|
||||
// 说明:避免不同用户复用同一 job id 导致“互相覆盖”。
|
||||
return { id: args.id, status: existing.status as JobStatus };
|
||||
}
|
||||
|
||||
const status = String(existing.status) as JobStatus;
|
||||
if (status === "running" || status === "queued") {
|
||||
await ctx.db.patch(existing._id, { updated_at: ts });
|
||||
// 说明:start 内部会检查状态,重复 schedule 不会造成重复执行。
|
||||
await ctx.scheduler.runAfter(debounceMs, internal.jobs.start, { id: args.id });
|
||||
return { id: args.id, status };
|
||||
}
|
||||
|
||||
await ctx.db.patch(existing._id, {
|
||||
type: args.type,
|
||||
status: "queued",
|
||||
payload: args.payload,
|
||||
result: null,
|
||||
error: null,
|
||||
updated_at: ts,
|
||||
started_at: null,
|
||||
finished_at: null,
|
||||
});
|
||||
await ctx.scheduler.runAfter(debounceMs, internal.jobs.start, { id: args.id });
|
||||
return { id: args.id, status: "queued" };
|
||||
}
|
||||
|
||||
await ctx.db.insert("jobs", {
|
||||
id: args.id,
|
||||
user_id: args.userId,
|
||||
type: args.type,
|
||||
status: "queued",
|
||||
payload: args.payload,
|
||||
result: null,
|
||||
error: null,
|
||||
created_at: ts,
|
||||
updated_at: ts,
|
||||
started_at: null,
|
||||
finished_at: null,
|
||||
});
|
||||
await ctx.scheduler.runAfter(debounceMs, internal.jobs.start, { id: args.id });
|
||||
return { id: args.id, status: "queued" };
|
||||
}
|
||||
|
||||
export function ingestDocumentJobId(documentId: string): string {
|
||||
return `ingest:document:${documentId}`;
|
||||
}
|
||||
|
||||
export function ingestMindmapJobId(docId: string, mindmapId: string): string {
|
||||
return `ingest:mindmap:${docId}:${mindmapId}`;
|
||||
}
|
||||
|
||||
export function ingestMediaAssetJobId(assetId: string): string {
|
||||
return `ingest:media:${assetId}`;
|
||||
}
|
||||
|
||||
export async function enqueueIngestDocumentJob(
|
||||
ctx: MutationCtx,
|
||||
args: { userId: string; documentId: string; debounceMs?: number },
|
||||
): Promise<{ id: string }> {
|
||||
const id = ingestDocumentJobId(args.documentId);
|
||||
await upsertJob(ctx, {
|
||||
id,
|
||||
userId: args.userId,
|
||||
type: "ingest.rag_index_document",
|
||||
payload: { documentId: args.documentId },
|
||||
debounceMs: args.debounceMs,
|
||||
});
|
||||
return { id };
|
||||
}
|
||||
|
||||
export async function enqueueIngestMindmapJob(
|
||||
ctx: MutationCtx,
|
||||
args: { userId: string; docId: string; mindmapId: string; debounceMs?: number },
|
||||
): Promise<{ id: string }> {
|
||||
const id = ingestMindmapJobId(args.docId, args.mindmapId);
|
||||
await upsertJob(ctx, {
|
||||
id,
|
||||
userId: args.userId,
|
||||
type: "ingest.rag_index_mindmap",
|
||||
payload: { docId: args.docId, mindmapId: args.mindmapId },
|
||||
debounceMs: args.debounceMs,
|
||||
});
|
||||
return { id };
|
||||
}
|
||||
|
||||
export async function enqueueIngestMediaAssetJob(
|
||||
ctx: MutationCtx,
|
||||
args: { userId: string; assetId: string; debounceMs?: number },
|
||||
): Promise<{ id: string }> {
|
||||
const id = ingestMediaAssetJobId(args.assetId);
|
||||
await upsertJob(ctx, {
|
||||
id,
|
||||
userId: args.userId,
|
||||
type: "ingest.rag_index_media_asset",
|
||||
payload: { assetId: args.assetId },
|
||||
debounceMs: args.debounceMs,
|
||||
});
|
||||
return { id };
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
type IngestTextArgs = {
|
||||
fileSource: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
export async function lightragIngestText(args: IngestTextArgs): Promise<{ trackId: string | null }> {
|
||||
const baseUrl = (process.env.LIGHTRAG_URL || "").trim().replace(/\/+$/, "");
|
||||
if (!baseUrl) {
|
||||
throw new Error("缺少环境变量:LIGHTRAG_URL");
|
||||
}
|
||||
|
||||
const fileSource = String(args.fileSource ?? "").trim();
|
||||
if (!fileSource) {
|
||||
throw new Error("缺少 fileSource");
|
||||
}
|
||||
|
||||
const text = String(args.text ?? "");
|
||||
if (!text.trim()) {
|
||||
return { trackId: null };
|
||||
}
|
||||
|
||||
const apiKey = (process.env.LIGHTRAG_API_KEY || "").trim();
|
||||
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
||||
if (apiKey) {
|
||||
headers["X-API-Key"] = apiKey;
|
||||
}
|
||||
|
||||
// 说明:LightRAG(>=1.4.x) 使用 /documents/text 作为“文本入库”接口。
|
||||
const resp = await fetch(`${baseUrl}/documents/text`, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({ text, file_source: fileSource }),
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const raw = await resp.text().catch(() => "");
|
||||
throw new Error(`LightRAG 入库失败:HTTP ${resp.status} ${raw.slice(0, 500)}`);
|
||||
}
|
||||
|
||||
const data = (await resp.json().catch(() => null)) as unknown;
|
||||
const trackId = (() => {
|
||||
if (!data || typeof data !== "object") return null;
|
||||
const v = (data as Record<string, unknown>).track_id;
|
||||
return typeof v === "string" ? v : null;
|
||||
})();
|
||||
return { trackId };
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
function pushText(out: string[], value: unknown) {
|
||||
const s = typeof value === "string" ? value : null;
|
||||
if (!s) return;
|
||||
const trimmed = s.replace(/\s+/g, " ").trim();
|
||||
if (!trimmed) return;
|
||||
out.push(trimmed);
|
||||
}
|
||||
|
||||
export function extractTextFromDocumentContent(content: unknown, maxChars = 60000): string {
|
||||
const out: string[] = [];
|
||||
|
||||
const readPropText = (value: unknown) => {
|
||||
if (!value || typeof value !== "object") return null;
|
||||
const t = (value as Record<string, unknown>).text;
|
||||
return typeof t === "string" ? t : null;
|
||||
};
|
||||
|
||||
const walk = (node: unknown) => {
|
||||
if (out.join("\n").length >= maxChars) return;
|
||||
|
||||
if (node == null) return;
|
||||
if (typeof node === "string") {
|
||||
pushText(out, node);
|
||||
return;
|
||||
}
|
||||
if (typeof node === "number" || typeof node === "boolean") return;
|
||||
|
||||
if (Array.isArray(node)) {
|
||||
for (const item of node) {
|
||||
walk(item);
|
||||
if (out.join("\n").length >= maxChars) return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof node === "object") {
|
||||
const obj = node as Record<string, unknown>;
|
||||
|
||||
// 兼容常见 BlockNote 结构:
|
||||
// - block.props.text
|
||||
// - block.content: [{ text: "..." }, ...]
|
||||
// - block.children: [...]
|
||||
pushText(out, obj.text);
|
||||
pushText(out, readPropText(obj.props));
|
||||
|
||||
const maybeContent = obj.content;
|
||||
if (Array.isArray(maybeContent)) {
|
||||
for (const seg of maybeContent) {
|
||||
if (typeof seg === "string") pushText(out, seg);
|
||||
else if (seg && typeof seg === "object") pushText(out, readPropText(seg));
|
||||
if (out.join("\n").length >= maxChars) return;
|
||||
}
|
||||
}
|
||||
|
||||
const children = obj.children;
|
||||
if (Array.isArray(children)) {
|
||||
for (const c of children) {
|
||||
walk(c);
|
||||
if (out.join("\n").length >= maxChars) return;
|
||||
}
|
||||
}
|
||||
|
||||
// 兜底:遍历其它字段(避免遗漏 title/caption 等)
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
if (k === "children" || k === "content" || k === "props" || k === "text") continue;
|
||||
if (typeof v === "string") pushText(out, v);
|
||||
else if (Array.isArray(v) || (v && typeof v === "object")) walk(v);
|
||||
if (out.join("\n").length >= maxChars) return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
walk(content);
|
||||
|
||||
const joined = out.join("\n").trim();
|
||||
return joined.length > maxChars ? `${joined.slice(0, maxChars)}…` : joined;
|
||||
}
|
||||
|
||||
type MindmapNode = { data?: { text?: unknown }; children?: unknown[] };
|
||||
|
||||
export function extractTextFromMindmapData(data: unknown, maxChars = 60000): string {
|
||||
const out: string[] = [];
|
||||
|
||||
const walk = (node: unknown) => {
|
||||
if (out.join("\n").length >= maxChars) return;
|
||||
if (!node || typeof node !== "object") return;
|
||||
const n = node as MindmapNode;
|
||||
pushText(out, n.data?.text);
|
||||
if (Array.isArray(n.children)) {
|
||||
for (const c of n.children) {
|
||||
walk(c);
|
||||
if (out.join("\n").length >= maxChars) return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
walk(data);
|
||||
|
||||
const joined = out.join("\n").trim();
|
||||
return joined.length > maxChars ? `${joined.slice(0, maxChars)}…` : joined;
|
||||
}
|
||||
Reference in New Issue
Block a user