weflow-cli 1.8.2 → 1.10.0
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/ARCHITECTURE.md +62 -26
- package/CHANGELOG.md +1290 -0
- package/OPERATIONS.md +144 -11
- package/README.en.md +20 -4
- package/README.md +30 -5
- package/bin/weflow-cli-electron.cjs +2 -0
- package/bin/weflow-cli.ts +996 -118
- package/dist/bin/weflow-cli.js +1038 -146
- package/dist/bin/weflow-cli.js.map +1 -1
- package/dist/mcp-server/http.d.ts +14 -0
- package/dist/mcp-server/http.d.ts.map +1 -0
- package/dist/mcp-server/http.js +84 -0
- package/dist/mcp-server/http.js.map +1 -0
- package/dist/mcp-server/httpConfig.d.ts +50 -0
- package/dist/mcp-server/httpConfig.d.ts.map +1 -0
- package/dist/mcp-server/httpConfig.js +104 -0
- package/dist/mcp-server/httpConfig.js.map +1 -0
- package/dist/mcp-server/index.d.ts +13 -1
- package/dist/mcp-server/index.d.ts.map +1 -1
- package/dist/mcp-server/index.js +58 -374
- package/dist/mcp-server/index.js.map +1 -1
- package/dist/src/core/dbPathService.d.ts +12 -0
- package/dist/src/core/dbPathService.d.ts.map +1 -1
- package/dist/src/core/dbPathService.js +20 -3
- package/dist/src/core/dbPathService.js.map +1 -1
- package/dist/src/panel/server.d.ts.map +1 -1
- package/dist/src/panel/server.js +61 -1
- package/dist/src/panel/server.js.map +1 -1
- package/dist/src/services/assistantEval.d.ts +69 -0
- package/dist/src/services/assistantEval.d.ts.map +1 -1
- package/dist/src/services/assistantEval.js +270 -8
- package/dist/src/services/assistantEval.js.map +1 -1
- package/dist/src/services/assistantScenes.d.ts +81 -0
- package/dist/src/services/assistantScenes.d.ts.map +1 -0
- package/dist/src/services/assistantScenes.js +322 -0
- package/dist/src/services/assistantScenes.js.map +1 -0
- package/dist/src/services/assistantService.d.ts +33 -4
- package/dist/src/services/assistantService.d.ts.map +1 -1
- package/dist/src/services/assistantService.js +102 -6
- package/dist/src/services/assistantService.js.map +1 -1
- package/dist/src/services/assistantSkills.d.ts +94 -0
- package/dist/src/services/assistantSkills.d.ts.map +1 -0
- package/dist/src/services/assistantSkills.js +330 -0
- package/dist/src/services/assistantSkills.js.map +1 -0
- package/dist/src/services/assistantTools.d.ts +34 -1
- package/dist/src/services/assistantTools.d.ts.map +1 -1
- package/dist/src/services/assistantTools.js +931 -88
- package/dist/src/services/assistantTools.js.map +1 -1
- package/dist/src/services/assistantTrace.d.ts +4 -0
- package/dist/src/services/assistantTrace.d.ts.map +1 -1
- package/dist/src/services/assistantTrace.js +9 -0
- package/dist/src/services/assistantTrace.js.map +1 -1
- package/dist/src/services/configService.d.ts +9 -0
- package/dist/src/services/configService.d.ts.map +1 -1
- package/dist/src/services/configService.js +5 -2
- package/dist/src/services/configService.js.map +1 -1
- package/dist/src/services/messageQuery.d.ts +18 -0
- package/dist/src/services/messageQuery.d.ts.map +1 -1
- package/dist/src/services/messageQuery.js +29 -0
- package/dist/src/services/messageQuery.js.map +1 -1
- package/dist/src/services/outboundSend.d.ts +53 -0
- package/dist/src/services/outboundSend.d.ts.map +1 -0
- package/dist/src/services/outboundSend.js +90 -0
- package/dist/src/services/outboundSend.js.map +1 -0
- package/dist/src/services/pythonBridge.d.ts.map +1 -1
- package/dist/src/services/pythonBridge.js +1 -0
- package/dist/src/services/pythonBridge.js.map +1 -1
- package/dist/src/services/wechatClient.d.ts +125 -0
- package/dist/src/services/wechatClient.d.ts.map +1 -0
- package/dist/src/services/wechatClient.js +344 -0
- package/dist/src/services/wechatClient.js.map +1 -0
- package/dist/src/services/wechatMedia.d.ts +44 -0
- package/dist/src/services/wechatMedia.d.ts.map +1 -0
- package/dist/src/services/wechatMedia.js +155 -0
- package/dist/src/services/wechatMedia.js.map +1 -0
- package/dist/src/services/wechatMessageService.d.ts +11 -0
- package/dist/src/services/wechatMessageService.d.ts.map +1 -1
- package/dist/src/services/wechatMessageService.js +55 -11
- package/dist/src/services/wechatMessageService.js.map +1 -1
- package/dist/src/types.d.ts +4 -0
- package/dist/src/types.d.ts.map +1 -1
- package/dist/src/utils/python.js +2 -2
- package/dist/src/utils/python.js.map +1 -1
- package/docs/BRANCHES.md +37 -0
- package/docs/CONTACT_DB_SCHEMA.md +1085 -0
- package/docs/DECISIONS.md +1624 -0
- package/docs/EXTENDING.md +98 -10
- package/docs/FUNCTIONAL-IMPROVEMENT-SPEC.md +6 -0
- package/docs/HEALTH-CHECK.md +12 -7
- package/docs/JEV/345/272/224/347/224/250/346/226/275/345/267/245/346/226/207/346/241/243.md +2 -2
- package/docs/MCP.md +58 -4
- package/docs/PROJECT_STATE.md +180 -11
- package/docs/RELEASING.md +115 -1
- package/docs/images/weflow-architecture-gpt-image-2.png +0 -0
- package/logo.png +0 -0
- package/mcp-server/http.ts +108 -0
- package/mcp-server/httpConfig.ts +126 -0
- package/mcp-server/index.ts +359 -681
- package/package.json +5 -3
- package/resources/js/graph3d/NOTICE.txt +35 -0
- package/resources/js/graph3d/d3-binarytree.min.js +2 -0
- package/resources/js/graph3d/d3-dispatch.min.js +2 -0
- package/resources/js/graph3d/d3-force-3d.min.js +2 -0
- package/resources/js/graph3d/d3-octree.min.js +2 -0
- package/resources/js/graph3d/d3-quadtree.min.js +2 -0
- package/resources/js/graph3d/d3-timer.min.js +2 -0
- package/resources/js/graph3d/layout.mjs +63 -0
- package/resources/js/graph3d/three.min.js +6 -0
- package/resources/js/graph3d/viewer2d.js +448 -0
- package/resources/panel/ball-position.cjs +48 -2
- package/resources/panel/index.html +18 -4
- package/resources/panel/main.cjs +137 -5
- package/resources/panel/mascot-base.png +0 -0
- package/resources/panel/mascot-blink-1.png +0 -0
- package/resources/panel/mascot-blink-2.png +0 -0
- package/resources/panel/mascot-iris.png +0 -0
- package/resources/panel/mascot-lift-down-1.png +0 -0
- package/resources/panel/mascot-lift-down-2.png +0 -0
- package/resources/panel/mascot-lift-down-3.png +0 -0
- package/resources/panel/mascot-lift-down-4.png +0 -0
- package/resources/panel/mascot-lift-down-5.png +0 -0
- package/resources/panel/mascot-lift-down-6.png +0 -0
- package/resources/panel/mascot-lift-up-1.png +0 -0
- package/resources/panel/mascot-lift-up-2.png +0 -0
- package/resources/panel/mascot-lift-up-3.png +0 -0
- package/resources/panel/mascot-lift-up-4.png +0 -0
- package/resources/panel/mascot-lift-up-5.png +0 -0
- package/resources/panel/mascot-lift-up-6.png +0 -0
- package/resources/panel/mascot-peek.png +0 -0
- package/resources/panel/mascot-tickle-1.png +0 -0
- package/resources/panel/mascot-tickle-2.png +0 -0
- package/resources/panel/mascot-tickle-3.png +0 -0
- package/resources/panel/mascot-tickle-4.png +0 -0
- package/resources/panel/panel.css +205 -59
- package/resources/panel/preload.cjs +26 -0
- package/resources/panel/renderer.js +646 -17
- package/scripts/_utils.py +1203 -931
- package/scripts/backfill_articles.py +202 -21
- package/scripts/biz_daily.py +26 -34
- package/scripts/bonds.py +331 -0
- package/scripts/chat_notes.py +561 -470
- package/scripts/chat_stats.py +8 -7
- package/scripts/check_readme_commands.py +426 -0
- package/scripts/compile_wiki.py +187 -59
- package/scripts/contact_schema.py +339 -0
- package/scripts/data/wechat_emoji.json +225 -0
- package/scripts/export_chat_html.py +3 -1
- package/scripts/fav_notes.py +4 -1
- package/scripts/graph_2d.py +151 -0
- package/scripts/graph_3d.py +646 -0
- package/scripts/health_check.py +163 -26
- package/scripts/mcp_bridge.py +6 -7
- package/scripts/nt_decrypt.py +3 -3
- package/scripts/panel_frames.py +683 -0
- package/scripts/pipeline.py +21 -10
- package/scripts/quality_eval.py +5 -0
- package/scripts/rag_chat.py +33 -16
- package/scripts/reply_debt.py +26 -6
- package/scripts/semantic_search.py +130 -22
- package/scripts/vault_rag.py +42 -16
- package/scripts/vault_search.py +44 -8
- package/scripts/wechat_emoticon.py +6 -0
- package/scripts/wechat_voice.py +9 -0
- package/scripts/wiki_lint.py +124 -24
- package/src/core/dbPathService.ts +22 -4
- package/src/panel/server.ts +58 -1
- package/src/services/assistantEval.ts +323 -12
- package/src/services/assistantScenes.ts +342 -0
- package/src/services/assistantService.ts +107 -6
- package/src/services/assistantSkills.ts +364 -0
- package/src/services/assistantTools.ts +2452 -1640
- package/src/services/assistantTrace.ts +9 -0
- package/src/services/configService.ts +415 -403
- package/src/services/messageQuery.ts +29 -0
- package/src/services/outboundSend.ts +126 -0
- package/src/services/pythonBridge.ts +1 -0
- package/src/services/wechatClient.ts +380 -0
- package/src/services/wechatMedia.ts +166 -0
- package/src/services/wechatMessageService.ts +62 -11
- package/src/types.ts +4 -0
- package/src/utils/python.ts +2 -2
- package/dist/src/utils/wechatEmoji.d.ts +0 -5
- package/dist/src/utils/wechatEmoji.d.ts.map +0 -1
- package/dist/src/utils/wechatEmoji.js +0 -35
- package/dist/src/utils/wechatEmoji.js.map +0 -1
package/resources/panel/main.cjs
CHANGED
|
@@ -21,6 +21,17 @@ const { trayMenuTemplate } = require('./tray-menu.cjs')
|
|
|
21
21
|
const { quickMenuTemplate } = require('./quick-menu.cjs')
|
|
22
22
|
|
|
23
23
|
const ENDPOINT_FILE = join(os.homedir(), '.weflow-cli', 'assistant_endpoint.json')
|
|
24
|
+
|
|
25
|
+
// 排查用:`WEFLOW_PANEL_DEBUG_PORT=9333` 时才开一个本机调试口(**默认关**,不设就没有)。
|
|
26
|
+
//
|
|
27
|
+
// 为什么需要它:「球看不见」这类问题我复现不了 —— 透明窗口**截图不可靠**(GDI 抓不到
|
|
28
|
+
// DWM 合成的分层窗口,拍到的往往是它后面的东西),而窗口是不是隐藏、页面算出来的
|
|
29
|
+
// `display`/`hidden` 是什么、有没有 JS 报错,全都在窗口**内部**。前两次只能靠"重启一下"
|
|
30
|
+
// 糊过去,第三次就该有个能问的入口了。开放的是 127.0.0.1 上的 CDP,和 Chrome 的调试口同类。
|
|
31
|
+
const DEBUG_PORT = Number(process.env.WEFLOW_PANEL_DEBUG_PORT || 0)
|
|
32
|
+
if (Number.isInteger(DEBUG_PORT) && DEBUG_PORT > 0) {
|
|
33
|
+
app.commandLine.appendSwitch('remote-debugging-port', String(DEBUG_PORT))
|
|
34
|
+
}
|
|
24
35
|
const POSITION_FILE = join(os.homedir(), '.weflow-cli', 'panel_position.json')
|
|
25
36
|
const COOKIE_NAME = 'weflow_panel'
|
|
26
37
|
/** 收起时气泡淡出多久。页面照着淡、主进程照着等——**只此一处**,两边不会走散。
|
|
@@ -37,6 +48,25 @@ let shortCircuitFailures = 0
|
|
|
37
48
|
*/
|
|
38
49
|
let ballAnchor = { side: 'left', anchorY: 'bottom' }
|
|
39
50
|
|
|
51
|
+
/**
|
|
52
|
+
* 半隐("躲起来"):球被推到屏幕左/右边松手时,只留 `PEEK_SIZE` 那么宽露在外面。
|
|
53
|
+
*
|
|
54
|
+
* 用户要的是一个朴素的桌宠行为:推到边上就藏起来、点一下再出来。三个决定写在明处:
|
|
55
|
+
*
|
|
56
|
+
* - **只做左右**:上下贴边在 Windows 上会跟任务栏、窗口贴边打架;而且一个圆球从上下边
|
|
57
|
+
* 半隐看着像被切了,不像"躲"。
|
|
58
|
+
* - **半隐时第一次点击只是"出来",不展开气泡**:藏起来的状态下点它,意思一定是"回来",
|
|
59
|
+
* 不是"跟我说话"。点第二下才展开。这条不用通知页面——**页面自己不记形态**(它只发请求、
|
|
60
|
+
* 照着 `panel:mode` 做),所以不展开就不会有任何状态错位。
|
|
61
|
+
* - **不持久化**:重启后是正常那张脸、离边 24px。半隐时窗口**整窗贴在边上、完全可见**
|
|
62
|
+
* (切边靠图给,窗口不出去),所以记下来的坐标是可达的——重启后球就停在屏幕边上,
|
|
63
|
+
* 不是不见了。也就是说这件事不需要标志位,也不会出"球丢了"的事故。
|
|
64
|
+
*
|
|
65
|
+
* 存的是 `{edge, area}` 而不是只存 edge:恢复要用那块工作区,而半隐时球心虽然还在屏内,
|
|
66
|
+
* 但按球心去找工作区会拿到"离球心最近的那块",不如直接记住当时用的是哪块。
|
|
67
|
+
*/
|
|
68
|
+
let hiddenAt = null
|
|
69
|
+
|
|
40
70
|
/**
|
|
41
71
|
* 跑一条 CLI 命令(目前只用来停助手)。**这里踩过一个坑,写法不能再简化**:
|
|
42
72
|
*
|
|
@@ -72,11 +102,39 @@ function spawnCli(cliArgs) {
|
|
|
72
102
|
// `--ball-size` / `--bubble-width` / `--bubble-gap`(CSS 拿不到 JS 的值),那几份由
|
|
73
103
|
// `test/panel-packaging.test.ts` 断言与本模块的这些值相等。
|
|
74
104
|
const { BALL_SIZE, EDGE_MARGIN, BUBBLE_SIZE, bubbleLayout, ballRectInWindow,
|
|
75
|
-
defaultBallPosition: defaultBall, clampInto, resolveStartPosition
|
|
105
|
+
defaultBallPosition: defaultBall, clampInto, resolveStartPosition,
|
|
106
|
+
edgeToHide, hiddenPosition, revealedPosition } = require('./ball-position.cjs')
|
|
76
107
|
|
|
77
108
|
function workAreas() { return screen.getAllDisplays().map((d) => d.workArea) }
|
|
78
109
|
function primaryWorkArea() { return screen.getPrimaryDisplay().workArea }
|
|
79
110
|
|
|
111
|
+
/**
|
|
112
|
+
* 鼠标位置的推送(给"眼珠跟着鼠标"用)。
|
|
113
|
+
*
|
|
114
|
+
* **为什么是轮询**:Electron 主进程**没有**鼠标移动事件,只有 `screen.getCursorScreenPoint()`
|
|
115
|
+
* 这个"主动去问"的 API。所以按固定间隔采样,而且**只在坐标真的变了时**才推一次——
|
|
116
|
+
* 鼠标不动时一次 IPC 都不发;鼠标在动时也只发十几分之一,剩下的交给渲染进程补间到 60fps。
|
|
117
|
+
*
|
|
118
|
+
* 间隔 120ms 是够的:眼珠那边有几百毫秒量级的缓动,采样比它快一截就行,再密只是白耗电。
|
|
119
|
+
* 窗口不可见或已销毁时**既不采样也不推**(收起来了就没人看,没必要测)。
|
|
120
|
+
*/
|
|
121
|
+
const CURSOR_POLL_MS = 120
|
|
122
|
+
let cursorTimer = null
|
|
123
|
+
let lastCursor = null
|
|
124
|
+
|
|
125
|
+
function startCursorFeed() {
|
|
126
|
+
if (cursorTimer) return
|
|
127
|
+
cursorTimer = setInterval(() => {
|
|
128
|
+
if (!win || win.isDestroyed() || !win.isVisible()) return
|
|
129
|
+
const point = screen.getCursorScreenPoint()
|
|
130
|
+
if (lastCursor && lastCursor.x === point.x && lastCursor.y === point.y) return
|
|
131
|
+
lastCursor = point
|
|
132
|
+
win.webContents.send('panel:cursor', { x: point.x, y: point.y })
|
|
133
|
+
}, CURSOR_POLL_MS)
|
|
134
|
+
// 别让这个定时器成为"进程还活着的理由"——它只是画面装饰,不该拖着 app 不退
|
|
135
|
+
if (typeof cursorTimer.unref === 'function') cursorTimer.unref()
|
|
136
|
+
}
|
|
137
|
+
|
|
80
138
|
/** 小球该在哪儿。**默认放右下角**——从前不给 `x/y`,于是它落在 Windows 顺手给的位置
|
|
81
139
|
* (本机实测 815,418,屏幕中偏左),那不叫"屏幕角落一个球"。 */
|
|
82
140
|
function defaultBallPosition() { return defaultBall(primaryWorkArea(), BALL_SIZE, EDGE_MARGIN) }
|
|
@@ -138,6 +196,31 @@ function schedulePositionSave() {
|
|
|
138
196
|
saveTimer = setTimeout(flushPosition, 400)
|
|
139
197
|
}
|
|
140
198
|
|
|
199
|
+
/** 拖球松手:贴到左右边就藏进去。没贴边什么都不做。 */
|
|
200
|
+
function maybeHideAtEdge() {
|
|
201
|
+
if (!ballMode || !win || win.isDestroyed()) return
|
|
202
|
+
const from = win.getContentBounds()
|
|
203
|
+
const hit = edgeToHide({ x: from.x, y: from.y }, workAreas())
|
|
204
|
+
if (!hit) return
|
|
205
|
+
const to = hiddenPosition({ x: from.x, y: from.y }, hit.edge, hit.area)
|
|
206
|
+
hiddenAt = { edge: hit.edge, area: hit.area }
|
|
207
|
+
win.setContentBounds({ x: to.x, y: to.y, width: BALL_SIZE, height: BALL_SIZE })
|
|
208
|
+
// 换姿势这件事只有主进程知道(它才管窗口贴哪条边),所以由它告诉页面:
|
|
209
|
+
// 页面照 `ball-peek` / `ball-peek-right` 换成探头那张脸、并摘掉圆角。
|
|
210
|
+
win.webContents.send('panel:mode', { mode: 'ball', peek: hit.edge, done: true })
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** 从半隐出来:完全露出、留出平时的边距,并记一次位置。 */
|
|
214
|
+
function revealBall() {
|
|
215
|
+
if (!hiddenAt || !win || win.isDestroyed()) return
|
|
216
|
+
const from = win.getContentBounds()
|
|
217
|
+
const to = revealedPosition({ x: from.x, y: from.y }, hiddenAt.edge, hiddenAt.area)
|
|
218
|
+
hiddenAt = null
|
|
219
|
+
savePosition(to.x, to.y)
|
|
220
|
+
win.setContentBounds({ x: to.x, y: to.y, width: BALL_SIZE, height: BALL_SIZE })
|
|
221
|
+
win.webContents.send('panel:mode', { mode: 'ball', peek: null, done: true })
|
|
222
|
+
}
|
|
223
|
+
|
|
141
224
|
/** 读端点文件。**任何一种不可信都当没读出来**(同 `src/panel/endpoint.ts` 的纪律) */
|
|
142
225
|
function readEndpoint() {
|
|
143
226
|
try {
|
|
@@ -272,7 +355,21 @@ async function buildWindow() {
|
|
|
272
355
|
* (`fade` → 页面开始淡出;`done` → 页面换成球形态),因为窗口要等淡完才能缩,而页面
|
|
273
356
|
* 要等窗口缩完才能把气泡摘掉——晚一帧就会在球那 96x96 的窗口里看见一条气泡的边。
|
|
274
357
|
*/
|
|
275
|
-
|
|
358
|
+
/**
|
|
359
|
+
* 等渲染进程真的画完一帧。
|
|
360
|
+
*
|
|
361
|
+
* **不能只 `setTimeout`**:那是猜它画完了没有,机器忙的时候会猜错 —— 而猜错的后果就是退回成
|
|
362
|
+
* "球先闪一下"那个原始 bug。`executeJavaScript` 能把页面里 rAF 的结果等回来;再加一个上限,
|
|
363
|
+
* 页面没就绪时宁可漏一次(球闪一下)也不能把展开卡住。
|
|
364
|
+
*/
|
|
365
|
+
function nextPaint(target, timeoutMs = 120) {
|
|
366
|
+
const painted = target.webContents
|
|
367
|
+
.executeJavaScript('new Promise(r => requestAnimationFrame(() => requestAnimationFrame(() => r(1))))')
|
|
368
|
+
.catch(() => 1)
|
|
369
|
+
return Promise.race([painted, new Promise((resolve) => setTimeout(resolve, timeoutMs))])
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function setMode(mode, opts) {
|
|
276
373
|
if (!win) return
|
|
277
374
|
const chat = mode === 'chat'
|
|
278
375
|
|
|
@@ -293,8 +390,25 @@ function setMode(mode, opts) {
|
|
|
293
390
|
ballAnchor = { side: layout.side, anchorY: layout.anchorY }
|
|
294
391
|
win.setAlwaysOnTop(false) // 对话时不必压着别的窗口
|
|
295
392
|
win.setSkipTaskbar(false)
|
|
393
|
+
win.setResizable(true) // 展开后可调大小(要在 setContentBounds 之前,见函数开头那段)
|
|
394
|
+
|
|
395
|
+
// **先摆锚、等页面画完,最后才改窗口尺寸。**(2026-09-29 用户报"点一下球先闪到别处再闪回来")
|
|
396
|
+
//
|
|
397
|
+
// 顺序反过来就是那个 bug:窗口先变大的那一两帧里,球还按**旧锚**画 —— 而球的锚在展开后
|
|
398
|
+
// 要换角(气泡在上方时球得钉在窗口上缘、气泡在右边时球得钉在左缘),默认锚是"右下"。
|
|
399
|
+
// 于是球被画在新窗口的另一个角上:实测纵向差 (气泡高 − 球高),肉眼就是"跳一下再回来"。
|
|
400
|
+
//
|
|
401
|
+
// 折叠那条路早就是这么做的(先把 `panel:mode` 发出去,等一帧再缩窗口,见下面 shrink 那段),
|
|
402
|
+
// 展开这条漏了同一件事 —— 两半本来就该对称。
|
|
403
|
+
//
|
|
404
|
+
// 为什么要发**两条**消息:第一条只摆锚(`anchorOnly`),第二条才切形态。切早了气泡会显出来,
|
|
405
|
+
// 而那会儿窗口还是 96px,气泡正好盖住球 —— 那是同一个坑的另一面。
|
|
406
|
+
win.webContents.send('panel:mode', {
|
|
407
|
+
mode: 'chat', side: layout.side, anchorY: layout.anchorY,
|
|
408
|
+
bubbleHeight: layout.bubbleHeight, anchorOnly: true,
|
|
409
|
+
})
|
|
410
|
+
await nextPaint(win)
|
|
296
411
|
win.setContentBounds(layout.window)
|
|
297
|
-
win.setResizable(true) // 展开后可调大小
|
|
298
412
|
// **展开之后要自己走到前面来。** 对话形态按设计不置顶,而"谁在最前面"此前完全靠运气:
|
|
299
413
|
// 右键那条路尤其明显——球是置顶的、看得见,可原生菜单一关,Windows 可能把焦点还给了
|
|
300
414
|
// 别的窗口,于是候选出在一个被压在后面的窗口里,用户的原话是"不知道消息返回到哪里了"。
|
|
@@ -377,7 +491,18 @@ if (!app.requestSingleInstanceLock()) {
|
|
|
377
491
|
app.on('second-instance', () => { win?.show(); win?.focus() })
|
|
378
492
|
|
|
379
493
|
app.whenReady().then(async () => {
|
|
380
|
-
|
|
494
|
+
// 眼珠跟着鼠标:主进程按 120ms 采样光标、只在变化时推。**不收发凭据、不出机器**。
|
|
495
|
+
startCursorFeed()
|
|
496
|
+
|
|
497
|
+
ipcMain.handle('panel:setMode', (_event, mode, opts) => {
|
|
498
|
+
// 半隐时**第一次点击只是"出来"**,不展开气泡(理由见 `hiddenAt` 那段注释)。
|
|
499
|
+
// 页面不记形态,所以这一次不展开不会留下任何不一致。
|
|
500
|
+
if (hiddenAt) {
|
|
501
|
+
revealBall()
|
|
502
|
+
return { revealed: true }
|
|
503
|
+
}
|
|
504
|
+
return setMode(mode, opts)
|
|
505
|
+
})
|
|
381
506
|
|
|
382
507
|
// 右键"快速回复"。**用原生菜单**:它画在窗口外面,所以球那个小窗口不用先展开
|
|
383
508
|
// (页内菜单做不到这一点——它会被窗口裁掉)。选中回一个名字,关掉没选回 null。
|
|
@@ -403,6 +528,9 @@ if (!app.requestSingleInstanceLock()) {
|
|
|
403
528
|
let dragOrigin = null
|
|
404
529
|
ipcMain.handle('panel:dragStart', (_event, point) => {
|
|
405
530
|
if (!win || win.isDestroyed()) return null
|
|
531
|
+
// 拖一个藏着的球 = 把它拉出来。**必须在读 bounds 之前**:`revealBall` 会挪窗口,
|
|
532
|
+
// 而下面的 `dragOrigin` 是拿窗口位置算的——先挪再记,球才不会在光标底下跳一下。
|
|
533
|
+
revealBall()
|
|
406
534
|
const from = win.getContentBounds()
|
|
407
535
|
// **尺寸也在这里记一次**(不只是位置)。理由见 dragMove 那段注释:
|
|
408
536
|
// 每次移动去读"当前尺寸"会在**可缩放**的窗口上让尺寸跟着位移一起长。
|
|
@@ -434,7 +562,11 @@ if (!app.requestSingleInstanceLock()) {
|
|
|
434
562
|
})
|
|
435
563
|
return null
|
|
436
564
|
})
|
|
437
|
-
ipcMain.handle('panel:dragEnd', () => {
|
|
565
|
+
ipcMain.handle('panel:dragEnd', () => {
|
|
566
|
+
dragOrigin = null
|
|
567
|
+
maybeHideAtEdge() // 松手时贴到左右边就藏进去
|
|
568
|
+
return null
|
|
569
|
+
})
|
|
438
570
|
ipcMain.handle('panel:info', () => ({ daemonRunning: !!readEndpoint(), endpointFile: ENDPOINT_FILE }))
|
|
439
571
|
/**
|
|
440
572
|
* 凭据失效时自愈:重读端点文件(守护进程每次启动都换 token),刷新 cookie 并重载。
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -104,7 +104,7 @@ header {
|
|
|
104
104
|
浏览器降级那条路靠这条 `display: none` 永远看不见球(那一侧 `hidden` 属性也还在)。 */
|
|
105
105
|
display: none;
|
|
106
106
|
/* 固定尺寸、钉在窗口的某个角上(下面的 shell 规则给角):**尺寸不跟着窗口长**,
|
|
107
|
-
否则展开过程中会看到一只被拉成巨型的猫。球形态下窗口就是
|
|
107
|
+
否则展开过程中会看到一只被拉成巨型的猫。球形态下窗口就是 96×96(= `--ball-size`),铺满即整窗。 */
|
|
108
108
|
position: fixed;
|
|
109
109
|
width: var(--ball-size);
|
|
110
110
|
height: var(--ball-size);
|
|
@@ -116,7 +116,7 @@ header {
|
|
|
116
116
|
/* **不要在这里写 `-webkit-app-region: drag`**:Windows 上拖拽区会吞掉鼠标事件,
|
|
117
117
|
页面收不到 click,球就点不开了(实测的故障就是这个)。拖动自己实现,见 renderer.js。 */
|
|
118
118
|
|
|
119
|
-
/*
|
|
119
|
+
/* 球本身**没有底色**(`.glow` 那层已在 2026-09-29 删掉,现在球上只有吉祥物这一层)。
|
|
120
120
|
**换 `mascot.png` 时:内容必须全部落在内切圆里。** 球(连 `.face` 自己)都是
|
|
121
121
|
`border-radius: 50%`,图会被裁成圆的——而这张图的内容一向是**铺满画布**的
|
|
122
122
|
(2026-09-27 那版源图 1254²,内容占 98%),直接铺上去,角上的东西就没了:
|
|
@@ -156,31 +156,69 @@ body.shell.bubble-right #ball { right: auto; left: 0; }
|
|
|
156
156
|
body.shell.anchor-top #ball { bottom: auto; top: 0; }
|
|
157
157
|
#ball > span { position: absolute; inset: 0; border-radius: 50%; pointer-events: none; }
|
|
158
158
|
|
|
159
|
-
|
|
160
|
-
background: none;
|
|
161
|
-
}
|
|
159
|
+
/* **球背后什么都没有**(2026-09-29 彻底去掉)。
|
|
162
160
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
161
|
+
这条线索两进两退,都记在这里,免得再来一次:
|
|
162
|
+
- 最早这层**一直**亮着(用户当初要的"会变动的背景");
|
|
163
|
+
- 2026-09-26 缩到"只在有状态时画",理由写的是"球背后永远有一圈彩色圆盘,'这张图是
|
|
164
|
+
透明的'根本看不出来";
|
|
165
|
+
- 2026-09-29 用户**又提了一次一模一样的话**("思考的时候背景不是透明的")—— 剩下的
|
|
166
|
+
就是"忙"时那一圈。同一个意见提两次,说明它要的不是"少画一点",是**不要**。
|
|
167
|
+
所以整层拿掉:元素、三条状态规则、`--hue`、`drift` 与 `breathe` 两个动画一起删。
|
|
168
|
+
|
|
169
|
+
**状态灯没有丢**:四个状态本来就各有一张脸(忙=思考、连不上=委屈、额度用完=累、
|
|
170
|
+
空闲=原图),那才是主信号;光晕只是"余光能看见"的补充。想再要一个不占背景的提示,
|
|
171
|
+
应该做成**外圈**(`box-shadow` 之类画在球外面),而不是再往猫背后放一层盘子。 */
|
|
175
172
|
|
|
176
|
-
/*
|
|
173
|
+
/* 上面那层:吉祥物本体,形状不变。
|
|
174
|
+
**注意是 `mascot-base.png` 而不是 `mascot.png`**:前者是把虹膜挖掉的版本(挖掉的地方填成
|
|
175
|
+
眼白),专门给"眼球跟着鼠标动"用的;后者眼球是画死的,拿它当底会看到两个瞳仁
|
|
176
|
+
(一个跟着动、一个不动)。两张图零偏移叠起来与原图**逐像素相同**,所以换底不改变外观。 */
|
|
177
177
|
#ball .face {
|
|
178
|
-
background-image: url('/panel/mascot.png');
|
|
178
|
+
background-image: url('/panel/mascot-base.png');
|
|
179
|
+
background-size: 100%;
|
|
180
|
+
background-position: center;
|
|
181
|
+
background-repeat: no-repeat;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/* 虹膜层:**唯一会动的那一层**,`renderer.js` 按鼠标方向给它 translate。
|
|
185
|
+
与 `.face` 同一张画布(256²)铺满,所以偏移量给对了它就还在原来的位置上。
|
|
186
|
+
只动 transform —— 合成器直接做,不重排、不重绘;这是这个效果几乎不耗电的原因。 */
|
|
187
|
+
#ball .iris {
|
|
188
|
+
background-image: url('/panel/mascot-iris.png');
|
|
179
189
|
background-size: 100%;
|
|
180
190
|
background-position: center;
|
|
181
191
|
background-repeat: no-repeat;
|
|
192
|
+
will-change: transform;
|
|
182
193
|
}
|
|
183
194
|
|
|
195
|
+
/* **另外四张脸的眼睛是画死的**(瞳孔与高光都在图上),往上叠一层会动的虹膜就会看到
|
|
196
|
+
两个瞳仁。所以那几张脸出现时把虹膜层藏掉 —— 于是球只在"空闲"和"被捏"两种情况下有眼神,
|
|
197
|
+
忙 / 连不上 / 额度用完时不动。**这是刻意的**:那三种状态本来就该用整张脸说话,
|
|
198
|
+
而且它们恰恰是"不空闲"的时候,眼睛还在追着鼠标看反而不对。 */
|
|
199
|
+
body.busy #ball .iris,
|
|
200
|
+
body.offline #ball .iris,
|
|
201
|
+
body.quota #ball .iris,
|
|
202
|
+
body.ball-happy #ball .iris { display: none; }
|
|
203
|
+
|
|
204
|
+
/* ---- 半隐:躲在屏幕边后面探头(`mascot-peek.png`) ----
|
|
205
|
+
|
|
206
|
+
这张图的左边**是一条笔直的切边**(内容贴着画布 x=0),放上去时窗口的左边就是屏幕的边,
|
|
207
|
+
于是看着就像猫被显示器边框挡住了。两条随之而来的规矩:
|
|
208
|
+
|
|
209
|
+
1. **必须去掉球的圆角**:`border-radius: 50%` 会把那条直切边剪成一段弧,一看就露馅。
|
|
210
|
+
2. **悬停不许放大**:`scale(1.02)` 会让切边离开屏幕边 1%(在 96px 上是 1px)——那 1px
|
|
211
|
+
就是一条缝。所以半隐时把放大那两条关掉。
|
|
212
|
+
|
|
213
|
+
右边那张**不另存素材**,把同一张水平镜像即可。 */
|
|
214
|
+
body.ball-peek #ball { border-radius: 0; }
|
|
215
|
+
body.ball-peek #ball .face { background-image: url('/panel/mascot-peek.png'); }
|
|
216
|
+
body.ball-peek-right #ball .face { transform: scaleX(-1); }
|
|
217
|
+
/* 这张脸的眼睛是画死的(同忙/累那几张),叠会动的虹膜就是两个瞳仁 */
|
|
218
|
+
body.ball-peek #ball .iris { display: none; }
|
|
219
|
+
body.ball-peek #ball:hover,
|
|
220
|
+
body.ball-peek #ball:active { transform: none; }
|
|
221
|
+
|
|
184
222
|
/* 三个**真状态**各有一张脸:忙(思考中)、连不上时委屈、额度用完时累。
|
|
185
223
|
接的全是现成的状态类(`setBallState` 早就挂 busy/offline/quota 了),**不新增状态机**;
|
|
186
224
|
光晕照旧画——它是"余光能看见"的环境灯,表情是盯着看才见的细节,两者说的是同一件事,
|
|
@@ -207,54 +245,72 @@ body.ball-happy #ball .face {
|
|
|
207
245
|
background-image: url('/panel/mascot-happy.png');
|
|
208
246
|
}
|
|
209
247
|
|
|
210
|
-
/*
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
248
|
+
/* ---- 被拎起来那一下(6 帧硬切) ----
|
|
249
|
+
|
|
250
|
+
按下球时它像被拎住后颈提起、松手落回。**硬切,不做淡入** —— 同上面 ball-happy 那条的理由:
|
|
251
|
+
换帧本来就是一帧的事,淡入反而像"图片正在加载"。
|
|
252
|
+
|
|
253
|
+
按住期间它同时**在被拎着左右摆**(摆动幅度与朝哪一侧偏由 JS 算,见 `renderer.js` 里那一节;
|
|
254
|
+
晃起来从**按下那一刻**就开始,不必先拖动),松手才停。下面第 1 条同时也是"摆多大都不会被裁"的依据。
|
|
255
|
+
|
|
256
|
+
五条规矩,每条都对应一处会静默出错的地方:
|
|
257
|
+
|
|
258
|
+
1. **必须写在 `body.ball-happy` 之后**:同特异性时后写的赢,而"正在被拎"比"被捏了一下"
|
|
259
|
+
更该被看见(按下那一下这两个类同时挂着)。
|
|
260
|
+
2. **动画期间把虹膜层藏掉**:这五帧的眼睛是画死的,叠上会动的虹膜就是两个瞳仁 ——
|
|
261
|
+
与忙 / 连不上 / 额度用完那几张同理。
|
|
262
|
+
3. **动画期间连悬停放大与按下压扁一起停**:帧本身已在逐帧收小(最小那帧比静止帧小 8%),
|
|
263
|
+
再叠 `scale(1.02)` 与 `:active` 的 `scale(.92)` 就成了缩两回,看起来是"躲开"而不是
|
|
264
|
+
"被提起来"。这条的选择器特异性高于那两条,所以能压住它们。
|
|
265
|
+
4. **切帧只换 `background-image`**:不动尺寸、不动位置,所以不会跳。
|
|
266
|
+
5. **过渡要短(45ms)**:这条 transform 现在不只是"按下那一下的姿势" —— 按住期间它一直在**左右摆**
|
|
267
|
+
(`dangleTick` 每 16ms 写一个新角度)。90ms 那版是照着"一次性倾斜"写的:一阶滞后在 900ms 的
|
|
268
|
+
周期上会吃掉约 15% 的幅度、再把画面拖后约 90ms,也就是**看到的比算出来的小一圈**,
|
|
269
|
+
而这条测试看不见(测试读的是 `--dangle-deg` 这个属性,不是渲染结果)。 */
|
|
270
|
+
body.ball-lift #ball { transform: rotate(var(--dangle-deg, 0deg)); transition: transform 45ms linear; }
|
|
271
|
+
body.ball-lift #ball .iris { display: none; }
|
|
272
|
+
body.ball-lift-up-1 #ball .face { background-image: url('/panel/mascot-lift-up-1.png'); }
|
|
273
|
+
body.ball-lift-up-2 #ball .face { background-image: url('/panel/mascot-lift-up-2.png'); }
|
|
274
|
+
body.ball-lift-up-3 #ball .face { background-image: url('/panel/mascot-lift-up-3.png'); }
|
|
275
|
+
body.ball-lift-up-4 #ball .face { background-image: url('/panel/mascot-lift-up-4.png'); }
|
|
276
|
+
body.ball-lift-up-5 #ball .face { background-image: url('/panel/mascot-lift-up-5.png'); }
|
|
277
|
+
body.ball-lift-up-6 #ball .face { background-image: url('/panel/mascot-lift-up-6.png'); }
|
|
278
|
+
body.ball-lift-down-1 #ball .face { background-image: url('/panel/mascot-lift-down-1.png'); }
|
|
279
|
+
body.ball-lift-down-2 #ball .face { background-image: url('/panel/mascot-lift-down-2.png'); }
|
|
280
|
+
body.ball-lift-down-3 #ball .face { background-image: url('/panel/mascot-lift-down-3.png'); }
|
|
281
|
+
body.ball-lift-down-4 #ball .face { background-image: url('/panel/mascot-lift-down-4.png'); }
|
|
282
|
+
body.ball-lift-down-5 #ball .face { background-image: url('/panel/mascot-lift-down-5.png'); }
|
|
283
|
+
body.ball-lift-down-6 #ball .face { background-image: url('/panel/mascot-lift-down-6.png'); }
|
|
284
|
+
|
|
285
|
+
/* ---- 挠痒痒(2026-10-02,倾斜已去掉) ----
|
|
286
|
+
|
|
287
|
+
鼠标在球身上划过就扭:4 张生图姿势帧循环(只负责表情与爪子的小动作),停下 260ms 收。
|
|
288
|
+
|
|
289
|
+
**这里曾经还有一条 `transform: rotate(var(--tickle-deg))`,2026-10-02 去掉了。** 倾斜与"被拎起来"
|
|
290
|
+
走的是同一个视觉通道(都是 `#ball` 上的 rotate),于是随手扫过看起来像一次小号起飞。用户的报告:
|
|
291
|
+
「为什么鼠标划动也会晃,而不是在起飞状态才会晃」—— 现在**旋转只归被拎那条**,划过只有帧、不转动。
|
|
292
|
+
两条规矩:
|
|
293
|
+
|
|
294
|
+
1. **必须写在这四条脸规则之后**:同特异性时后写的赢,挠痒痒的姿势要压过空闲脸。
|
|
295
|
+
2. **藏掉虹膜层**:这 4 帧的眼睛是画死的,叠上会动的虹膜就是两个瞳仁。
|
|
296
|
+
|
|
297
|
+
(反向断言在 `test/panel-lift-frames.test.ts`:挠痒痒那块 CSS 里不许再出现 rotate。) */
|
|
298
|
+
body.ball-tickle #ball .iris { display: none; }
|
|
299
|
+
body.ball-tickle-1 #ball .face { background-image: url('/panel/mascot-tickle-1.png'); }
|
|
300
|
+
body.ball-tickle-2 #ball .face { background-image: url('/panel/mascot-tickle-2.png'); }
|
|
301
|
+
body.ball-tickle-3 #ball .face { background-image: url('/panel/mascot-tickle-3.png'); }
|
|
302
|
+
body.ball-tickle-4 #ball .face { background-image: url('/panel/mascot-tickle-4.png'); }
|
|
230
303
|
|
|
231
304
|
/* ---------------------------------------------------------------- 状态 */
|
|
232
305
|
|
|
233
|
-
/*
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
/* 8 秒转一圈色相(不是 3 秒):呼吸的节奏交给 1.5s,色相再快就变成闪了 */
|
|
237
|
-
animation: drift 8s linear infinite, breathe 1.5s ease-in-out infinite;
|
|
238
|
-
}
|
|
239
|
-
@keyframes breathe {
|
|
240
|
-
0%, 100% { transform: scale(1); opacity: 1; }
|
|
241
|
-
50% { transform: scale(1.08); opacity: .82; }
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/* 连不上 / 出错:换成暖色并停住流动——不动的东西才会被注意到 */
|
|
245
|
-
body.offline #ball { --hue: 12; }
|
|
246
|
-
body.offline #ball .glow { animation: none; opacity: .9; }
|
|
247
|
-
|
|
248
|
-
/* 今天的额度用完了:琥珀色 */
|
|
249
|
-
body.quota #ball { --hue: 38; }
|
|
250
|
-
body.quota #ball .glow { animation: none; opacity: .85; }
|
|
306
|
+
/* 状态的表达**全在那四张脸上**(见上面 `.face` 那几条):忙=思考、连不上=委屈、
|
|
307
|
+
额度用完=累、空闲=原图。原先这里还有一层会变色/呼吸的光晕,2026-09-29 按用户要求
|
|
308
|
+
整层去掉了——理由写在上一条注释里(同一个意见提了两次)。 */
|
|
251
309
|
|
|
252
310
|
/* 系统说"减少动态效果"就照办:停掉所有流动,只留静止的光晕。
|
|
253
311
|
展开/收起那两段也一起停(下面那几条,**写在原来那条之后**——测试盯的是这一段开头
|
|
254
312
|
240 个字符里就有 `animation: none`,把新规则插在前面会把它挤出窗口)。 */
|
|
255
313
|
@media (prefers-reduced-motion: reduce) {
|
|
256
|
-
#ball .glow,
|
|
257
|
-
body.busy #ball .glow { animation: none; }
|
|
258
314
|
body.mode-chat #bubble { animation: none; }
|
|
259
315
|
body.mode-chat.closing #bubble { animation: none; opacity: 0; }
|
|
260
316
|
#ball { transition: none; }
|
|
@@ -345,6 +401,41 @@ main {
|
|
|
345
401
|
|
|
346
402
|
/* 助手回答下的复制按钮。**常显**而不是 hover 才出现:这是个常驻小窗,
|
|
347
403
|
鼠标常常不在它上面,hover 等于没有。做得很淡,不跟正文抢注意力。 */
|
|
404
|
+
/* "思考过程":挂在助手那条回答下面,**默认折叠**。
|
|
405
|
+
摊开会把每次回答都变成一屏日志,而不想看的人还得滚过它;想核对"它到底查没查"的人
|
|
406
|
+
点一下展开。视觉上刻意压暗压小——它是附注,不是回答的一部分。 */
|
|
407
|
+
.thinking {
|
|
408
|
+
margin-top: 6px;
|
|
409
|
+
font-size: 12px;
|
|
410
|
+
color: var(--dim);
|
|
411
|
+
}
|
|
412
|
+
.thinking > summary {
|
|
413
|
+
cursor: pointer;
|
|
414
|
+
list-style: none; /* 默认那个三角在深色底上很难看,自己画一个 */
|
|
415
|
+
user-select: none;
|
|
416
|
+
}
|
|
417
|
+
.thinking > summary::before {
|
|
418
|
+
content: '▸ ';
|
|
419
|
+
display: inline-block;
|
|
420
|
+
transition: transform .12s ease;
|
|
421
|
+
}
|
|
422
|
+
.thinking[open] > summary::before { transform: rotate(90deg); }
|
|
423
|
+
.thinking > summary:hover { color: var(--text); }
|
|
424
|
+
.thinking-line {
|
|
425
|
+
margin: 4px 0 0 14px;
|
|
426
|
+
font-family: ui-monospace, Consolas, monospace;
|
|
427
|
+
white-space: pre-wrap;
|
|
428
|
+
word-break: break-word;
|
|
429
|
+
opacity: .85;
|
|
430
|
+
}
|
|
431
|
+
/* 模型真返回的推理内容(多数模型不给,所以这一段常常根本不出现) */
|
|
432
|
+
.thinking-reasoning {
|
|
433
|
+
border-left: 2px solid var(--line);
|
|
434
|
+
padding-left: 8px;
|
|
435
|
+
font-family: inherit;
|
|
436
|
+
opacity: .75;
|
|
437
|
+
}
|
|
438
|
+
|
|
348
439
|
.turn .copy {
|
|
349
440
|
display: block;
|
|
350
441
|
margin-top: 6px;
|
|
@@ -422,3 +513,58 @@ footer {
|
|
|
422
513
|
font-size: 12px;
|
|
423
514
|
}
|
|
424
515
|
footer .warn { color: var(--warn); }
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
/* ---- "它记住了什么"(只读) ----
|
|
519
|
+
|
|
520
|
+
盖在对话之上的一层:记忆是"关于我"的东西,与某一句问答不同,混进对话流里会让人以为
|
|
521
|
+
那是这一轮说的话。只读、不经过模型 —— `/api/memory` 只返回数据,页面只渲染。
|
|
522
|
+
写在整个文件末尾是有意的:那条 reduced-motion 断言看的是它自己那段开头 240 个字符,
|
|
523
|
+
往前面插规则会把它挤出窗口。 */
|
|
524
|
+
#memory-panel {
|
|
525
|
+
position: absolute;
|
|
526
|
+
inset: 0;
|
|
527
|
+
display: flex;
|
|
528
|
+
flex-direction: column;
|
|
529
|
+
background: var(--panel);
|
|
530
|
+
border-radius: var(--bubble-radius);
|
|
531
|
+
z-index: 3;
|
|
532
|
+
}
|
|
533
|
+
#memory-panel[hidden] { display: none; }
|
|
534
|
+
.memhead {
|
|
535
|
+
display: flex;
|
|
536
|
+
align-items: center;
|
|
537
|
+
gap: 8px;
|
|
538
|
+
padding: 10px 12px;
|
|
539
|
+
border-bottom: 1px solid var(--line);
|
|
540
|
+
}
|
|
541
|
+
.memhead .status { flex: 1; }
|
|
542
|
+
.memlist {
|
|
543
|
+
flex: 1;
|
|
544
|
+
overflow-y: auto;
|
|
545
|
+
padding: 10px 12px;
|
|
546
|
+
display: flex;
|
|
547
|
+
flex-direction: column;
|
|
548
|
+
gap: 10px;
|
|
549
|
+
}
|
|
550
|
+
.memlist .fact { background: var(--bg); border: 1px solid var(--line); border-radius: 10px; padding: 8px 10px; }
|
|
551
|
+
.memlist .fact p { margin: 0; }
|
|
552
|
+
.memlist .muted { color: var(--dim); font-size: 12px; }
|
|
553
|
+
.memlist .fact .muted { margin-top: 4px; }
|
|
554
|
+
|
|
555
|
+
/* ---- 答完了高兴一下 + 偶尔眨一下眼(2026-10-04) ----
|
|
556
|
+
|
|
557
|
+
两条都由 `renderer.js` 判"什么时候不播",这里只管长什么样。
|
|
558
|
+
|
|
559
|
+
跳:**没有生图帧**,是位移 + 轻微缩放 + 现有的眯眼笑脸 —— 模型画"蹲下去"一定会把身体压扁,
|
|
560
|
+
而跳本来就只是位移。空中那步的 0.96 不是随手取的:内容到球心 125.25 / 圆半径 128,
|
|
561
|
+
缩到 0.96 之后才抬得起那 7 像素(120.2 + 7 = 127.2 < 128)。改数之前先重算这条。
|
|
562
|
+
|
|
563
|
+
眨眼:2 帧,**眼睛以外与静止帧逐像素相同**(出图后只把眼睛带贴回来),所以藏虹膜层就够,
|
|
564
|
+
不会整颗球闪一下。 */
|
|
565
|
+
body.ball-hop #ball { transform: translateY(var(--hop-y, 0px)) scale(var(--hop-scale, 1)); transition: transform 95ms ease-out; }
|
|
566
|
+
body.ball-hop-happy #ball .face { background-image: url('/panel/mascot-happy.png'); }
|
|
567
|
+
body.ball-hop #ball .iris { display: none; }
|
|
568
|
+
body.ball-blink #ball .iris { display: none; }
|
|
569
|
+
body.ball-blink-1 #ball .face { background-image: url('/panel/mascot-blink-1.png'); }
|
|
570
|
+
body.ball-blink-2 #ball .face { background-image: url('/panel/mascot-blink-2.png'); }
|
|
@@ -73,7 +73,33 @@ contextBridge.exposeInMainWorld('weflowPanel', {
|
|
|
73
73
|
anchorY: payload && payload.anchorY === 'top' ? 'top' : 'bottom',
|
|
74
74
|
bubbleHeight: clampInt(payload && payload.bubbleHeight, 10000, 1),
|
|
75
75
|
fadeMs: clampInt(payload && payload.fadeMs, 2000),
|
|
76
|
+
// 半隐:'left' / 'right' / null(null = 正常那张脸)。只认这两个字符串,
|
|
77
|
+
// 别的一律当 null —— 和上面几个字段同一条规矩,不把主进程给的东西原样透传。
|
|
78
|
+
peek: payload && (payload.peek === 'left' || payload.peek === 'right') ? payload.peek : null,
|
|
76
79
|
done: !!(payload && payload.done),
|
|
77
80
|
}))
|
|
78
81
|
},
|
|
82
|
+
/**
|
|
83
|
+
* 鼠标在**屏幕**上的位置,用来做"眼珠跟着鼠标"。
|
|
84
|
+
*
|
|
85
|
+
* **为什么必须由主进程给**:球只有 96×96,鼠标绝大多数时间在窗口**外面**,
|
|
86
|
+
* 渲染进程的 `mousemove` 收不到任何东西。`screen.getCursorScreenPoint()` 只有主进程有。
|
|
87
|
+
* 主进程按固定间隔采样、**只在坐标变了的时候**推一次(见 main.cjs 那段)。
|
|
88
|
+
*
|
|
89
|
+
* 载荷照上面的规矩收窄:两个整数、夹进屏幕坐标的合理范围;坏载荷**直接丢掉**,
|
|
90
|
+
* 不交给页面。它只在**本机进程内**走(主进程 → 这个窗口的渲染进程),不出机器、不进日志。
|
|
91
|
+
*/
|
|
92
|
+
onCursor: (cb) => {
|
|
93
|
+
if (typeof cb !== 'function') return
|
|
94
|
+
const clamp = (value) => {
|
|
95
|
+
const n = Math.round(Number(value))
|
|
96
|
+
return Number.isFinite(n) ? Math.min(Math.max(n, -32768), 32767) : null
|
|
97
|
+
}
|
|
98
|
+
ipcRenderer.on('panel:cursor', (_event, payload) => {
|
|
99
|
+
const x = clamp(payload && payload.x)
|
|
100
|
+
const y = clamp(payload && payload.y)
|
|
101
|
+
if (x === null || y === null) return
|
|
102
|
+
cb({ x, y })
|
|
103
|
+
})
|
|
104
|
+
},
|
|
79
105
|
})
|