🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

QQ群友 lilith1257/dsh-plugin-qq-groupmate

DeepSeek Harness
lilith125746 天前有提交已提供中文文档扫描:含敏感能力

让 AI 以群友身份住进 QQ 群聊,按群切换人设

30.8/100
综合分
1
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/9/18 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

自动兼容检查有告警:未发布到 npm registry,仅可从源码安装。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add lilith1257/dsh-plugin-qq-groupmate
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:仅索引
是什么
dsh 原生插件 · platform
装得上吗
静态安装检查有提示项,装前建议看一眼 README
L4 · 真实安装等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
更新放缓:最近一次提交在 46 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查⚠ 装前注意

未发布到 npm registry,仅可从源码安装

展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✗npm 包dsh-plugin-qq-groupmate(未发布到 npm,仅可源码安装)
✓Node 引擎未声明 engines.node
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/12 08:16:01

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件发起外部网络请求
查看扫描证据(共 146 处,展示前 132 条)
  • 读取本地文件
    lib/index.js:545
    const parsed = JSON.parse(readFileSync(`${dataDir}/state.json`, 'utf8'));
  • 读取本地文件
    lib/index.js:976
    assetsDir: existsSync(`${dataDir}/tarot/cards`) ? `${dataDir}/tarot/cards` : bundledAssetPath('assets', 'tarot-cards'),
  • 读取本地文件
    lib/character.js:106
    if (!existsSync(dir))
  • 读取本地文件
    lib/character.js:110
    entries = readdirSync(dir).filter(name => CARD_EXTENSIONS.some(ext => name.toLowerCase().endsWith(ext)));
  • 读取本地文件
    lib/character.js:119
    const parsed = JSON.parse(readFileSync(join(dir, file), 'utf8'));
  • 读取本地文件
    lib/knowledge.js:103
    const text = readFileSync(filePath, 'utf8');
  • 读取本地文件
    lib/knowledge.js:153
    if (!existsSync(dir))
  • 读取本地文件
    lib/knowledge.js:157
    files = readdirSync(dir).filter(name => name.toLowerCase().endsWith('.md')).sort();
  • 执行外部命令固定命令
    lib/launcher.js:104
    const output = execFileSync('netstat', ['-ano', '-p', 'tcp'], { encoding: 'utf8', windowsHide: true, timeout: 5000 });
  • 执行外部命令固定命令
    lib/launcher.js:112
    const output = execFileSync('lsof', ['-nP', `-iTCP:${String(port)}`, '-sTCP:LISTEN', '-t'], { encoding: 'utf8', timeout: 5000 });
  • 执行外部命令固定命令
    lib/launcher.js:149
    execFileSync('taskkill', ['/PID', String(pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true, timeout: 10_000 });
  • 读取本地文件
    lib/launcher.js:172
    for (const entry of readdirSync(root, { withFileTypes: true })) {
  • 读取本地文件
    lib/launcher.js:194
    if (existsSync(nodeExe) && existsSync(entry)) {
  • 读取本地文件
    lib/launcher.js:199
    if (existsSync(bat))
  • 读取本地文件
    lib/launcher.js:495
    const cwd = resolved.workDir !== '' && existsSync(resolved.workDir) ? resolved.workDir : undefined;
  • 执行外部命令固定命令
    lib/launcher.js:497
    const child = spawn('cmd.exe', buildConsoleStartArgs(exe, args), buildConsoleSpawnOptions(cwd));
  • 读取本地文件
    lib/launcher.js:517
    const cwd = resolved.workDir !== '' && existsSync(resolved.workDir) ? resolved.workDir : undefined;
  • 执行外部命令固定命令
    lib/launcher.js:526
    ? spawn('cmd.exe', ['/d', '/c', 'call', exe, ...args], options)
  • 执行外部命令来自配置/变量
    lib/launcher.js:527
    : spawn(exe, args, options);
  • 写入本地文件
    lib/launcher.js:555
    mkdirSync(dirname(logPath), { recursive: true });
  • 写入本地文件
    lib/launcher.js:562
    appendFileSync(logPath, chunk);
  • 读取本地文件
    lib/learner.js:114
    if (existsSync(this.pathOf(scope))) {
  • 读取本地文件
    lib/learner.js:115
    const raw = JSON.parse(readFileSync(this.pathOf(scope), 'utf8'));
  • 发起网络请求
    lib/imagegen.js:143
    const request = https.request(options, (response) => {
  • 发起网络请求
    lib/imagegen.js:307
    const response = await this.request(`${this.base()}/api/me?token=${encodeURIComponent(this.settings.token)}`);
  • 发起网络请求
    lib/imagegen.js:624
    const response = await this.request(url, signal === undefined ? undefined : { signal }, timeoutMs);
  • 发起网络请求
    lib/imagegen.js:665
    const response = await this.request(`${this.base()}/api/jobs`, {
  • 发起网络请求
    lib/imagegen.js:692
    const response = await this.request(url);
  • 发起网络请求
    lib/imagegen.js:760
    return fetch(url, { ...init, signal });
  • 执行外部命令来自配置/变量
    lib/rphub-browser.js:415
    this.edge = spawn(browserPath, args, { stdio: 'ignore', windowsHide: true });
  • 发起网络请求
    lib/rphub-browser.js:457
    const response = await fetch(`http://127.0.0.1:${String(this.settings.browserPort)}/json/list`, {
  • 写入本地文件
    lib/log.js:45
    mkdirSync(dirname(target), { recursive: true });
  • 写入本地文件
    lib/log.js:53
    appendFileSync(target, line, 'utf8');
  • 写入本地文件
    lib/memory.js:47
    mkdirSync(join(this.dir, 'groups'), { recursive: true });
  • 写入本地文件
    lib/memory.js:48
    mkdirSync(join(this.dir, 'private'), { recursive: true });
  • 写入本地文件
    lib/memory.js:72
    writeFileSync(join(this.dir, 'README.md'), lines.join('\n'), 'utf8');
  • 写入本地文件
    lib/memory.js:82
    mkdirSync(dirname(file), { recursive: true });
  • 读取本地文件
    lib/memory.js:84
    if (!existsSync(file)) {
  • 读取本地文件
    lib/memory.js:91
    if (!this.lastDay.has(file) && existsSync(file)) {
  • 读取本地文件
    lib/memory.js:92
    const existing = readFileSync(file, 'utf8');
  • 写入本地文件
    lib/memory.js:103
    appendFileSync(file, `${header}- **${clockKey(entry.time)} ${entry.speaker}**:${text}\n`, 'utf8');
  • 删除本地文件
    lib/memory.js:113
    rmSync(file, { force: true });
  • 读取本地文件
    lib/memory.js:123
    if (!existsSync(file))
  • 读取本地文件
    lib/memory.js:126
    return readFileSync(file, 'utf8')
  • 读取本地文件
    lib/memory.js:138
    if (!existsSync(folder))
  • 写入本地文件
    lib/notes.js:347
    mkdirSync(this.deps.dir, { recursive: true });
  • 写入本地文件
    lib/notes.js:348
    writeFileSync(`${this.deps.dir}/state.json`, JSON.stringify(this.state, null, 2), 'utf8');
  • 读取本地文件
    lib/stickers.js:64
    if (statSync(this.settings.dir).mtimeMs === this.lastScan)
  • 读取本地文件
    lib/stickers.js:76
    this.lastScan = statSync(this.settings.dir).mtimeMs;
  • 读取本地文件
    lib/stickers.js:81
    if (!this.settings.enabled || !existsSync(this.settings.dir))
  • 读取本地文件
    lib/stickers.js:84
    for (const entry of readdirSync(this.settings.dir)) {
  • 写入本地文件
    lib/stickers.js:125
    mkdirSync(this.settings.dir, { recursive: true });
  • 读取本地文件
    lib/stickers.js:128
    for (const entry of readdirSync(this.settings.dir)) {
  • 删除本地文件
    lib/stickers.js:130
    rmSync(join(this.settings.dir, entry), { force: true });
  • 写入本地文件
    lib/stickers.js:132
    writeFileSync(join(this.settings.dir, `${clean}${extension}`), data);
  • 读取本地文件
    lib/stickers.js:180
    const data = readFileSync(path);
  • 读取本地文件
    lib/summary.js:129
    if (existsSync(this.pathOf(scope))) {
  • 读取本地文件
    lib/summary.js:130
    const raw = JSON.parse(readFileSync(this.pathOf(scope), 'utf8'));
  • 发起网络请求
    lib/api-client.js:237
    const response = await fetch(`${config.baseUrl}/models`, { headers, signal: AbortSignal.timeout(20000) });
  • 发起网络请求
    lib/api-client.js:290
    response = await fetch(`${provider.baseUrl}/chat/completions`, {
  • 发起网络请求
    lib/api-client.js:358
    response = await fetch(`${provider.baseUrl}/messages`, {
  • 读取本地文件
    lib/store.js:31
    if (!existsSync(path))
  • 读取本地文件
    lib/store.js:34
    return JSON.parse(readFileSync(path, 'utf8'));
  • 写入本地文件
    lib/store.js:42
    mkdirSync(dirname(path), { recursive: true });
  • 写入本地文件
    lib/store.js:44
    writeFileSync(temp, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
  • 写入本地文件
    lib/store.js:45
    renameSync(temp, path);
  • 读取本地文件
    lib/bridge.js:103
    readdirSync(dir);
  • 写入本地文件
    lib/bridge.js:106
    mkdirSync(dir, { recursive: true });
  • 读取本地文件
    lib/bridge.js:109
    const files = readdirSync(dir)
  • 读取本地文件
    lib/bridge.js:116
    const signal = parseBridgeEvent(readFileSync(path, 'utf8'), now);
  • 删除本地文件
    lib/bridge.js:131
    rmSync(path, { force: true });
  • 执行外部命令固定命令
    lib/desktop.js:38
    const child = spawn('powershell.exe', ['-NoProfile', '-NonInteractive', '-EncodedCommand', encoded], {
  • 写入本地文件
    lib/desktop.js:190
    mkdirSync(target, { recursive: true });
  • 执行外部命令固定命令
    lib/desktop.js:191
    const child = spawn('explorer.exe', [target], { detached: true, stdio: 'ignore', windowsHide: false });
  • 写入本地文件
    lib/desktop.js:206
    mkdirSync(dir, { recursive: true });
  • 执行外部命令固定命令
    lib/desktop.js:285
    const child = spawn('rundll32.exe', ['url.dll,FileProtocolHandler', url], { detached: true, stdio: 'ignore' });
  • 执行外部命令固定命令
    lib/desktop.js:332
    const child = spawn('rundll32.exe', ['user32.dll,LockWorkStation'], { detached: true, stdio: 'ignore' });
  • 执行外部命令固定命令
    lib/desktop.js:347
    const child = spawn('rundll32.exe', ['powrprof.dll,SetSuspendState 0,1,0'], { detached: true, stdio: 'ignore' });
  • 读取本地文件
    lib/tarot.js:226
    return existsSync(path) ? path : undefined;
  • 读取本地文件
    lib/native-voice.js:30
    if (!existsSync(indexFile))
  • 读取本地文件
    lib/native-voice.js:33
    const parsed = JSON.parse(readFileSync(indexFile, 'utf8'));
  • 读取本地文件
    lib/native-voice.js:39
    if (!existsSync(path))
  • 写入本地文件
    lib/recall.js:73
    mkdirSync(this.root, { recursive: true });
  • 写入本地文件
    lib/recall.js:140
    mkdirSync(dir, { recursive: true });
  • 读取本地文件
    lib/recall.js:146
    if (existsSync(file)) {
  • 读取本地文件
    lib/recall.js:148
    const parsed = JSON.parse(readFileSync(file, 'utf8'));
  • 写入本地文件
    lib/recall.js:158
    writeFileSync(tmp, JSON.stringify({ block, lines: merged }), 'utf8');
  • 写入本地文件
    lib/recall.js:159
    renameSync(tmp, file);
  • 读取本地文件
    lib/recall.js:170
    if (!existsSync(dir))
  • 读取本地文件
    lib/recall.js:173
    for (const name of readdirSync(dir)) {
  • 读取本地文件
    lib/recall.js:178
    const parsed = JSON.parse(readFileSync(join(dir, name), 'utf8'));
  • 写入本地文件
    lib/recall.js:206
    writeFileSync(tmp, JSON.stringify(plain), 'utf8');
  • 写入本地文件
    lib/recall.js:207
    renameSync(tmp, join(dir, '_index.json'));
  • 读取本地文件
    lib/recall.js:219
    if (existsSync(file)) {
  • 删除本地文件
    lib/recall.js:325
    rmSync(join(dir, name), { force: true });
  • 读取本地文件
    lib/voice.js:225
    if (configured !== undefined && configured !== '' && existsSync(configured))
  • 读取本地文件
    lib/voice.js:228
    if (existsSync(candidate))
  • 读取本地文件
    lib/voice.js:249
    if (!existsSync(calm))
  • 读取本地文件
    lib/voice.js:254
    if (files[style] !== undefined && existsSync(styled))
  • 读取本地文件
    lib/voice.js:264
    if (existsSync(calmAux))
  • 发起网络请求
    lib/voice.js:303
    const response = await fetch(endpoint, {
  • 读取本地文件
    lib/voice.js:394
    const hasRuntime = runtimeDir !== undefined && existsSync(join(runtimeDir, 'gpt-sovits', 'api_v2.py'));
  • 发起网络请求
    lib/voice.js:424
    await fetch(endpoint, {
  • 执行外部命令来自配置/变量
    lib/voice.js:459
    const child = spawn(hostPath, ['--voice-host', '--parent', String(process.pid), '--language', language], {
  • 执行外部命令来自配置/变量
    lib/voice.js:488
    const child = spawn(python, ['api_v2.py', '-a', '127.0.0.1', '-p', String(port), '-c', config], {
  • 发起网络请求
    lib/weather.js:64
    const response = await fetch(url);
  • 请求外部主机ipwho.is
    lib/weather.js:98
    const response = await fetch('https://ipwho.is/?fields=success,message,city,region,country,latitude,longitude');
  • 发起网络请求
    lib/weather.js:98
    const response = await fetch('https://ipwho.is/?fields=success,message,city,region,country,latitude,longitude');
  • 写入本地文件
    lib/wife.js:73
    mkdirSync(deps.dir, { recursive: true });
  • 读取本地文件
    lib/people.js:179
    if (existsSync(peopleFile)) {
  • 读取本地文件
    lib/people.js:181
    const raw = JSON.parse(readFileSync(peopleFile, 'utf8'));
  • 读取本地文件
    lib/people.js:194
    if (existsSync(eventsFile)) {
  • 读取本地文件
    lib/people.js:196
    const raw = JSON.parse(readFileSync(eventsFile, 'utf8'));
  • 读取本地文件
    lib/people.js:215
    if (!existsSync(file))
  • 读取本地文件
    lib/people.js:218
    const raw = JSON.parse(readFileSync(file, 'utf8'));
  • 写入本地文件
    lib/people.js:274
    mkdirSync(dirname(this.file(name)), { recursive: true });
  • 写入本地文件
    lib/people.js:276
    writeFileSync(temp, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
  • 写入本地文件
    lib/people.js:277
    renameSync(temp, this.file(name));
  • 读取本地文件
    lib/reminders.js:244
    if (!existsSync(this.file))
  • 读取本地文件
    lib/reminders.js:247
    const raw = JSON.parse(readFileSync(this.file, 'utf8'));
  • 写入本地文件
    lib/reminders.js:262
    mkdirSync(dirname(this.file), { recursive: true });
  • 写入本地文件
    lib/reminders.js:264
    writeFileSync(this.file, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
  • 发起网络请求
    lib/vision.js:54
    const response = await fetch(url, { signal: AbortSignal.timeout(15000) });
  • 读取本地文件
    lib/outbound.js:181
    const wav = readFileSync(entry.path);
  • 删除本地文件
    lib/outbound.js:408
    rmSync(String(file.data.file).slice('file://'.length), { force: true });
  • 写入本地文件
    lib/outbound.js:435
    writeFileSync(path, image.buffer);
  • 发起网络请求
    lib/bili.js:119
    const response = await fetch(url, { headers, signal: controller.signal });
  • 发起网络请求
    lib/safe-fetch.js:232
    const request = mod.request({
  • 发起网络请求
    lib/web-search.js:94
    const response = await fetch(custom.baseUrl.trim(), {
  • 发起网络请求
    lib/web-search.js:157
    const response = await fetch(url, {
  • 发起网络请求
    rphub/qq-bridge.js:190
    let job = await fetch(`${baseUrl}/api/jobs`, {
  • 发起网络请求
    rphub/qq-bridge.js:204
    job = await fetch(`${baseUrl}/api/jobs/${encodeURIComponent(job.id)}?token=${encodeURIComponent(params.token)}`)
扫描于 2026/11/2(1 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库
涉及外部主机:ipwho.is

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

依赖的 DSH / Cordis 模块
@deepseek-ai/cordis@deepseek-ai/dsh-llm@deepseek-ai/dsh-settings@deepseek-ai/schemastery
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

功能亮点

  • AI以群友身份入群
  • 按群切换人设

README

由 DeepSeek 最新模型翻译生成
dsh-plugin-qq-groupmate 仅自用:个人自用项目,按需更新,不承诺兼容性与技术支持。感谢ds老师。 让 DeepSeek Harness 里的 AI 以普通群友的身份住进 QQ 群。 通过 OneBot 11 接协议端,用角色卡定义人设,每个群可以独立切换角色与模型;所有配置都在 dsh 设置页的插件卡片里改,改完即时生效,不用重启。 功能 - OneBot 11 接入:正向 / 反向 WebSocket 都支持,NapCat、SnowLuma、LLOneBot、Lagrange、go-cqhttp 等协议端均可 - 角色卡:人设、性格、说话风格、示例对话;兼容 SillyTavern V2 卡片,也支持一份 JSON 里的原生格式 - 按群独立配置:每个群单独指定角色、模型、触发规则、上下文长度,和 dsh 主会话互不干扰 - 触发控制:被 @ / 关键词前缀 / 概率接话,带冷却;私聊也响应 - 上下文与记忆:多轮上下文 + 长期记忆压缩,群里的印象能沉淀下来 - 命令系统:.help、角色切换、印象管理、提醒、贴纸、天气、塔罗等,命令前缀可配 - 可选生图:模型输出 [生图:标签] 就画一张发进群(需要自备 Nai2API 兼容服务,见下文)
展开完整文档(README 剩余 1 节)
dsh-plugin-qq-groupmate

仅自用:个人自用项目,按需更新,不承诺兼容性与技术支持。感谢ds老师。

让 DeepSeek Harness 里的 AI 以普通群友的身份住进 QQ 群。

通过 OneBot 11 接协议端,用角色卡定义人设,每个群可以独立切换角色与模型;所有配置都在 dsh 设置页的插件卡片里改,改完即时生效,不用重启。

功能

- OneBot 11 接入:正向 / 反向 WebSocket 都支持,NapCat、SnowLuma、LLOneBot、Lagrange、go-cqhttp 等协议端均可
- 角色卡:人设、性格、说话风格、示例对话;兼容 SillyTavern V2 卡片,也支持一份 JSON 里的原生格式
- 按群独立配置:每个群单独指定角色、模型、触发规则、上下文长度,和 dsh 主会话互不干扰
- 触发控制:被 @ / 关键词前缀 / 概率接话,带冷却;私聊也响应
- 上下文与记忆:多轮上下文 + 长期记忆压缩,群里的印象能沉淀下来
- 命令系统:.help、角色切换、印象管理、提醒、贴纸、天气、塔罗等,命令前缀可配
- 可选生图:模型输出 [生图:标签] 就画一张发进群(需要自备 Nai2API 兼容服务,见下文)
- 语音回复(可选):本地 GPT-SoVITS 合成语音发成 QQ 语音,中文 / 日文两套音色各自独立,情绪自动切参考音
- 本机可靠路由:问时间 / 日期 / 星期直接在本机回答,不花模型额度
- 设置页可视化:全部配置都在设置页里改,带开关、下拉、说明文案

安装

前置:dsh 已装好,且 PATH 上有 pnpm(dsh plugin 是一层 pnpm 转发器)。

profile 名按自己用的填:web / desktop / headless
dsh plugin --profile web add github:lilith1257/dsh-plugin-qq-groupmate

装完这个包装声明了 dsh.bundle,dsh 会把它自动并进 profile 的组合层,不需要手改 cordis.patch.yml。重启 dsh 生效。

更新 / 卸载:

dsh plugin --profile web update dsh-plugin-qq-groupmate
dsh plugin --profile web remove dsh-plugin-qq-groupmate

接协议端

默认是反向 WS:插件在本机 127.0.0.1:3001 监听,协议端连进来。

以 NapCat / SnowLuma 为例,在协议端的网络配置里加一个反向 WebSocket:

ws://127.0.0.1:3001

如果要走正向 WS(插件主动连协议端),在设置页把 mode 改成 forward,并填协议端的 WS 地址。

两边都能配 accessToken,填一致即可;留空表示不校验。

配置

设置页 →「插件」→ QQ 群友。几个常改的:

| 配置 | 说明 |
| mode / listenPort / wsUrl | 接入方式与端口 |
| defaultCharacter | 默认角色卡的 id |
| provider / model | 群聊用哪个模型;也可以给单个群单独指定 |
| trigger | 什么时候接话:被 @、前缀、随机概率、冷却、是否响应私聊 |
| context | 上下文轮数与字符上限 |
| groups | 按群覆盖角色与模型 |
| dataDir / charactersDir | 数据目录与角色卡目录 |

角色卡

角色卡是 JSON,放在 /characters/ 下(默认 ~/.dsh/qq-groupmate/characters/),文件名去掉扩展名就是角色 id。

仓库自带一张示例卡 characters/default.json(角色名「小可」)。刚装完数据目录还是空的,插件会先用随包那张兜底;往数据目录里放了自己的卡之后,就只读数据目录。

支持的字段:

{
"name": "小可",
"aliases": ["小可"],
"persona": "你是谁、怎么说话", "personality": "性格", "background": "背景与偏好", "speakingStyle": "说话风格", "scenario": "所处场景", "greeting": "第一句问候",
"exampleDialogues": ["群友: 在吗\n小可: 在的"],
"extraPrompt": "额外规则", "provider": "可选,覆盖模型服务商", "model": "可选,覆盖模型" }

SillyTavern V2 卡片({"spec":"chara_card_v2","data":{...}})也能直接丢进去。

生图(可选)

生图默认关。要用的话需要自己准备一个 Nai2API 兼容的出图服务,然后在设置页里填:

- 服务地址(imageGen.baseUrl):你的出图服务地址,例如 https://your-host
- 密钥(imageGen.token):该服务的 token

另有一条「网页引擎」路线:用无头浏览器驱动一个线上生图页面,让它自己拼参数出图。要启用得在配置里加上 imageGen.pageUrl(生图页面地址)——留空就等于关闭网页引擎,只走直连接口。

语音(可选)

回复可以顺带发一条 QQ 语音:本地 GPT-SoVITS(api_v2 格式接口)合成,合成失败只影响语音,文字照常发。

- 中文音色:voice.endpoint(默认 http://127.0.0.1:9880/tts),参考音放 voice.refsDir(默认 /voice/refs)
- 日文音色:voice.japaneseEndpoint(默认 http://127.0.0.1:9881/tts),参考音放 voice.japaneseRefsDir
- 语言选择:voice.language = auto(默认,回复里出现假名就走日文音色)/ zh / ja
- 参考音目录留空会自动探测:语音运行时隔壁的 voice(与 voice/jp),也就是 …/data/LilithTextInjector/voice 这种部署结构,装好运行时就能直接用
- 参考音文件名:calm-reference.wav、excited-reference.wav、wronged-reference.wav、sleepy-reference.wav(日文另加 calm-aux-reference.wav)
- 自动拉起:voice.autoStart 打开且端点是本机回环时,插件会按语言各自拉起一次本地服务(先试随包宿主,宿主起不来就改用运行时的 venv 直接跑 api_v2.py,配置按语言选 zh / ja 的 cuda / cpu 配置)
- 情绪 → 语气:按回复内容自动挑参考音(晚安 / 困 → 困倦,被逗 / 开心 → 开心,难过 / 被凶 → 委屈,其余 → 平静)

缓存命中(为什么请求要「前缀稳定」)

上游(DeepSeek 官方、中转站)按请求前缀做上下文缓存:前缀里改一个字,它后面的全部内容都要重新计费。群聊一轮动辄几万 token 的历史,前缀一抖就是整段 miss——后台看到的缓存命中率会趋近于零。

所以提示词被拆成两半:

- 系统提示只放「这个角色 + 这个群不变的东西」(人设、群聊规则、表情包与画图说明、工具箱说明)——同一角色同一群里逐轮逐字节一致,永远排在请求最前面;
- 每轮都会变的运行上下文(长期记忆、知识检索、群友档案、称呼指引、天气、成员列表)单独成块,插在消息序列末尾、当前消息之前。

历史裁剪同样是块状的(一次至少丢 16 条),不是每来一条就滑一格:滑动窗口会让前缀每轮都漂移。

记忆本身也必须有上限:长期记忆按金字塔合并(每 mergeCount 条并成一条,最高层封顶 8 条、每条目标 1200 字,越久远越浓缩),进提示词的部分再受条数与字数预算约束(先放最高层的浓缩记忆,剩下的预算才给最近的细节)——只取最近的会把老记忆的骨架丢掉。

打开调试日志时,每轮会打一条 缓存前缀 system=xxxxxxxx(1250 字)消息=… 条 上下文块=… 字 首条=…:同一角色同一群里 system 指纹应当每轮完全相同,变了就说明有动态内容被塞回了系统提示;「上下文块」的字数应当稳定在几千到一万多,写成几十万就说明某类累积数据又失控了。

数据目录

默认 $DSH_HOME/qq-groupmate(DSH_HOME 没设就是 ~/.dsh)。里面有:

- characters/ 角色卡
- state.json 群配置与运行状态
- qq-groupmate.log 运行日志
- tarot/cards/ 塔罗卡面(没有就用随包那份)
- 记忆、印象、贴纸、语音等子目录

目录结构

lib/ host 半(运行时 JS,Node 直接跑) client.js 浏览器半(设置页卡片) rphub/qq-bridge.js 网页引擎注入用的桥接脚本 characters/ 随包示例角色卡 assets/tarot-cards/ 塔罗卡面 knowledge/ 可选的群内答疑知识库(默认关) cordis.patch.yml 组合包补丁(默认配置)

这个仓库发布的是运行时产物,lib/*.js 就是实际跑的代码,不是压缩过的 bundle,可以直接读、直接改。

许可

MIT。随包的 knowledge/rp-hub-faq.md 是 Roleplay Hub(RP-Hub)1.9.3 的衍生整理,按 CC BY-NC 4.0 授权,详见该文件头部。
预览分享卡片长什么样 ↓lilith1257/dsh-plugin-qq-groupmate
分享 · DeepSeek Harness Hub
lilith1257/dsh-plugin-qq-groupmate

让 AI 以群友身份住进 QQ 群聊,按群切换人设

这是你的仓库?认领条目

回到顶部