Skip to content

Instantly share code, notes, and snippets.

@vikiboss
Last active October 22, 2024 15:02
Show Gist options
  • Save vikiboss/05b93ae9e37d0b68056438bf211942c5 to your computer and use it in GitHub Desktop.
Save vikiboss/05b93ae9e37d0b68056438bf211942c5 to your computer and use it in GitHub Desktop.
Plugins for KiviBot v1
import { definePlugin, http } from 'kivibot'
export default definePlugin({
name: 'sd',
version: '1.0.0',
setup(ctx) {
ctx.handle('message', async (e) => {
if (e.raw_message.startsWith('#sd ')) {
const hasRight = ctx.isOwner(e) || ctx.isAdmin(e)
if (!hasRight) {
e.reply('内测中,仅对管理员开放。')
return
}
const prompt = e.raw_message.replace('#sd ', '')
try {
const { message_id } = await e.reply('生成中,请耐心等待...')
const data = await SDweb(prompt)
await e.reply(ctx.oicq.segment.image(`base64://${data.images[0]}`), true)
message_id && (await ctx.bot.deleteMsg(message_id))
} catch (err: any) {
e.reply(`出错了:${err?.message || err.toString()}`)
}
}
})
},
})
async function SDweb(prompt: string) {
const body = createBody(prompt)
// 换成自己的 API
const { data } = await http.post('http://localhost:8080/sdapi/v1/txt2img', body)
return data
}
function createBody(prompt: string) {
return {
denoising_strength: 0,
prompt: `masterpiece, best quality, ${prompt}`,
negative_prompt:
'lowres, bad anatomy, bad hands, text, error, missing fingers, extra digit, fewer digits, cropped, worst quality, low quality, normal quality, jpeg artifacts, signature, watermark, username, blurry, bad lighting, bad composition, bad perspective, bad shading, bad colors, bad contrast, bad anatomy, bad hands, bad feet, bad face, bad hair, bad eyes, bad nose, bad mouth, bad ears, bad neck, bad shoulders, bad arms, bad elbows, bad wrists, bad fingers, bad torso, bad chest, bad breasts, bad back, bad hips, bad legs, bad knees, bad ankles, bad feet, bad toes, bad clothing, bad armor, bad weapons, bad background, bad foreground, bad environment, bad animals, bad creatures, bad monsters, bad dragons, bad robots, bad mechs, bad vehicles, bad buildings, bad architecture, bad plants, bad trees, bad rocks, bad water, bad fire, bad smoke, bad clouds, bad sky, bad sun, bad moon, bad stars, bad planets, bad magic, bad spells, bad effects, bad powers, bad abilities, bad weapons, bad armor, bad clothing, bad accessories, bad jewelry, bad tattoos, bad scars, bad wounds, bad blood, bad gore',
seed: -1,
batch_size: 1,
n_iter: 1,
steps: 20,
cfg_scale: 7,
width: 864,
height: 1216,
restore_faces: false,
tiling: false,
override_settings: { sd_model_checkpoint: 'tPonynai3_v65.safetensors [a2e734ac29]' },
sampler_index: 'DPM++ 2M',
}
}
import { definePlugin } from 'kivibot'
import OpenAI from 'openai'
export default definePlugin({
name: 'GPT',
version: '1.0.0',
setup(ctx) {
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY, // 配置环境变量,或者直接写死
baseURL: 'https://api.chatanywhere.tech/v1', // chatanywhere 的 baseURL
// baseURL: 'https://gpt.viki.moe/v1', // 如果使用官方 API,则使用这个 baseURL
})
ctx.handle('message', async (e) => {
const text = ctx.getText(e)
if (['%', '%'].some((cmd) => text.startsWith(cmd))) {
const content = text.replace(/^\s*[%%]/, '')
const data = await openai.chat.completions.create(
{
model: 'gpt-3.5-turbo',
max_tokens: 1024,
temperature: 0.24,
messages: [
{
role: 'system',
content:
'请你扮演一个被安排在 QQ 群中的全能问答机器人,你拥有海量的知识储备,可以回答各种问题,且每次回答都不少于 100 个字符,你的回答生动而形象,回复内容当中的很多 emoji 的运用得体而不胡哨,所有回复将被渲染成漂亮好看的 Markdown,你尽可能的使用 markdown 强大的语法和生动形象的语言来呈现你的回答,除非群友特殊说明,请尽可能使用中文回复,接下来请你回复或解答以下群友的问题,必要时可以用生动形象的语言,也可以带一点幽默的群友搞怪口吻。请直接回复下列内容:',
},
{ role: 'user', content }
],
},
{ timeout: 60000 },
)
const res = (data.choices[0]?.message?.content ?? '').trim()
res && (await e.reply(res, true))
}
})
},
})
import { definePlugin } from 'kivibot'
import OpenAI from "openai";
import path from 'path';
import fs from 'fs';
// 基础配置
const baseConfig = {
  MAX_MESSAGES: 10,
  TTS_URL: "http://127.0.0.1:5000/tts?text=",
  OPENAI_CONFIG: {
    baseURL: 'https://api.deepseek.com',
    apiKey: ''
  },
  AI_MODEL: "deepseek-chat",
  TRIGGER_WORD: "汐汐",
  CONFIG_DIR: './config/',
  PERSONA_DIR: './default/',
  DEFAULT_PERSONA_FILE: 'default.json',
  DIRECT_TRIGGER: "$", // 添加专业回复直接触发的前缀
};
interface Message {
  时间: string;
  角色: string;
  内容: string;
}
interface GroupConfig {
  tts: boolean;
  persona: string;
}
const groupMessagesMap: Map<number, Message[]> = new Map();
const groupConfigMap: Map<number, GroupConfig> = new Map();
function loadGroupConfig(groupId: number): GroupConfig {
  const configPath = path.join(__dirname, baseConfig.CONFIG_DIR, `${groupId}.json`);
  if (fs.existsSync(configPath)) {
    return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
  }
  // 如果配置文件不存在,创建默认配置并保存
  const defaultConfig: GroupConfig = { tts: true, persona: 'default' };
  saveGroupConfig(groupId, defaultConfig);
  return defaultConfig;
}
function saveGroupConfig(groupId: number, config: GroupConfig) {
  const configDir = path.join(__dirname, baseConfig.CONFIG_DIR);
  if (!fs.existsSync(configDir)) {
    fs.mkdirSync(configDir, { recursive: true });
  }
  const configPath = path.join(configDir, `${groupId}.json`);
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
  groupConfigMap.set(groupId, config);
}
function loadPersona(personaName: string) {
  const personaPath = path.join(__dirname, baseConfig.PERSONA_DIR, `${personaName}.json`);
  if (fs.existsSync(personaPath)) {
    return JSON.parse(fs.readFileSync(personaPath, 'utf-8')).prompt;
  }
  return [];
}
function ensureDefaultPersonaExists() {
  const defaultPersonaPath = path.join(__dirname, baseConfig.PERSONA_DIR, baseConfig.DEFAULT_PERSONA_FILE);
  if (!fs.existsSync(defaultPersonaPath)) {
    const defaultContent = {
      "prompt": [
        {
          "content": "You are a helpful assistant",
          "role": "system"
        }
      ]
    };
    fs.writeFileSync(defaultPersonaPath, JSON.stringify(defaultContent, null, 2));
    console.log(`Created default persona file at ${defaultPersonaPath}`);
  }
}
export default definePlugin({
  name: 'asi',
  version: '1.0.0',
  setup(ctx) {
    const openai = new OpenAI(baseConfig.OPENAI_CONFIG);
    // 确保默认人格文件存在
    ensureDefaultPersonaExists();
    ctx.handle('message.group', async e => handleGroupMessage(ctx, e, openai));
    ctx.handle('message.private', async e => handlePrivateMessage(ctx, e, openai));
  },
})
async function handleGroupMessage(ctx, e, openai) {
  const { group_id, time, sender, raw_message } = e;
  const formattedTime = formatDate(new Date(time * 1000));
  const senderName = sender.card || sender.nickname;
  // 确保每个群都有配置
  if (!groupConfigMap.has(group_id)) {
    groupConfigMap.set(group_id, loadGroupConfig(group_id));
  }
  const groupConfig = groupConfigMap.get(group_id)!;
  // 检查是否是管理员命令,并且发送者是群管理员或机器人管理员
  if (raw_message.startsWith('#asi ') && (ctx.isAdmin(e) || ctx.isOwner(e))) {
    handleAdminCommand(ctx, e, groupConfig);
    return;
  }
  // 检查是否是直接触发
  if (raw_message.startsWith(baseConfig.DIRECT_TRIGGER)) {
    const directMessage = raw_message.slice(baseConfig.DIRECT_TRIGGER.length).trim();
    await handleDirectMessage(ctx, e, openai, directMessage);
    return;
  }
  updateGroupMessageLog(group_id, formattedTime, "user", senderName, raw_message);
  if (raw_message.startsWith(baseConfig.TRIGGER_WORD) || e.atme) {
    const groupMessages = getGroupMessages(group_id);
    const contextMessages = groupMessages.map(msg => ({
      role: msg.角色,
      content: msg.内容.split(': ')[1] // 只取冒号后面的内容
    }));
   
    const persona = loadPersona(groupConfig.persona);
    const messages = [...persona, ...contextMessages];
   
    const assistantMessage = await getAIResponse(openai, messages);
    // 添加类型检查
    if (typeof assistantMessage !== 'string') {
      console.error('AI 响应格式错误');
      e.reply('抱歉,我遇到了一些问题。请稍后再试。');
      return;
    }
    const replyContent = assistantMessage || '抱歉,我现在无法回答。';
    if (groupConfig.tts) {
      e.reply(ctx.oicq.segment.record(baseConfig.TTS_URL + replyContent) || 'error');
    } else {
      e.reply(replyContent);
    }
    updateGroupMessageLog(group_id, formatDate(new Date()), "assistant", "汐汐", replyContent);
    console.log(`\n========== 群 ${group_id} 的内存数据 ==========`);
    console.log(JSON.stringify(getGroupMessages(group_id), null, 2));
    console.log('==========================================\n');
  }
}
function handleAdminCommand(ctx, e, groupConfig: GroupConfig) {
  const { group_id, raw_message } = e;
 
  // 再次检查权限,以防万一
  if (!ctx.isAdmin(e) && !ctx.isOwner(e)) {
    e.reply('抱歉,您没有权限执行此命令。');
    return;
  }
  const command = raw_message.split(' ')[1];
  switch (command) {
    case '帮助':
      e.reply(`
        #asi 帮助 - 显示此帮助信息
        #asi nature <persona> - 设置人格 (例如: #asi nature bing)
        #asi nature list - 列出可用的人格
        #asi tts on/off - 开启/关闭语音回复
      `);
      break;
    case 'nature':
      const personaName = raw_message.split(' ')[2];
      if (personaName === 'list') {
        const personas = fs.readdirSync(path.join(__dirname, baseConfig.PERSONA_DIR))
          .filter(file => file.endsWith('.json'))
          .map(file => file.replace('.json', ''));
        e.reply(`可用的人格: ${personas.join(', ')}`);
      } else if (fs.existsSync(path.join(__dirname, baseConfig.PERSONA_DIR, `${personaName}.json`))) {
        groupConfig.persona = personaName;
        saveGroupConfig(group_id, groupConfig);
        e.reply(`已将人格设置为 ${personaName}`);
      } else {
        e.reply('无效的人格名称');
      }
      break;
    case 'tts':
      const status = raw_message.split(' ')[2];
      if (status === 'on' || status === 'off') {
        groupConfig.tts = status === 'on';
        saveGroupConfig(group_id, groupConfig);
        e.reply(`语音回复已${groupConfig.tts ? '开启' : '关闭'}`);
      } else {
        e.reply('无效的 tts 状态,请使用 on 或 off');
      }
      break;
    default:
      e.reply('无效的命令,请使用 #asi 帮助 查看可用命令');
  }
}
// 其他函数保持不变...
async function handlePrivateMessage(ctx, e, openai) {
  const { sender, raw_message } = e;
 
  if (raw_message.startsWith('#')) return;
  // 检查是否是直接触发
  if (raw_message.startsWith(baseConfig.DIRECT_TRIGGER)) {
    const directMessage = raw_message.slice(baseConfig.DIRECT_TRIGGER.length).trim();
    await handleDirectMessage(ctx, e, openai, directMessage);
    return;
  }
  // 原有的私聊处理逻辑
  const persona = loadPersona('default');
  const userMessage = { role: "user", content: raw_message };
  const messages = [...persona, userMessage];
 
  const assistantMessage = await getAIResponse(openai, messages);
  // 添加类型检查
  if (typeof assistantMessage !== 'string') {
    console.error('AI 响应格式错误');
    e.reply('抱歉,我遇到了一些问题。请稍后再试。');
    return;
  }
  e.reply(assistantMessage || '抱歉,我现在无法回答。');
}
async function handleDirectMessage(ctx, e, openai, message: string) {
  const persona = loadPersona('default');
  const userMessage = { role: "user", content: message };
  const messages = [...persona, userMessage];
 
  const assistantMessage = await getAIResponse(openai, messages);
  if (typeof assistantMessage !== 'string') {
    console.error('AI 响应格式错误');
    e.reply('抱歉,我遇到了一些问题。请稍后再试。');
    return;
  }
  const replyContent = assistantMessage || '抱歉,我现在无法回答。';
  e.reply(replyContent);
}
function formatDate(date: Date): string {
  const pad = (num: number) => String(num).padStart(2, '0');
  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
}
function updateGroupMessageLog(groupId: number, time: string, role: string, senderName: string, message: string) {
  let messages = groupMessagesMap.get(groupId) || [];
  messages.push({ 时间: time, 角色: role, 内容: `${senderName}: ${message}` });
  if (messages.length > baseConfig.MAX_MESSAGES) {
    messages = messages.slice(-baseConfig.MAX_MESSAGES);
  }
  groupMessagesMap.set(groupId, messages);
}
function getGroupMessages(groupId: number): Message[] {
  return groupMessagesMap.get(groupId) || [];
}
async function getAIResponse(openai: OpenAI, messages: any[]): Promise<string> {
  try {
    const completion = await openai.chat.completions.create({
      messages,
      model: baseConfig.AI_MODEL,
    });
    return completion.choices[0].message.content || '抱歉,我现在无法回答。';
  } catch (error) {
    console.error('获取 AI 响应时出错:', error);
    return '抱歉,发生了一些错误。请稍后再试。';
  }
}
import { definePlugin } from 'kivibot'
import type { oicq, AllMessageEvent } from 'kivibot'
interface Word {
keyword: string | RegExp | (string | RegExp)[] | ((event: AllMessageEvent) => boolean)
reply: oicq.Sendable | oicq.Sendable[] | ((event: AllMessageEvent) => oicq.Sendable | Promise<oicq.Sendable>)
groupOnly?: boolean
adminOnly?: boolean
}
// 忽略的 QQ 号列表
const ignoreUinList: number[] = []
// 开启的群列表
const enableGroupList: number[] = []
const wordList: Word[] = [
{
keyword: ['早', '早安', '早啊', '早上好', '早上好啊', '早安啊'],
reply: ['早什么早'],
},
]
export default definePlugin({
name: 'keyword',
version: '1.0.0',
setup(ctx) {
ctx.handle('message', async (event) => {
if (ignoreUinList.includes(event.sender.user_id)) return
if (ctx.isGroupMsg(event) && !enableGroupList.includes(event.group_id)) return
for (const word of wordList) {
const keywords = ctx.ensureArray(word.keyword)
for (const keyword of keywords) {
let matched = false
if (ctx.isString(keyword)) {
if (event.raw_message.includes(keyword)) {
if (word.adminOnly && !ctx.isAdmin(event)) {
return
}
if (word.groupOnly && !ctx.isGroupMsg(event)) {
return
}
matched = true
}
}
if (keyword instanceof RegExp) {
if (keyword.test(event.raw_message)) {
if (word.adminOnly && !ctx.isAdmin(event)) {
return
}
if (word.groupOnly && !ctx.isGroupMsg(event)) {
return
}
matched = true
return
}
}
if (ctx.isFunction(keyword)) {
if (keyword(event)) {
if (word.adminOnly && !ctx.isAdmin(event)) {
return
}
if (word.groupOnly && !ctx.isGroupMsg(event)) {
return
}
matched = true
return
}
}
if (matched) {
const replyContent = ctx.isFunction(word.reply)
? await word.reply(event)
: ctx.randomItem(ctx.ensureArray(word.reply))
event.reply(replyContent)
}
}
}
})
},
})
import { definePlugin, wait, type oicq } from 'kivibot'
const REG = {
self: /^\s*([赞超草])我\s*$/, // 不需要匹配其他词就改为 /^\s*赞我\s*$/
other: /^\s*([赞超草])[它她他]/i, // 不需要匹配其他词就改为 /^\s*赞[它她他]\s*$/
}
export default definePlugin({
name: '点赞',
version: '1.0.0',
setup(ctx) {
ctx.handle('message.group', async (e) => {
const selfMatches = e.raw_message.match(REG.self)
if (selfMatches) {
await responseLike.call(ctx.bot, e.sender.user_id, e.group, e.seq)
return
}
const otherMatches = e.raw_message.match(REG.other)
const id = +(e.message.find((m) => m.type === 'at')?.qq ?? 0)
if (otherMatches && id && !Number.isNaN(id)) {
await responseLike.call(ctx.bot, id, e.group, e.seq)
return
}
})
},
})
async function responseLike(this: oicq.Client, id: number, group: oicq.Group, seq: number) {
let count = 0
try {
while (true) {
const isOK = await this.sendLike(id, 10)
if (isOK) {
count += 10
} else {
break
}
}
} catch {}
if (count) {
await group.setReaction(seq, '201', 1)
} else {
await group.setReaction(seq, '174', 1)
}
}
import { definePlugin } from 'kivibot'
// 通知的群
const pushGroups = []
export default definePlugin({
name: 'cron',
version: '1.2.0',
cron: [
['0 0 6 * * *', async (ctx) => ctx.noticeGroups.call(ctx.bot, pushGroups, '起きて!起きて!起きて!')],
['0 0 12 * * *', async (ctx) => ctx.noticeGroups.call(ctx.bot, pushGroups, '食べて!食べて!食べて!')],
['0 0 0 * * *', async (ctx) => ctx.noticeGroups.call(ctx.bot, pushGroups, '寝て!寝て!寝て!')],
],
})
// 需要在 index.ts 旁边创建一个 data.json,内容是 [],空数组,注意使用 utf-8 编码
import { definePlugin } from 'kivibot'
import todo from './todo.json'
export default definePlugin({
name: 'todo',
version: '1.0.0',
setup(ctx) {
const store = ctx.createVanilla({ todo: todo as Record<number, TodoItem[]> })
store.subscribe(async () => {
const { todo } = store.snapshot()
const filePath = ctx.path.join(__dirname, './todo.json')
ctx.fs.writeFileSync(filePath, JSON.stringify(todo))
})
ctx.handle('message', async (e) => {
const text = ctx.getText(e)
if (!text) return
if (text.match(/^todo\s+help$/)) {
return await e.reply(
ctx
.dedent(
`
〓 待办插件帮助 〓
todo - 查看所有待办
todo add <title> - 添加
todo done <id> - 标记完成
todo undo <id> - 还原标记
todo delete <id> - 删除
todo clear - 清空
`,
)
.trim(),
)
}
if (text.match(/^todo$/i)) {
// biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
const userTodo = (store.mutate.todo[e.sender.user_id] ??= [])
if (userTodo.length <= 12) {
const todoListStr = userTodo
.map((item) => `- [${item.status === 'done' ? 'x' : ' '}] [${item.id}] ${item.title}`)
.join('\n')
return await e.reply(`〓 待办事项 〓\n${todoListStr ? todoListStr : '[事情都干完啦~]'}`)
}
const msgs = await ctx.bot.makeForwardMsg(
userTodo.slice(0, 99).map((item) => ({
nickname: e.sender.nickname,
user_id: e.sender.user_id,
message: `- [${item.status === 'done' ? 'x' : ''}] [${item.id}] ${item.title}`,
})),
)
await e.reply(msgs)
}
if (text.match(/^todo\s+clear$/)) {
// biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
const userTodo = (store.mutate.todo[e.sender.user_id] ??= [])
userTodo.length = 0
return await e.reply('已清空待办事项')
}
if (text.match(/^todo\s+done/)) {
// biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
const userTodo = (store.mutate.todo[e.sender.user_id] ??= [])
const id = text.replace(/^todo\s+done/, () => '').trim()
if (!id) return await e.reply('请提供待办事项 ID', true)
const item = userTodo.find((item) => item.id.toLowerCase() === id.toLowerCase())
if (!item) return await e.reply('未找到待办事项', true)
item.status = 'done'
item.updatedAt = Date.now()
return await e.reply(`已标记待办事项 【${item.title}】 为完成`)
}
if (text.match(/^todo\s+undo/)) {
// biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
const userTodo = (store.mutate.todo[e.sender.user_id] ??= [])
const id = text.replace(/^todo\s+undo/, () => '').trim()
if (!id) return await e.reply('请提供待办事项 ID', true)
const item = userTodo.find((item) => item.id.toLowerCase() === id.toLowerCase())
if (!item) return await e.reply('未找到待办事项', true)
item.status = 'todo'
item.updatedAt = Date.now()
return await e.reply(`已标记待办事项 【${item.title}】 为未完成`)
}
if (text.match(/^todo\s+delete/)) {
// biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
const userTodo = (store.mutate.todo[e.sender.user_id] ??= [])
const id = text.replace(/^todo\s+delete/, () => '').trim()
if (!id) return await e.reply('请提供待办事项 ID', true)
const idx = userTodo.findIndex((item) => item.id.toLowerCase() === id.toLowerCase())
if (idx === -1) return await e.reply('未找到待办事项', true)
const item = userTodo.splice(idx, 1)
return await e.reply(`已删除待办事项 【${item[0].title}】`)
}
if (text.match(/^todo\s+add/)) {
// biome-ignore lint/suspicious/noAssignInExpressions: <explanation>
const userTodo = (store.mutate.todo[e.sender.user_id] ??= [])
const title = text.replace(/^todo\s+add/, () => '').trim()
if (!title) return await e.reply('请提供待办事项的标题', true)
userTodo.push({
id: uuid(),
title,
status: 'todo',
createdAt: Date.now(),
updatedAt: Date.now(),
})
return await e.reply(`已添加待办事项:${title}`)
}
})
},
})
function uuid() {
return Math.random().toString(16).slice(2, 6).toUpperCase()
}
interface TodoItem {
id: string
title: string
status: 'todo' | 'done'
createdAt: number
updatedAt: number
}
import { definePlugin, http } from 'kivibot'
const token = process.env.GITHUB_TOKEN || ''
const gistId = '05b93ae9e37d0b68056438bf211942c5'
const cache = {
data: {} as any,
}
export default definePlugin({
name: 'gist',
version: '1.2.0',
setup(ctx) {
ctx.handle('message', async (e) => {
if (e.raw_message === '#插件市场') {
if (!cache.data.files) await updateCache()
const files = cache.data.files as any[]
const updatedAt = ctx.dayjs(cache.data.updated_at).format('YYYY-MM-DD HH:mm:ss')
e.reply(
`〓 插件市场 〓\n\n${Object.keys(files)
.sort()
.map((name, idx) => `📦 ${idx + 1}. ${name.replace('.ts', '')}`)
.join('\n')}\n\n更新时间: ${updatedAt}\n获取方式:#获取插件 <序号>`,
)
}
if (e.raw_message.startsWith('#获取插件')) {
const index = Number.parseInt(e.raw_message.replace('#获取插件', '').trim(), 10)
if (Number.isNaN(index)) {
e.reply('请输入正确的序号', true)
return
}
if (!cache.data.files) await updateCache()
const targetKey = Object.keys(cache.data.files as any[]).sort()[index - 1]
const target = cache.data.files[targetKey]
if (!target) {
e.reply('该序号对应插件不存在', true)
return
}
if (ctx.isGroupMsg(e)) {
await e.reply(`已通过私聊发送插件 ${target.filename.replace('.ts', '')},请查收`, true)
}
const friend = ctx.bot.pickFriend(e.sender.user_id)
if (!friend) {
e.reply('请先添加机器人为好友以便接收私聊消息', true)
return
}
await friend.sendMsg(
`〓 插件详情 〓\n\n文件名: ${target.filename}\n大小: ${ctx.filesize(target.size)}\n\n下载直链:${target.raw_url}`,
)
if (!target.truncated && target.content.length <= 3_800) {
await friend.sendMsg(target.content)
} else {
await friend.sendMsg('代码内容过长,请自行下载查看')
}
}
})
},
})
async function updateCache() {
const { data } = await http.get(`https://proxy.viki.moe/gists/${gistId}?proxy-host=api.github.com`, {
headers: {
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
})
cache.data = data
setTimeout(
() => {
cache.data = {}
},
1 * 60 * 1_000,
)
}
import { definePlugin } from 'kivibot'
import type { KiviPluginContext, AllMessageEvent, oicq } from 'kivibot'
// 推送的群号
const pushGroups = []
export default definePlugin({
name: '看世界',
version: '1.2.0',
setup(ctx) {
ctx.handle('message', async (e) => {
ctx.runWithErrorHandler.call(
ctx.bot,
async () => {
const txt = ctx.getText(e)
if (!txt) return
if (['60s', '60秒', '看世界'].includes(txt)) {
const news = await fetch60s.call(ctx, e)
if (!news) return await e.reply('获取 60s 新闻失败')
await e.reply(news)
}
if (['抖音热搜'].includes(txt)) {
const { message_id } = await e.reply('图片上传中,请等待...', true)
const msgs = await fetchDouyin.call(ctx, e)
if (!msgs) return await e.reply('获取抖音热搜失败')
await e.reply(msgs)
ctx.isGroupMsg(e) && (await e.group.recallMsg(message_id))
}
if (['知乎热搜'].includes(txt)) {
const msgs = await fetchZhihu.call(ctx, e)
if (!msgs) return await e.reply('获取知乎热搜失败')
await e.reply(msgs)
}
if (['知乎热榜', '知乎热门问题'].includes(txt)) {
const { message_id } = await e.reply('图片上传中,请等待...', true)
const msgs = await fetchZhihuHot.call(ctx, e)
if (!msgs) return await e.reply('获取知乎热榜失败')
await e.reply(msgs)
ctx.isGroupMsg(e) && (await e.group.recallMsg(message_id))
}
if (['b站热搜'].includes(txt.toLowerCase())) {
const msgs = await fetchBili.call(ctx, e)
if (!msgs) return await e.reply('获取 B 站热搜失败')
await e.reply(msgs)
}
if (['微博热搜'].includes(txt.toLowerCase())) {
const msgs = await fetchWeibo.call(ctx, e)
if (!msgs) return await e.reply('获取微博热搜失败')
await e.reply(msgs)
}
if (['热搜', '新闻'].includes(txt)) {
const { message_id } = await e.reply('正在获取,请稍等...')
const news = [
await fetchDouyin.call(ctx, e),
await fetchZhihuHot.call(ctx, e),
await fetch60s.call(ctx, e),
await fetchWeibo.call(ctx, e),
await fetchBili.call(ctx, e),
await fetchZhihu.call(ctx, e),
].filter(Boolean) as oicq.Sendable[]
if (!news.length) return await e.reply('获取热搜失败')
const messages = await ctx.bot.makeForwardMsg(
news.map((item) => ({
user_id: e?.sender?.user_id,
nickname: e?.sender?.nickname,
message: item,
})),
)
await e.reply(messages)
ctx.isGroupMsg(e) && (await e.group.recallMsg(message_id))
}
},
e,
)
})
},
cron: [
[
'0 8 * * *', // 每天早上八点推送
async (ctx) => {
const news = await fetch60s.call(ctx)
if (!news) return
await ctx.noticeGroups.call(ctx.bot, pushGroups, news)
},
],
],
})
async function fetch60s(this: KiviPluginContext, e?: AllMessageEvent): Promise<oicq.Sendable | null> {
const { data } = await this.http.get('https://60s.viki.moe?v2=1')
const { news } = data?.data ?? {}
if (!news || !news.length) return null
const { nickname = 'QQ 用户', user_id = this.bot.uid } = e?.sender || {}
const messages = await this.bot.makeForwardMsg([
{ nickname, user_id, message: '〓 每天 60 秒看世界 〓' },
...news.slice(0, 20).map((item: any, idx: number) => ({
nickname,
user_id,
message: `【${idx + 1}】${item}\n\n详情: https://www.baidu.com/s?wd=${encodeURIComponent(item)}`,
})),
])
return messages
}
async function fetchDouyin(this: KiviPluginContext, e: AllMessageEvent): Promise<oicq.Sendable | null> {
const { data } = await this.http.get('https://60s.viki.moe/douyin')
const list = (data?.data ?? []) as any[]
if (!list.length) return null
const messages = await this.bot.makeForwardMsg([
{ nickname: e.sender.nickname, user_id: e.sender.user_id, message: '〓 抖音热搜榜 〓' },
...list.slice(0, 10).map((item, idx) => ({
nickname: e.sender.nickname,
user_id: e.sender.user_id,
message: [
this.oicq.segment.image(item.cover),
`【${idx + 1}】${item.word}\n时间:${this.dayjs(item.active_time).format('YYYY/MM/DD HH:mm:ss')}\n\n详情: https://www.douyin.com/root/search/${encodeURIComponent(item.word)}`,
],
})),
])
return messages
}
async function fetchZhihu(this: KiviPluginContext, e: AllMessageEvent): Promise<oicq.Sendable | null> {
const { data } = await this.http.get('https://60s.viki.moe/zhihu')
const list = (data?.data ?? []) as any[]
if (!list.length) return null
const messages = await this.bot.makeForwardMsg([
{ nickname: e.sender.nickname, user_id: e.sender.user_id, message: '〓 知乎热搜榜 〓' },
...list.map((item, idx) => ({
nickname: e.sender.nickname,
user_id: e.sender.user_id,
message: `【${idx + 1}】${item.query}\n\n详情: https://www.zhihu.com/search?q=${encodeURIComponent(item.query)}`,
})),
])
return messages
}
async function fetchZhihuHot(this: KiviPluginContext, e: AllMessageEvent): Promise<oicq.Sendable | null> {
const { data } = await this.http.get('https://60s.viki.moe/zhihu-hot')
const list = (data?.data ?? []) as any[]
if (!list.length) return null
const messages = await this.bot.makeForwardMsg([
{ nickname: e.sender.nickname, user_id: e.sender.user_id, message: '〓 知乎热门问题 〓' },
...list.slice(0, 10).map((item, idx) => ({
nickname: e.sender.nickname,
user_id: e.sender.user_id,
message: [
this.oicq.segment.image(item.cover),
`【${idx + 1}】【${item.metrics}】${item.title}\n\n${item.detail}\n\n详情: ${item.link}`,
],
})),
])
return messages
}
async function fetchBili(this: KiviPluginContext, e: AllMessageEvent): Promise<oicq.Sendable | null> {
const { data } = await this.http.get('https://60s.viki.moe/bili')
const list = (data?.data ?? []) as any[]
if (!list.length) return null
const messages = await this.bot.makeForwardMsg([
{ nickname: e.sender.nickname, user_id: e.sender.user_id, message: '〓 B 站热搜榜 〓' },
...list.map((item, idx) => {
const word = item.show_name || item.keyword
return {
nickname: e.sender.nickname,
user_id: e.sender.user_id,
message: `【${idx + 1}】${word}\n\n详情: https://search.bilibili.com/all?keyword=${encodeURIComponent(word)}`,
}
}),
])
return messages
}
async function fetchWeibo(this: KiviPluginContext, e: AllMessageEvent): Promise<oicq.Sendable | null> {
const { data } = await this.http.get('https://60s.viki.moe/weibo')
const list = (data?.data ?? []) as any[]
if (!list.length) return null
const messages = await this.bot.makeForwardMsg([
{ nickname: e.sender.nickname, user_id: e.sender.user_id, message: '〓 微博热搜榜 〓' },
...list.map((item, idx) => {
const icon = item.icon_desc ? `[${item.icon_desc}] ` : ''
return {
nickname: e.sender.nickname,
user_id: e.sender.user_id,
message: `【${idx + 1}】${icon}${item.word || item.note}\n\n详情: https://s.weibo.com/weibo?q=${encodeURIComponent(item.word_scheme)}`,
}
}),
])
return messages
}
import { definePlugin, http } from 'kivibot'
export default definePlugin({
name: 'fb',
version: '1.0.0',
setup(ctx) {
ctx.handle('message.group', async (e) => {
const reg = /^\s*(每日)?发病\s*(.*)$/
const matches = e.raw_message.match(reg)
if (matches) {
const name = matches[2] || (e.sender.card ?? e.sender.nickname)
e.reply(await fetchFbMsgs(name))
}
})
ctx.handle('notice.group.poke', async (e) => {
const { target_id, operator_id } = e
if (target_id === ctx.bot.uin) {
const member = e.group.pickMember(operator_id)
const msg = await fetchFbMsgs((member.card || operator_id).toString())
e.group.sendMsg(msg)
}
})
},
})
async function fetchFbMsgs(name: string) {
const { data } = await http.get('https://fb.viki.moe', { params: { name } })
return data
}
import { definePlugin, type oicq } from 'kivibot'
export default definePlugin({
name: '群管理',
version: '1.1.0',
setup(ctx) {
ctx.handle('message.group', async (e) => {
if (!e.group.is_owner) return
if (e.raw_message.startsWith('#设置群管理')) {
if (!ctx.isOwner(e)) return
const targetId = e.message.find((m) => m.type === 'at')?.qq || 0
if (!targetId || targetId === 'all') return
await e.group.setAdmin(targetId, true)
await e.reply('done', true)
}
if (e.raw_message.startsWith('#取消群管理')) {
if (!ctx.isOwner(e)) return
const targetId = e.message.find((m) => m.type === 'at')?.qq || 0
if (!targetId || targetId === 'all') return
await e.group.setAdmin(targetId, false)
await e.reply('done', true)
}
if (e.raw_message.startsWith('#我要头衔')) {
const newTitle = e.raw_message.replace('#我要头衔', '').trim()
await setTitle(e.sender.user_id, newTitle, e)
}
if (e.raw_message.startsWith('#设置头衔')) {
if (!ctx.hasRight(e)) return
const targetId = e.message.find((m) => m.type === 'at')?.qq || 0
if (!targetId || targetId === 'all') return
const newTitle = ctx.getText(e).replace('#设置头衔', '').trim()
await setTitle(targetId, newTitle, e)
}
})
},
})
async function setTitle(targetId: number, newTitle: string, e: oicq.GroupMessageEvent) {
await e.group.setTitle(targetId, newTitle)
await e.reply(newTitle ? 'done' : '头衔已清空', true)
}
import { definePlugin, type oicq } from 'kivibot'
export default definePlugin({
name: 'title',
version: '1.1.0',
setup(ctx) {
ctx.handle('message.group', async (e) => {
if (!e.group.is_owner) return
if (e.raw_message.startsWith('#设置群管理')) {
if (!ctx.isOwner(e)) return
const targetId = e.message.find((m) => m.type === 'at')?.qq || 0
if (!targetId || targetId === 'all') return
await e.group.setAdmin(targetId, true)
await e.reply('done', true)
}
if (e.raw_message.startsWith('#取消群管理')) {
if (!ctx.isOwner(e)) return
const targetId = e.message.find((m) => m.type === 'at')?.qq || 0
if (!targetId || targetId === 'all') return
await e.group.setAdmin(targetId, false)
await e.reply('done', true)
}
if (e.raw_message.startsWith('#我要头衔')) {
const newTitle = e.raw_message.replace('#我要头衔', '').trim()
await setTitle(e.sender.user_id, newTitle, e)
}
if (e.raw_message.startsWith('#设置头衔')) {
if (!ctx.hasRight(e)) return
const targetId = e.message.find((m) => m.type === 'at')?.qq || 0
if (!targetId || targetId === 'all') return
const newTitle = ctx.getText(e).replace('#设置头衔', '').trim()
await setTitle(targetId, newTitle, e)
}
})
},
})
async function setTitle(targetId: number, newTitle: string, e: oicq.GroupMessageEvent) {
await e.group.setTitle(targetId, newTitle)
await e.reply(newTitle ? 'done' : '头衔已清空', true)
}
import { definePlugin } from 'kivibot'
export default definePlugin({
name: '请求处理',
version: '1.1.0',
setup(ctx) {
ctx.handle('request.friend.add', async (e) => {
e.approve()
ctx.noticeMainOwner.call(ctx.bot, [
ctx.oicq.segment.image(ctx.getQQAvatarLink(e.user_id)),
`〓 收到好友请求 〓\nQQ: ${e.nickname}\n昵称: ${e.nickname}\n来自: ${e.source}\n验证消息: ${e.comment}\n\n已自动同意`,
])
})
ctx.handle('request.friend.invite', (e) => {
ctx.noticeMainOwner.call(ctx.bot, [
ctx.oicq.segment.image(ctx.getGroupAvatarLink(e.group_id)),
`〓 收到拉群请求 〓\n群号: ${e.group_id}\n群名: ${e.group_name}`,
ctx.oicq.segment.image(ctx.getQQAvatarLink(e.user_id)),
`邀请人 QQ: ${e.user_id}\n邀请人昵称: ${e.nickname}\n邀请人角色: ${e.role}`,
])
// 如果是主人的好友,自动同意
if (ctx.hasRight(e.user_id)) {
e.approve(true)
} else {
// 否则拒绝
// e.approve(false)
}
})
},
})
import { dedent, definePlugin } from 'kivibot'
const map = new Map<number, [string, string]>()
const reasonsDataURL =
'https://proxy.viki.moe/vikiboss/4a970efd8b81b0069ae02d1db42e7303/raw/4ecffb1befebb4b4bd930f9bdf62f00b37069c38/aq-center-reasons.json?proxy-host=gist.githubusercontent.com'
export default definePlugin({
name: 'aq',
version: '1.5.0',
async setup(ctx) {
const { data = {} } = await ctx.http.get(reasonsDataURL)
Object.entries(data as { [key in string]: any }).map(([_k, v]) => {
map.set(v.reason, [v.reasonDesc || v.title, v.description])
})
ctx.handle('message', async (e) => {
if (['机器人违规记录', '机器人赛博案底'].includes(e.raw_message) && ctx.isOwner(e)) {
const authCode = await ctx.getAuthCodeOfBot.call(ctx.bot, 1109907872)
const list = await ctx.getViolationRecords.call(ctx.bot, authCode, 1109907872)
const messages = await ctx.bot.makeForwardMsg(makeRecordListMessage(list, e.sender.user_id, e.sender.nickname))
await e.reply(`共查询到 ${list.length} 条违规记录。`, true)
await e.reply(messages)
}
if (['我的违规记录', '违规记录', '我的赛博案底', '赛博案底'].includes(e.raw_message)) {
const { url, code } = await ctx.requestLoginViaDevTools.call(ctx.bot)
const { message_id } = await e.reply(`请在一分钟内点击下列链接登录。\n${url}`, true)
const timeout = setTimeout(async () => {
clearInterval(interval)
ctx.isGroupMsg(e) && (await e.group.recallMsg(message_id))
await e.reply('登录超时,请重新发送「赛博案底」获取新的链接。', true)
}, 60_000)
const interval = setInterval(async () => {
const { status, ticket } = await ctx.queryDevToolsLoginStatus.call(ctx.bot, code)
if (status === 'Wait') return
clearInterval(interval)
clearTimeout(timeout)
if (status === 'Used') {
ctx.isGroupMsg(e) && (await e.group.recallMsg(message_id))
return
}
if (status === 'OK' && ticket) {
ctx.isGroupMsg(e) && (await e.group.recallMsg(message_id))
const authCode = await ctx.getAuthCodeViaTicket.call(ctx.bot, ticket, 1109907872)
const list = await ctx.getViolationRecords.call(ctx.bot, authCode, 1109907872)
if (!list.length) {
await e.reply('泰酷辣!没有查到任何违规记录!', true)
ctx.isGroupMsg(e) && (await e.group.recallMsg(message_id))
return
}
const messages = await ctx.bot.makeForwardMsg(
makeRecordListMessage(list, e.sender.user_id, e.sender.nickname),
)
ctx.isGroupMsg(e) && (await e.group.recallMsg(message_id))
await e.reply(`共查询到 ${list.length} 条违规记录。`, true)
await e.reply(messages)
return
}
ctx.isGroupMsg(e) && (await e.group.recallMsg(message_id))
await e.reply('登录失败,未知错误', true)
}, 1000)
}
})
},
})
function makeRecordListMessage(
list: {
type: string
time: string
duration: string
reason: number
}[],
id: number,
nickname: string,
) {
return list.map((v) => {
const reason = map.get(v.reason) || String(v.reason)
const duration = +v.duration
? `强制冻结 ${Math.round((+v.duration - +v.time) / 24 / 60 / 60)} 天,限制登录`
: '临时冻结,可根据引导解冻'
return {
user_id: id,
message: dedent(`
违规时间:${new Date(+v.time * 1000).toLocaleString('zh-CN')}
违规原因:${Array.isArray(reason) ? reason[0] : reason}
详细说明:${Array.isArray(reason) ? reason[1] : reason}
处理结果:${duration}
`),
nickname,
time: Date.now(),
}
})
}
import { definePlugin } from 'kivibot'
interface ImpartItem {
length: number
injectedValue: number
injectedCount: number
ejaculatedValue: number
ejaculateCount: number
lastEjaculateAt: number
masturbateCount: number
}
const defaultItem = {
length: 6,
injectedValue: 0,
injectedCount: 0,
ejaculatedValue: 0,
ejaculateCount: 0,
lastEjaculateAt: 0,
masturbateCount: 0,
}
const interval = 1 * 60 * 60 * 1000 // 1 hours
export default definePlugin({
name: '赛博银趴',
version: '1.1.0',
async setup(ctx) {
const filename = ctx.path.join(__dirname, './config.json')
const config = !ctx.fs.existsSync(filename) ? [] : require(filename)
const store = ctx.createVanilla({ data: config as [number, ImpartItem][] })
store.subscribe(() => {
ctx.fs.writeFileSync(filename, JSON.stringify(store.mutate.data))
})
function getItem(id: number): ImpartItem {
let item = store.mutate.data.find((x) => x[0] === id)?.[1]
if (!item) {
item = { ...defaultItem }
store.mutate.data.push([id, item])
}
return item
}
function isInInterval(last: number): boolean {
return Date.now() - last < interval
}
function nextTime(last: number): string {
if (!last || !isInInterval(last)) return '现在!!!'
const res = (last + interval - Date.now()) / 1000 / 60
const min = Math.floor(res)
const sec = Math.floor((res - min) * 60)
return min ? `${min} 分 ${sec} 秒后` : `${sec} 秒后`
}
ctx.handle('message.group', async (e) => {
const text = ctx.getText(e)
if (!text) return
const item = getItem(e.sender.user_id)
if (['打搅', '打胶'].includes(text)) {
if (isInInterval(item.lastEjaculateAt)) {
return e.reply(
`你已经榨不出任何东西啦,等会再来吧。杂鱼~ 杂鱼~(${nextTime(item.lastEjaculateAt)}可再次释放)`,
true,
)
}
const value = ctx.randomInt(1, 100)
const randomGrow = ctx.randomInt(10, 100) / 1000
item.ejaculateCount++
item.ejaculatedValue += value
item.masturbateCount++
item.lastEjaculateAt = Date.now()
item.length = Math.round((randomGrow + item.length) * 1000) / 1000
return e.reply(
`打胶结束啦,真舒服~ 你释放了 ${value} 毫升 DNA 🧬!你的牛牛变长了 ${randomGrow} 厘米!目前长度 ${item.length} 厘米。`,
true,
)
}
if (['我的牛牛', '俺的牛牛', '我的牛子', '俺的牛子'].includes(text)) {
return e.reply(
[
ctx.oicq.segment.image(ctx.getQQAvatarLink(e.sender.user_id, 100)),
ctx.dedent(
`
牛牛长度: ${item.length} 厘米
共计打胶: ${item.masturbateCount} 次
共计释放: ${item.ejaculateCount} 次
总释放量: ${item.ejaculatedValue} 毫升 DNA 🧬
共计被注入: ${item.injectedCount} 次
总被注入量: ${item.injectedValue} 毫升 DNA 🧬
上次释放: ${item.lastEjaculateAt ? ctx.dayjs(item.lastEjaculateAt).format('YYYY-MM-DD HH:mm:ss') : '还没有释放过'}
下次可释放: ${nextTime(item.lastEjaculateAt)}
`,
),
],
true,
)
}
if (['牛牛榜', '牛牛排行榜', '牛子榜', '牛子排行榜'].some((e) => text === e)) {
const top = store.mutate.data
.filter(([id, item]) => item.length > 6)
.map(([id, item]) => ({ id, length: item.length }))
.sort((a, b) => b.length - a.length)
.slice(0, 99)
const gm = ctx.bot.gml.get(e.group_id)
const msgs = await e.group.makeForwardMsg(
top.map((item) => {
const friend = ctx.bot.fl.get(item.id)
const info = gm?.get(item.id)
return {
nickname: friend?.nickname || info?.card || '牛牛排行榜',
user_id: item.id,
message: `👤 ${friend?.nickname || info?.card || '[不愿透露姓名的群友]'}\n🆔 ${item.id}\n🐮 ${item.length} 厘米`,
}
}),
)
return e.reply(msgs)
}
// 以下需要 @ 或者回复某人
const id = await ctx.getMentionedUserId(e)
if (!id) return
const targetItem = getItem(id)
if (['它的牛牛', '他的牛牛', '她的牛牛'].some((e) => text === e)) {
return e.reply(
[
ctx.oicq.segment.image(ctx.getQQAvatarLink(id, 100)),
ctx.dedent(
`
牛牛长度: ${targetItem.length} 厘米
共计打胶: ${targetItem.masturbateCount} 次
共计释放: ${targetItem.ejaculateCount} 次
总释放量: ${targetItem.ejaculatedValue} 毫升 DNA 🧬
共计被注入: ${targetItem.injectedCount} 次
总被注入量: ${targetItem.injectedValue} 毫升 DNA 🧬
上次释放: ${targetItem.lastEjaculateAt ? ctx.dayjs(targetItem.lastEjaculateAt).format('YYYY-MM-DD HH:mm:ss') : '还没有释放过'}
下次可释放: ${nextTime(targetItem.lastEjaculateAt)}
`,
),
],
true,
)
}
if (['透'].some((e) => text === e)) {
if (isInInterval(item.lastEjaculateAt)) {
return e.reply(
[
ctx.oicq.segment.at(e.sender.user_id),
` 你已经榨不出任何东西啦,等会再来吧小杂鱼~(冷却至 ${nextTime(item.lastEjaculateAt)})`,
],
true,
)
}
const isSuccess = Math.random() < 0.5
if (!isSuccess) {
return await e.reply(
[
ctx.oicq.segment.image(ctx.getQQAvatarLink(e.sender.user_id, 100)),
'啊,没想到 ',
ctx.oicq.segment.at(e.sender.user_id),
' 床技不如人... 被 ',
ctx.oicq.segment.at(id),
' 反透了!呜呜呜... 你的牛牛没有任何变化...',
],
true,
)
}
const length = ctx.randomInt(1, 100) / 1000
const value = ctx.randomInt(1, 100)
item.length = Math.round((length + item.length) * 1000) / 1000
item.ejaculateCount++
item.ejaculatedValue += value
item.lastEjaculateAt = Date.now()
targetItem.injectedCount++
targetItem.injectedValue += value
return await e.reply(
[
ctx.oicq.segment.image(ctx.getQQAvatarLink(id, 100)),
'好耶,',
ctx.oicq.segment.at(e.sender.user_id),
' 一口气给 ',
ctx.oicq.segment.at(id),
' 注入了 ',
String(value),
` 毫升 DNA 🧬!你的牛牛变长了 ${length} 厘米!目前长度 ${item.length} 厘米!`,
],
true,
)
}
})
},
})
import { definePlugin } from 'kivibot'
export default definePlugin({
name: 'retire',
version: '1.0.0',
setup(ctx) {
ctx.handle('message', async (e) => {
if (e.raw_message.startsWith('#退休计算')) {
const [year = '', month = '', type = ''] = e.raw_message
.replace('#退休计算', '')
.trim()
.split(' ')
.map((v) => v.trim())
.filter(Boolean)
if (year.length !== 4 || +month > 12 || +month < 1 || !['男', '女50', '女55'].includes(type)) {
e.reply(
ctx.dedent(`
参数有误,请参考使用说明 👇
#退休计算 <年份> <月份> <类型>
类型包含:男、女50、女55
例如:
#退休计算 1990 12 男
#退休计算 2000 5 女55
#退休计算 2004 8 女50
`),
)
return
}
const sexType = type === '男' ? 'male' : type === '女55' ? 'female55' : 'female50'
const data = calculateRetirement(+year, +month, sexType)
e.reply(
ctx.dedent(
`
〓 法定退休年龄计算结果 〓
出生年月:${year}年${month}月
退休年龄:${data.retirementAge}
退休时间:${data.retirementTime}
延迟月数:${data.delayMonths}
`,
),
)
}
})
},
})
type RetirementInfo = {
retirementAge: string
retirementTime: string
delayMonths: string
}
function calculateRetirement(
yearOfBirth: number,
monthOfBirth: number,
type: 'male' | 'female50' | 'female55',
): RetirementInfo {
const monthDiff = (fromYear: number, fromMonth: number, toYear: number, toMonth: number): number => {
return (toYear - fromYear) * 12 + toMonth - fromMonth
}
const addMonths = (date: Date, months: number): Date => {
date.setMonth(date.getMonth() + months)
return date
}
let retirementAge = ''
let retirementTime = ''
let delayMonths = 0
if (type === 'male') {
if (yearOfBirth < 1965) {
retirementAge = '60岁'
delayMonths = 0
} else if (yearOfBirth > 1976) {
retirementAge = '63岁'
delayMonths = 36
} else {
const diff = Math.ceil(monthDiff(1965, 1, yearOfBirth, monthOfBirth) / 4)
const extraYears = Math.floor(diff / 12)
const extraMonths = diff % 12
retirementAge = `${60 + extraYears}岁${extraMonths > 0 ? `${extraMonths}个月` : ''}`
delayMonths = diff
}
} else if (type === 'female55') {
if (yearOfBirth < 1970) {
retirementAge = '55岁'
delayMonths = 0
} else if (yearOfBirth > 1981) {
retirementAge = '58岁'
delayMonths = 36
} else {
const diff = Math.ceil(monthDiff(1970, 1, yearOfBirth, monthOfBirth) / 4)
const extraYears = Math.floor(diff / 12)
const extraMonths = diff % 12
retirementAge = `${55 + extraYears}岁${extraMonths > 0 ? `${extraMonths}个月` : ''}`
delayMonths = diff
}
} else if (type === 'female50') {
if (yearOfBirth < 1975) {
retirementAge = '50岁'
delayMonths = 0
} else if (yearOfBirth > 1984) {
retirementAge = '55岁'
delayMonths = 60
} else {
const diff = Math.ceil(monthDiff(1975, 1, yearOfBirth, monthOfBirth) / 2)
const extraYears = Math.floor(diff / 12)
const extraMonths = diff % 12
retirementAge = `${50 + extraYears}岁${extraMonths > 0 ? `${extraMonths}个月` : ''}`
delayMonths = diff
}
}
const retirementStartDate = addMonths(
new Date(yearOfBirth, monthOfBirth - 1),
(type === 'male' ? 60 : type === 'female55' ? 55 : 50) * 12 + delayMonths,
)
retirementTime = `${retirementStartDate.getFullYear()}年${retirementStartDate.getMonth() + 1}月`
return {
retirementAge,
retirementTime,
delayMonths: `${delayMonths}个月`,
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment