weflow-cli 1.8.1 → 1.9.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.
Files changed (100) hide show
  1. package/CHANGELOG.md +359 -0
  2. package/OPERATIONS.md +79 -0
  3. package/README.en.md +9 -2
  4. package/README.md +12 -2
  5. package/bin/weflow-cli-electron.cjs +2 -0
  6. package/bin/weflow-cli.ts +648 -79
  7. package/dist/bin/weflow-cli.js +675 -78
  8. package/dist/bin/weflow-cli.js.map +1 -1
  9. package/dist/mcp-server/http.d.ts +14 -0
  10. package/dist/mcp-server/http.d.ts.map +1 -0
  11. package/dist/mcp-server/http.js +84 -0
  12. package/dist/mcp-server/http.js.map +1 -0
  13. package/dist/mcp-server/httpConfig.d.ts +50 -0
  14. package/dist/mcp-server/httpConfig.d.ts.map +1 -0
  15. package/dist/mcp-server/httpConfig.js +104 -0
  16. package/dist/mcp-server/httpConfig.js.map +1 -0
  17. package/dist/mcp-server/index.d.ts +13 -1
  18. package/dist/mcp-server/index.d.ts.map +1 -1
  19. package/dist/mcp-server/index.js +39 -373
  20. package/dist/mcp-server/index.js.map +1 -1
  21. package/dist/src/panel/server.d.ts.map +1 -1
  22. package/dist/src/panel/server.js +26 -1
  23. package/dist/src/panel/server.js.map +1 -1
  24. package/dist/src/services/assistantEval.d.ts +69 -0
  25. package/dist/src/services/assistantEval.d.ts.map +1 -1
  26. package/dist/src/services/assistantEval.js +270 -8
  27. package/dist/src/services/assistantEval.js.map +1 -1
  28. package/dist/src/services/assistantScenes.d.ts +81 -0
  29. package/dist/src/services/assistantScenes.d.ts.map +1 -0
  30. package/dist/src/services/assistantScenes.js +322 -0
  31. package/dist/src/services/assistantScenes.js.map +1 -0
  32. package/dist/src/services/assistantService.d.ts +14 -4
  33. package/dist/src/services/assistantService.d.ts.map +1 -1
  34. package/dist/src/services/assistantService.js +82 -6
  35. package/dist/src/services/assistantService.js.map +1 -1
  36. package/dist/src/services/assistantSkills.d.ts +94 -0
  37. package/dist/src/services/assistantSkills.d.ts.map +1 -0
  38. package/dist/src/services/assistantSkills.js +330 -0
  39. package/dist/src/services/assistantSkills.js.map +1 -0
  40. package/dist/src/services/assistantTools.d.ts +7 -0
  41. package/dist/src/services/assistantTools.d.ts.map +1 -1
  42. package/dist/src/services/assistantTools.js +770 -61
  43. package/dist/src/services/assistantTools.js.map +1 -1
  44. package/dist/src/services/assistantTrace.d.ts +4 -0
  45. package/dist/src/services/assistantTrace.d.ts.map +1 -1
  46. package/dist/src/services/assistantTrace.js +9 -0
  47. package/dist/src/services/assistantTrace.js.map +1 -1
  48. package/dist/src/services/configService.d.ts +7 -0
  49. package/dist/src/services/configService.d.ts.map +1 -1
  50. package/dist/src/services/configService.js +4 -2
  51. package/dist/src/services/configService.js.map +1 -1
  52. package/dist/src/services/outboundSend.d.ts +53 -0
  53. package/dist/src/services/outboundSend.d.ts.map +1 -0
  54. package/dist/src/services/outboundSend.js +90 -0
  55. package/dist/src/services/outboundSend.js.map +1 -0
  56. package/dist/src/services/pythonBridge.d.ts.map +1 -1
  57. package/dist/src/services/pythonBridge.js +1 -0
  58. package/dist/src/services/pythonBridge.js.map +1 -1
  59. package/dist/src/utils/python.js +2 -2
  60. package/dist/src/utils/python.js.map +1 -1
  61. package/docs/BRANCHES.md +37 -0
  62. package/docs/DECISIONS.md +350 -0
  63. package/docs/EXTENDING.md +29 -3
  64. package/docs/MCP.md +56 -2
  65. package/docs/PROJECT_STATE.md +121 -9
  66. package/mcp-server/http.ts +108 -0
  67. package/mcp-server/httpConfig.ts +126 -0
  68. package/mcp-server/index.ts +344 -681
  69. package/package.json +1 -1
  70. package/resources/panel/ball-position.cjs +48 -2
  71. package/resources/panel/index.html +6 -4
  72. package/resources/panel/main.cjs +137 -5
  73. package/resources/panel/mascot-base.png +0 -0
  74. package/resources/panel/mascot-iris.png +0 -0
  75. package/resources/panel/mascot-peek.png +0 -0
  76. package/resources/panel/panel.css +107 -63
  77. package/resources/panel/preload.cjs +26 -0
  78. package/resources/panel/renderer.js +167 -10
  79. package/scripts/_utils.py +47 -1
  80. package/scripts/backfill_articles.py +202 -21
  81. package/scripts/biz_daily.py +8 -31
  82. package/scripts/bonds.py +331 -0
  83. package/scripts/chat_notes.py +551 -470
  84. package/scripts/chat_stats.py +8 -7
  85. package/scripts/compile_wiki.py +24 -2
  86. package/scripts/export_chat_html.py +3 -1
  87. package/scripts/mcp_bridge.py +6 -7
  88. package/scripts/wechat_voice.py +9 -0
  89. package/scripts/wiki_lint.py +30 -7
  90. package/src/panel/server.ts +25 -1
  91. package/src/services/assistantEval.ts +323 -12
  92. package/src/services/assistantScenes.ts +342 -0
  93. package/src/services/assistantService.ts +81 -6
  94. package/src/services/assistantSkills.ts +364 -0
  95. package/src/services/assistantTools.ts +738 -59
  96. package/src/services/assistantTrace.ts +9 -0
  97. package/src/services/configService.ts +412 -403
  98. package/src/services/outboundSend.ts +126 -0
  99. package/src/services/pythonBridge.ts +1 -0
  100. package/src/utils/python.ts +2 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "weflow-cli",
3
- "version": "1.8.1",
3
+ "version": "1.9.0",
4
4
  "description": "让 AI 读懂你的微信 — 公众号日报·聊天导出·知识库搜索·MCP Server",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,6 +29,52 @@ function defaultBallPosition(workArea, ballSize = BALL_SIZE, margin = EDGE_MARGI
29
29
  }
30
30
  }
31
31
 
32
+ /**
33
+ * 半隐("躲起来"):贴住屏幕左右边时,**整窗留在工作区内、贴着边**。
34
+ *
35
+ * 不再把窗口推到屏幕外:姿势换成了"身子藏在边后面探头"(`mascot-peek.png`),
36
+ * 那张图的左边是一条笔直的切边,窗口贴边时它正好落在屏幕边上——**切的错觉由图和位置一起给**,
37
+ * 窗口自己不用出去。顺带三个好处:不在屏幕外的窗口不会跟贴边/任务栏打架、
38
+ * 露出的部分整个可点、而且**窗口完整可见**(上次那版是半截在屏幕外)。
39
+ */
40
+ const HIDE_SNAP = 16
41
+
42
+ /**
43
+ * 松手的位置算不算"贴住了某块屏的左右边"。
44
+ *
45
+ * 只做左右:上下贴边在 Windows 上会跟任务栏、窗口贴边打架,而且一个圆球从上下边半隐
46
+ * 看着也不像"躲",像被切了。
47
+ *
48
+ * @returns {{edge:'left'|'right', area:object}|null} 连那块工作区一起返回——
49
+ * 调用方就不用再拿球心去找工作区了(半隐时球心本来就在屏幕外,找不到)。
50
+ */
51
+ function edgeToHide(pos, workAreas, ballSize = BALL_SIZE, snap = HIDE_SNAP) {
52
+ for (const area of workAreas) {
53
+ const cy = pos.y + ballSize / 2
54
+ // 球得落在这块屏的**纵向**范围内,否则它贴的是另一块屏的边
55
+ if (cy < area.y || cy > area.y + area.height) continue
56
+ const leftGap = pos.x - area.x
57
+ const rightGap = (area.x + area.width) - (pos.x + ballSize)
58
+ if (leftGap <= snap && leftGap >= -ballSize) return { edge: 'left', area }
59
+ if (rightGap <= snap && rightGap >= -ballSize) return { edge: 'right', area }
60
+ }
61
+ return null
62
+ }
63
+
64
+ /** 半隐时窗口该放哪:**贴着工作区的左/右边,整窗留着**(切边由图给,见上)。 */
65
+ function hiddenPosition(pos, edge, workArea, ballSize = BALL_SIZE) {
66
+ if (edge === 'left') return { x: workArea.x, y: pos.y }
67
+ if (edge === 'right') return { x: workArea.x + workArea.width - ballSize, y: pos.y }
68
+ return { x: pos.x, y: pos.y }
69
+ }
70
+
71
+ /** 从半隐恢复:完全露出来,并且按平时的规矩留出 `EDGE_MARGIN`,别贴在边上。 */
72
+ function revealedPosition(pos, edge, workArea, ballSize = BALL_SIZE, margin = EDGE_MARGIN) {
73
+ if (edge === 'left') return { x: workArea.x + margin, y: pos.y }
74
+ if (edge === 'right') return { x: workArea.x + workArea.width - ballSize - margin, y: pos.y }
75
+ return { x: pos.x, y: pos.y }
76
+ }
77
+
32
78
  /** 把一个 `size x size` 的方块挪进给定工作区(只挪,不改尺寸) */
33
79
  function clampInto(x, y, size, workArea) {
34
80
  const maxX = workArea.x + workArea.width - size
@@ -147,7 +193,7 @@ function ballRectInWindow(windowRect, anchor, ballSize = BALL_SIZE) {
147
193
  }
148
194
 
149
195
  module.exports = {
150
- BALL_SIZE, EDGE_MARGIN, BUBBLE_SIZE, BUBBLE_GAP,
196
+ BALL_SIZE, EDGE_MARGIN, BUBBLE_SIZE, BUBBLE_GAP, HIDE_SNAP,
151
197
  defaultBallPosition, clampInto, isReachable, resolveStartPosition, bubbleLayout,
152
- ballRectInWindow,
198
+ ballRectInWindow, edgeToHide, hiddenPosition, revealedPosition,
153
199
  }
@@ -12,11 +12,13 @@
12
12
  奇怪的方窗口,所以那边永远看不见球(`hidden` 属性与 CSS 里那条 `display:none`
13
13
  都是为它留的)。 -->
14
14
  <button id="ball" type="button" title="第二大脑(点击展开 · 可拖动)" hidden>
15
- <!-- 两层:底下一层是会动的光晕(背景),上面一层是吉祥物。
16
- 分开是为了让光晕能变而猫不动——直接给 #ball 设 background 的话,
17
- 光晕与猫共用同一层,动不了其中一个。 -->
18
- <span class="glow" aria-hidden="true"></span>
15
+ <!-- 两层:底下是球面(`mascot-base.png`,眼球位置是空的),上面是可以单独平移的虹膜。
16
+ 分开是为了让**眼珠能跟着鼠标动而猫不动**——直接给 #ball 设 background 的话,
17
+ 两者共用同一层,就动不了其中一个。
18
+ 注意底面用的是 `mascot-base.png` 而**不是** `mascot.png`:后者眼球是画死的,
19
+ 直接用会看到两个瞳仁(一个动的、一个不动的)。两张图零偏移叠起来与原图逐像素相同。 -->
19
20
  <span class="face" aria-hidden="true"></span>
21
+ <span class="iris" aria-hidden="true"></span>
20
22
  </button>
21
23
 
22
24
  <!-- 对话气泡。**单独一层**:它要有自己的圆角与底色,而展开时窗口比它大一圈
@@ -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 } = require('./ball-position.cjs')
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
- function setMode(mode, opts) {
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
- ipcMain.handle('panel:setMode', (_event, mode, opts) => setMode(mode, opts))
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', () => { dragOrigin = null; return null })
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
@@ -104,7 +104,7 @@ header {
104
104
  浏览器降级那条路靠这条 `display: none` 永远看不见球(那一侧 `hidden` 属性也还在)。 */
105
105
  display: none;
106
106
  /* 固定尺寸、钉在窗口的某个角上(下面的 shell 规则给角):**尺寸不跟着窗口长**,
107
- 否则展开过程中会看到一只被拉成巨型的猫。球形态下窗口就是 76×76,铺满即整窗。 */
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
- /* 球本身**没有底色**:下面那层 `.glow` 才是"背景",上面 `.face` 是吉祥物。
119
+ /* 球本身**没有底色**(`.glow` 那层已在 2026-09-29 删掉,现在球上只有吉祥物这一层)。
120
120
  **换 `mascot.png` 时:内容必须全部落在内切圆里。** 球(连 `.face` 自己)都是
121
121
  `border-radius: 50%`,图会被裁成圆的——而这张图的内容一向是**铺满画布**的
122
122
  (2026-09-27 那版源图 1254²,内容占 98%),直接铺上去,角上的东西就没了:
@@ -129,8 +129,17 @@ header {
129
129
  那是因为它的内容本来只占 59%x67%——**余量是结果,不是规则**。) */
130
130
  background-color: transparent;
131
131
 
132
- /* 投影跟着猫的轮廓走(不是跟着那个不存在的圆),透明主体靠它跟浅色壁纸分开 */
133
- filter: drop-shadow(0 6px 12px rgba(0, 0, 0, .50));
132
+ /* **不画投影**(2026-09-29 用户要求去掉的)。
133
+ 它原先在这里,理由是"透明主体靠它跟浅色壁纸分开";用户看到的是**猫轮廓外一圈灰黑影**,
134
+ 读成了"背景没完全透明"。**先澄清一件事:那不是图的问题** —— 实测这几张 PNG 的四角是
135
+ `(0,0,0,0)`,实心区 alpha 253/255,低 alpha 像素只有 674 个且都贴着实心区 4-8 像素
136
+ (是描边锯齿,不是底)。所以问题在这一行 CSS,不在抠图。
137
+ 代价要知道:去掉之后深色猫在某些壁纸上的分离感会弱一点。当初试过浅/中/深三种底,
138
+ 但那回测的是"**透明底**读不读得清",不是"**没有投影**读不读得清" —— 所以这条是新的、
139
+ 还没验过的视觉变化。真觉得飘的话,把它改回一行很淡的就够:
140
+ `filter: drop-shadow(0 2px 4px rgba(0, 0, 0, .22));`
141
+ 悬停时仍然看得出反馈:`transform: scale(1.02)` 与 `brightness(1.08)` 都还在。 */
142
+ filter: none;
134
143
  /* 回弹曲线(不是 ease):松手时从压扁弹回原状会**轻微过冲**一下,那就是"弹"的感觉。
135
144
  窗口就是球这么大,过冲出来的那点边缘靠出图时留的 2% 余量兜住。 */
136
145
  transition: transform .16s cubic-bezier(.34, 1.56, .64, 1), filter .16s ease;
@@ -147,31 +156,69 @@ body.shell.bubble-right #ball { right: auto; left: 0; }
147
156
  body.shell.anchor-top #ball { bottom: auto; top: 0; }
148
157
  #ball > span { position: absolute; inset: 0; border-radius: 50%; pointer-events: none; }
149
158
 
150
- #ball .glow {
151
- background: none;
152
- }
159
+ /* **球背后什么都没有**(2026-09-29 彻底去掉)。
153
160
 
154
- /* 光晕**只在有状态时画出来**。做成软边光晕而不是硬边圆盘:硬边就又回到"一只盘子"了,
155
- 而软边在浅底与深底上都只是"猫身后的一团光",不需要再靠一圈亮环去定边界。 */
156
- body.busy #ball .glow,
157
- body.offline #ball .glow,
158
- body.quota #ball .glow {
159
- background:
160
- radial-gradient(circle at 50% 46%,
161
- hsl(var(--hue) 82% 58% / .95) 0%,
162
- hsl(calc(var(--hue) + 26) 78% 46% / .78) 38%,
163
- hsl(calc(var(--hue) + 44) 70% 38% / .32) 62%,
164
- transparent 78%);
165
- }
161
+ 这条线索两进两退,都记在这里,免得再来一次:
162
+ - 最早这层**一直**亮着(用户当初要的"会变动的背景");
163
+ - 2026-09-26 缩到"只在有状态时画",理由写的是"球背后永远有一圈彩色圆盘,'这张图是
164
+ 透明的'根本看不出来";
165
+ - 2026-09-29 用户**又提了一次一模一样的话**("思考的时候背景不是透明的")—— 剩下的
166
+ 就是"忙"时那一圈。同一个意见提两次,说明它要的不是"少画一点",是**不要**。
167
+ 所以整层拿掉:元素、三条状态规则、`--hue`、`drift` 与 `breathe` 两个动画一起删。
166
168
 
167
- /* 上面那层:吉祥物本体,形状不变 */
169
+ **状态灯没有丢**:四个状态本来就各有一张脸(忙=思考、连不上=委屈、额度用完=累、
170
+ 空闲=原图),那才是主信号;光晕只是"余光能看见"的补充。想再要一个不占背景的提示,
171
+ 应该做成**外圈**(`box-shadow` 之类画在球外面),而不是再往猫背后放一层盘子。 */
172
+
173
+ /* 上面那层:吉祥物本体,形状不变。
174
+ **注意是 `mascot-base.png` 而不是 `mascot.png`**:前者是把虹膜挖掉的版本(挖掉的地方填成
175
+ 眼白),专门给"眼球跟着鼠标动"用的;后者眼球是画死的,拿它当底会看到两个瞳仁
176
+ (一个跟着动、一个不动)。两张图零偏移叠起来与原图**逐像素相同**,所以换底不改变外观。 */
168
177
  #ball .face {
169
- 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');
170
189
  background-size: 100%;
171
190
  background-position: center;
172
191
  background-repeat: no-repeat;
192
+ will-change: transform;
173
193
  }
174
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
+
175
222
  /* 三个**真状态**各有一张脸:忙(思考中)、连不上时委屈、额度用完时累。
176
223
  接的全是现成的状态类(`setBallState` 早就挂 busy/offline/quota 了),**不新增状态机**;
177
224
  光晕照旧画——它是"余光能看见"的环境灯,表情是盯着看才见的细节,两者说的是同一件事,
@@ -198,54 +245,16 @@ body.ball-happy #ball .face {
198
245
  background-image: url('/panel/mascot-happy.png');
199
246
  }
200
247
 
201
- /* 色相流动 —— 现在只在**忙碌**时跑(空闲时那一层根本不画)。
202
- 8 秒一圈由 `body.busy` 那条给;色相本身只是被 `drift` 驱动的数值。 */
203
- @property --hue { syntax: '<number>'; inherits: true; initial-value: 224; }
204
- #ball { --hue: 224; }
205
- @keyframes drift {
206
- 0% { --hue: 224; } /* 蓝 */
207
- 25% { --hue: 268; } /* 紫 */
208
- 50% { --hue: 316; } /* 品红 */
209
- 75% { --hue: 190; } /* 青 */
210
- 100% { --hue: 224; }
211
- }
212
-
213
- /* **空闲时球后面什么都没有**(2026-09-26 改)。
214
-
215
- 这一层原来一直亮着,那是用户当初要的"会变动的背景"。但两个代价在用过之后才显出来:
216
- 球背后永远有一圈彩色圆盘,**"这张图是透明的"这件事根本看不出来**;而它同时兼着状态灯,
217
- 一直亮着就等于没有状态灯——离线、忙碌、额度用完,球长得都一样。
218
-
219
- 现在:**静止 = 只有猫,亮起来 = 有话说**。这是那条需求的**范围缩小,不是取消**——
220
- 色相流动还在,只是改在忙碌时发生(下面那条)。 */
221
-
222
248
  /* ---------------------------------------------------------------- 状态 */
223
249
 
224
- /* 正在想事情:光晕变亮、转得快,并且轻轻呼吸。
225
- 这一条的用处不只是好看——球收起来的时候,此前**完全看不出它在干活**。 */
226
- body.busy #ball .glow {
227
- /* 8 秒转一圈色相(不是 3 秒):呼吸的节奏交给 1.5s,色相再快就变成闪了 */
228
- animation: drift 8s linear infinite, breathe 1.5s ease-in-out infinite;
229
- }
230
- @keyframes breathe {
231
- 0%, 100% { transform: scale(1); opacity: 1; }
232
- 50% { transform: scale(1.08); opacity: .82; }
233
- }
234
-
235
- /* 连不上 / 出错:换成暖色并停住流动——不动的东西才会被注意到 */
236
- body.offline #ball { --hue: 12; }
237
- body.offline #ball .glow { animation: none; opacity: .9; }
238
-
239
- /* 今天的额度用完了:琥珀色 */
240
- body.quota #ball { --hue: 38; }
241
- body.quota #ball .glow { animation: none; opacity: .85; }
250
+ /* 状态的表达**全在那四张脸上**(见上面 `.face` 那几条):忙=思考、连不上=委屈、
251
+ 额度用完=累、空闲=原图。原先这里还有一层会变色/呼吸的光晕,2026-09-29 按用户要求
252
+ 整层去掉了——理由写在上一条注释里(同一个意见提了两次)。 */
242
253
 
243
254
  /* 系统说"减少动态效果"就照办:停掉所有流动,只留静止的光晕。
244
255
  展开/收起那两段也一起停(下面那几条,**写在原来那条之后**——测试盯的是这一段开头
245
256
  240 个字符里就有 `animation: none`,把新规则插在前面会把它挤出窗口)。 */
246
257
  @media (prefers-reduced-motion: reduce) {
247
- #ball .glow,
248
- body.busy #ball .glow { animation: none; }
249
258
  body.mode-chat #bubble { animation: none; }
250
259
  body.mode-chat.closing #bubble { animation: none; opacity: 0; }
251
260
  #ball { transition: none; }
@@ -257,7 +266,7 @@ body.quota #ball .glow { animation: none; opacity: .85; }
257
266
  /* 悬停:微微放大 + 阴影加深 + 提亮 —— 三样一起才够"这里能点"。
258
267
  **放大只敢到 1.02**:出图按"实心像素最远点 ≤ 0.98 × 半径"缩放(见 `.face` 那段),
259
268
  1.02 倍时圆外只剩 1 个像素(实测),再大就开始削掉猫的外缘。 */
260
- #ball:hover { transform: scale(1.02); filter: drop-shadow(0 8px 16px rgba(0, 0, 0, .58)) brightness(1.08); }
269
+ #ball:hover { transform: scale(1.02); filter: brightness(1.08); }
261
270
  /* 按下:压扁。**只往小里做** —— 放大或位移会把内容顶出窗口、被窗口边裁掉。 */
262
271
  #ball:active { transform: scale(.92); }
263
272
 
@@ -336,6 +345,41 @@ main {
336
345
 
337
346
  /* 助手回答下的复制按钮。**常显**而不是 hover 才出现:这是个常驻小窗,
338
347
  鼠标常常不在它上面,hover 等于没有。做得很淡,不跟正文抢注意力。 */
348
+ /* "思考过程":挂在助手那条回答下面,**默认折叠**。
349
+ 摊开会把每次回答都变成一屏日志,而不想看的人还得滚过它;想核对"它到底查没查"的人
350
+ 点一下展开。视觉上刻意压暗压小——它是附注,不是回答的一部分。 */
351
+ .thinking {
352
+ margin-top: 6px;
353
+ font-size: 12px;
354
+ color: var(--dim);
355
+ }
356
+ .thinking > summary {
357
+ cursor: pointer;
358
+ list-style: none; /* 默认那个三角在深色底上很难看,自己画一个 */
359
+ user-select: none;
360
+ }
361
+ .thinking > summary::before {
362
+ content: '▸ ';
363
+ display: inline-block;
364
+ transition: transform .12s ease;
365
+ }
366
+ .thinking[open] > summary::before { transform: rotate(90deg); }
367
+ .thinking > summary:hover { color: var(--text); }
368
+ .thinking-line {
369
+ margin: 4px 0 0 14px;
370
+ font-family: ui-monospace, Consolas, monospace;
371
+ white-space: pre-wrap;
372
+ word-break: break-word;
373
+ opacity: .85;
374
+ }
375
+ /* 模型真返回的推理内容(多数模型不给,所以这一段常常根本不出现) */
376
+ .thinking-reasoning {
377
+ border-left: 2px solid var(--line);
378
+ padding-left: 8px;
379
+ font-family: inherit;
380
+ opacity: .75;
381
+ }
382
+
339
383
  .turn .copy {
340
384
  display: block;
341
385
  margin-top: 6px;
@@ -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
  })