Files
TurboHR/backend/src/services/rag.service.ts
T
selfrelease 3f39c56f4b feat: 部署到服务器+移动端适配+帮助系统RAG集成+网页图标
- 部署脚本 deploy.sh:一键部署到 154.8.162.18,端口3400,Nginx conf.d
- 应用名改为「企业用工专家」(登录/注册/侧边栏/员工端等6处)
- 移动端适配修复:Attendance/Dashboard/Settings/Contracts 6处grid响应式
- 帮助系统:HelpModal 组件 + TopNav 帮助图标 + RAG语义搜索集成
  - 后端 rag.service.ts 新增23条帮助种子数据 + searchHelp()
  - 前端搜索调用 /ai/rag/help-search,RAG不可用时回退关键词匹配
- 网页图标 favicon.svg(indigo主色+建筑人形图案)
- index.html 标题改为「企业用工专家」
2026-07-27 10:10:07 +08:00

157 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import OpenAI from 'openai'
import prisma from '../lib/prisma'
const apiKey = process.env.DASHSCOPE_API_KEY || ''
const baseURL = 'https://dashscope.aliyuncs.com/compatible-mode/v1'
const client = new OpenAI({ apiKey, baseURL })
const EMBEDDING_MODEL = 'text-embedding-v2'
interface KnowledgeSeed {
title: string
content: string
source: string
category: string
}
const SEED_DATA: KnowledgeSeed[] = [
{ title: '劳动合同法 第十条 建立劳动关系应当订立书面合同', content: '建立劳动关系,应当订立书面劳动合同。已建立劳动关系,未同时订立书面劳动合同的,应当自用工之日起一个月内订立书面劳动合同。', source: '劳动合同法', category: '合同签订' },
{ title: '劳动合同法 第八十二条 未签书面合同双倍工资', content: '用人单位自用工之日起超过一个月不满一年未与劳动者订立书面劳动合同的,应当向劳动者每月支付二倍的工资。', source: '劳动合同法', category: '合同签订' },
{ title: '劳动合同法 第十四条 无固定期限劳动合同', content: '连续订立二次固定期限劳动合同续订的,应当订立无固定期限劳动合同。劳动者在该用人单位连续工作满十年的,应当订立无固定期限劳动合同。', source: '劳动合同法', category: '合同签订' },
{ title: '劳动合同法 第十九条 试用期期限', content: '三个月以上不满一年试用期不得超过一个月;一年以上不满三年不得超过二个月;三年以上不得超过六个月。同一用人单位与同一劳动者只能约定一次试用期。', source: '劳动合同法', category: '试用期' },
{ title: '劳动合同法 第二十条 试用期工资', content: '试用期工资不得低于本单位相同岗位最低档工资或劳动合同约定工资的百分之八十,并不得低于最低工资标准。', source: '劳动合同法', category: '试用期' },
{ title: '劳动合同法 第三十九条 过失性辞退', content: '严重违反规章制度、严重失职造成重大损害、被依法追究刑事责任等情形,用人单位可以解除劳动合同。', source: '劳动合同法', category: '解除终止' },
{ title: '劳动合同法 第四十条 无过失性辞退', content: '提前三十日书面通知或额外支付一个月工资后可解除:医疗期满不能从事原工作、不能胜任经培训仍不胜任、客观情况重大变化未能协商一致。', source: '劳动合同法', category: '解除终止' },
{ title: '劳动合同法 第四十一条 经济性裁员', content: '裁减二十人以上或占职工总数百分之十以上,需提前三十日向工会说明,方案报劳动行政部门。优先留用长期合同、无固定期限合同、家庭无其他就业人员。', source: '劳动合同法', category: '解除终止' },
{ title: '劳动合同法 第四十二条 不得解除的情形', content: '职业病、因工负伤丧失劳动能力、医疗期内、孕期产期哺乳期、连续工作满十五年距退休不足五年等情形,不得依第四十条第四十一条解除。', source: '劳动合同法', category: '解除终止' },
{ title: '劳动合同法 第四十七条 经济补偿计算', content: '每满一年支付一个月工资。六个月以上不满一年按一年计算;不满六个月支付半个月工资。月工资指解除前十二个月平均工资。高于社平工资三倍的按三倍计,年限最高十二年。', source: '劳动合同法', category: '经济补偿' },
{ title: '劳动合同法 第八十七条 违法解除赔偿金', content: '用人单位违反本法规定解除或终止劳动合同的,应当依照第四十七条经济补偿标准的二倍向劳动者支付赔偿金。', source: '劳动合同法', category: '经济补偿' },
{ title: '劳动法 第四十一条 加班时间上限', content: '一般每日不得超过一小时;特殊原因每日不得超过三小时,每月不得超过三十六小时。', source: '劳动法', category: '加班' },
{ title: '劳动法 第四十四条 加班工资标准', content: '延长工作时间不低于工资150%;休息日加班不能补休的不低于200%;法定休假日不低于300%。', source: '劳动法', category: '加班' },
{ title: '社会保险法 第五十八条 参保登记', content: '用人单位应当自用工之日起三十日内为其职工向社会保险经办机构申请办理社会保险登记。', source: '社会保险法', category: '社保' },
{ title: '劳动合同法 第八十二条 二倍工资起算', content: '用人单位自用工之日起满一年不与劳动者订立书面劳动合同的,视为用人单位与劳动者已订立无固定期限劳动合同。', source: '劳动合同法', category: '合同签订' },
]
let initialized = false
export async function ensureRAGTable() {
if (initialized) return
await prisma.$executeRaw`CREATE EXTENSION IF NOT EXISTS vector`
await prisma.$executeRaw`
CREATE TABLE IF NOT EXISTS rag_knowledge (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
source TEXT NOT NULL,
category TEXT NOT NULL,
embedding vector(1536),
created_at TIMESTAMPTZ DEFAULT now()
)
`
await prisma.$executeRaw`CREATE INDEX IF NOT EXISTS rag_knowledge_embedding_idx ON rag_knowledge USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100)`
initialized = true
}
async function getEmbedding(text: string): Promise<number[]> {
const res = await client.embeddings.create({ model: EMBEDDING_MODEL, input: text })
return res.data[0]?.embedding || []
}
export async function seedKnowledgeBase() {
await ensureRAGTable()
const count = await prisma.$queryRaw`SELECT count(*)::int as c FROM rag_knowledge` as any
if (count[0]?.c > 0) return
for (let i = 0; i < SEED_DATA.length; i++) {
const item = SEED_DATA[i]
const embedding = await getEmbedding(`${item.title} ${item.content}`)
await prisma.$executeRaw`
INSERT INTO rag_knowledge (id, title, content, source, category, embedding)
VALUES (${`rag-${String(i).padStart(3, '0')}`}, ${item.title}, ${item.content}, ${item.source}, ${item.category}, ${embedding}::vector)
`
}
}
export async function searchKnowledge(query: string, topK: number = 3): Promise<string[]> {
await ensureRAGTable()
const queryEmbedding = await getEmbedding(query)
const results = await prisma.$queryRaw`
SELECT title, content, source, 1 - (embedding <=> ${queryEmbedding}::vector) as similarity
FROM rag_knowledge
ORDER BY embedding <=> ${queryEmbedding}::vector
LIMIT ${topK}
` as any[]
return results
.filter((r) => r.similarity > 0.3)
.map((r) => `${r.title}\n${r.content}\n(来源:${r.source},相似度:${(r.similarity * 100).toFixed(0)}%`)
}
/** 帮助文档种子数据 — 通俗易懂的系统使用指南 */
const HELP_SEED_DATA: KnowledgeSeed[] = [
{ title: '如何添加新员工', content: '点击左侧菜单「员工管理」,点击右上角「添加员工」按钮,填写员工姓名、手机号、入职日期等基本信息,点击保存即可。带*号的是必填项,其他可以以后再补。', source: '使用帮助', category: '系统帮助-员工管理' },
{ title: '如何修改员工信息', content: '在员工列表中点击员工姓名进入详情页,然后点击右上角编辑按钮即可修改信息。所有信息都可以随时修改。', source: '使用帮助', category: '系统帮助-员工管理' },
{ title: '员工离职怎么处理', content: '请到「解聘管理」页面处理离职流程,系统会自动帮您计算经济补偿金、生成解聘协议书等法律文件。不要直接删除员工记录,保留记录有助于日后查证。直接删除会导致该员工的所有历史记录丢失。', source: '使用帮助', category: '系统帮助-员工管理' },
{ title: '合同类型有哪些', content: '常见合同类型:固定期限合同(有明确到期日)、无固定期限合同(没有到期日,长期雇佣)、完成任务合同(以完成某项工作为期限)、未签合同。员工入职1个月内必须签订书面合同,否则企业需要支付双倍工资。', source: '使用帮助', category: '系统帮助-合同管理' },
{ title: '合同到期会提醒吗', content: '系统会自动检测即将到期的合同,并在顶部通知铃铛处显示提醒数字。默认提前30天提醒,您可以在通知设置中修改天数。', source: '使用帮助', category: '系统帮助-合同管理' },
{ title: '什么是合同确认', content: '合同确认是指员工通过手机查看并确认自己的劳动合同内容。系统会生成一个链接,员工用手机打开即可查看合同详情并确认签字。您可以在员工详情的合同信息标签页中发起确认。', source: '使用帮助', category: '系统帮助-合同管理' },
{ title: '怎么发工资', content: '点击左侧菜单薪税管理,点击创建发薪批次,选择月份和批次类型,系统自动从员工信息中拉取数据并计算个税和社保扣款,确认无误后点击发放即可。系统会自动计算个税、社保扣款等。', source: '使用帮助', category: '系统帮助-工资社保' },
{ title: '社保基数和比例怎么设置', content: '在社保管理页面设置公司所在地的社保缴纳基数上下限和各项比例(养老、医疗、失业、工伤、生育)。这些数据通常每年会调整一次,请关注当地社保局通知。社保基数和比例各地不同,请以当地社保局公布的最新标准为准。', source: '使用帮助', category: '系统帮助-工资社保' },
{ title: '员工怎么查看工资条', content: '系统提供员工端(手机访问),员工可以通过手机查看自己的工资条。您在发薪后系统会自动生成工资条,员工登录员工端即可查看。', source: '使用帮助', category: '系统帮助-工资社保' },
{ title: '加班费怎么算', content: '系统会根据您设置的加班倍率自动计算。默认规则:工作日加班1.5倍,休息日加班2倍,法定节假日加班3倍。您可以在薪税管理的加班配置中修改倍率。', source: '使用帮助', category: '系统帮助-工资社保' },
{ title: '风险检测是什么意思', content: '系统会自动扫描您的员工和合同数据,发现可能存在的法律风险。比如合同到期未续签、试用期超长、未签合同等。风险分为高、中、低三个等级,建议优先处理高风险项。', source: '使用帮助', category: '系统帮助-风险检测' },
{ title: '发现风险后怎么处理', content: '在首页总览页面可以看到风险概览。点击风险项可以跳转到对应员工详情,然后根据系统建议进行处理。处理完成后风险会自动消除。建议每周查看一次风险提醒。', source: '使用帮助', category: '系统帮助-风险检测' },
{ title: 'AI助手能帮我做什么', content: 'AI助手可以回答劳动法相关问题,比如员工试用期最多多久、经济补偿金怎么计算、未婚怀孕能休产假吗、加班费怎么算等。就像问一个专业律师一样,用大白话问就行。', source: '使用帮助', category: '系统帮助-AI助手' },
{ title: '判赔预测是什么', content: '如果您要解聘员工,AI可以根据员工的信息(工资、工龄、合同等)预测可能需要支付的经济补偿金金额,帮您提前做好预算。在AI助手页面选择判赔预测模式即可使用。AI预测结果仅供参考,实际金额请以律师建议或法院判决为准。', source: '使用帮助', category: '系统帮助-AI助手' },
{ title: '怎么设置提醒通知', content: '在通知管理页面可以设置各类提醒:合同到期提前提醒天数、未签合同提醒、试用期到期提醒等。点击通知铃铛图标可以查看所有未读提醒。', source: '使用帮助', category: '系统帮助-通知设置' },
{ title: '怎么修改密码', content: '点击右上角您的用户名,进入设置页面可以修改密码。建议定期更换密码以保护账号安全。', source: '使用帮助', category: '系统帮助-通知设置' },
{ title: '怎么修改公司信息', content: '在设置页面可以修改公司名称、行业、规模等基本信息。这些信息会影响风险检测的准确性,请如实填写。', source: '使用帮助', category: '系统帮助-通知设置' },
{ title: '可以在手机上使用吗', content: '可以。用手机浏览器打开本网站即可,手机版会自动显示底部导航栏。建议添加到手机桌面像App一样使用。苹果手机Safari打开点击底部分享按钮选择添加到主屏幕。安卓手机Chrome打开点击右上角菜单选择添加到主屏幕。', source: '使用帮助', category: '系统帮助-快速入门' },
{ title: '第一次使用该从哪里开始', content: '建议按以下顺序:1添加员工信息,2填写合同信息,3设置社保基数和比例,4创建发薪批次,5有不懂的随时点帮助图标查看。不用担心填错,所有信息都可以随时修改。', source: '使用帮助', category: '系统帮助-快速入门' },
{ title: '企业用工专家是什么', content: '这是一个帮您管理员工、合同、工资和社保的工具,可以把它理解为一个「人事小助手」,帮您把繁琐的人事工作变得简单。比如记录员工信息、提醒合同到期、计算工资社保、生成法律文档等。', source: '使用帮助', category: '系统帮助-快速入门' },
{ title: '我的数据安全吗', content: '您的数据存储在加密的云端服务器上,只有您本人登录后才能查看。我们不会将您的数据分享给任何第三方。所有敏感信息如身份证号都经过加密存储。', source: '使用帮助', category: '系统帮助-常见问题' },
{ title: '可以导出数据吗', content: '可以。在员工管理页面可以导出员工名单为Excel文件。工资批次也可以导出为Excel方便财务对账。', source: '使用帮助', category: '系统帮助-常见问题' },
{ title: '可以多人同时使用吗', content: '可以。在设置页面可以添加多个HR账号,不同账号可以设置不同权限。比如一个管理员、几个普通HR。', source: '使用帮助', category: '系统帮助-常见问题' },
]
/** 初始化帮助文档到 RAG 知识库 */
export async function seedHelpKnowledge() {
await ensureRAGTable()
// 检查是否已注入帮助文档
const count = await prisma.$queryRaw`SELECT count(*)::int as c FROM rag_knowledge WHERE category LIKE ${'系统帮助%'}` as any
if (count[0]?.c > 0) return
for (let i = 0; i < HELP_SEED_DATA.length; i++) {
const item = HELP_SEED_DATA[i]
const embedding = await getEmbedding(`${item.title} ${item.content}`)
await prisma.$executeRaw`
INSERT INTO rag_knowledge (id, title, content, source, category, embedding)
VALUES (${`help-${String(i).padStart(3, '0')}`}, ${item.title}, ${item.content}, ${item.source}, ${item.category}, ${embedding}::vector)
`
}
}
/** 语义搜索帮助文档(仅搜索系统帮助分类) */
export async function searchHelp(query: string, topK: number = 5): Promise<{ title: string; content: string; source: string; category: string; similarity: number }[]> {
await ensureRAGTable()
const queryEmbedding = await getEmbedding(query)
const results = await prisma.$queryRaw`
SELECT title, content, source, category, 1 - (embedding <=> ${queryEmbedding}::vector) as similarity
FROM rag_knowledge
WHERE category LIKE ${'系统帮助%'}
ORDER BY embedding <=> ${queryEmbedding}::vector
LIMIT ${topK}
` as any[]
return results
.filter((r) => r.similarity > 0.3)
.map((r) => ({ title: r.title, content: r.content, source: r.source, category: r.category, similarity: r.similarity }))
}
export async function addKnowledge(title: string, content: string, source: string, category: string) {
await ensureRAGTable()
const embedding = await getEmbedding(`${title} ${content}`)
const id = `rag-${Date.now()}`
await prisma.$executeRaw`
INSERT INTO rag_knowledge (id, title, content, source, category, embedding)
VALUES (${id}, ${title}, ${content}, ${source}, ${category}, ${embedding}::vector)
`
return { id }
}