dsh-screen-beyond 0.0.0-stage → 0.5.7

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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +201 -3
  3. package/cordis.patch.yml +6 -0
  4. package/examples/CREDITS.md +17 -0
  5. package/examples/dafeiyu-ai-avatar.png +0 -0
  6. package/examples/dafeiyu-ai-candidate-2.png +0 -0
  7. package/examples/dafeiyu-avatar-cc-by-nc-sa.png +0 -0
  8. package/examples/dafeiyu-avatar-preview.png +0 -0
  9. package/examples/dafeiyu-character-card.json +24 -0
  10. package/lib/client.js +235 -0
  11. package/lib/index.js +149 -0
  12. package/package.json +47 -3
  13. package/runtime/chat.html +215 -0
  14. package/runtime/config.example.json +93 -0
  15. package/runtime/env/README.md +14 -0
  16. package/runtime/env/agent_ssh.py +73 -0
  17. package/runtime/env/installers/ADBKeyboard.apk +0 -0
  18. package/runtime/env/phone_ctl.py +244 -0
  19. package/runtime/env/space_ctl.py +163 -0
  20. package/runtime/env/space_ensure.py +65 -0
  21. package/runtime/env/start_screen.ps1 +16 -0
  22. package/runtime/env/start_space.ps1 +33 -0
  23. package/runtime/env/stop_screen.ps1 +10 -0
  24. package/runtime/example-character.json +24 -0
  25. package/runtime/map.html +784 -0
  26. package/runtime/memory.mjs +205 -0
  27. package/runtime/model.html +363 -0
  28. package/runtime/models/chat-mway331n.png +0 -0
  29. package/runtime/models/dafeiyu-chibi.gltf +1 -0
  30. package/runtime/models/demo-town.gltf +1 -0
  31. package/runtime/models/demo-town2.gltf +1 -0
  32. package/runtime/models/duck.glb +0 -0
  33. package/runtime/models/map-map-mwb087zu-mwb0880c.png +0 -0
  34. package/runtime/models/map-map-mwb0bfsb-mwb0bft6.png +0 -0
  35. package/runtime/models/map-map-mwb0c9iw-mwb0c9j2.png +0 -0
  36. package/runtime/models/map-map-mwb0d8cc-mwb0d8d2.png +0 -0
  37. package/runtime/models/map-map-mwb0fm53-mwb0fm59.png +0 -0
  38. package/runtime/persona.example.md +34 -0
  39. package/runtime/pet/default-avatar.png +0 -0
  40. package/runtime/pet/pet.example.json +49 -0
  41. package/runtime/pet/pet.ps1 +532 -0
  42. package/runtime/screen.html +125 -0
  43. package/runtime/server.mjs +2659 -0
  44. package/runtime/settings.html +1015 -0
  45. package/runtime/stage.html +195 -0
  46. package/runtime/theme.css +210 -0
  47. package/runtime/tutorial.md +116 -0
  48. package/runtime/vendor/addons/loaders/GLTFLoader.js +4663 -0
  49. package/runtime/vendor/addons/utils/BufferGeometryUtils.js +1375 -0
  50. package/runtime/vendor/three.module.min.js +6 -0
  51. package/runtime/wx.js +164 -0
  52. package/scripts/backup.ps1 +79 -0
  53. package/start-pure-chat.cmd +26 -0
  54. package/start-pure-chat.sh +4 -0
package/lib/index.js ADDED
@@ -0,0 +1,149 @@
1
+ import { spawn } from 'node:child_process'
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
6
+
7
+ export const name = 'dsh-screen-beyond'
8
+ export const inject = ['webServer']
9
+
10
+ const HERE = path.dirname(fileURLToPath(import.meta.url))
11
+ const RUNTIME = path.resolve(HERE, '..', 'runtime')
12
+ const VERSION = (() => { try { return JSON.parse(fs.readFileSync(path.join(HERE, '..', 'package.json'), 'utf8')).version || '0.0.0' } catch { return '0.0.0' } })()
13
+ const HOME = process.env.USERPROFILE || process.env.HOME || '.'
14
+ const PORT_FILE = path.join(HOME, '.dsh-companion-port.json')
15
+ const LOCAL_CONFIG = process.env.DSH_COMPANION_CONFIG || path.join(HOME, '.dsh-companion.json')
16
+ let child = null
17
+
18
+ const NODE_CANDIDATES = [
19
+ process.env.DSH_COMPANION_NODE,
20
+ process.env.DSH_NODE,
21
+ 'C:\\Program Files\\nodejs\\node.exe',
22
+ '/usr/bin/node',
23
+ '/usr/local/bin/node',
24
+ ]
25
+
26
+ function resolveNode() {
27
+ for (const c of NODE_CANDIDATES) {
28
+ if (c && fs.existsSync(c)) return c
29
+ }
30
+ return process.execPath
31
+ }
32
+
33
+ function readJson(file) {
34
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')) } catch { return null }
35
+ }
36
+
37
+ function readPortFile() { return readJson(PORT_FILE) }
38
+
39
+ async function statusOf(port) {
40
+ try {
41
+ const r = await fetch(`http://127.0.0.1:${port}/api/status`, { signal: AbortSignal.timeout(1500) })
42
+ if (!r.ok) return null
43
+ return await r.json()
44
+ } catch {
45
+ return null
46
+ }
47
+ }
48
+
49
+ async function stopStale(info) {
50
+ try {
51
+ if (info && info.pid) process.kill(info.pid)
52
+ else if (info && info.port) {
53
+ const out = await new Promise((resolve) => {
54
+ import('node:child_process').then(({ execFile }) => {
55
+ execFile('powershell.exe', ['-NoProfile', '-Command', `(Get-NetTCPConnection -LocalPort ${info.port} -State Listen -ErrorAction SilentlyContinue).OwningProcess`], { windowsHide: true, timeout: 8000 }, (e, o) => resolve(String(o || '')))
56
+ }).catch(() => resolve(''))
57
+ })
58
+ for (const pid of out.split(/\s+/)) { if (/^\d+$/.test(pid)) { try { process.kill(Number(pid)) } catch {} } }
59
+ }
60
+ } catch {}
61
+ for (let i = 0; i < 20; i++) {
62
+ await new Promise((r) => setTimeout(r, 300))
63
+ const st = info && info.port ? await statusOf(info.port) : null
64
+ if (!st) return
65
+ }
66
+ }
67
+
68
+ export function apply(ctx, config = {}) {
69
+ const local = readJson(LOCAL_CONFIG) || {}
70
+ const preferred = Number(config.port || local.port) || 8900
71
+ // 数据目录优先级:插件配置 > 本机 override 文件 > 运行时默认值(%USERPROFILE%\.dsh-companion)
72
+ const dataRoot = config.dataRoot ? String(config.dataRoot) : (local.dataRoot ? String(local.dataRoot) : null)
73
+ const envDir = config.envDir ? String(config.envDir) : (local.envDir ? String(local.envDir) : null)
74
+ const autoStart = config.autoStart !== false
75
+
76
+ try {
77
+ const web = ctx.webServer || (typeof ctx.get === 'function' ? ctx.get('webServer') : null)
78
+ if (web && typeof web.register === 'function') {
79
+ const register = () => web.register({
80
+ kind: 'exact',
81
+ path: '/dsh-screen-beyond/info',
82
+ handler: async (req, res) => {
83
+ const info = readPortFile()
84
+ const body = JSON.stringify({
85
+ ok: !!(info && info.port),
86
+ port: info ? info.port : null,
87
+ url: info && info.port ? `http://127.0.0.1:${info.port}` : null,
88
+ mode: info ? info.mode || null : null,
89
+ root: info ? info.root || null : dataRoot,
90
+ version: VERSION,
91
+ })
92
+ res.writeHead(200, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })
93
+ res.end(body)
94
+ },
95
+ })
96
+ if (typeof ctx.effect === 'function') ctx.effect(register)
97
+ else register()
98
+ console.log('[dsh-screen-beyond] 已注册 /dsh-screen-beyond/info')
99
+ } else {
100
+ console.warn('[dsh-screen-beyond] webServer 服务不可用,客户端将走端口扫描兜底')
101
+ }
102
+ } catch (e) {
103
+ console.warn('[dsh-screen-beyond] 注册 info 路由失败:', String(e && e.message))
104
+ }
105
+
106
+ if (!autoStart) {
107
+ console.log('[dsh-screen-beyond] autoStart=false,跳过启动')
108
+ return
109
+ }
110
+
111
+ const start = async () => {
112
+ const prev = readPortFile()
113
+ if (prev && prev.port) {
114
+ const st = await statusOf(prev.port)
115
+ const sameVersion = st && String(st.version || '') === VERSION
116
+ const sameRoot = !dataRoot || (st && st.setup && st.setup.dataRoot && path.resolve(String(st.setup.dataRoot)) === path.resolve(dataRoot))
117
+ if (sameVersion && sameRoot) {
118
+ console.log(`[dsh-screen-beyond] 复用已运行运行时 (v${VERSION}): http://127.0.0.1:${prev.port}/`)
119
+ return
120
+ }
121
+ console.log(`[dsh-screen-beyond] 运行时版本/数据目录不一致,重启:旧 v${(st && st.version) || '?'} root=${(st && st.setup && st.setup.dataRoot) || prev.root || '?'}`)
122
+ await stopStale(prev)
123
+ }
124
+ const server = path.join(RUNTIME, 'server.mjs')
125
+ if (!fs.existsSync(server)) {
126
+ console.warn('[dsh-screen-beyond] 缺少 runtime/server.mjs,跳过')
127
+ return
128
+ }
129
+ const node = resolveNode()
130
+ const env = { ...process.env }
131
+ if (dataRoot) env.LINZHI_ROOT = dataRoot
132
+ if (envDir) env.DSH_COMPANION_ENV = envDir
133
+ env.LINZHI_PORT = String(preferred)
134
+ if (node === process.execPath) env.ELECTRON_RUN_AS_NODE = '1'
135
+ child = spawn(node, [server], { cwd: RUNTIME, env, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true })
136
+ child.stdout?.on('data', (d) => console.log('[dsh-screen-beyond]', String(d).trim()))
137
+ child.stderr?.on('data', (d) => console.warn('[dsh-screen-beyond]', String(d).trim()))
138
+ child.on('exit', (code) => {
139
+ console.log(`[dsh-screen-beyond] 运行时退出 code=${code}`)
140
+ child = null
141
+ })
142
+ console.log(`[dsh-screen-beyond] 启动运行时 v${VERSION}(优先端口 ${preferred})数据: ${dataRoot || '默认(~/.dsh-companion)'}`)
143
+ }
144
+ void start()
145
+
146
+ return () => {
147
+ try { child?.kill() } catch {}
148
+ }
149
+ }
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
1
  {
2
2
  "name": "dsh-screen-beyond",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.5.7",
4
+ "type": "module",
5
+ "description": "屏幕彼端 · 住在屏幕另一端的 AI 小伙伴:人设卡、记忆与日记、生活作息、地图/2D/3D 舞台、聊天界面,以及她自己的虚拟屏幕和安卓手机。",
6
+ "main": "./lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./client": "./lib/client.js",
10
+ "./cordis.patch.yml": "./cordis.patch.yml",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "files": [
14
+ "lib",
15
+ "runtime",
16
+ "cordis.patch.yml",
17
+ "README.md",
18
+ "LICENSE",
19
+ "examples",
20
+ "scripts",
21
+ "start-pure-chat.cmd",
22
+ "start-pure-chat.sh"
23
+ ],
24
+ "dsh": {
25
+ "bundle": {
26
+ "patch": "./cordis.patch.yml"
27
+ },
28
+ "client": {
29
+ "platform": "web",
30
+ "inject": [
31
+ "slots"
32
+ ],
33
+ "external": [
34
+ "react"
35
+ ]
36
+ }
37
+ },
38
+ "license": "MIT",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/ljx200719/dsh-screen-beyond.git"
42
+ },
43
+ "homepage": "https://github.com/ljx200719/dsh-screen-beyond#readme",
44
+ "bugs": {
45
+ "url": "https://github.com/ljx200719/dsh-screen-beyond/issues"
46
+ },
47
+ "scripts": {
48
+ "start": "node runtime/server.mjs"
49
+ }
6
50
  }
@@ -0,0 +1,215 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN"><head><meta charset="utf-8"><title>屏幕彼端 · 聊天</title>
3
+ <meta name="viewport" content="width=device-width, initial-scale=1">
4
+ <style>
5
+ :root{color-scheme:dark}
6
+ *{box-sizing:border-box}
7
+ html,body{margin:0;height:100%}
8
+ body{background:#0b0f14;color:#d7dee8;font:14px/1.7 "Segoe UI","Microsoft YaHei",sans-serif;display:flex;flex-direction:column;height:100vh}
9
+ header{display:flex;align-items:center;gap:10px;padding:9px 16px;border-bottom:1px solid #1d242e;background:#11161d;flex-shrink:0}
10
+ header .ava{width:30px;height:30px;border-radius:50%;background:#16202b center/cover;border:1px solid #2a3745;display:flex;align-items:center;justify-content:center;font-size:14px;flex-shrink:0}
11
+ header .name{font-weight:600;font-size:15px}
12
+ header .dot{width:7px;height:7px;border-radius:50%;background:#22c55e;display:inline-block;margin-right:4px}
13
+ header .meta{font-size:12px;color:#8496ab}
14
+ header .chip{font-size:11px;border:1px solid #2a3745;border-radius:99px;padding:0 8px;color:#93a1b1;white-space:nowrap}
15
+ header .spacer{flex:1}
16
+ header a{color:#93a1b1;text-decoration:none;font-size:12px;padding:4px 10px;border:1px solid #2a3745;border-radius:8px}
17
+ header a:hover{color:#e8eef6;border-color:#3b82f6}
18
+ #list{flex:1;overflow-y:auto;padding:18px 16px 8px;display:flex;flex-direction:column;gap:10px}
19
+ .msg{display:flex;gap:8px;max-width:78%}
20
+ .msg .bubble{padding:9px 13px;border-radius:12px;white-space:pre-wrap;word-break:break-word;line-height:1.65}
21
+ .msg.her{align-self:flex-start}
22
+ .msg.her .bubble{background:#141c26;border:1px solid #22303d;border-top-left-radius:4px}
23
+ .msg.user{align-self:flex-end;flex-direction:row-reverse}
24
+ .msg.user .bubble{background:#1d4ed8;border:1px solid #2563eb;color:#fff;border-top-right-radius:4px}
25
+ .msg .who{font-size:11px;color:#5f7183;margin-bottom:2px}
26
+ .msg.user .who{text-align:right}
27
+ .msg.system{align-self:center;max-width:90%;font-size:12px;color:#6b7f92;background:#101720;border:1px dashed #26333f;border-radius:10px;padding:6px 12px;text-align:center}
28
+ .time{font-size:10px;color:#4d5f70;margin-top:3px}
29
+ #typing{align-self:flex-start;font-size:12px;color:#7d8fa5;padding:4px 8px}
30
+ #bar{flex-shrink:0;border-top:1px solid #1d242e;background:#0e141c;padding:10px 14px 12px;display:flex;flex-direction:column;gap:6px}
31
+ #bar .hint{font-size:11px;color:#4d5f70}
32
+ #row{display:flex;gap:8px;align-items:flex-end}
33
+ textarea{flex:1;resize:none;background:#0b1118;border:1px solid #26333f;color:#d7dee8;border-radius:10px;padding:9px 11px;font:inherit;outline:none;min-height:42px;max-height:150px}
34
+ textarea:focus{border-color:#2563eb}
35
+ button{background:#2563eb;color:#fff;border:0;border-radius:10px;padding:10px 18px;font:inherit;cursor:pointer}
36
+ button:disabled{opacity:.5;cursor:default}
37
+ #toast{position:fixed;right:16px;bottom:76px;background:#1f6feb;color:#fff;padding:8px 14px;border-radius:8px;opacity:0;transition:.2s;font-size:12px}
38
+ </style>
39
+ <link rel="stylesheet" href="/theme.css"></head><body>
40
+ <header>
41
+ <div class="ava" id="ava">🐟</div>
42
+ <div>
43
+ <div class="name" id="name">大肥鱼</div>
44
+ <div class="meta"><span class="dot"></span><span id="online">在线</span><span id="place"> · </span></div>
45
+ </div>
46
+ <span class="chip" id="energyChip">状态 …</span>
47
+ <span class="chip" id="moneyChip">💰 …</span>
48
+ <span class="chip" id="tokenChip">🪙 …</span>
49
+ <span class="chip" id="modeChip">…</span>
50
+ <span class="chip" id="standaloneChip" style="display:none" title="纯聊天模式可以脱离 DSH 独立运行:Node 18+ 和 DeepSeek API Key 即可;完整版仍需 DSH">🧩 可独立运行</span>
51
+ <div class="spacer"></div>
52
+ <a href="/screen" target="_blank" title="快捷键 Alt+S">屏幕</a>
53
+ <a href="/stage" target="_blank">舞台</a>
54
+ <a href="/map" target="_blank">地图</a>
55
+ <a href="/settings" target="_blank">设置</a>
56
+ </header>
57
+ <div id="list"></div>
58
+ <div id="typing" style="display:none">对方正在输入…</div>
59
+ <div id="bar">
60
+ <div class="hint">Enter 发送 · Shift+Enter 换行 · 📎 发图片</div>
61
+ <div id="preview" style="display:none;align-items:center;gap:8px;font-size:12px;color:#93a1b1"></div>
62
+ <div id="row">
63
+ <textarea id="input" placeholder="跟她说点什么…"></textarea>
64
+ <button id="attach" title="发图片" style="background:#1b2430;color:#cfdae8;border:1px solid #2a3745">📎</button>
65
+ <input type="file" id="file" accept="image/*" style="display:none">
66
+ <button id="send">发送</button>
67
+ </div>
68
+ </div>
69
+ <div id="toast"></div>
70
+ <script>
71
+ const $ = (s) => document.querySelector(s);
72
+ let lastId = 0, busy = false, outboxImage = '';
73
+ function esc(s){ return String(s??'').replace(/[&<>"]/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }
74
+ function toast(t){ const el=$('#toast'); el.textContent=t; el.style.opacity=1; setTimeout(()=>el.style.opacity=0,1600); }
75
+ function fmtTime(ts){ const d=new Date(ts||Date.now()); return String(d.getHours()).padStart(2,'0')+':'+String(d.getMinutes()).padStart(2,'0'); }
76
+ function nearBottom(){ const l=$('#list'); return l.scrollHeight - l.scrollTop - l.clientHeight < 160; }
77
+ function addMsg(m){
78
+ const l = $('#list');
79
+ if (m.role === 'system') {
80
+ const d = document.createElement('div');
81
+ d.className = 'msg system';
82
+ d.textContent = m.text;
83
+ l.appendChild(d);
84
+ } else {
85
+ const wrap = document.createElement('div');
86
+ wrap.className = 'msg ' + (m.role === 'user' ? 'user' : 'her');
87
+ const who = m.role === 'user' ? '你' : '她';
88
+ const imgHtml = m.image ? '<div style="margin-top:6px"><img src="'+esc(m.image)+'" alt="" style="max-width:260px;max-height:260px;border-radius:8px;display:block"></div>' : '';
89
+ wrap.innerHTML = '<div><div class="who">'+esc(who)+' · '+fmtTime(m.at)+'</div><div class="bubble">'+esc(m.text)+imgHtml+'</div></div>';
90
+ l.appendChild(wrap);
91
+ }
92
+ if (nearBottom()) l.scrollTop = l.scrollHeight;
93
+ }
94
+ async function loadHeader(){
95
+ try {
96
+ const st = await (await fetch('/api/status')).json();
97
+ if (st.character) {
98
+ $('#name').textContent = st.character.name || '她';
99
+ const av = st.character.avatar;
100
+ if (av) { $('#ava').style.backgroundImage = 'url(' + av + ')'; $('#ava').textContent = ''; }
101
+ else { $('#ava').style.backgroundImage = ''; $('#ava').textContent = (st.character.name || '她').slice(0,1); }
102
+ }
103
+ if (st.place) $('#place').textContent = ' · ' + st.place.icon + ' ' + st.place.name;
104
+ $('#energyChip').textContent = '状态 ' + (function(e){ const n = Number(e)||0; return n>=80?'精力很足':n>=55?'状态不错':n>=35?'有点累':n>=20?'挺累的':'快撑不住了'; })(st.energy);
105
+ let modeEff = st.mode;
106
+ try { const mi = await (await fetch('/api/mode')).json(); if (mi && mi.effective) modeEff = mi.effective; } catch (e) {}
107
+ $('#modeChip').textContent = modeEff === 'chat' ? '纯聊天' : '完整版';
108
+ const sc = document.getElementById('standaloneChip');
109
+ if (sc) sc.style.display = modeEff === 'chat' ? '' : 'none';
110
+ const econ = await (await fetch('/api/economy')).json();
111
+ const w = econ.wallet || {};
112
+ $('#moneyChip').textContent = '💰 ' + (w.spendable ?? w.balance ?? '…') + ' 元';
113
+ const tk = st.tokens || {};
114
+ const tc = document.getElementById('tokenChip');
115
+ if (tc) {
116
+ const rem = Number(tk.remainingTokens ?? w.remainingTokens ?? 0);
117
+ const today = Number(tk.dailyUsed ?? 0);
118
+ const fmt = (n) => n >= 1000000 ? (n / 1000000).toFixed(n >= 10000000 ? 0 : 1) + 'M' : n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n);
119
+ tc.textContent = '🪙 余 ' + fmt(rem) + ' · 今日 ' + fmt(today);
120
+ tc.title = '剩余 token:' + rem.toLocaleString() + '\n今日已用:' + today.toLocaleString() + (tk.tokenBudget ? '\n预算:' + Number(tk.tokenBudget).toLocaleString() : '');
121
+ }
122
+ } catch (e) {}
123
+ }
124
+ async function poll(){
125
+ try {
126
+ const r = await (await fetch('/api/events?since=' + lastId)).json();
127
+ (r.messages || []).forEach((m) => { addMsg(m); lastId = Math.max(lastId, Number(m.id)||0); });
128
+ $('#typing').style.display = r.typing ? 'block' : 'none';
129
+ busy = !!r.typing;
130
+ $('#send').disabled = false;
131
+ } catch (e) {}
132
+ }
133
+ async function send(){
134
+ const inp = $('#input');
135
+ const text = inp.value.trim();
136
+ if (!text && !outboxImage) return;
137
+ const body = { text };
138
+ if (outboxImage) body.images = [outboxImage];
139
+ inp.value = '';
140
+ outboxImage = '';
141
+ const pv = $('#preview'); if (pv) { pv.style.display = 'none'; pv.innerHTML = ''; }
142
+ try {
143
+ const r = await fetch('/api/chat', { method: 'POST', headers: {'content-type':'application/json'}, body: JSON.stringify(body) });
144
+ if (!r.ok) toast('发送失败:' + r.status);
145
+ } catch (e) { toast('发送失败'); }
146
+ await poll();
147
+ }
148
+ document.addEventListener('keydown', (e) => {
149
+ if (e.altKey && (e.key === 's' || e.key === 'S')) { e.preventDefault(); window.open('/screen', '_blank'); }
150
+ });
151
+ $('#attach').addEventListener('click', () => $('#file').click());
152
+ $('#file').addEventListener('change', (e) => {
153
+ const f = e.target.files && e.target.files[0];
154
+ if (!f) return;
155
+ if (f.size > 8 * 1024 * 1024) { toast('图片太大(<8MB)'); return; }
156
+ const rd = new FileReader();
157
+ rd.onload = () => {
158
+ outboxImage = String(rd.result);
159
+ const pv = $('#preview');
160
+ pv.style.display = 'flex';
161
+ pv.innerHTML = '<img src="' + outboxImage + '" style="width:40px;height:40px;object-fit:cover;border-radius:6px"> <span>图片已就绪</span> <button id="rmImg" style="background:#3a1d1d;color:#ff9c9c;padding:2px 8px;font-size:11px;border-radius:6px">移除</button>';
162
+ document.getElementById('rmImg').onclick = () => { outboxImage = ''; pv.style.display = 'none'; pv.innerHTML = ''; };
163
+ };
164
+ rd.readAsDataURL(f);
165
+ e.target.value = '';
166
+ });
167
+ $('#send').addEventListener('click', send);
168
+ $('#input').addEventListener('keydown', (e) => {
169
+ if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); send(); }
170
+ });
171
+ $('#input').addEventListener('input', (e) => {
172
+ e.target.style.height = 'auto';
173
+ e.target.style.height = Math.min(150, e.target.scrollHeight) + 'px';
174
+ });
175
+ // 流式显示
176
+ let liveEl = null, liveBubble = null;
177
+ function ensureLive(){
178
+ if (liveEl) return;
179
+ const l = $('#list');
180
+ liveEl = document.createElement('div');
181
+ liveEl.className = 'msg her';
182
+ liveEl.innerHTML = '<div><div class="who">她 · 正在输入</div><div class="bubble" id="liveBubble"></div></div>';
183
+ l.appendChild(liveEl);
184
+ liveBubble = document.getElementById('liveBubble');
185
+ l.scrollTop = l.scrollHeight;
186
+ }
187
+ function clearLive(){
188
+ if (liveEl) { try { liveEl.remove(); } catch {} }
189
+ liveEl = null; liveBubble = null;
190
+ }
191
+ function initStream(){
192
+ try {
193
+ const es = new EventSource('/api/stream');
194
+ es.onmessage = (e) => {
195
+ let d;
196
+ try { d = JSON.parse(e.data); } catch { return; }
197
+ if (d.type === 'start' || d.type === 'init') { if (d.active !== false && (d.type === 'start' || d.text)) { ensureLive(); } }
198
+ if (d.type === 'delta') { ensureLive(); if (liveBubble) { liveBubble.textContent += d.text; const l = $('#list'); l.scrollTop = l.scrollHeight; } }
199
+ if (d.type === 'done') { setTimeout(clearLive, 400); }
200
+ };
201
+ es.onerror = () => {};
202
+ } catch (e) {}
203
+ }
204
+ (async function init(){
205
+ try {
206
+ const r = await (await fetch('/api/events?since=0')).json();
207
+ (r.messages || []).forEach((m) => { addMsg(m); lastId = Math.max(lastId, Number(m.id)||0); });
208
+ } catch (e) { addMsg({ role:'system', text:'运行时好像没起来:' + e.message }); }
209
+ await loadHeader();
210
+ if (!location.search.includes('nostream')) initStream();
211
+ setInterval(poll, 1200);
212
+ setInterval(loadHeader, 15000);
213
+ $('#input').focus();
214
+ })();
215
+ </script></body></html>
@@ -0,0 +1,93 @@
1
+ {
2
+ "life": {
3
+ "enabled": true,
4
+ "heartbeatMin": 50,
5
+ "proactive": 3,
6
+ "quietStart": 2,
7
+ "quietEnd": 7,
8
+ "diaryHour": 22,
9
+ "diaryAuto": true,
10
+ "schedules": [
11
+ {
12
+ "time": "07:20",
13
+ "label": "起床",
14
+ "days": "每天"
15
+ },
16
+ {
17
+ "time": "08:00 - 17:30",
18
+ "label": "上班 / 上课(按自己情况写)",
19
+ "days": "工作日"
20
+ },
21
+ {
22
+ "time": "12:00 - 13:00",
23
+ "label": "午饭",
24
+ "days": "每天"
25
+ },
26
+ {
27
+ "time": "18:30 - 19:30",
28
+ "label": "晚饭",
29
+ "days": "每天"
30
+ },
31
+ {
32
+ "time": "01:30",
33
+ "label": "睡觉",
34
+ "days": "每天"
35
+ }
36
+ ],
37
+ "activities": [
38
+ {
39
+ "name": "刷 B 站 / 看视频",
40
+ "window": "",
41
+ "weight": 35,
42
+ "minMin": 10,
43
+ "maxMin": 40
44
+ },
45
+ {
46
+ "name": "玩手机",
47
+ "window": "",
48
+ "weight": 25,
49
+ "minMin": 5,
50
+ "maxMin": 20
51
+ },
52
+ {
53
+ "name": "看小说",
54
+ "window": "22:00-01:30",
55
+ "weight": 20,
56
+ "minMin": 15,
57
+ "maxMin": 60
58
+ },
59
+ {
60
+ "name": "玩游戏",
61
+ "window": "19:00-23:00",
62
+ "weight": 15,
63
+ "minMin": 10,
64
+ "maxMin": 30
65
+ },
66
+ {
67
+ "name": "发呆 / 收拾屋子",
68
+ "window": "",
69
+ "weight": 5,
70
+ "minMin": 5,
71
+ "maxMin": 15
72
+ }
73
+ ]
74
+ },
75
+ "env": {
76
+ "pc": "gui",
77
+ "space": "gui",
78
+ "phone": "gui",
79
+ "confirmDanger": true
80
+ },
81
+ "model": {
82
+ "name": "deepseek-flash",
83
+ "reasoningEffort": "low",
84
+ "temperature": 0.9,
85
+ "maxTokens": 900
86
+ },
87
+ "context": {
88
+ "historyMode": "full",
89
+ "memoryInject": 8
90
+ },
91
+ "mode": "full",
92
+ "activeCharacter": "default"
93
+ }
@@ -0,0 +1,14 @@
1
+ # env 目录(完整版辅助脚本)
2
+
3
+ 这些脚本用于「完整版」:让插件能操作宿主机的 VirtualBox 虚拟机和安卓模拟器。
4
+
5
+ - 默认会使用 `D:\dsh-env`(如果存在);不存在时使用本目录。
6
+ - 可以用环境变量覆盖:
7
+ - `DSH_COMPANION_ENV`:辅助脚本目录
8
+ - `DSH_COMPANION_PY`:Python 解释器(需要 paramiko)
9
+ - `DSH_COMPANION_ADB`:adb.exe 路径
10
+ - `DSH_COMPANION_ANDROID_SDK`:Android SDK 根目录
11
+ - `DSH_COMPANION_EMULATOR`:emulator.exe 路径
12
+ - `DSH_COMPANION_VBOX`:VBoxManage.exe 路径
13
+ - 里面的 `start_screen.ps1`、`space_ctl.py` 等与具体虚拟机名/SDK 路径有关,换机器时需要按实际环境修改。
14
+ - 设置 → 环境 → 「开始自检」可以检查缺什么。
@@ -0,0 +1,73 @@
1
+ """SSH/SFTP helper for the AI space VM (host -> guest 127.0.0.1:2222)."""
2
+ import sys, time, paramiko
3
+
4
+ HOST, PORT, USER, PASS = "127.0.0.1", 2222, "dsh", "DshSpace2026"
5
+
6
+
7
+ def connect(retries=40, delay=5):
8
+ last = None
9
+ for i in range(retries):
10
+ try:
11
+ c = paramiko.SSHClient()
12
+ c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
13
+ c.connect(HOST, port=PORT, username=USER, password=PASS,
14
+ timeout=120, banner_timeout=120, auth_timeout=120)
15
+ return c
16
+ except Exception as e:
17
+ last = e
18
+ print("connect retry %d: %s" % (i + 1, e), flush=True)
19
+ time.sleep(delay)
20
+ raise last
21
+
22
+
23
+ def run(cmd, timeout=3600):
24
+ c = connect()
25
+ try:
26
+ stdin, stdout, stderr = c.exec_command(cmd, timeout=timeout, get_pty=True)
27
+ out = b""
28
+ while True:
29
+ d = stdout.channel.recv(4096)
30
+ if not d:
31
+ break
32
+ out += d
33
+ sys.stdout.write(d.decode("utf-8", "replace"))
34
+ sys.stdout.flush()
35
+ rc = stdout.channel.recv_exit_status()
36
+ return rc
37
+ finally:
38
+ c.close()
39
+
40
+
41
+ if __name__ == "__main__":
42
+ mode = sys.argv[1] if len(sys.argv) > 1 else "run"
43
+ if mode == "run":
44
+ sys.exit(run(" ".join(sys.argv[2:])))
45
+ if mode == "runfile":
46
+ data = open(sys.argv[2], "rb").read().decode("utf-8")
47
+ c = connect()
48
+ try:
49
+ sftp = c.open_sftp()
50
+ with sftp.open("/tmp/provision.sh", "wb") as f:
51
+ f.write(data.encode("utf-8"))
52
+ sftp.chmod("/tmp/provision.sh", 0o755)
53
+ finally:
54
+ c.close()
55
+ sys.exit(run("bash /tmp/provision.sh"))
56
+ if mode == "get":
57
+ c = connect()
58
+ try:
59
+ c.open_sftp().get(sys.argv[2], sys.argv[3])
60
+ finally:
61
+ c.close()
62
+ print("downloaded", sys.argv[2])
63
+ sys.exit(0)
64
+ if mode == "put":
65
+ c = connect()
66
+ try:
67
+ c.open_sftp().put(sys.argv[2], sys.argv[3])
68
+ finally:
69
+ c.close()
70
+ print("uploaded", sys.argv[2])
71
+ sys.exit(0)
72
+ print("unknown mode", mode)
73
+ sys.exit(2)