7e720d9bfc
1. 政策法规库(RAG知识库): - 新增6条北京地区单方解除劳动合同工作指引种子数据 - 涵盖通知工会程序、函件内容要求、回执要求、监督提示函、仲裁审查等 - 企业用户通过AI问答可检索到北京地区工会通知规定 2. 地区差异化合规检查: - getChecklistForReason 增加 orgCity 参数 - 工会通知检查项仅北京地区显示(FAULT/NONFAULT/LAYOFF) - 前端解聘方式说明中北京工会提示仅北京地区动态显示 - 非北京地区不显示工会通知检查项,避免误导 3. 工会回执上传+证据链留存: - 后端新增3个接口:上传回执文件、保存回执信息、获取回执信息 - 回执信息保存到草稿 checklistOverrides - 自动追加到证据链(appendEvidence),作为劳动仲裁举证材料 - 前端合规检查步骤增加工会回执上传区域 - 确认提交步骤展示回执文件链接 - 新增 uploads 静态文件服务 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
167 lines
21 KiB
TypeScript
167 lines
21 KiB
TypeScript
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: '合同签订' },
|
||
// ===== 北京地区地方性指引 =====
|
||
{ title: '北京:规范用人单位单方解除劳动合同工作指引(试行)- 通知工会程序', content: '用人单位单方解除劳动合同的,应当提前五个工作日将理由书面通知本单位工会;尚未建立工会组织的,应当通知上一级工会。上一级工会,原则上是用人单位实际经营地的乡镇、街道、园区、开发区总工会。用人单位隶属区产业工会的,应通知其所属区产业工会。用人单位隶属市产业工会的,应通知其所属市产业工会中的上级工会。用人单位可联系所在地的乡镇、街道、园区、开发区总工会,或拨打12351职工服务热线,咨询本单位对应的上一级工会等事宜。', source: '北京市协调劳动关系三方', category: '解除终止' },
|
||
{ title: '北京:通知工会函内容要求', content: '用人单位单方解除劳动合同书面通知工会时,通知文本应当写明劳动者基本情况(姓名、性别、年龄、身份证号、工作岗位、工作年限、劳动合同期限、联系方式),解除劳动合同所依据的基本事实,解除劳动合同援引的法律、法规、本单位规章制度的相关条款,以及用人单位的联系人和联系方式等内容。', source: '北京市协调劳动关系三方', category: '解除终止' },
|
||
{ title: '北京:工会回执要求', content: '用人单位书面通知工会的送达方式为直接送达、邮寄送达等。用人单位工会收到通知后,应及时出具书面回执;上一级工会收到尚未建立工会组织的用人单位通知后,确定用人单位属于联系范围的,应出具书面回执,认为不属于的,应及时提醒用人单位。', source: '北京市协调劳动关系三方', category: '解除终止' },
|
||
{ title: '北京:工会劳动法律监督提示函', content: '工会认为用人单位违反法律、法规和有关合同的,应当自收到用人单位书面通知五个工作日内,通过发放《工会劳动法律监督提示函》等方式提出意见建议。用人单位应当研究工会的意见,并将处理结果书面通知工会。', 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: '在左侧菜单「团队」分组下点击「培训记录」进入管理页面。点击「新增」按钮选择员工,填写培训主题、日期、讲师、时长等信息。保存后记录状态为「待签收」,员工可在员工端「我的记录」中签收或拒绝。列表显示签收状态(待签收/已签收/拒绝签收),支持按员工姓名搜索。开启「电子签署设置 → 培训记录电子签」后,员工签收时需走电子签署流程,签收记录自动进入证据链。', 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 }
|
||
}
|