weflow-cli 1.7.0 → 1.8.1

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 (200) hide show
  1. package/ARCHITECTURE.md +3 -2
  2. package/CHANGELOG.md +2108 -2
  3. package/OPERATIONS.md +810 -1
  4. package/README.en.md +96 -10
  5. package/README.md +34 -9
  6. package/bin/weflow-cli.ts +937 -28
  7. package/dist/bin/weflow-cli.js +955 -28
  8. package/dist/bin/weflow-cli.js.map +1 -1
  9. package/dist/mcp-server/index.js +39 -13
  10. package/dist/mcp-server/index.js.map +1 -1
  11. package/dist/src/core/appMsgFormat.d.ts +37 -0
  12. package/dist/src/core/appMsgFormat.d.ts.map +1 -0
  13. package/dist/src/core/appMsgFormat.js +106 -0
  14. package/dist/src/core/appMsgFormat.js.map +1 -0
  15. package/dist/src/core/ntCore.d.ts +19 -2
  16. package/dist/src/core/ntCore.d.ts.map +1 -1
  17. package/dist/src/core/ntCore.js +11 -1
  18. package/dist/src/core/ntCore.js.map +1 -1
  19. package/dist/src/core/sqlcipherCore.d.ts +0 -12
  20. package/dist/src/core/sqlcipherCore.d.ts.map +1 -1
  21. package/dist/src/core/sqlcipherCore.js +5 -64
  22. package/dist/src/core/sqlcipherCore.js.map +1 -1
  23. package/dist/src/core/wcdbCore.d.ts +1 -1
  24. package/dist/src/core/wcdbCore.d.ts.map +1 -1
  25. package/dist/src/core/wcdbCore.js +5 -1
  26. package/dist/src/core/wcdbCore.js.map +1 -1
  27. package/dist/src/panel/client.d.ts +25 -0
  28. package/dist/src/panel/client.d.ts.map +1 -0
  29. package/dist/src/panel/client.js +34 -0
  30. package/dist/src/panel/client.js.map +1 -0
  31. package/dist/src/panel/endpoint.d.ts +45 -0
  32. package/dist/src/panel/endpoint.d.ts.map +1 -0
  33. package/dist/src/panel/endpoint.js +132 -0
  34. package/dist/src/panel/endpoint.js.map +1 -0
  35. package/dist/src/panel/launch.d.ts +50 -0
  36. package/dist/src/panel/launch.d.ts.map +1 -0
  37. package/dist/src/panel/launch.js +74 -0
  38. package/dist/src/panel/launch.js.map +1 -0
  39. package/dist/src/panel/server.d.ts +23 -0
  40. package/dist/src/panel/server.d.ts.map +1 -0
  41. package/dist/src/panel/server.js +413 -0
  42. package/dist/src/panel/server.js.map +1 -0
  43. package/dist/src/panel/userId.d.ts +37 -0
  44. package/dist/src/panel/userId.d.ts.map +1 -0
  45. package/dist/src/panel/userId.js +56 -0
  46. package/dist/src/panel/userId.js.map +1 -0
  47. package/dist/src/services/assistantDaemon.d.ts +20 -3
  48. package/dist/src/services/assistantDaemon.d.ts.map +1 -1
  49. package/dist/src/services/assistantDaemon.js +46 -9
  50. package/dist/src/services/assistantDaemon.js.map +1 -1
  51. package/dist/src/services/assistantEval.d.ts +141 -0
  52. package/dist/src/services/assistantEval.d.ts.map +1 -0
  53. package/dist/src/services/assistantEval.js +497 -0
  54. package/dist/src/services/assistantEval.js.map +1 -0
  55. package/dist/src/services/assistantMemory.d.ts +106 -4
  56. package/dist/src/services/assistantMemory.d.ts.map +1 -1
  57. package/dist/src/services/assistantMemory.js +396 -61
  58. package/dist/src/services/assistantMemory.js.map +1 -1
  59. package/dist/src/services/assistantPrivacy.d.ts +15 -2
  60. package/dist/src/services/assistantPrivacy.d.ts.map +1 -1
  61. package/dist/src/services/assistantPrivacy.js +25 -6
  62. package/dist/src/services/assistantPrivacy.js.map +1 -1
  63. package/dist/src/services/assistantRouter.d.ts +41 -0
  64. package/dist/src/services/assistantRouter.d.ts.map +1 -0
  65. package/dist/src/services/assistantRouter.js +151 -0
  66. package/dist/src/services/assistantRouter.js.map +1 -0
  67. package/dist/src/services/assistantService.d.ts +209 -3
  68. package/dist/src/services/assistantService.d.ts.map +1 -1
  69. package/dist/src/services/assistantService.js +599 -75
  70. package/dist/src/services/assistantService.js.map +1 -1
  71. package/dist/src/services/assistantTools.d.ts +217 -1
  72. package/dist/src/services/assistantTools.d.ts.map +1 -1
  73. package/dist/src/services/assistantTools.js +1116 -59
  74. package/dist/src/services/assistantTools.js.map +1 -1
  75. package/dist/src/services/assistantTrace.d.ts +47 -0
  76. package/dist/src/services/assistantTrace.d.ts.map +1 -0
  77. package/dist/src/services/assistantTrace.js +150 -0
  78. package/dist/src/services/assistantTrace.js.map +1 -0
  79. package/dist/src/services/chatService.d.ts +1 -1
  80. package/dist/src/services/chatService.d.ts.map +1 -1
  81. package/dist/src/services/chatService.js +5 -2
  82. package/dist/src/services/chatService.js.map +1 -1
  83. package/dist/src/services/configService.d.ts +9 -0
  84. package/dist/src/services/configService.d.ts.map +1 -1
  85. package/dist/src/services/configService.js +15 -8
  86. package/dist/src/services/configService.js.map +1 -1
  87. package/dist/src/services/pythonBridge.d.ts +41 -0
  88. package/dist/src/services/pythonBridge.d.ts.map +1 -0
  89. package/dist/src/services/pythonBridge.js +119 -0
  90. package/dist/src/services/pythonBridge.js.map +1 -0
  91. package/dist/src/services/syncService.d.ts +8 -2
  92. package/dist/src/services/syncService.d.ts.map +1 -1
  93. package/dist/src/services/syncService.js +26 -7
  94. package/dist/src/services/syncService.js.map +1 -1
  95. package/dist/src/services/wechatMessageService.d.ts +25 -0
  96. package/dist/src/services/wechatMessageService.d.ts.map +1 -1
  97. package/dist/src/services/wechatMessageService.js +69 -7
  98. package/dist/src/services/wechatMessageService.js.map +1 -1
  99. package/dist/src/services/wereadService.d.ts +33 -6
  100. package/dist/src/services/wereadService.d.ts.map +1 -1
  101. package/dist/src/services/wereadService.js +27 -2
  102. package/dist/src/services/wereadService.js.map +1 -1
  103. package/dist/src/utils/atomicWrite.d.ts +4 -0
  104. package/dist/src/utils/atomicWrite.d.ts.map +1 -0
  105. package/dist/src/utils/atomicWrite.js +60 -0
  106. package/dist/src/utils/atomicWrite.js.map +1 -0
  107. package/dist/src/utils/dateRange.d.ts +15 -0
  108. package/dist/src/utils/dateRange.d.ts.map +1 -1
  109. package/dist/src/utils/dateRange.js +47 -0
  110. package/dist/src/utils/dateRange.js.map +1 -1
  111. package/dist/src/utils/text.d.ts +18 -0
  112. package/dist/src/utils/text.d.ts.map +1 -0
  113. package/dist/src/utils/text.js +20 -0
  114. package/dist/src/utils/text.js.map +1 -0
  115. package/docs/AI_INTERFACE.md +2 -0
  116. package/docs/BRANCHES.md +13 -4
  117. package/docs/DECISIONS.md +1424 -0
  118. package/docs/EXTENDING.md +187 -0
  119. package/docs/JEV/345/272/224/347/224/250/346/226/275/345/267/245/346/226/207/346/241/243.md +310 -0
  120. package/docs/MCP.md +23 -4
  121. package/docs/PROJECT_STATE.md +210 -9
  122. package/docs/SETUP.md +22 -0
  123. package/docs/SYNC_CONTRACT.md +31 -3
  124. package/mcp-server/index.ts +681 -657
  125. package/package.json +84 -83
  126. package/resources/panel/ball-position.cjs +153 -0
  127. package/resources/panel/index.html +46 -0
  128. package/resources/panel/main.cjs +483 -0
  129. package/resources/panel/mascot-happy.png +0 -0
  130. package/resources/panel/mascot-sorry.png +0 -0
  131. package/resources/panel/mascot-thinking.png +0 -0
  132. package/resources/panel/mascot-tired.png +0 -0
  133. package/resources/panel/mascot.png +0 -0
  134. package/resources/panel/package.json +7 -0
  135. package/resources/panel/panel.css +415 -0
  136. package/resources/panel/preload.cjs +79 -0
  137. package/resources/panel/quick-menu.cjs +197 -0
  138. package/resources/panel/renderer.js +470 -0
  139. package/resources/panel/tray-menu.cjs +40 -0
  140. package/resources/panel/tray.png +0 -0
  141. package/scripts/_utils.py +425 -54
  142. package/scripts/annual_report.py +4 -2
  143. package/scripts/article_notes.py +423 -0
  144. package/scripts/backfill_articles.py +438 -0
  145. package/scripts/biz_daily.py +590 -105
  146. package/scripts/chat_notes.py +470 -0
  147. package/scripts/classify_daily.py +10 -18
  148. package/scripts/compile_wiki.py +1060 -46
  149. package/scripts/create_reading_notes.py +199 -14
  150. package/scripts/decide.py +253 -0
  151. package/scripts/draft_reply.py +671 -0
  152. package/scripts/export_chat_html.py +35 -33
  153. package/scripts/extract_todos.py +31 -5
  154. package/scripts/fav_notes.py +318 -0
  155. package/scripts/fix_topics.py +2 -1
  156. package/scripts/generate_ai_report.py +214 -9
  157. package/scripts/generate_html.py +18 -3
  158. package/scripts/generate_review.py +2 -2
  159. package/scripts/jev_client.py +294 -0
  160. package/scripts/jev_probe.py +440 -0
  161. package/scripts/nt_common.py +82 -0
  162. package/scripts/nt_decrypt.py +1755 -1460
  163. package/scripts/pipeline.py +27 -2
  164. package/scripts/promote_ideas.py +19 -4
  165. package/scripts/quality_eval.py +707 -0
  166. package/scripts/rag_chat.py +4 -4
  167. package/scripts/read_image.py +187 -0
  168. package/scripts/reply_debt.py +488 -0
  169. package/scripts/route_cards.py +453 -0
  170. package/scripts/semantic_search.py +208 -49
  171. package/scripts/user_notes.py +315 -0
  172. package/scripts/vault_rag.py +13 -7
  173. package/scripts/vault_search.py +21 -4
  174. package/scripts/wiki_lint.py +429 -0
  175. package/src/core/appMsgFormat.ts +117 -0
  176. package/src/core/ntCore.ts +27 -2
  177. package/src/core/sqlcipherCore.ts +5 -61
  178. package/src/core/wcdbCore.ts +6 -2
  179. package/src/panel/client.ts +49 -0
  180. package/src/panel/endpoint.ts +144 -0
  181. package/src/panel/launch.ts +101 -0
  182. package/src/panel/server.ts +424 -0
  183. package/src/panel/userId.ts +72 -0
  184. package/src/services/assistantDaemon.ts +57 -9
  185. package/src/services/assistantEval.ts +611 -0
  186. package/src/services/assistantMemory.ts +413 -57
  187. package/src/services/assistantPrivacy.ts +25 -6
  188. package/src/services/assistantRouter.ts +191 -0
  189. package/src/services/assistantService.ts +681 -74
  190. package/src/services/assistantTools.ts +1149 -58
  191. package/src/services/assistantTrace.ts +175 -0
  192. package/src/services/chatService.ts +6 -2
  193. package/src/services/configService.ts +403 -385
  194. package/src/services/pythonBridge.ts +139 -0
  195. package/src/services/syncService.ts +37 -9
  196. package/src/services/wechatMessageService.ts +73 -7
  197. package/src/services/wereadService.ts +43 -7
  198. package/src/utils/atomicWrite.ts +56 -0
  199. package/src/utils/dateRange.ts +46 -0
  200. package/src/utils/text.ts +20 -0
@@ -0,0 +1,415 @@
1
+ /* 面板的样式。刻意不引任何外部资源(CSP 是 default-src 'none'),也没有字体图标。 */
2
+ :root {
3
+ --bg: #17181c;
4
+ --panel: #1f2126;
5
+ --line: #2e3138;
6
+ --text: #e6e7ea;
7
+ --dim: #9aa0aa;
8
+ --accent: #4c8bf5;
9
+ --warn: #e0a23c;
10
+ --err: #e5645f;
11
+ /* 球多大。CSS 拿不到 JS 的值,所以这里是**第二份**——与 `ball-position.cjs` 的
12
+ `BALL_SIZE`(main.cjs 也从那里取)必须一致,`test/panel-packaging.test.ts` 断言这一点。 */
13
+ --ball-size: 96px;
14
+ /* 气泡多大、离球多远、圆角多少。同样是 `ball-position.cjs`(`BUBBLE_SIZE` / `BUBBLE_GAP`)
15
+ 的第二份拷贝:JS 那边用它算窗口矩形,CSS 这边用它排版,两边必须一致——有测试钉住。 */
16
+ --bubble-width: 420px;
17
+ /* 气泡的高度**会被页面改**(屏幕不够高时变矮,好让球仍然待在窗口的角上不动),
18
+ 这个值是页面还没说话时的默认。`ball-position.cjs` 的 BUBBLE_SIZE.height 是同一个数。 */
19
+ --bubble-height: 560px;
20
+ --bubble-gap: 12px;
21
+ --bubble-radius: 14px;
22
+ }
23
+
24
+ * { box-sizing: border-box; }
25
+
26
+ html, body {
27
+ height: 100%;
28
+ margin: 0;
29
+ background: var(--bg);
30
+ color: var(--text);
31
+ font: 14px/1.55 -apple-system, "Segoe UI", "Microsoft YaHei", system-ui, sans-serif;
32
+ }
33
+
34
+ /* 有外壳时窗口是**透明的**:展开后窗口比气泡大一圈,多出来的是球占的那条与它们之间的空隙,
35
+ 那一圈必须能透出桌面(用户要的就是"图标旁边冒出一个气泡")。所以底色不能再画在 body 上,
36
+ 改由 `#bubble` 自己画。降级那条路(没有 shell,页面铺满整个浏览器窗)照旧用 body 的底色。 */
37
+ html:has(body.shell),
38
+ body.shell { background: transparent; }
39
+
40
+ body { display: flex; flex-direction: column; }
41
+
42
+ /* ---------------------------------------------------------------- 对话气泡 */
43
+
44
+ /* 单独一层:自己的圆角与底色。**不用 `overflow: hidden`**——尾巴是它的子节点,
45
+ 会被一起剪掉;代价只是带底色的那几条要自己圆角(目前只有 header)。 */
46
+ #bubble {
47
+ position: relative; /* 尾巴的定位基准 */
48
+ display: flex;
49
+ flex-direction: column;
50
+ flex: 1;
51
+ min-height: 0;
52
+ background: var(--bg);
53
+ }
54
+
55
+ /* 有外壳时它是屏幕上一块**面板**,钉在窗口里离球最远的那条边上(球在窗口那一角,
56
+ 气泡占剩下的地方)。窗口这一整块的尺寸由主进程一次算好(`bubbleLayout`),
57
+ 页面这边只管"贴哪条边"。 */
58
+ body.shell #bubble {
59
+ position: fixed;
60
+ width: var(--bubble-width);
61
+ min-width: var(--bubble-width);
62
+ height: var(--bubble-height);
63
+ border-radius: var(--bubble-radius);
64
+ }
65
+ /* 气泡钉在窗口离球最远的那一侧,竖向与球同边(球贴窗口底 → 气泡也贴底,反之亦然)。
66
+ 窗口整块的尺寸由主进程算(`bubbleLayout`),这里只负责"球在哪一侧、贴哪条边"。 */
67
+ body.shell.bubble-left #bubble { left: 0; }
68
+ body.shell.bubble-right #bubble { right: 0; }
69
+ body.shell.anchor-top #bubble { top: 0; }
70
+ body.shell:not(.anchor-top) #bubble { bottom: 0; }
71
+
72
+ /* 小尾巴:指向球的一个斜方块。它贴在气泡靠球那条边上,高度对球心——
73
+ 球贴窗口底时球心离底 38px(76 高),尾巴 12 高,所以 bottom: 32px。
74
+ 底色取那一条子元素的 --panel(底对齐时是输入栏,顶对齐时是标题栏,两处同色)。 */
75
+ #tail {
76
+ position: absolute;
77
+ width: 12px;
78
+ height: 12px;
79
+ background: var(--panel);
80
+ transform: rotate(45deg);
81
+ }
82
+ body.shell.bubble-left #tail { right: -6px; bottom: 32px; }
83
+ body.shell.bubble-right #tail { left: -6px; bottom: 32px; }
84
+ body.shell.anchor-top #tail { bottom: auto; top: 32px; }
85
+
86
+ header {
87
+ display: flex;
88
+ align-items: baseline;
89
+ gap: 10px;
90
+ padding: 10px 14px;
91
+ border-bottom: 1px solid var(--line);
92
+ background: var(--panel);
93
+ -webkit-app-region: drag; /* Electron 里整条顶栏可以拖窗 */
94
+ }
95
+
96
+ .brand { font-weight: 600; }
97
+ .status { color: var(--dim); font-size: 12px; }
98
+
99
+ /* ---------------------------------------------------------------- 悬浮球形态 */
100
+
101
+ #ball {
102
+ /* 球**一直在场**(有外壳时,见下面 `body.shell`):展开成气泡时它不消失、不淡出,
103
+ 就地待着当锚点——用户要的是"这个图标在说话",说话的人当然不能先撤了。
104
+ 浏览器降级那条路靠这条 `display: none` 永远看不见球(那一侧 `hidden` 属性也还在)。 */
105
+ display: none;
106
+ /* 固定尺寸、钉在窗口的某个角上(下面的 shell 规则给角):**尺寸不跟着窗口长**,
107
+ 否则展开过程中会看到一只被拉成巨型的猫。球形态下窗口就是 76×76,铺满即整窗。 */
108
+ position: fixed;
109
+ width: var(--ball-size);
110
+ height: var(--ball-size);
111
+ z-index: 2; /* 压在气泡之上:气泡要从它后面滑出来 */
112
+ border-radius: 50%;
113
+ border: 0;
114
+ padding: 0;
115
+ cursor: pointer;
116
+ /* **不要在这里写 `-webkit-app-region: drag`**:Windows 上拖拽区会吞掉鼠标事件,
117
+ 页面收不到 click,球就点不开了(实测的故障就是这个)。拖动自己实现,见 renderer.js。 */
118
+
119
+ /* 球本身**没有底色**:下面那层 `.glow` 才是"背景",上面 `.face` 是吉祥物。
120
+ **换 `mascot.png` 时:内容必须全部落在内切圆里。** 球(连 `.face` 自己)都是
121
+ `border-radius: 50%`,图会被裁成圆的——而这张图的内容一向是**铺满画布**的
122
+ (2026-09-27 那版源图 1254²,内容占 98%),直接铺上去,角上的东西就没了:
123
+ 实测绿气泡整块(10,368 个实心像素)落在圆外。所以出图时按
124
+ 「实心像素到内容中心的最远距离 ≤ 0.98 × 半径」缩放,再居中贴到 256²:
125
+ 现图 1226x1247 → 214x218,圆外实心像素 0,占画布 84%x85%。
126
+ **余量留 2%(不是 0)是有用的**:悬停时球放大到 1.02,那 2% 正好兜住,
127
+ 实测 1.02 倍时圆外只剩 1 个像素;压到 0.98 以下就没地方放这个放大了。
128
+ (上一版源图 1024²、内容 600x689 时靠"裁到包围盒 + 12% 余量"就够,
129
+ 那是因为它的内容本来只占 59%x67%——**余量是结果,不是规则**。) */
130
+ background-color: transparent;
131
+
132
+ /* 投影跟着猫的轮廓走(不是跟着那个不存在的圆),透明主体靠它跟浅色壁纸分开 */
133
+ filter: drop-shadow(0 6px 12px rgba(0, 0, 0, .50));
134
+ /* 回弹曲线(不是 ease):松手时从压扁弹回原状会**轻微过冲**一下,那就是"弹"的感觉。
135
+ 窗口就是球这么大,过冲出来的那点边缘靠出图时留的 2% 余量兜住。 */
136
+ transition: transform .16s cubic-bezier(.34, 1.56, .64, 1), filter .16s ease;
137
+ }
138
+
139
+ body.shell #ball { display: grid; }
140
+ /* 球钉在窗口的**某一个角**上,由气泡在哪一侧、贴哪条边决定(`ball-position.cjs` 的
141
+ `bubbleLayout` 保证那个角就是球原来所在的位置)。
142
+ **这条是"球全程不动"的全部**:窗口改大小时,只要球所在的这个角没动,球在屏幕上的位置
143
+ 就不动——哪怕窗口是在页面收到通知之前就变大了。上一版让球按坐标钉、窗口逐帧补间,
144
+ 窗口每改一次大小,球都要等页面把新坐标设回来才回位,那就是用户看到的"闪好多"。 */
145
+ body.shell #ball { right: 0; bottom: 0; }
146
+ body.shell.bubble-right #ball { right: auto; left: 0; }
147
+ body.shell.anchor-top #ball { bottom: auto; top: 0; }
148
+ #ball > span { position: absolute; inset: 0; border-radius: 50%; pointer-events: none; }
149
+
150
+ #ball .glow {
151
+ background: none;
152
+ }
153
+
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
+ }
166
+
167
+ /* 上面那层:吉祥物本体,形状不变 */
168
+ #ball .face {
169
+ background-image: url('/panel/mascot.png');
170
+ background-size: 100%;
171
+ background-position: center;
172
+ background-repeat: no-repeat;
173
+ }
174
+
175
+ /* 三个**真状态**各有一张脸:忙(思考中)、连不上时委屈、额度用完时累。
176
+ 接的全是现成的状态类(`setBallState` 早就挂 busy/offline/quota 了),**不新增状态机**;
177
+ 光晕照旧画——它是"余光能看见"的环境灯,表情是盯着看才见的细节,两者说的是同一件事,
178
+ 一个给余光一个给注视,不重复。
179
+ **这三条必须写在下面 `ball-happy` 之前**:CSS 同特异性时后写的赢,而"用户正捏着它"
180
+ 比"助手在忙"更该被看见(你捏它的时候它就该笑)。
181
+
182
+ `mascot-thinking.png`(2026-09-29 换的,前身叫 `mascot-focus.png`):`busy` 的含义就是
183
+ "发出请求、在等回复"(`renderer.js` 的 `ask()` 一进来就挂上),所以这张脸画的是**思考**
184
+ ——眼神往左上瞟、眉平、嘴是一条平线、爪子搁在下巴上。出图用 `gpt-image-2-ca` 走
185
+ `/images/edits`(拿 `mascot.png` 当底,只改表情),再按上面那套参数归一化:
186
+ 内容框 171x212、最远实心距离 125.2(上限 125.4)、**圆外实心像素 0**。
187
+ 第一版提示词出的是"发愁"(嘴向下弯、眉压低),与 `sorry` 那张撞情绪,所以提示词里
188
+ 明确写了"不是难过、不是发愁"。 */
189
+ body.busy #ball .face { background-image: url('/panel/mascot-thinking.png'); }
190
+ body.offline #ball .face { background-image: url('/panel/mascot-sorry.png'); }
191
+ body.quota #ball .face { background-image: url('/panel/mascot-tired.png'); }
192
+
193
+ /* 被捏一下时的脸:眯眼笑。**硬切,不做淡入** —— 换表情本来就是一帧的事,淡入反而像
194
+ "图片正在加载"。两张图是用**同一套裁剪与缩放参数**从同一张源图生成的(实测差异只有
195
+ 3.5% 的像素,全部落在眼睛那一条上),所以切的时候脸不会跳、尺寸也不变。
196
+ 改 `background-image` 只此一处,`.face` 的基础规则仍在上面那条里。 */
197
+ body.ball-happy #ball .face {
198
+ background-image: url('/panel/mascot-happy.png');
199
+ }
200
+
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
+ /* ---------------------------------------------------------------- 状态 */
223
+
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; }
242
+
243
+ /* 系统说"减少动态效果"就照办:停掉所有流动,只留静止的光晕。
244
+ 展开/收起那两段也一起停(下面那几条,**写在原来那条之后**——测试盯的是这一段开头
245
+ 240 个字符里就有 `animation: none`,把新规则插在前面会把它挤出窗口)。 */
246
+ @media (prefers-reduced-motion: reduce) {
247
+ #ball .glow,
248
+ body.busy #ball .glow { animation: none; }
249
+ body.mode-chat #bubble { animation: none; }
250
+ body.mode-chat.closing #bubble { animation: none; opacity: 0; }
251
+ #ball { transition: none; }
252
+ /* 形变也要停。只关 transition 的话,悬停/按下会**瞬间**跳到放大/压扁的形状,
253
+ 那看起来更像故障而不是"少动一点"。 */
254
+ #ball:hover, #ball:active { transform: none; }
255
+ }
256
+
257
+ /* 悬停:微微放大 + 阴影加深 + 提亮 —— 三样一起才够"这里能点"。
258
+ **放大只敢到 1.02**:出图按"实心像素最远点 ≤ 0.98 × 半径"缩放(见 `.face` 那段),
259
+ 1.02 倍时圆外只剩 1 个像素(实测),再大就开始削掉猫的外缘。 */
260
+ #ball:hover { transform: scale(1.02); filter: drop-shadow(0 8px 16px rgba(0, 0, 0, .58)) brightness(1.08); }
261
+ /* 按下:压扁。**只往小里做** —— 放大或位移会把内容顶出窗口、被窗口边裁掉。 */
262
+ #ball:active { transform: scale(.92); }
263
+
264
+ /* 球形态:把气泡整个藏掉,窗口缩回球那么大(96x96,主进程那边做)。
265
+ 底色早就不是 body 的了(见上面 `body.shell` 那两条)——气泡自己画,窗口是透明的。 */
266
+ body.mode-ball #bubble { display: none; }
267
+ /* 球形态下不许出现滚动条:busy 的呼吸动画会 scale(1.08),
268
+ 溢出的那点内容会在球那 96x96 的窗口里长出滚动条(实测:右侧冒出上下箭头)。 */
269
+ body.mode-ball { overflow: hidden; }
270
+
271
+ /* 对话形态(有外壳时)才显示"收起" */
272
+ body.mode-chat #collapse { display: inline-block; }
273
+
274
+ /* header 顶在气泡的圆角上,得自己把上面两个角修圆(气泡那层没开 overflow: hidden,
275
+ 开了会把尾巴剪掉)。只有它需要:下面几层的底边不是气泡的底边(footer 才是,而它没有底色)。 */
276
+ body.shell header { border-radius: var(--bubble-radius) var(--bubble-radius) 0 0; }
277
+
278
+ /* 展开时气泡淡进来。放在**稳定态那个类**上(不是临时的过渡类):只要页面变成对话形态
279
+ 就播一次——第一次打开、从球展开、托盘展开都算,它本来就是"气泡出现了"这件事。
280
+ **这段动画是全部动效**:窗口那边主进程是一次到位的,不逐帧改大小——透明窗口每 resize
281
+ 一次 DWM 就要重新合成一次,用户实测"闪好多才弹出"就是这么来的。这里只动 opacity 与
282
+ transform,合成器直接做,不重新布局、不改窗口。
283
+ `transform-origin` 取球那一侧:气泡看起来是从球那儿"长出来"的,而不是凭空出现。
284
+ **不给 `main` 加 transform**:`#log` 是滚动容器,加位移会多出一个包含块。 */
285
+ /* 原点取球心那一侧:球贴窗口底时球心离底 38px(球 76 高),贴顶时离顶 38px */
286
+ body.mode-chat #bubble {
287
+ transform-origin: right calc(100% - 38px);
288
+ animation: bubble-in .2s cubic-bezier(.2, .8, .2, 1) both;
289
+ }
290
+ body.mode-chat.bubble-right #bubble { transform-origin: left calc(100% - 38px); }
291
+ body.mode-chat.anchor-top #bubble { transform-origin: right 38px; }
292
+ body.mode-chat.bubble-right.anchor-top #bubble { transform-origin: left 38px; }
293
+ @keyframes bubble-in {
294
+ from { opacity: 0; transform: scale(.94); }
295
+ to { opacity: 1; transform: none; }
296
+ }
297
+
298
+ /* 收起:先把气泡淡掉(**窗口这会儿还是大的**,所以这 130ms 真看得见),主进程等这段时间
299
+ 过去再缩窗口,缩完才通知页面把气泡摘掉(`display: none`)。顺序不能反:提前摘掉就成了
300
+ "啪"地消失,晚摘掉就会在球那 96x96 的窗口里露出一条气泡的边。
301
+ **这里用 animation 而不是 transition**:上面那条 `bubble-in` 带着 `both` 填充,而动画在
302
+ 层叠里**压过**过渡——写 `opacity: 0` 会被它一直覆盖着,收起时看着就像没反应。
303
+ 两条都是动画时,靠"更靠后的选择器 + 更高的特指度"(多一个 `.closing`)取胜。 */
304
+ body.mode-chat.closing #bubble { animation: bubble-out .13s ease both; }
305
+ @keyframes bubble-out {
306
+ from { opacity: 1; }
307
+ to { opacity: 0; }
308
+ }
309
+
310
+ .ghost {
311
+ margin-left: auto;
312
+ background: transparent;
313
+ color: var(--dim);
314
+ border: 1px solid var(--line);
315
+ padding: 2px 10px;
316
+ font-weight: 400;
317
+ -webkit-app-region: no-drag;
318
+ }
319
+ .ghost:hover { color: var(--text); }
320
+
321
+ main {
322
+ flex: 1;
323
+ overflow-y: auto;
324
+ padding: 12px 14px;
325
+ display: flex;
326
+ flex-direction: column;
327
+ gap: 10px;
328
+ }
329
+
330
+ .turn { max-width: 92%; white-space: pre-wrap; word-break: break-word; }
331
+ .turn.me { align-self: flex-end; background: var(--accent); color: #fff; padding: 7px 11px; border-radius: 12px 12px 2px 12px; }
332
+ .turn.it { align-self: flex-start; background: var(--panel); padding: 7px 11px; border-radius: 12px 12px 12px 2px; }
333
+ .turn.sys { align-self: center; color: var(--dim); font-size: 12px; text-align: center; }
334
+ .turn.err { align-self: stretch; border: 1px solid var(--err); color: var(--err); padding: 7px 11px; border-radius: 10px; }
335
+ .turn.pending { color: var(--dim); font-style: italic; }
336
+
337
+ /* 助手回答下的复制按钮。**常显**而不是 hover 才出现:这是个常驻小窗,
338
+ 鼠标常常不在它上面,hover 等于没有。做得很淡,不跟正文抢注意力。 */
339
+ .turn .copy {
340
+ display: block;
341
+ margin-top: 6px;
342
+ padding: 1px 7px;
343
+ font: inherit;
344
+ font-size: 11px;
345
+ color: var(--dim);
346
+ background: transparent;
347
+ border: 1px solid var(--line);
348
+ border-radius: 999px;
349
+ cursor: pointer;
350
+ }
351
+ .turn .copy:hover { color: var(--text); border-color: var(--dim); }
352
+
353
+ /* ---------------------------------------------------------------- 还没聊过时的开头 */
354
+
355
+ .empty {
356
+ margin: auto 0; /* 垂直居中一点,不要贴着顶 */
357
+ display: flex;
358
+ flex-direction: column;
359
+ align-items: flex-start;
360
+ gap: 8px;
361
+ padding: 8px 2px;
362
+ }
363
+ .empty-title { font-weight: 600; }
364
+ .empty-sub { color: var(--dim); font-size: 12px; margin-bottom: 6px; }
365
+
366
+ /* 例子做成扁平的胶囊按钮:全局 button 那套(蓝底、大内边距)在这儿太重了 */
367
+ .chip {
368
+ background: transparent;
369
+ border: 1px solid var(--line);
370
+ color: var(--text);
371
+ font-weight: 400;
372
+ font-size: 13px;
373
+ padding: 5px 12px;
374
+ border-radius: 999px;
375
+ }
376
+ .chip:hover { border-color: var(--accent); color: #fff; }
377
+
378
+ form {
379
+ display: flex;
380
+ gap: 8px;
381
+ padding: 10px 12px;
382
+ border-top: 1px solid var(--line);
383
+ background: var(--panel);
384
+ }
385
+
386
+ textarea {
387
+ flex: 1;
388
+ resize: none;
389
+ padding: 8px 10px;
390
+ border: 1px solid var(--line);
391
+ border-radius: 8px;
392
+ background: var(--bg);
393
+ color: var(--text);
394
+ font: inherit;
395
+ }
396
+ textarea:focus { outline: none; border-color: var(--accent); }
397
+
398
+ button {
399
+ padding: 0 16px;
400
+ border: 0;
401
+ border-radius: 8px;
402
+ background: var(--accent);
403
+ color: #fff;
404
+ font: inherit;
405
+ font-weight: 600;
406
+ cursor: pointer;
407
+ }
408
+ button:disabled { opacity: .45; cursor: default; }
409
+
410
+ footer {
411
+ padding: 6px 14px 10px;
412
+ color: var(--dim);
413
+ font-size: 12px;
414
+ }
415
+ footer .warn { color: var(--warn); }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * 渲染进程与主进程之间**唯一**的那道缝。
3
+ *
4
+ * 刻意只暴露三件事,而且全是窗口层面的:**没有凭据、没有端口、没有文件系统、没有 URL**。
5
+ * 页面自己用 `fetch('/api/...')` 跟守护进程说话(相对路径 + HttpOnly cookie),
6
+ * 不需要经过主进程,所以也就没有理由把凭据递给它。
7
+ *
8
+ * 订阅只开**一个**,而且通道名**写死在这里**:风险从来不是"能收推送",而是
9
+ * "渲染进程能决定通道名"——那等于把整条 IPC 面交出去。所以不给通用的 `on(name, cb)`。
10
+ */
11
+ const { contextBridge, ipcRenderer } = require('electron')
12
+
13
+ contextBridge.exposeInMainWorld('weflowPanel', {
14
+ /**
15
+ * 'ball' 收成小球,'chat' 展开成对话窗。
16
+ *
17
+ * `opts.animate === false` 表示"别做动画,直接到位"——页面读 `prefers-reduced-motion`
18
+ * 之后传进来。**为什么让页面来读**:那个媒体查询只有渲染进程有,主进程这边没有等价 API;
19
+ * 而窗口几何的补间在主进程,CSS 管不着它。默认是**动画**(不传就是动画)。
20
+ */
21
+ setMode: (mode, opts) => ipcRenderer.invoke('panel:setMode',
22
+ mode === 'chat' ? 'chat' : 'ball',
23
+ { animate: !opts || opts.animate !== false }),
24
+ /** 'window' 只关窗(助手继续跑),'assistant' 连助手一起停 */
25
+ quit: (what) => ipcRenderer.invoke('panel:quit', what === 'assistant' ? 'assistant' : 'window'),
26
+ /**
27
+ * 拖拽。**为什么不是 `-webkit-app-region: drag`**:在 Windows 上拖拽区会**吞掉鼠标事件**,
28
+ * 页面收不到 click——球就点不开了(这正是实测到的故障)。所以拖拽改成自己实现:
29
+ * 主进程记下按下时的窗口位置与指针位置,移动时按差值 setPosition。
30
+ */
31
+ dragStart: (x, y) => ipcRenderer.invoke('panel:dragStart', { x, y }),
32
+ dragMove: (x, y) => ipcRenderer.invoke('panel:dragMove', { x, y }),
33
+ dragEnd: () => ipcRenderer.invoke('panel:dragEnd'),
34
+
35
+ /**
36
+ * 右键快速回复:把名单交给主进程去弹**原生菜单**,返回用户选中的那个名字(没选就是 null)。
37
+ *
38
+ * 为什么菜单要由主进程弹:原生菜单画在窗口外面,所以球形态那 76x76 的窗口不用先展开成
39
+ * 对话窗——而"点右键顺手把窗口弹出来"正是用户不要的那一下。
40
+ * 名单在这里**收窄**:只要字符串、去掉空的、最多 20 个(主进程那侧还会再去一次重)。
41
+ */
42
+ openQuickMenu: (labels) => ipcRenderer.invoke('panel:quickMenu',
43
+ (Array.isArray(labels) ? labels : [])
44
+ .filter((name) => typeof name === 'string' && name.trim())
45
+ .map((name) => name.trim())
46
+ .slice(0, 20)),
47
+
48
+ /** 主进程侧的实际状态,供界面显示"关窗后助手还在跑"这类事实 */
49
+ info: () => ipcRenderer.invoke('panel:info'),
50
+ /**
51
+ * 凭据失效时请主进程换一张(重读端点文件 + 刷新 cookie + 重载)。
52
+ *
53
+ * 页面**看不到 token**,也不该看到——所以这件事只能由主进程做,页面只负责触发。
54
+ * 助手没在跑、或 token 本来就没变时它**不重载**,返回一个 code 让页面自己决定怎么说。
55
+ */
56
+ repair: () => ipcRenderer.invoke('panel:repair'),
57
+ /**
58
+ * 主进程把窗口切成了球形态/气泡形态时通知页面。**通道名是常量,不由调用方给**。
59
+ *
60
+ * 载荷在这里**收窄**过再交出去:`mode` 只认 'ball'/'chat',`side` 只认 'left'/'right',
61
+ * `anchorY` 只认 'top'/'bottom',`bubbleHeight` 夹进 1..10000,`fadeMs` 夹进 0..2000,
62
+ * `done` 只认布尔。跟通道名同一个道理——不把主进程给的东西原样透传。
63
+ */
64
+ onMode: (cb) => {
65
+ if (typeof cb !== 'function') return
66
+ const clampInt = (value, max, min = 0) => {
67
+ const n = Math.round(Number(value))
68
+ return Number.isFinite(n) ? Math.min(Math.max(n, min), max) : min
69
+ }
70
+ ipcRenderer.on('panel:mode', (_event, payload) => cb({
71
+ mode: payload && payload.mode === 'ball' ? 'ball' : 'chat',
72
+ side: payload && payload.side === 'right' ? 'right' : 'left',
73
+ anchorY: payload && payload.anchorY === 'top' ? 'top' : 'bottom',
74
+ bubbleHeight: clampInt(payload && payload.bubbleHeight, 10000, 1),
75
+ fadeMs: clampInt(payload && payload.fadeMs, 2000),
76
+ done: !!(payload && payload.done),
77
+ }))
78
+ },
79
+ })