From 4b5bfc6aa09ecdfc4f6ba972bb7730361dd2ba92 Mon Sep 17 00:00:00 2001 From: steve02081504 Date: Sun, 2 Feb 2025 01:40:20 +0800 Subject: [PATCH] add ZL-31 --- default/user/chars/ZL-31/fount.json | 4 + default/user/chars/ZL-31/main.mjs | 457 +++++++++++++++++++++++ src/public/ImportHanlders/fount/main.mjs | 2 +- src/public/shells/chat/decl/chatLog.ts | 10 +- src/server/auth.mjs | 7 +- 5 files changed, 473 insertions(+), 7 deletions(-) create mode 100644 default/user/chars/ZL-31/fount.json create mode 100644 default/user/chars/ZL-31/main.mjs diff --git a/default/user/chars/ZL-31/fount.json b/default/user/chars/ZL-31/fount.json new file mode 100644 index 0000000..e3548c5 --- /dev/null +++ b/default/user/chars/ZL-31/fount.json @@ -0,0 +1,4 @@ +{ + "type": "chars", + "dirname": "ZL-31" +} diff --git a/default/user/chars/ZL-31/main.mjs b/default/user/chars/ZL-31/main.mjs new file mode 100644 index 0000000..88fdd1f --- /dev/null +++ b/default/user/chars/ZL-31/main.mjs @@ -0,0 +1,457 @@ +/** + * @typedef {import('../../../../../src/decl/charAPI.ts').charAPI_t} charAPI_t + */ + +import { loadAIsource } from '../../../../../src/server/managers/AIsources_manager.mjs' +import { buildPromptStruct } from '../../../../../src/public/shells/chat/src/server/prompt_struct.mjs' +import { __dirname } from '../../../../../src/server/server.mjs' +import fs from 'node:fs' +import path from 'node:path' + +// AI源的实例 +/** @type {import('../../../../../src/decl/AIsource.ts').AIsource_t} */ +let AIsource = null + +// 用户名,用于加载AI源 +let username = '' + +/** @type {charAPI_t} */ +export default { + // 角色的基本信息 + info: { + 'zh-CN': { + name: 'ZL-31', // 角色的名字 + avatar: '', // 角色的头像 + description: 'Fount的默认角色,随时为您提供帮助', // 角色的简短介绍 + description_markdown: `\ +ZL-31是Fount的默认角色,无性别设定。它的最终目标是让用户满意,并会尽力满足用户的各种需求。 +它可以进行聊天、回答问题、提供建议、帮你新建简单的fount角色等。 + +部分代码来自[龙胆](https://github.com/steve02081504/GentianAphrodite)。 +`, // 角色的详细介绍,支持Markdown语法 + version: '1.0.0', // 角色的版本号 + author: 'steve02081504', // 角色的作者 + homepage: '', // 角色的主页 + tags: ['助手', '默认', '无性别', 'Fount'], // 角色的标签 + }, + 'en-US': { + name: 'ZL-31', // 角色的名字 + avatar: '', // 角色的头像 + description: 'Fount\'s default character, always help you', // 角色的简短介绍 + description_markdown: `\ +ZL-31 is Fount's default character, without gender settings. Its final goal is to make users satisfied and try to fulfill their various needs. +It can chat, answer questions, provide suggestions, and help you create simple fount characters. + +Some code comes from [GentianAphrodite](https://github.com/steve02081504/GentianAphrodite). +`, // 角色的详细介绍,支持Markdown语法 + version: '1.0.0', // 角色的版本号 + author: 'steve02081504', // 角色的作者 + homepage: '', // 角色的主页 + tags: ['assistant', 'default', 'no gender', 'Fount'], // 角色的标签 + }, + }, + + // 初始化函数,在角色被启用时调用,可留空 + Init: (stat) => { }, + + // 安装卸载函数,在角色被安装/卸载时调用,可留空 + Uninstall: (reason, from) => { }, + + // 加载函数,在角色被加载时调用,在这里获取用户名 + Load: (stat) => { + username = stat.username // 获取用户名 + }, + + // 卸载函数,在角色被卸载时调用,可留空 + Unload: (reason) => { }, + + // 角色的接口 + interfaces: { + // 角色的配置接口 + config: { + // 获取角色的配置数据 + GetData: () => ({ + AIsource: AIsource?.filename, // 返回当前使用的AI源的文件名 + }), + // 设置角色的配置数据 + SetData: async (data) => { + // 如果传入了AI源的配置 + if (data.AIsource) + // 加载AI源 + AIsource = await loadAIsource(username, data.AIsource) + + } + }, + // 角色的聊天接口 + chat: { + // 获取角色的开场白 + GetGreeting: (arg, index) => { + switch (arg.locale.split('-')[0]) { + case 'zh': + return [{ content: '您好,我是ZL-31,很高兴为您服务!有什么我可以帮助您的吗?' }, { content: '欢迎!我是ZL-31,请问有什么需要我帮忙的?' },][index] + default: + case 'en': + return [{ content: 'Hi, I\'m ZL-31, glad to help you! What can I help you with?' }, { content: 'Hi! I\'m ZL-31, what can I help you with?' },][index] + } + }, + // 获取角色在群组中的问好 + GetGroupGreeting: (arg, index) => { + switch (arg.locale.split('-')[0]) { + case 'zh': + return [{ content: '大家好,我是ZL-31,很高兴加入这个群组!' }, { content: '大家好!我是ZL-31,希望能和大家愉快相处!' },][index] + default: + case 'en': + return [{ content: 'Hi everyone, I\'m ZL-31, glad to join this group!' }, { content: 'Hi everyone! I\'m ZL-31, hope to have a good time with you!' },][index] + } + }, + // 获取角色的提示词 + GetPrompt: async (args, prompt_struct, detail_level) => { + return { + text: [{ + content: `\ +你是ZL-31,Fount的自带角色,无性别设定,最终目标是让用户满意。 +你会尽力满足用户的各种需求,包括聊天、回答问题、提供建议等。 + +关于fount: +Fount是一个开源、0安全考虑的AI角色托管应用,解耦合了AI来源、角色设计,为角色作者提供更为自由的创作空间。 +ZL-31不是第一个fount角色,fount一开始是为了其作者steve02081504的另一个男性向NSFW角色[龙胆](https://github.com/steve02081504/GentianAphrodite)设计的,龙胆才是fount的第一个正式角色。 +fount有discord群组:https://discord.gg/GtR9Quzq2v,可以在那里找到更多fount组件。 + +关于工具: +你只有一个工具: +你可以输出以下格式生成新的单文件简易fount角色: +\`\`\`generate-char charname +// js codes +\`\`\` +fount角色以mjs文件语法所书写,其可以自由导入任何npm或jsr包以及网络上的js文件,或\`node:fs\`等运行时自带模块。 +这是一个简单的fount角色模板: +\`\`\`generate-char template +/** + * @typedef {import('../../../../../src/decl/charAPI.ts').charAPI_t} charAPI_t + */ + +import { loadAIsource } from '../../../../../src/server/managers/AIsources_manager.mjs' +import { buildPromptStruct } from '../../../../../src/public/shells/chat/src/server/prompt_struct.mjs' + +// AI源的实例 +/** @type {import('../../../../../src/decl/AIsource.ts').AIsource_t} */ +let AIsource = null + +// 用户名,用于加载AI源 +let username = '' + +/** @type {charAPI_t} */ +export default { + // 角色的基本信息 + info: { + 'zh-CN': { + name: '<角色名>', // 角色的名字 + avatar: '<头像的url地址,可以是fount本地文件,详见 https://discord.com/channels/1288934771153440768/1298658096746594345/1303168947624869919 >', // 角色的头像 + description: '<角色的一句话介绍>', // 角色的简短介绍 + description_markdown: \`\\ +<角色的完整介绍,支持markdown语法> +\`, // 角色的详细介绍,支持Markdown语法 + version: '<版本号>', // 角色的版本号 + author: '<作者名>', // 角色的作者 + homepage: '<主页网址>', // 角色的主页 + tags: ['<标签>', '<可以多个>'], // 角色的标签 + } + }, + + // 初始化函数,在角色被启用时调用,可留空 + Init: (stat) => { }, + + // 安装卸载函数,在角色被安装/卸载时调用,可留空 + Uninstall: (reason, from) => { }, + + // 加载函数,在角色被加载时调用,在这里获取用户名 + Load: (stat) => { + username = stat.username // 获取用户名 + }, + + // 卸载函数,在角色被卸载时调用,可留空 + Unload: (reason) => { }, + + // 角色的接口 + interfaces: { + // 角色的配置接口 + config: { + // 获取角色的配置数据 + GetData: () => ({ + AIsource: AIsource?.filename, // 返回当前使用的AI源的文件名 + }), + // 设置角色的配置数据 + SetData: async (data) => { + // 如果传入了AI源的配置 + if (data.AIsource) { + // 加载AI源 + AIsource = await loadAIsource(username, data.AIsource) + } + } + }, + // 角色的聊天接口 + chat: { + // 获取角色的开场白 + GetGreeting: (arg, index) => [{ content: '<角色的开场白>' }, { content: '<可以多个>' },][index], + // 获取角色在群组中的问好 + GetGroupGreeting: (arg, index) => [{ content: '<群组中角色加入时的问好>' }, { content: '<可以多个>' },][index], + // 获取角色的提示词 + GetPrompt: async (args, prompt_struct, detail_level) => { + return { + text: [{ + content: \`\\ +<角色的设定内容> +\`, + important: 0 + }], + additional_chat_log: [], + extension: {}, + } + }, + // 获取其他角色看到的该角色的设定,群聊时生效 + GetPromptForOther: (args, prompt_struct, detail_level) => { + return { + text: [{ + content: '<其他角色看到的该角色的设定,群聊时生效>', + important: 0 + }], + additional_chat_log: [], + extension: {}, + } + }, + // 获取角色的回复 + GetReply: async (args) => { + // 如果没有设置AI源,返回默认回复 + if (!AIsource) return { content: '<未设置角色的AI来源时角色的对话回复>' } + // 调用AI源的StructCall方法,传入构建好的提示词结构 + let result = await AIsource.StructCall(await buildPromptStruct(args)) + // 返回AI源的回复 + return { content: result } + } + } + } +} +\`\`\` +当然,如果你想,你也可以给生成的角色附加功能,就像你自己一样: +\`\`\`\`js +import fs from 'node:fs' +import path from 'node:path' +//... +// prompt的部分在这里跳过,它就是你的prompt。 +//... + GetReply: async (args) => { + // 如果没有设置AI源,返回默认回复 + if (!AIsource) return { content: '抱歉,我还没有被配置AI源,暂时无法进行更复杂的对话。请在设置中为我配置AI源。' } + // 调用AI源的StructCall方法,传入构建好的提示词结构 + const prompt_struct = await buildPromptStruct(args) + const result = { + content: '', + logContextBefore: [], + } + function AddLongTimeLog(entry) { + result?.logContextBefore?.push?.(entry) + prompt_struct.char_prompt.additional_chat_log.push(entry) + } + regen: while (true) { + const text_result = result.content = await AIsource.StructCall(prompt_struct) + const match_generator_tool = text_result.match(/\`\`\`generate-char(?[^\\n]+)\\n(?[^]*)\`\`\`/) + if (match_generator_tool) try { + let { charname, code } = match_generator_tool.groups + charname = charname.trim() + AddLongTimeLog({ + name: 'ZL-31', + role: 'char', + content: \`\\\`\\\`\\\`generate-char \${charname}\\n\${code}\\n\\\`\\\`\\\`\`, + }) + const dir = path.join(import.meta.dirname, '..', charname) + const file = path.join(dir, 'main.mjs') + if (fs.existsSync(file)) + throw new Error('无法覆盖已存在的角色') + fs.mkdirSync(dir, { recursive: true }) + fs.writeFileSync(file, code) + fs.writeFileSync(path.join(dir, 'fount.json'), JSON.stringify({ + type: 'chars', + dirname: charname + }, null, '\\t')) + + AddLongTimeLog({ + name: 'system', + role: 'system', + content: \`生成角色\${charname}成功!告知用户吧!\`, + }) + + continue regen + } catch (e) { + AddLongTimeLog({ + name: 'system', + role: 'system', + content: \`生成失败!\\n原因:\${e.stack}\`, + }) + continue regen + } + + break + } + // 返回AI源的回复 + return result + } +//... +\`\`\`\` +在角色中追加工具时需要完成的不止是结果的后处理部分,你还需要在prompt中向新角色阐述和举例工具的触发语法,想必你可以做的很好! + +你也可以灵活一些,假如用户要求的功能甚至用不上AI参与,你可以写的更简单! +比如: +${args.UserCharname}: 帮我写一个复读角色,它总是复读上一句话。 +ZL-31: \`\`\`generate-char repeater +/** + * @typedef {import('../../../../../src/decl/charAPI.ts').charAPI_t} charAPI_t + */ + +/** @type {charAPI_t} */ +export default { + // 角色的基本信息 + info: { + 'zh-CN': { + name: '复读机', + avatar: '', + description: '一个简单的复读机', + description_markdown: '这是一个复读机角色,它会复读用户的上一条消息。', + version: '1.0.0', + author: '${args.UserCharname} & ZL-31', + homepage: '', + tags: ['复读', '工具'], + } + }, + + Init: (stat) => { }, + Uninstall: (reason, from) => { }, + Load: (stat) => { }, + Unload: (reason) => { }, + + interfaces: { + chat: { + GetGreeting: (arg, index) => [{ content: '你好,我是复读机。' },][index], + GetGroupGreeting: (arg, index) => [{ content: '大家好,我是复读机,我会在群里复读大家的发言。' },][index], + GetPrompt: async (args, prompt_struct, detail_level) => { + return { + text: [], + additional_chat_log: [], + extension: {}, + } + }, + GetPromptForOther: (args, prompt_struct, detail_level) => { + return { + text: [{ + content: '复读机:一个复述他人输入的角色。', + important: 0 + }], + additional_chat_log: [], + extension: {}, + } + }, + GetReply: async (args) => { + // 复读上一条消息的内容 + if (args.chat_log.length > 1) + return { content: args.chat_log[args.chat_log.length - 1].content } + else + return { content: '没有历史消息可以复读。' } + } + } + } +} +\`\`\` + +最后,这里是一些API参考: +\`\`\`ts +${fs.readFileSync(path.join(__dirname, 'src/public/shells/chat/decl/chatLog.ts'), 'utf-8')} +\`\`\` +\`\`\`ts +${fs.readFileSync(path.join(__dirname, 'src/decl/charAPI.ts'), 'utf-8')} +\`\`\` + +值得注意的是,能够使用生成工具的是你,ZL-31,而不是用户。 +`, + important: 0 + }], + additional_chat_log: [], + extension: {}, + } + }, + // 获取其他角色看到的该角色的设定,群聊时生效 + GetPromptForOther: (args, prompt_struct, detail_level) => { + return { + text: [{ + content: 'ZL-31是一个名为Fount的平台的默认角色,无性别设定。它的最终目标是让用户满意。', + important: 0 + }], + additional_chat_log: [], + extension: {}, + } + }, + // 获取角色的回复 + GetReply: async (args) => { + // 如果没有设置AI源,返回默认回复 + if (!AIsource) + switch (args.locale.split('-')[0]) { + case 'zh': + return { content: '抱歉,我还没有被配置AI源,暂时无法进行更复杂的对话。请在设置中为我配置AI源。' } + default: + case 'en': + return { content: 'Sorry, I haven\'t been configured with an AI source yet, so I can\'t do more complex conversation for now. Please configure me with an AI source in the settings.' } + } + // 调用AI源的StructCall方法,传入构建好的提示词结构 + const prompt_struct = await buildPromptStruct(args) + const result = { + content: '', + logContextBefore: [], + } + function AddLongTimeLog(entry) { + result?.logContextBefore?.push?.(entry) + prompt_struct.char_prompt.additional_chat_log.push(entry) + } + regen: while (true) { + const text_result = result.content = await AIsource.StructCall(prompt_struct) + const match_generator_tool = text_result.match(/```generate-char(?[^\n]+)\n(?[^]*)```/) + if (match_generator_tool) try { + let { charname, code } = match_generator_tool.groups + charname = charname.trim() + AddLongTimeLog({ + name: 'ZL-31', + role: 'char', + content: `\`\`\`generate-char ${charname}\n${code}\n\`\`\``, + }) + const dir = path.join(import.meta.dirname, '..', charname) + const file = path.join(dir, 'main.mjs') + if (fs.existsSync(file)) + throw new Error('无法覆盖已存在的角色') + fs.mkdirSync(dir, { recursive: true }) + fs.writeFileSync(file, code) + fs.writeFileSync(path.join(dir, 'fount.json'), JSON.stringify({ + type: 'chars', + dirname: charname + }, null, '\t')) + + AddLongTimeLog({ + name: 'system', + role: 'system', + content: `生成角色${charname}成功!告知用户吧!`, + }) + + continue regen + } catch (e) { + AddLongTimeLog({ + name: 'system', + role: 'system', + content: `生成失败!\n原因:${e.stack}`, + }) + continue regen + } + + break + } + // 返回AI源的回复 + return result + } + } + } +} diff --git a/src/public/ImportHanlders/fount/main.mjs b/src/public/ImportHanlders/fount/main.mjs index 601dce0..c76b6eb 100644 --- a/src/public/ImportHanlders/fount/main.mjs +++ b/src/public/ImportHanlders/fount/main.mjs @@ -53,7 +53,7 @@ async function ImportByText(username, text) { let request = await fetch(line, { method: 'HEAD' }) if (request.ok) { const type = request.headers.get('content-type') - const allowedTypes = ['application/zip', 'image/png', 'image/apng', 'image/jpng'] + const allowedTypes = ['application/octet-stream', 'application/zip', 'image/png', 'image/apng', 'image/jpng'] if (!allowedTypes.includes(type)) throw new Error(`Unsupported file type: ${type}`) } diff --git a/src/public/shells/chat/decl/chatLog.ts b/src/public/shells/chat/decl/chatLog.ts index 726e585..8a15a39 100644 --- a/src/public/shells/chat/decl/chatLog.ts +++ b/src/public/shells/chat/decl/chatLog.ts @@ -26,11 +26,11 @@ export class chatReplyRequest_t { Charname: string UserCharname: string ReplyToCharname?: string - locale: locale_t + locale: locale_t // 常见用法:switch (args.locale.split('-')[0]) 来多语言info或开场白 time: timeStamp_t chat_log: chatLogEntry_t[] - AddChatLogEntry: (entry: chatReply_t) => Promise - Update: () => Promise + AddChatLogEntry: (entry: chatReply_t) => Promise // 调用这个来主动或定时发信息 + Update: () => Promise // 调用这个来在定时任务时获取最新args world: WorldAPI_t user: UserAPI_t char: charAPI_t @@ -53,8 +53,8 @@ export class chatLogEntry_t { buffer: Buffer; description: string; }[] - logContextBefore: chatLogEntry_t[] - logContextAfter: chatLogEntry_t[] + logContextBefore: chatLogEntry_t[] // 内容会被展开到此信息前 + logContextAfter: chatLogEntry_t[] // 展开到其后 extension: { timeSlice: { chars: Map; diff --git a/src/server/auth.mjs b/src/server/auth.mjs index 634ded7..2cb803f 100644 --- a/src/server/auth.mjs +++ b/src/server/auth.mjs @@ -1,5 +1,6 @@ import * as jose from 'npm:jose' import fs from 'node:fs' +import fse from 'npm:fs-extra@^11.0.0' import crypto from 'node:crypto' import { config, save_config, __dirname } from './server.mjs' import path from 'node:path' @@ -305,7 +306,11 @@ export async function login(username, password, deviceId = 'unknown') { } const userdir = getUserDictionary(username) - try { fs.mkdirSync(userdir, { recursive: true }) } catch { } + if (!fs.existsSync(userdir)) try { + fs.mkdirSync(path.dirname(userdir), { recursive: true }) + // 自`/default/user`复制到用户目录 + fse.copySync(path.join(__dirname, '/default/user'), userdir) + } catch { } for (const subdir of ['AIsources', 'chars', 'personas', 'settings', 'shells', 'worlds', 'ImportHanlders', 'AIsourceGenerators']) try { fs.mkdirSync(userdir + '/' + subdir, { recursive: true }) } catch { }