tmp
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import z from "zod";
|
||||
import mongoose from "mongoose";
|
||||
import { DBModel } from "../../../../models/index.js";
|
||||
|
||||
const healthProfileQueryTool = tool(
|
||||
async ({ profileId, userId, name, mobile, page = 1, pageSize = 20 }) => {
|
||||
try {
|
||||
// 按档案 _id 精确查询单条
|
||||
if (profileId) {
|
||||
if (!mongoose.Types.ObjectId.isValid(profileId)) {
|
||||
return { success: false, error: `Invalid profile ID: ${profileId}` };
|
||||
}
|
||||
const doc = await DBModel.HealthProfile.findById(profileId).lean();
|
||||
if (!doc) {
|
||||
return { success: false, error: `未找到 ID 为 ${profileId} 的健康档案` };
|
||||
}
|
||||
return { success: true, data: [doc], total: 1 };
|
||||
}
|
||||
|
||||
const filter = {};
|
||||
|
||||
if (userId) {
|
||||
if (!mongoose.Types.ObjectId.isValid(userId)) {
|
||||
return { success: false, error: `Invalid user ID: ${userId}` };
|
||||
}
|
||||
filter.userId = new mongoose.Types.ObjectId(userId);
|
||||
}
|
||||
if (name) {
|
||||
// 转义正则特殊字符,避免查询报错或 ReDoS;姓名支持中文、全拼、拼音首字母匹配
|
||||
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const re = { $regex: escaped, $options: "i" };
|
||||
filter.$or = [{ "profile.name": re }, { "profile.pinyin": re }, { "profile.pinyinFL": re }];
|
||||
}
|
||||
if (mobile) {
|
||||
filter["profile.mobile"] = mobile;
|
||||
}
|
||||
|
||||
const skip = (page - 1) * pageSize;
|
||||
|
||||
const [list, total] = await Promise.all([
|
||||
DBModel.HealthProfile.find(filter)
|
||||
.sort({ "meta.createtime": -1 })
|
||||
.skip(skip)
|
||||
.limit(pageSize)
|
||||
.lean(),
|
||||
DBModel.HealthProfile.countDocuments(filter),
|
||||
]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: list,
|
||||
total,
|
||||
page,
|
||||
pageSize,
|
||||
};
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "health_profile_query",
|
||||
description:
|
||||
"查询患者健康档案。支持按档案 ID 精确查询,或按用户 ID、患者姓名(模糊)、患者电话筛选,返回分页结果。",
|
||||
schema: z.object({
|
||||
profileId: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("健康档案 ID(_id),提供时精确查询单条,忽略其他筛选条件"),
|
||||
userId: z.string().optional().describe("所属用户ID(精确匹配)"),
|
||||
name: z.string().optional().describe("患者姓名(支持中文模糊、全拼、拼音首字母匹配,如 张三/zhangsan/zs)"),
|
||||
mobile: z.string().optional().describe("患者电话(精确匹配)"),
|
||||
page: z.number().int().min(1).optional().describe("页码,从 1 开始"),
|
||||
pageSize: z
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(50)
|
||||
.optional()
|
||||
.describe("每页数量(1-50),默认 20"),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
export { healthProfileQueryTool };
|
||||
@@ -0,0 +1,140 @@
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import z from "zod";
|
||||
import mongoose from "mongoose";
|
||||
import { DBModel } from "../../../../models/index.js";
|
||||
import { genPinyin, genPinyinFL } from "../../../../models/schema/health_profile.js";
|
||||
|
||||
const PROFILE_KEYS = ["name", "mobile", "sex", "birth", "idnumber"];
|
||||
const LOCATION_KEYS = ["country", "province", "city", "district", "address"];
|
||||
const HEALTH_KEYS = ["height", "weight", "bloodType", "remark"];
|
||||
|
||||
const profileSchema = z
|
||||
.object({
|
||||
name: z.string().optional().describe("患者姓名"),
|
||||
mobile: z.string().optional().describe("患者电话"),
|
||||
sex: z.enum(["male", "female"]).optional().describe("性别"),
|
||||
birth: z.string().optional().describe("出生年月(YYYY-MM-DD)"),
|
||||
idnumber: z.string().optional().describe("证件号(身份证/护照等)"),
|
||||
})
|
||||
.optional()
|
||||
.describe("患者基本信息");
|
||||
|
||||
const locationSchema = z
|
||||
.object({
|
||||
country: z.string().optional().describe("国家(国外用户填国家,国内默认中国)"),
|
||||
province: z.string().optional().describe("省"),
|
||||
city: z.string().optional().describe("市"),
|
||||
district: z.string().optional().describe("区/县"),
|
||||
address: z.string().optional().describe("详细地址"),
|
||||
})
|
||||
.optional()
|
||||
.describe("所在地信息");
|
||||
|
||||
const healthSchema = z
|
||||
.object({
|
||||
height: z.number().optional().describe("身高(cm)"),
|
||||
weight: z.number().optional().describe("体重(kg)"),
|
||||
bloodType: z.string().optional().describe("血型"),
|
||||
remark: z.string().optional().describe("备注"),
|
||||
})
|
||||
.optional()
|
||||
.describe("健康信息");
|
||||
|
||||
const healthProfileSetTool = tool(
|
||||
async ({ action, profileId, userId, profile, location, health }) => {
|
||||
try {
|
||||
if (action === "create") {
|
||||
if (!profile || (!profile.name && !profile.mobile)) {
|
||||
return { success: false, error: "创建健康档案至少需要提供患者姓名或电话" };
|
||||
}
|
||||
|
||||
const docData = {
|
||||
profile: { ...profile },
|
||||
location: { ...location },
|
||||
health: { ...health },
|
||||
meta: { createtime: new Date(), updatetime: new Date() },
|
||||
};
|
||||
if (userId) {
|
||||
if (!mongoose.Types.ObjectId.isValid(userId)) {
|
||||
return { success: false, error: `Invalid user ID: ${userId}` };
|
||||
}
|
||||
docData.userId = new mongoose.Types.ObjectId(userId);
|
||||
}
|
||||
|
||||
const doc = await DBModel.HealthProfile.create(docData);
|
||||
return { success: true, data: doc };
|
||||
}
|
||||
|
||||
if (action === "update") {
|
||||
if (!profileId) {
|
||||
return { success: false, error: "更新健康档案必须提供 profileId(可先用 health_profile_query 查询定位)" };
|
||||
}
|
||||
if (!mongoose.Types.ObjectId.isValid(profileId)) {
|
||||
return { success: false, error: `Invalid profile ID: ${profileId}` };
|
||||
}
|
||||
|
||||
const update = {};
|
||||
if (userId) {
|
||||
if (!mongoose.Types.ObjectId.isValid(userId)) {
|
||||
return { success: false, error: `Invalid user ID: ${userId}` };
|
||||
}
|
||||
update.userId = new mongoose.Types.ObjectId(userId);
|
||||
}
|
||||
if (profile) {
|
||||
for (const key of PROFILE_KEYS) {
|
||||
if (profile[key] !== undefined) update[`profile.${key}`] = profile[key];
|
||||
}
|
||||
// 更新姓名时同步拼音字段(findByIdAndUpdate 不触发 pre("save"))
|
||||
if (profile.name !== undefined) {
|
||||
update["profile.pinyin"] = genPinyin(profile.name);
|
||||
update["profile.pinyinFL"] = genPinyinFL(profile.name);
|
||||
}
|
||||
}
|
||||
if (location) {
|
||||
for (const key of LOCATION_KEYS) {
|
||||
if (location[key] !== undefined) update[`location.${key}`] = location[key];
|
||||
}
|
||||
}
|
||||
if (health) {
|
||||
for (const key of HEALTH_KEYS) {
|
||||
if (health[key] !== undefined) update[`health.${key}`] = health[key];
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(update).length === 0) {
|
||||
return { success: false, error: "未提供任何要更新的字段" };
|
||||
}
|
||||
update["meta.updatetime"] = new Date();
|
||||
|
||||
const updated = await DBModel.HealthProfile.findByIdAndUpdate(
|
||||
profileId,
|
||||
{ $set: update },
|
||||
{ new: true }
|
||||
);
|
||||
if (!updated) {
|
||||
return { success: false, error: `未找到 ID 为 ${profileId} 的健康档案` };
|
||||
}
|
||||
return { success: true, data: updated };
|
||||
}
|
||||
|
||||
return { success: false, error: "未知的 action,可选值:create / update" };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "health_profile_set",
|
||||
description:
|
||||
"创建或更新患者健康档案。create 新建档案(至少提供患者姓名或电话);update 按档案 ID 局部更新,仅修改提供的字段。更新前建议先用 health_profile_query 查询现有内容。",
|
||||
schema: z.object({
|
||||
action: z.enum(["create", "update"]).describe("操作类型:create 新建 / update 更新"),
|
||||
profileId: z.string().optional().describe("健康档案 ID(_id),update 时必填"),
|
||||
userId: z.string().optional().describe("所属用户ID(可选,用于关联小程序用户)"),
|
||||
profile: profileSchema,
|
||||
location: locationSchema,
|
||||
health: healthSchema,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
export { healthProfileSetTool };
|
||||
@@ -0,0 +1,32 @@
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import z from "zod";
|
||||
import { DBModel } from "../../../../models/index.js";
|
||||
|
||||
const hospitalInfoGetTool = tool(
|
||||
async ({ hospitalId }) => {
|
||||
try {
|
||||
if (!hospitalId) {
|
||||
return { success: false, error: "hospitalId 不能为空" };
|
||||
}
|
||||
|
||||
const org = await DBModel.Organization.findById(hospitalId).lean();
|
||||
if (!org) {
|
||||
return { success: false, error: `未找到 id 为「${hospitalId}」的机构` };
|
||||
}
|
||||
|
||||
return { success: true, data: org };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "hospital_info_get",
|
||||
description:
|
||||
"根据医院 id 获取医院完整信息。id 可通过 hospital_info_list 获取。",
|
||||
schema: z.object({
|
||||
hospitalId: z.string().describe("医院 id(MongoDB ObjectId,来自 hospital_info_list)"),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
export { hospitalInfoGetTool };
|
||||
@@ -0,0 +1,38 @@
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import z from "zod";
|
||||
import { DBModel } from "../../../../models/index.js";
|
||||
|
||||
const hospitalInfoListTool = tool(
|
||||
async ({ keyword }) => {
|
||||
try {
|
||||
const filter = {};
|
||||
if (keyword) {
|
||||
const escaped = String(keyword).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const re = { $regex: escaped, $options: "i" };
|
||||
filter.$or = [{ name: re }, { pinyin: re }, { pinyinFL: re }];
|
||||
}
|
||||
|
||||
const total = await DBModel.Organization.countDocuments(filter);
|
||||
// 轻量列表:只取 id+名称,用于先定位医院,再按 id 查详情
|
||||
const orgs = await DBModel.Organization.find(filter)
|
||||
.sort({ name: 1 })
|
||||
.limit(1000)
|
||||
.select("name")
|
||||
.lean();
|
||||
|
||||
return { success: true, total, data: orgs.map((o) => ({ id: o._id, name: o.name })) };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "hospital_info_list",
|
||||
description:
|
||||
"获取医院(名称、id)对列表,支持按名称/拼音关键词过滤,不传关键词返回全部。",
|
||||
schema: z.object({
|
||||
keyword: z.string().optional().describe("医院名称/拼音/拼音首字母关键词(如 协和、xiehe),不传返回全部"),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
export { hospitalInfoListTool };
|
||||
@@ -0,0 +1,63 @@
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import z from "zod";
|
||||
import { DBModel } from "../../../../models/index.js";
|
||||
|
||||
const hospitalInfoQueryTool = tool(
|
||||
async ({ hospitalName, department, doctorName }) => {
|
||||
try {
|
||||
if (!hospitalName) {
|
||||
return { success: false, error: "hospitalName 不能为空" };
|
||||
}
|
||||
|
||||
const filter = { name: { $regex: hospitalName, $options: "i" } };
|
||||
const total = await DBModel.Organization.countDocuments(filter);
|
||||
if (total === 0) {
|
||||
return { success: false, error: `未找到名称包含「${hospitalName}」的机构` };
|
||||
}
|
||||
|
||||
// 医院总量小,一次最多取 10 家,命中过多时由调用方细化名称
|
||||
const orgs = await DBModel.Organization.find(filter).limit(10).lean();
|
||||
|
||||
const data = orgs.map((org) => {
|
||||
const fa = org.forAgent ?? {};
|
||||
let departments = fa.departments ?? [];
|
||||
let doctors = fa.doctors ?? [];
|
||||
if (department) {
|
||||
departments = departments.filter((d) => d.name?.includes(department));
|
||||
doctors = doctors.filter((d) => d.department?.includes(department));
|
||||
}
|
||||
if (doctorName) {
|
||||
doctors = doctors.filter((d) => d.name?.includes(doctorName));
|
||||
}
|
||||
return {
|
||||
_id: org._id,
|
||||
name: org.name,
|
||||
level: org.level ?? "",
|
||||
address: org.address ?? {},
|
||||
infoUpdatedAt: fa.updatedAt ?? null,
|
||||
overview: fa.overview ?? "",
|
||||
photos: fa.photos ?? [],
|
||||
departments,
|
||||
doctors,
|
||||
guides: fa.guides ?? {},
|
||||
};
|
||||
});
|
||||
|
||||
return { success: true, total, data };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "hospital_info_query",
|
||||
description:
|
||||
"查询医院就医信息:医院总览(门诊/挂号/缴费/医保/交通)、科室(位置/电话/就诊提示)、医生(职称/擅长/号别/坐班)、场景指南(初诊/住院/检查须知/老人服务)、照片。按医院名称模糊查询,可按科室、医生名过滤。最多返回 10 家医院信息。",
|
||||
schema: z.object({
|
||||
hospitalName: z.string().describe("医院名称,支持模糊匹配(如 协和)"),
|
||||
department: z.string().optional().describe("科室名称过滤(如 风湿免疫科、放射科)"),
|
||||
doctorName: z.string().optional().describe("医生姓名过滤"),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
export { hospitalInfoQueryTool };
|
||||
@@ -0,0 +1,143 @@
|
||||
import { tool } from "@langchain/core/tools";
|
||||
import z from "zod";
|
||||
import { DBModel } from "../../../../models/index.js";
|
||||
|
||||
const hospitalInfoSetTool = tool(
|
||||
async ({ hospitalName, section, action, content, guides, entry, entryName, updatedBy }) => {
|
||||
try {
|
||||
if (!hospitalName) {
|
||||
return { success: false, error: "hospitalName 不能为空" };
|
||||
}
|
||||
|
||||
// 定位医院:名称必须唯一命中
|
||||
const orgs = await DBModel.Organization.find({ name: { $regex: hospitalName, $options: "i" } }).exec();
|
||||
if (orgs.length === 0) {
|
||||
return { success: false, error: `未找到名称包含「${hospitalName}」的机构,请确认医院名称后重试` };
|
||||
}
|
||||
if (orgs.length > 1) {
|
||||
return {
|
||||
success: false,
|
||||
error: `名称包含「${hospitalName}」的机构有 ${orgs.length} 家(${orgs.map((o) => o.name).join("、")}),请使用更精确的名称`,
|
||||
};
|
||||
}
|
||||
const org = orgs[0];
|
||||
const fa = org.forAgent ?? {};
|
||||
|
||||
const update = {};
|
||||
|
||||
if (section === "overview") {
|
||||
// 文本块全量替换
|
||||
if (typeof content !== "string") {
|
||||
return { success: false, error: "overview 需要提供 content 文本(写入前请先查询现有内容并合并)" };
|
||||
}
|
||||
update["forAgent.overview"] = content;
|
||||
} else if (section === "guides") {
|
||||
// 场景键值对浅合并,值为 null 表示删除该场景
|
||||
if (!guides || typeof guides !== "object" || Array.isArray(guides)) {
|
||||
return { success: false, error: "guides 需要提供对象,键为场景名,值为文本(值为 null 表示删除该场景)" };
|
||||
}
|
||||
const merged = { ...(fa.guides ?? {}) };
|
||||
for (const [key, value] of Object.entries(guides)) {
|
||||
if (value === null) {
|
||||
delete merged[key];
|
||||
} else {
|
||||
merged[key] = value;
|
||||
}
|
||||
}
|
||||
update["forAgent.guides"] = merged;
|
||||
} else if (section === "photos" || section === "departments" || section === "doctors") {
|
||||
// 列表区块:按锚点 upsert(新增或整条替换)/ remove
|
||||
const list = [...(fa[section] ?? [])];
|
||||
|
||||
if (action === "upsert") {
|
||||
if (!entry || typeof entry !== "object") {
|
||||
return { success: false, error: "upsert 需要提供 entry 对象" };
|
||||
}
|
||||
let idx = -1;
|
||||
if (section === "photos") {
|
||||
if (!entry.url) return { success: false, error: "photos 条目必须包含 url 作为锚点" };
|
||||
idx = list.findIndex((p) => p.url === entry.url);
|
||||
} else if (section === "doctors") {
|
||||
if (!entry.name || !entry.department) {
|
||||
return { success: false, error: "doctors 条目必须包含 name 和 department 作为锚点" };
|
||||
}
|
||||
idx = list.findIndex((d) => d.name === entry.name && d.department === entry.department);
|
||||
} else {
|
||||
if (!entry.name) return { success: false, error: "departments 条目必须包含 name 作为锚点" };
|
||||
idx = list.findIndex((d) => d.name === entry.name);
|
||||
}
|
||||
if (idx >= 0) {
|
||||
list[idx] = { ...list[idx], ...entry };
|
||||
} else {
|
||||
list.push(entry);
|
||||
}
|
||||
update[`forAgent.${section}`] = list;
|
||||
} else if (action === "remove") {
|
||||
if (!entryName) {
|
||||
return {
|
||||
success: false,
|
||||
error: `remove 需要提供 entryName(${section === "photos" ? "照片url" : section === "doctors" ? "医生姓名" : "科室名称"})`,
|
||||
};
|
||||
}
|
||||
if (section === "photos") {
|
||||
update[`forAgent.${section}`] = list.filter((p) => p.url !== entryName);
|
||||
} else {
|
||||
update[`forAgent.${section}`] = list.filter((d) => d.name !== entryName);
|
||||
}
|
||||
} else {
|
||||
return { success: false, error: "列表区块(photos/departments/doctors)需要指定 action: upsert 或 remove" };
|
||||
}
|
||||
} else {
|
||||
return { success: false, error: "未知的 section,可选值:overview / photos / departments / doctors / guides" };
|
||||
}
|
||||
|
||||
update["forAgent.updatedAt"] = new Date();
|
||||
if (updatedBy) {
|
||||
update["forAgent.updatedBy"] = updatedBy;
|
||||
}
|
||||
|
||||
const updated = await DBModel.Organization.findByIdAndUpdate(org._id, { $set: update }, { new: true }).exec();
|
||||
return { success: true, data: updated.forAgent };
|
||||
} catch (error) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "hospital_info_set",
|
||||
description:
|
||||
"录入/更新医院就医信息。按区块维护:overview(医院级文本,全量替换,先查后改避免丢失)、guides(场景指南键值对,浅合并)、photos/departments/doctors(列表,按锚点 upsert 或 remove)。医院名称必须唯一命中。",
|
||||
schema: z.object({
|
||||
hospitalName: z.string().describe("医院名称(需唯一命中,如 北京协和医院)"),
|
||||
section: z
|
||||
.enum(["overview", "photos", "departments", "doctors", "guides"])
|
||||
.describe("要更新的区块"),
|
||||
action: z
|
||||
.enum(["set", "upsert", "remove"])
|
||||
.optional()
|
||||
.describe("操作类型:overview/guides 用 set;photos/departments/doctors 用 upsert 或 remove"),
|
||||
content: z.string().optional().describe("overview 区块的完整文本(全量替换)"),
|
||||
guides: z
|
||||
.record(z.string(), z.string().nullable())
|
||||
.optional()
|
||||
.describe('guides 区块的场景键值对,如 {"住院流程": "..."};值为 null 表示删除该场景'),
|
||||
entry: z
|
||||
.object({
|
||||
name: z.string().optional().describe("科室名或医生名(锚点)"),
|
||||
department: z.string().optional().describe("医生所属科室(医生锚点之一)"),
|
||||
detail: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("信息全文:科室(位置/电话/类型/提示)或医生(职称/擅长/号别/坐班停诊)"),
|
||||
photo: z.string().optional().describe("照片链接"),
|
||||
url: z.string().optional().describe("照片链接(photos 锚点)"),
|
||||
caption: z.string().optional().describe("照片说明"),
|
||||
})
|
||||
.optional()
|
||||
.describe("photos/departments/doctors 条目(upsert 时必填)"),
|
||||
entryName: z.string().optional().describe("remove 的锚点:科室名称 / 医生姓名 / 照片url"),
|
||||
updatedBy: z.string().optional().describe("操作人标识(用户ID或姓名),用于审计"),
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
export { hospitalInfoSetTool };
|
||||
@@ -20,4 +20,11 @@ export { getEnvTool } from './system/envs.js';
|
||||
|
||||
// db
|
||||
export { createEscortRecordQueryTool } from './db/escort_record_query.js';
|
||||
export { escortRecordQueryTool } from './db/escort_record_query_admin.js';
|
||||
export { escortRecordSetTool } from './db/escort_record_set.js';
|
||||
export { healthProfileQueryTool } from './db/health_profile_query.js';
|
||||
export { healthProfileSetTool } from './db/health_profile_set.js';
|
||||
export { hospitalInfoListTool } from './db/hospital_info_list.js';
|
||||
export { hospitalInfoGetTool } from './db/hospital_info_get.js';
|
||||
export { hospitalInfoQueryTool } from './db/hospital_info_query.js';
|
||||
export { hospitalInfoSetTool } from './db/hospital_info_set.js';
|
||||
|
||||
Reference in New Issue
Block a user