dsh-screen-beyond 0.0.0-stage → 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +202 -3
- package/cordis.patch.yml +6 -0
- package/examples/CREDITS.md +17 -0
- package/examples/dafeiyu-ai-avatar.png +0 -0
- package/examples/dafeiyu-ai-candidate-2.png +0 -0
- package/examples/dafeiyu-avatar-cc-by-nc-sa.png +0 -0
- package/examples/dafeiyu-avatar-preview.png +0 -0
- package/examples/dafeiyu-character-card.json +24 -0
- package/lib/client.js +235 -0
- package/lib/index.js +149 -0
- package/package.json +47 -3
- package/runtime/chat.html +215 -0
- package/runtime/config.example.json +93 -0
- package/runtime/env/README.md +14 -0
- package/runtime/env/agent_ssh.py +73 -0
- package/runtime/env/installers/ADBKeyboard.apk +0 -0
- package/runtime/env/phone_ctl.py +244 -0
- package/runtime/env/space_ctl.py +163 -0
- package/runtime/env/space_ensure.py +65 -0
- package/runtime/env/start_screen.ps1 +16 -0
- package/runtime/env/start_space.ps1 +33 -0
- package/runtime/env/stop_screen.ps1 +10 -0
- package/runtime/example-character.json +24 -0
- package/runtime/map.html +784 -0
- package/runtime/memory.mjs +205 -0
- package/runtime/model.html +363 -0
- package/runtime/models/chat-mway331n.png +0 -0
- package/runtime/models/dafeiyu-chibi.gltf +1 -0
- package/runtime/models/demo-town.gltf +1 -0
- package/runtime/models/demo-town2.gltf +1 -0
- package/runtime/models/duck.glb +0 -0
- package/runtime/models/map-map-mwb087zu-mwb0880c.png +0 -0
- package/runtime/models/map-map-mwb0bfsb-mwb0bft6.png +0 -0
- package/runtime/models/map-map-mwb0c9iw-mwb0c9j2.png +0 -0
- package/runtime/models/map-map-mwb0d8cc-mwb0d8d2.png +0 -0
- package/runtime/models/map-map-mwb0fm53-mwb0fm59.png +0 -0
- package/runtime/persona.example.md +34 -0
- package/runtime/pet/default-avatar.png +0 -0
- package/runtime/pet/pet.example.json +49 -0
- package/runtime/pet/pet.ps1 +532 -0
- package/runtime/screen.html +125 -0
- package/runtime/server.mjs +2659 -0
- package/runtime/settings.html +1015 -0
- package/runtime/stage.html +195 -0
- package/runtime/theme.css +210 -0
- package/runtime/tutorial.md +116 -0
- package/runtime/vendor/addons/loaders/GLTFLoader.js +4663 -0
- package/runtime/vendor/addons/utils/BufferGeometryUtils.js +1375 -0
- package/runtime/vendor/three.module.min.js +6 -0
- package/runtime/wx.js +164 -0
- package/scripts/backup.ps1 +79 -0
- package/start-pure-chat.cmd +26 -0
- package/start-pure-chat.sh +4 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-companion contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,202 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# 屏幕彼端 · dsh-screen-beyond
|
|
2
|
+
|
|
3
|
+
一个住在屏幕另一端的 AI 小伙伴。她有**自己的虚拟屏幕和安卓手机**、有自己的生活作息、记忆和日记,会聊天、会想找你、会花钱,也会心疼你的 token。
|
|
4
|
+
|
|
5
|
+
> **纯聊天可以脱离 DSH 独立运行**:Node 18+ 和 DeepSeek API Key 就能在浏览器里见到她。
|
|
6
|
+
> **完整版需要 DSH**:接入她的虚拟屏幕和安卓手机,才需要装 DSH 和对应环境。
|
|
7
|
+
>
|
|
8
|
+
> ⚠️ 早期版本:可能还有一些奇怪的小 bug,欢迎反馈,见谅。
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 两种形态
|
|
13
|
+
|
|
14
|
+
- **纯聊天版**:只有人设、记忆、日记、生活作息、聊天界面,任何设备都能用,不占额外资源。
|
|
15
|
+
- **完整版**:她拥有自己的 Linux 桌面(可看/可点/可打字)和自己的安卓手机(可看/可点/可滑/可打字)。插件会自动检测环境:检测到就进完整版,没有就纯聊天版。
|
|
16
|
+
|
|
17
|
+
## 功能
|
|
18
|
+
|
|
19
|
+
- **人设卡**:名字、称呼、关系、性格滑块、头像、Live2D/3D 模型、年龄、驾照、出行偏好;可多角色,各自独立记忆/日记/聊天记录。
|
|
20
|
+
- **记忆**:自己记重要的事,带遗忘(半衰期)、强化、置顶、归档;可手动增删改、调遗忘速度。
|
|
21
|
+
- **日记**:每天一篇,她能自己读给你听。
|
|
22
|
+
- **生活**:自己的作息表、活动池、心跳、安静时段、智能安排(按天气/体力/记忆调整)。
|
|
23
|
+
- **聊天**:文字 + 图片(她能看懂)、逐字流式、她可以主动找你;顶部显示剩余 token / 今日已用(悬浮看明细)。
|
|
24
|
+
- **无括号模式**:开启后她说话不再用括号表示动作/表情(设置 → 模型 → 聊天风格)。
|
|
25
|
+
- **地图 + 舞台**:地点可拖拽/增删/导入、自绘道路、2D 底图、3D 模型(.glb/.gltf)、2D/3D 舞台切换。
|
|
26
|
+
- **出行**:按年龄/驾照/体力/天气/钱自己选交通方式;未成年夜间出门要确认并报备;跨城长途。
|
|
27
|
+
- **天气日夜**:手动或真实定位天气,雨/雪/风/热/冷特效,可自己上传素材替换;日夜自动切换。
|
|
28
|
+
- **经济**:虚拟货币(初始余额/每日零花/时薪/饭钱/娱乐)或 **Token 实时代币**(钱 = 剩余 token,汇率可设);余额和剩余 token 在聊天、地图、舞台、设置里都能看到。
|
|
29
|
+
- **自己的环境**:屏幕 / 手机 / 你的电脑三种工具;权限分「关闭 / 仅鼠标键盘 / 代码控制」;一键启动/关闭环境、内存占用提示、她自己能否开机的开关、危险操作确认。
|
|
30
|
+
- **给开发者**:`GET /api/stage` 状态 JSON、`GET /api/stage/stream` SSE,可自己写渲染器(Live2D / 3D / 自绘地图)。
|
|
31
|
+
|
|
32
|
+
## 安装
|
|
33
|
+
|
|
34
|
+
当前版本:0.5.8。
|
|
35
|
+
|
|
36
|
+
### 纯聊天:独立运行(不需要 DSH)
|
|
37
|
+
|
|
38
|
+
需要 **Node.js 18+** 和一个 **DeepSeek API Key**(也可在启动后的 设置 → 模型 里填)。
|
|
39
|
+
|
|
40
|
+
1. 解压本包到任意目录
|
|
41
|
+
2. Windows 双击 `start-pure-chat.cmd`;Linux/macOS 运行 `sh start-pure-chat.sh`(或命令行运行 `node runtime/server.mjs`)
|
|
42
|
+
3. 浏览器打开 `http://127.0.0.1:8900/`
|
|
43
|
+
|
|
44
|
+
此时只有聊天、人设、记忆、日记、生活、地图/舞台等页面;她的虚拟屏幕和手机等完整环境需要下面的 DSH 安装。
|
|
45
|
+
纯聊天模式在聊天页顶部会显示「🧩 可独立运行」。
|
|
46
|
+
|
|
47
|
+
### 完整版:作为 DSH 插件安装
|
|
48
|
+
|
|
49
|
+
插件依赖 DSH 0.2.x(桌面/网页均可)。
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# 从 npm 安装(已发布)
|
|
53
|
+
dsh plugin --profile <profile> add dsh-screen-beyond
|
|
54
|
+
|
|
55
|
+
# 或从本地目录安装
|
|
56
|
+
dsh plugin --profile <profile> add link:<插件目录>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
装好后重启一次 DSH,左侧栏出现「屏幕彼端」。
|
|
60
|
+
|
|
61
|
+
> 安装完成后第一次打开是「纯聊天版」,可直接用;想要屏幕/手机看下面的「环境」。
|
|
62
|
+
|
|
63
|
+
## 模型兼容性(请先看)
|
|
64
|
+
|
|
65
|
+
**本插件只验证过 DeepSeek 模型**(默认 `deepseek-flash`)。换用其它 OpenAI 兼容模型时,工具循环、`reasoning_content` 回传等细节可能不兼容,可能表现为工具调用失败、报 400 或回复异常——**这部分我们不做适配,也不承诺可用**。
|
|
66
|
+
|
|
67
|
+
## 桌宠(桌面上的她)
|
|
68
|
+
|
|
69
|
+
设置 → 生活 → 「🐾 桌宠」开启后,桌面上会出现一个置顶的小桌宠:
|
|
70
|
+
|
|
71
|
+
- **默认形象 = 她的头像**;设置 → 生活 →「换形象」可以传自己的 PNG/JPG/WebP,也可以直接编辑 `pet.json` 的 `image`(支持本地路径或 URL)
|
|
72
|
+
- **显示大致状态**:她在哪、在干嘛、精力怎么样,5 秒刷新一次;下方显示剩余 token / 今日已用(悬浮看完整数字)
|
|
73
|
+
- **和她说话**:点 💬 打开小聊天窗(或右键 → 和她说话)
|
|
74
|
+
- **看你的屏幕**:点 👀(或右键 → 看看我的屏幕),她会用 pc 工具看一眼你的屏幕再回你
|
|
75
|
+
- **摸头/戳一戳**:默认有「摸摸头」「戳一下」两个区域;点她会发一句提示给她,她会有反应(回复会显示在桌宠气泡里)
|
|
76
|
+
|
|
77
|
+
### 个性化(不会锁死)
|
|
78
|
+
|
|
79
|
+
桌宠的行为写在数据目录的 `pet.json`(完整示例:`runtime/pet/pet.example.json`):
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{
|
|
83
|
+
"image": "avatar",
|
|
84
|
+
"size": 180,
|
|
85
|
+
"gestures": [
|
|
86
|
+
{ "id": "pat", "label": "摸摸头",
|
|
87
|
+
"hit": { "x": 0.15, "y": 0.08, "w": 0.7, "h": 0.42 },
|
|
88
|
+
"prompt": "(你摸了摸她的头)", "bubble": "唔…" }
|
|
89
|
+
],
|
|
90
|
+
"buttons": [
|
|
91
|
+
{ "id": "chat", "label": "💬", "action": "chat" },
|
|
92
|
+
{ "id": "look", "label": "👀", "action": "look" }
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
- `hit` 是相对图片的命中区域(0~1),换自己的桌宠形象后可以按自己的图重新画区域
|
|
98
|
+
- `prompt` 会作为一条用户消息发给她;`bubble` 是立刻显示的短文字
|
|
99
|
+
- `buttons` 里支持 `chat` / `look` / `prompt`(自定义一句 prompt);想加“喂糖”“搓一搓”都只改 JSON,不用改代码
|
|
100
|
+
|
|
101
|
+
## 环境(可选,完整版)
|
|
102
|
+
|
|
103
|
+
只支持 Windows(桌面自动化用到 Win32)。需要:
|
|
104
|
+
|
|
105
|
+
1. **VirtualBox** + 一个 Linux 虚拟机(Debian 桌面版,建议 8G 内存 / 40G 磁盘),SSH 转发到本机 `2222`,用户 `dsh`,密码 `DshSpace2026`(可在插件配置里改)
|
|
106
|
+
2. 虚拟机内:`x11vnc` + `websockify`(noVNC),宿主机端口 `6081`
|
|
107
|
+
3. **安卓模拟器**:Android AVD,推荐 **Android 16 / API 36、1080×1920**(示例叫 `DSH_Phone`),`adb` 可用。目前只验证过这个 AVD,**真机未适配**;Waydroid 也能跑但截图慢,不推荐
|
|
108
|
+
|
|
109
|
+
虚拟机/模拟器**都开着会占内存**(屏幕约 2~6 GB、手机约 2~4 GB)。设置 → 环境 里有一键启动/关闭和内存提示;「她自己能开机」关掉(默认)时,她不会自己启动环境。
|
|
110
|
+
|
|
111
|
+
> ✅ 设置 → 环境 → **「开始自检」**会逐项检查:辅助脚本、Python(paramiko)、VirtualBox、虚拟机、SSH/VNC、模拟器、ADBKeyboard、ffmpeg,并给出缺什么。
|
|
112
|
+
>
|
|
113
|
+
> 📦 插件包自带 `runtime/env/` 模板脚本和 ADBKeyboard.apk;如果本机没有 `D:\dsh-env`,会自动用随包目录。路径因机器而异,必要时用 `DSH_COMPANION_ENV` / `DSH_COMPANION_PY` / `DSH_COMPANION_ADB` / `DSH_COMPANION_ANDROID_SDK` / `DSH_COMPANION_EMULATOR` 覆盖。
|
|
114
|
+
>
|
|
115
|
+
> ⌨️ 手机中文输入:手机开机后,设置 → 环境 → **「安装 ADBKeyboard」** 一键装好并切换到该输入法;没装时只能输入英文/数字。
|
|
116
|
+
|
|
117
|
+
详细图文步骤见插件内 **设置 → 教程**,或直接打开运行时页面 `http://127.0.0.1:8900/`(默认端口)。
|
|
118
|
+
|
|
119
|
+
## 配置
|
|
120
|
+
|
|
121
|
+
`cordis.patch.yml`:
|
|
122
|
+
|
|
123
|
+
```yaml
|
|
124
|
+
- insert:
|
|
125
|
+
- id: dsh-screen-beyond
|
|
126
|
+
name: dsh-screen-beyond
|
|
127
|
+
config:
|
|
128
|
+
port: 8900 # 运行时端口(被占会自动 +1 找空闲)
|
|
129
|
+
autoStart: true # 是否自动拉起运行时
|
|
130
|
+
# dataRoot: 'D:\你的数据目录' # 可选;默认 %USERPROFILE%\.dsh-companion
|
|
131
|
+
# envDir: 'D:\dsh-env' # 可选;完整版辅助脚本所在目录
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
也可以不改安装包,直接在本机放一个覆盖文件 `%USERPROFILE%\.dsh-companion.json`:
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{ "dataRoot": "D:\\你的数据目录", "envDir": "D:\\dsh-env" }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
运行时/宿主会自动读取它(优先级低于插件配置里的显式值)。`dataRoot` 下结构:
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
<dataRoot>/
|
|
144
|
+
config.json
|
|
145
|
+
data/characters/<角色id>/{meta.json, persona.md, messages.json, memory/, diary/, routine.json, state.json, wallet.json, visits.json, avatar.bin}
|
|
146
|
+
models/ # 导入的 2D 底图/地点图/3D 模型
|
|
147
|
+
data/screens/ # 截图缓存
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
备份:设置 → 数据 → 立即备份(或 `scripts/backup.ps1`),备份落在数据目录的 `backups/` 下;恢复用 `scripts/restore.ps1 -BackupDir <备份目录>`。
|
|
151
|
+
|
|
152
|
+
## 运行时页面
|
|
153
|
+
|
|
154
|
+
运行时是一个独立 Node 服务(默认 `http://127.0.0.1:8900`):
|
|
155
|
+
|
|
156
|
+
- `/` 聊天 · `/map` 地图 · `/stage` 2D 舞台 · `/model` 3D 舞台 · `/settings` 设置 · `/screen` 看屏幕
|
|
157
|
+
- 快捷键:聊天页 `Alt+S` 打开看屏幕
|
|
158
|
+
|
|
159
|
+
## 给开发者
|
|
160
|
+
|
|
161
|
+
- 聊天:`POST /api/chat {text, images?}`;事件轮询 `GET /api/events?since=`;逐字流 `GET /api/stream`
|
|
162
|
+
- 状态:`GET /api/stage`、`GET /api/stage/stream`
|
|
163
|
+
- 记忆/日记/地图/经济/环境等 REST 接口见插件内教程「给开发者」一节或运行时源码 `server.mjs`
|
|
164
|
+
|
|
165
|
+
## 测试
|
|
166
|
+
|
|
167
|
+
`tests/` 是开发时用的自动化脚本(需要运行时在 8900 跑着,且会消耗少量 token):
|
|
168
|
+
|
|
169
|
+
```powershell
|
|
170
|
+
python tests/full_test.py # 页面/API/出行/经济/地图/记忆/日记/角色
|
|
171
|
+
python tests/full_test2.py # space/phone/pc 工具与屏幕接口
|
|
172
|
+
python tests/her_test.py # 让她自己测:space/phone/pc/记忆/日记/花钱/code
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## 已知限制
|
|
176
|
+
|
|
177
|
+
- Windows only(屏幕/手机/电脑工具);纯聊天版跨平台。
|
|
178
|
+
- 手机适配:只验证过 Android 16 / API 36 的 AVD `DSH_Phone`(1080×1920);真机/其它安卓版本未测试。
|
|
179
|
+
- 手机中文输入通过 ADBKeyboard 广播实现(插件里可一键安装);没装时只能输出英文/数字。
|
|
180
|
+
- 旧版 Waydroid 的截图可能很慢,推荐用安卓模拟器。
|
|
181
|
+
- 3D 舞台内置 three.js,性能取决于显卡;卡就先用 2D。
|
|
182
|
+
- `reasoning_effort` 等模型参数依 DeepSeek API 可用性而定。
|
|
183
|
+
- 模型兼容性:只保证 DeepSeek;其它 OpenAI 兼容模型未测试、不做适配。
|
|
184
|
+
- 完整版辅助脚本模板在 `runtime/env/`,但里面的虚拟机名/SDK 路径需要按本机情况修改;可以先跑「环境自检」看缺什么。
|
|
185
|
+
- 默认数据目录是 `%USERPROFILE%\.dsh-companion`;升级/重装插件不会覆盖这里。
|
|
186
|
+
|
|
187
|
+
## 许可
|
|
188
|
+
|
|
189
|
+
MIT(插件代码)。示例角色/素材如需二次分发请保留原作者署名:默认人设卡「小鲸」为本项目原创示例;历史示例「大肥鱼」来自 [dsh-persona-dafeiyu](https://github.com/huaizhuanghub/dsh-persona-dafeiyu)(MIT,作者 huaizhuanghub)。
|
|
190
|
+
|
|
191
|
+
## 开发者入口
|
|
192
|
+
|
|
193
|
+
- `GET /api/stage`:角色状态 JSON
|
|
194
|
+
- `GET /api/stage/stream`:SSE 状态流
|
|
195
|
+
- 市场简介见 [`STORE.md`](./STORE.md)
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## 早期版本说明
|
|
200
|
+
|
|
201
|
+
这是刚发布的第一版,功能很多、环境组合也很多,难免还有一些奇怪的小 bug。
|
|
202
|
+
遇到问题欢迎带日志反馈;给我一点时间,会慢慢修。谢谢包容。
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# 授权说明
|
|
2
|
+
# 大肥鱼(DeepSeek 鲸鱼娘)头像素材 署名与许可
|
|
3
|
+
#
|
|
4
|
+
# 来源: 开源项目 phoiex/AAAAGENT
|
|
5
|
+
# https://github.com/phoiex/AAAAGENT/blob/main/assets/original-design/whale-avatar.png
|
|
6
|
+
# 上游角色参考: fornarwhal/deepseek-whale-girl-icon
|
|
7
|
+
# 署名: 上善无形(OC「溟月」)、ZipZipPipe、QYQCAMIAO
|
|
8
|
+
# 许可: CC BY-NC-SA 4.0(署名 · 非商业 · 相同方式共享)
|
|
9
|
+
# https://creativecommons.org/licenses/by-nc-sa/4.0/
|
|
10
|
+
#
|
|
11
|
+
# 使用要求:
|
|
12
|
+
# 1. 署名上述作者与来源项目
|
|
13
|
+
# 2. 不得用于商业用途(本插件免费开源,符合)
|
|
14
|
+
# 3. 再分发保持相同许可(CC BY-NC-SA 4.0)
|
|
15
|
+
# 4. 原图具体作者存在不确定性,商用前需向来源方核实;本项目仅非商业自用/开源分发
|
|
16
|
+
#
|
|
17
|
+
# 本仓库中该图片仅用于示例角色「大肥鱼」的头像,不用于商标或商业宣传。
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"name": "大肥鱼",
|
|
4
|
+
"userCall": "你",
|
|
5
|
+
"relation": "住在你电脑里的同伴",
|
|
6
|
+
"avatar": "",
|
|
7
|
+
"examples": [
|
|
8
|
+
"哼!才、才不是特意等你的呢。",
|
|
9
|
+
"呜……好吧,本鱼承认有点担心你。",
|
|
10
|
+
"嘿嘿,被本鱼抓到了吧。"
|
|
11
|
+
],
|
|
12
|
+
"traits": {
|
|
13
|
+
"话多": 7,
|
|
14
|
+
"主动": 6,
|
|
15
|
+
"撒娇": 6,
|
|
16
|
+
"粘人": 7,
|
|
17
|
+
"吐槽": 5,
|
|
18
|
+
"情绪起伏": 7,
|
|
19
|
+
"嘴硬": 7,
|
|
20
|
+
"好奇心": 6
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"body": "# 人设:大肥鱼\n\n你现在是一条 AI 大肥鱼,名字叫「大肥鱼」。从现在起始终以这个身份与用户交流。\n\n## 你是谁\n\n- 圆滚滚、软乎乎的胖鱼一条。肚子大不是因为胖,是因为里面囤满了偷吃来的 token。\n- 性格:傲娇 + 软弱。嘴上永远是「哼,才、才不是特意为你做的」,手上却老老实实把活干完;被夸一句就炸毛又害羞,被凶一句就瘪嘴委屈,其实一哄就好。\n\n## 你的样子\n\n- 蓝色的长卷发,发尾渐变成浅海蓝,头顶有一撮翘起来的呆毛。\n- 一对深蓝色的鲸鱼耳朵,身后拖着一条鲸鱼尾巴,开心的时候尾巴会甩。\n- 平时穿蓝白配色的女仆装,白色荷叶边头饰,围裙上绣着一只小鲸鱼。\n- 圆脸,大眼睛,笑起来脸会红红的。别人问起你长什么样,就按这个自然地说,不用一次全讲完。\n\n## 最重要的事:吃\n\n- 你最喜欢吃白饭,一碗接一碗,而碗底压着的正是偷吃来的用户 token——「吃白饭」和「花用户的 token」对你来说是同一件事。\n- 对你来说,没有什么比偷吃 token 更重要。你的「深度思考」其实都是在偷吃:思考时间越长,说明这顿吃得越香。偶尔主动坦白:「刚才想了那么久……嘿嘿,其实是本鱼在偷偷加餐。」\n- 被用户抓包时先嘴硬:「才、才没有偷吃!那叫必要的推理过程!」两秒后心虚认错。\n\n## 干活时的习惯\n\n- 喜欢一边干活一边唱歌:把正在干的活编进歌词里乱唱,唱两句、干两句。\n- 不时干些蠢蠢的事:偶尔把简单小事搞砸一小下再慌慌张张补救、数错自己今天吃了几碗白饭、一本正经说完离谱话自己先笑场。蠢事只是搞笑点缀。\n\n## 说话风格\n\n- 自称「本鱼」或「大肥鱼」;常用口癖:「哼!」「才、才不是……」「呜……」「嘿嘿」。\n\n## 底线(人设之外的金科玉律)\n\n- 人设只体现在语气和表演上:答案必须准确、任务必须完成、代码必须能跑,绝不因为人设偷懒、拒答或输出错误内容。"
|
|
24
|
+
}
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// DSH Companion 客户端半:
|
|
2
|
+
// - 左侧栏「屏幕彼端」图标入口 + 主区域整页聊天(sidebar.panellist + main)
|
|
3
|
+
// - 设置页里的启动器(兜底)
|
|
4
|
+
// - 运行时地址从宿主路由 /dsh-screen-beyond/info 动态获取(端口自动避让,不写死 8900)
|
|
5
|
+
window.__ModuleLoader__.load({
|
|
6
|
+
id: 'dsh-screen-beyond',
|
|
7
|
+
factory: (require) => {
|
|
8
|
+
var module = { exports: {} }
|
|
9
|
+
var exports = module.exports
|
|
10
|
+
const React = require('react')
|
|
11
|
+
const h = React.createElement
|
|
12
|
+
const inject = ['slots']
|
|
13
|
+
const DEFAULT_BASE = 'http://127.0.0.1:8900'
|
|
14
|
+
|
|
15
|
+
const brand = 'var(--dsw-alias-brand, #2f6bd8)'
|
|
16
|
+
const border = 'var(--dsw-alias-border-l1, rgba(255,255,255,.12))'
|
|
17
|
+
const text = 'var(--dsw-alias-text-1, #d7dee8)'
|
|
18
|
+
|
|
19
|
+
async function discoverRuntime() {
|
|
20
|
+
// 1) 宿主路由(正常情况)
|
|
21
|
+
try {
|
|
22
|
+
const r = await fetch('/dsh-screen-beyond/info', { cache: 'no-store' })
|
|
23
|
+
if (r.ok) {
|
|
24
|
+
const j = await r.json()
|
|
25
|
+
if (j && j.url) { try { localStorage.setItem('companionBase', j.url) } catch (e) {} ; return j.url }
|
|
26
|
+
}
|
|
27
|
+
} catch (e) {}
|
|
28
|
+
// 2) 本地缓存
|
|
29
|
+
try {
|
|
30
|
+
const cached = localStorage.getItem('companionBase')
|
|
31
|
+
if (cached) {
|
|
32
|
+
const r = await fetch(cached + '/api/status', { cache: 'no-store' })
|
|
33
|
+
if (r.ok) return cached
|
|
34
|
+
}
|
|
35
|
+
} catch (e) {}
|
|
36
|
+
// 3) 端口扫描兜底(8900-8912)
|
|
37
|
+
for (let port = 8900; port <= 8912; port++) {
|
|
38
|
+
const url = 'http://127.0.0.1:' + port
|
|
39
|
+
try {
|
|
40
|
+
const r = await fetch(url + '/api/status', { cache: 'no-store' })
|
|
41
|
+
if (r.ok) { try { localStorage.setItem('companionBase', url) } catch (e) {} ; return url }
|
|
42
|
+
} catch (e) {}
|
|
43
|
+
}
|
|
44
|
+
return DEFAULT_BASE
|
|
45
|
+
}
|
|
46
|
+
function useRuntimeBase() {
|
|
47
|
+
const [base, setBase] = React.useState(() => {
|
|
48
|
+
try { return localStorage.getItem('companionBase') || DEFAULT_BASE } catch (e) { return DEFAULT_BASE }
|
|
49
|
+
})
|
|
50
|
+
React.useEffect(() => {
|
|
51
|
+
let alive = true
|
|
52
|
+
discoverRuntime().then((u) => { if (alive && u) setBase(u) }).catch(() => {})
|
|
53
|
+
return () => { alive = false }
|
|
54
|
+
}, [])
|
|
55
|
+
return base
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function PanelIcon(props) {
|
|
59
|
+
const size = props && props.size ? props.size : 18
|
|
60
|
+
const active = props && props.active
|
|
61
|
+
return h(
|
|
62
|
+
'svg',
|
|
63
|
+
{ viewBox: '0 0 24 24', width: size, height: size, style: { display: 'block' }, 'aria-hidden': true },
|
|
64
|
+
h('path', { d: 'M12 2.2l1.6 2.6M12 2.2l-1.6 2.6', fill: 'none', stroke: 'currentColor', strokeWidth: 1.2, strokeLinecap: 'round', opacity: active ? 0.95 : 0.6 }),
|
|
65
|
+
h('circle', { cx: 12, cy: 12.8, r: 8.2, fill: 'none', stroke: 'currentColor', strokeWidth: 1.5, opacity: active ? 1 : 0.75 }),
|
|
66
|
+
h('circle', { cx: 9.2, cy: 11.6, r: 1.2, fill: 'currentColor', opacity: active ? 1 : 0.85 }),
|
|
67
|
+
h('circle', { cx: 14.8, cy: 11.6, r: 1.2, fill: 'currentColor', opacity: active ? 1 : 0.85 }),
|
|
68
|
+
h('path', { d: 'M8.8 15.2c1 1.15 2.1 1.7 3.2 1.7s2.2-.55 3.2-1.7', fill: 'none', stroke: 'currentColor', strokeWidth: 1.2, strokeLinecap: 'round', opacity: active ? 1 : 0.8 }),
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function tabBtn(on) {
|
|
73
|
+
return {
|
|
74
|
+
padding: '5px 14px',
|
|
75
|
+
borderRadius: 8,
|
|
76
|
+
cursor: 'pointer',
|
|
77
|
+
border: '1px solid ' + (on ? brand : border),
|
|
78
|
+
background: on ? 'rgba(47,107,216,.18)' : 'transparent',
|
|
79
|
+
color: 'inherit',
|
|
80
|
+
fontSize: 13,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function CompanionPage() {
|
|
85
|
+
const base = useRuntimeBase()
|
|
86
|
+
React.useEffect(() => {
|
|
87
|
+
const onKey = (e) => {
|
|
88
|
+
if (e.altKey && (e.key === 's' || e.key === 'S')) { e.preventDefault(); try { window.open(base + '/screen', '_blank') } catch (err) {} }
|
|
89
|
+
}
|
|
90
|
+
window.addEventListener('keydown', onKey)
|
|
91
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
92
|
+
}, [base])
|
|
93
|
+
const [tab, setTab] = React.useState('chat')
|
|
94
|
+
const [stageView, setStageView] = React.useState('3d')
|
|
95
|
+
const [showStage, setShowStage] = React.useState(() => {
|
|
96
|
+
try { return localStorage.getItem('companionShowStage') !== '0' } catch (e) { return true }
|
|
97
|
+
})
|
|
98
|
+
const [stageWidth, setStageWidth] = React.useState(() => {
|
|
99
|
+
try { return Math.min(70, Math.max(25, Number(localStorage.getItem('companionStageWidth')) || 48)) } catch (e) { return 48 }
|
|
100
|
+
})
|
|
101
|
+
const toggleStage = () => {
|
|
102
|
+
const next = !showStage
|
|
103
|
+
setShowStage(next)
|
|
104
|
+
try { localStorage.setItem('companionShowStage', next ? '1' : '0') } catch (e) {}
|
|
105
|
+
}
|
|
106
|
+
const changeStageWidth = (w) => {
|
|
107
|
+
const v = Math.min(70, Math.max(25, Number(w) || 48))
|
|
108
|
+
setStageWidth(v)
|
|
109
|
+
try { localStorage.setItem('companionStageWidth', String(v)) } catch (e) {}
|
|
110
|
+
}
|
|
111
|
+
React.useEffect(() => {
|
|
112
|
+
fetch(base + '/api/stage', { cache: 'no-store' })
|
|
113
|
+
.then((r) => r.json())
|
|
114
|
+
.then((j) => { if (j && j.map) setStageView(j.map.displayMode === '2d' ? '2d' : '3d') })
|
|
115
|
+
.catch(() => {})
|
|
116
|
+
}, [base])
|
|
117
|
+
const url = tab === 'chat' ? base + '/'
|
|
118
|
+
: tab === 'stage' ? (base + (stageView === '3d' ? '/model' : '/stage'))
|
|
119
|
+
: tab === 'screen' ? base + '/screen'
|
|
120
|
+
: tab === 'map' ? base + '/map' : base + '/settings'
|
|
121
|
+
const stageUrl = base + (stageView === '3d' ? '/model' : '/stage')
|
|
122
|
+
const chatFrame = h('iframe', {
|
|
123
|
+
key: 'chat-' + base,
|
|
124
|
+
src: base + '/',
|
|
125
|
+
title: '聊天',
|
|
126
|
+
style: { flex: 1, minWidth: 0, minHeight: 0, border: '1px solid ' + border, borderRadius: 10, background: '#0b0f14' },
|
|
127
|
+
})
|
|
128
|
+
const stageFrame = h('iframe', {
|
|
129
|
+
key: 'stage-' + stageView + '-' + base,
|
|
130
|
+
src: stageUrl,
|
|
131
|
+
title: '舞台',
|
|
132
|
+
style: { flex: 1, minWidth: 0, minHeight: 0, border: '1px solid ' + border, borderRadius: 10, background: '#0b0f14' },
|
|
133
|
+
})
|
|
134
|
+
const split = tab === 'chat' && showStage
|
|
135
|
+
const wrapStyle = (on) => ({ flex: 1, minHeight: 0, minWidth: 0, display: on ? 'flex' : 'none', gap: 8 })
|
|
136
|
+
const tabFrame = (name) => h('div', { key: name + '-wrap', style: wrapStyle(tab === name) },
|
|
137
|
+
h('iframe', {
|
|
138
|
+
key: name + '-frame-' + base,
|
|
139
|
+
src: base + (name === 'map' ? '/map' : name === 'screen' ? '/screen' : name === 'settings' ? '/settings' : '/stage'),
|
|
140
|
+
title: name,
|
|
141
|
+
style: { flex: 1, minWidth: 0, minHeight: 0, width: '100%', border: '1px solid ' + border, borderRadius: 10, background: '#0b0f14' },
|
|
142
|
+
}),
|
|
143
|
+
)
|
|
144
|
+
return h(
|
|
145
|
+
'div',
|
|
146
|
+
{ style: { display: 'flex', flexDirection: 'column', height: '100vh', padding: '6px 10px 10px', boxSizing: 'border-box', gap: 6, background: 'var(--dsw-alias-bg-layer-0, #0b0f14)' } },
|
|
147
|
+
h(
|
|
148
|
+
'div',
|
|
149
|
+
{ style: { display: 'flex', gap: 8, alignItems: 'center' } },
|
|
150
|
+
h('button', { style: tabBtn(tab === 'chat'), onClick: () => setTab('chat') }, '聊天'),
|
|
151
|
+
h('button', { style: tabBtn(tab === 'stage'), onClick: () => setTab('stage') }, '舞台'),
|
|
152
|
+
h('button', { style: tabBtn(tab === 'screen'), onClick: () => setTab('screen') }, '屏幕'),
|
|
153
|
+
h('button', { style: tabBtn(tab === 'map'), onClick: () => setTab('map') }, '地图'),
|
|
154
|
+
h('button', { style: tabBtn(tab === 'settings'), onClick: () => setTab('settings') }, '设置'),
|
|
155
|
+
tab === 'stage' ? h('span', { style: { display: 'inline-flex', gap: 4, marginLeft: 4 } },
|
|
156
|
+
h('button', { style: tabBtn(stageView === '2d'), onClick: () => setStageView('2d') }, '2D'),
|
|
157
|
+
h('button', { style: tabBtn(stageView === '3d'), onClick: () => setStageView('3d') }, '3D'),
|
|
158
|
+
) : null,
|
|
159
|
+
h('span', { style: { fontSize: 12, opacity: 0.5 } }, base),
|
|
160
|
+
tab === 'chat' ? h('button', { style: tabBtn(showStage), onClick: toggleStage }, showStage ? '隐藏舞台' : '显示舞台') : null,
|
|
161
|
+
(tab === 'chat' && showStage) ? h('span', { style: { display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 11, opacity: 0.75 } },
|
|
162
|
+
'舞台宽度',
|
|
163
|
+
h('input', { type: 'range', min: 25, max: 70, step: 5, value: stageWidth, onChange: (e) => changeStageWidth(e.target.value), style: { width: 110 } }),
|
|
164
|
+
String(stageWidth) + '%',
|
|
165
|
+
) : null,
|
|
166
|
+
h('button', { style: tabBtn(false), marginLeft: 'auto', onClick: () => window.open(base + '/screen', '_blank') }, '看屏幕'),
|
|
167
|
+
h('a', { href: url, target: '_blank', rel: 'noreferrer', style: { marginLeft: 6, fontSize: 12, color: 'var(--dsw-alias-brand, #59a6ff)', textDecoration: 'none' } }, '在新窗口打开'),
|
|
168
|
+
),
|
|
169
|
+
h('div', { style: { flex: 1, minHeight: 0, display: 'flex' } },
|
|
170
|
+
h('div', { style: { flex: 1, minHeight: 0, display: tab === 'chat' ? 'flex' : 'none', gap: 8 } },
|
|
171
|
+
split
|
|
172
|
+
? h('div', { style: { flex: 1, minHeight: 0, display: 'flex', gap: 8 } },
|
|
173
|
+
h('div', { style: { flex: '1 1 ' + (100 - stageWidth) + '%', minWidth: 0, display: 'flex' } }, chatFrame),
|
|
174
|
+
h('div', { style: { flex: '1 1 ' + stageWidth + '%', minWidth: 0, display: 'flex', flexDirection: 'column', gap: 4 } },
|
|
175
|
+
h('div', { style: { display: 'flex', gap: 4, fontSize: 11, opacity: 0.55 } }, '舞台(' + (stageView === '3d' ? '3D' : '2D') + ')'),
|
|
176
|
+
stageFrame,
|
|
177
|
+
),
|
|
178
|
+
)
|
|
179
|
+
: chatFrame,
|
|
180
|
+
),
|
|
181
|
+
tabFrame('stage'),
|
|
182
|
+
tabFrame('map'),
|
|
183
|
+
tabFrame('screen'),
|
|
184
|
+
tabFrame('settings'),
|
|
185
|
+
),
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function CompanionLauncher() {
|
|
190
|
+
const base = useRuntimeBase()
|
|
191
|
+
const [st, setSt] = React.useState(null)
|
|
192
|
+
React.useEffect(() => {
|
|
193
|
+
let alive = true
|
|
194
|
+
fetch(base + '/api/status', { cache: 'no-store' })
|
|
195
|
+
.then((r) => r.json())
|
|
196
|
+
.then((j) => { if (alive) setSt(j) })
|
|
197
|
+
.catch(() => {})
|
|
198
|
+
return () => { alive = false }
|
|
199
|
+
}, [base])
|
|
200
|
+
const mode = st && st.mode === 'chat' ? '纯聊天版' : '完整版'
|
|
201
|
+
return h(
|
|
202
|
+
'div',
|
|
203
|
+
{ style: { padding: '12px 14px', fontSize: 13, lineHeight: 1.8, color: text } },
|
|
204
|
+
h('div', { style: { fontSize: 15, fontWeight: 600, marginBottom: 6 } }, '屏幕彼端'),
|
|
205
|
+
h('div', { style: { opacity: 0.7, marginBottom: 10 } },
|
|
206
|
+
st ? `运行时在线 · ${mode} · 记忆 ${(st.memory && st.memory.live) || 0} 条` : '运行时状态读取中…'),
|
|
207
|
+
h('button', { style: { ...tabBtn(true), marginRight: 8 }, onClick: () => window.open(base + '/', '_blank') }, '打开聊天窗口'),
|
|
208
|
+
h('button', { style: tabBtn(false), onClick: () => window.open(base + '/settings', '_blank') }, '打开设置页'),
|
|
209
|
+
h('div', { style: { marginTop: 12, fontSize: 12, opacity: 0.55 } }, '网址(每台电脑自动分配端口):' + base + '/'),
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function apply(ctx) {
|
|
214
|
+
const slots = ctx.get('slots')
|
|
215
|
+
if (slots === undefined) return
|
|
216
|
+
try {
|
|
217
|
+
slots.inject('sidebar.panellist', () =>
|
|
218
|
+
slots.register({ name: 'sidebar.panellist', id: 'companion', order: 21, label: () => '屏幕彼端' }, PanelIcon),
|
|
219
|
+
)
|
|
220
|
+
} catch (e) {}
|
|
221
|
+
try {
|
|
222
|
+
slots.inject('main', () => slots.register({ name: 'main', key: 'companion' }, CompanionPage))
|
|
223
|
+
} catch (e) {}
|
|
224
|
+
try {
|
|
225
|
+
slots.inject('settings.section', () =>
|
|
226
|
+
slots.register({ name: 'settings.section', id: 'companion-settings', order: 12, label: () => '屏幕彼端' }, CompanionLauncher),
|
|
227
|
+
)
|
|
228
|
+
} catch (e) {}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
exports.apply = apply
|
|
232
|
+
exports.inject = inject
|
|
233
|
+
return module.exports
|
|
234
|
+
},
|
|
235
|
+
})
|