weflow-cli 1.7.0 → 1.8.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 (194) hide show
  1. package/ARCHITECTURE.md +3 -2
  2. package/CHANGELOG.md +1949 -1
  3. package/OPERATIONS.md +774 -1
  4. package/README.en.md +93 -7
  5. package/README.md +31 -6
  6. package/bin/weflow-cli.ts +934 -27
  7. package/dist/bin/weflow-cli.js +952 -27
  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 +18 -4
  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 +590 -75
  70. package/dist/src/services/assistantService.js.map +1 -1
  71. package/dist/src/services/assistantTools.d.ts +205 -1
  72. package/dist/src/services/assistantTools.d.ts.map +1 -1
  73. package/dist/src/services/assistantTools.js +1089 -58
  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/utils/atomicWrite.d.ts +4 -0
  100. package/dist/src/utils/atomicWrite.d.ts.map +1 -0
  101. package/dist/src/utils/atomicWrite.js +60 -0
  102. package/dist/src/utils/atomicWrite.js.map +1 -0
  103. package/dist/src/utils/dateRange.d.ts +15 -0
  104. package/dist/src/utils/dateRange.d.ts.map +1 -1
  105. package/dist/src/utils/dateRange.js +47 -0
  106. package/dist/src/utils/dateRange.js.map +1 -1
  107. package/dist/src/utils/text.d.ts +18 -0
  108. package/dist/src/utils/text.d.ts.map +1 -0
  109. package/dist/src/utils/text.js +20 -0
  110. package/dist/src/utils/text.js.map +1 -0
  111. package/docs/AI_INTERFACE.md +2 -0
  112. package/docs/DECISIONS.md +1424 -0
  113. package/docs/EXTENDING.md +187 -0
  114. package/docs/JEV/345/272/224/347/224/250/346/226/275/345/267/245/346/226/207/346/241/243.md +310 -0
  115. package/docs/MCP.md +23 -4
  116. package/docs/PROJECT_STATE.md +299 -98
  117. package/docs/SETUP.md +22 -0
  118. package/docs/SYNC_CONTRACT.md +31 -3
  119. package/mcp-server/index.ts +681 -657
  120. package/package.json +84 -83
  121. package/resources/panel/ball-position.cjs +153 -0
  122. package/resources/panel/index.html +46 -0
  123. package/resources/panel/main.cjs +447 -0
  124. package/resources/panel/mascot-focus.png +0 -0
  125. package/resources/panel/mascot-happy.png +0 -0
  126. package/resources/panel/mascot-sorry.png +0 -0
  127. package/resources/panel/mascot-tired.png +0 -0
  128. package/resources/panel/mascot.png +0 -0
  129. package/resources/panel/package.json +7 -0
  130. package/resources/panel/panel.css +407 -0
  131. package/resources/panel/preload.cjs +72 -0
  132. package/resources/panel/quick-menu.cjs +197 -0
  133. package/resources/panel/renderer.js +441 -0
  134. package/resources/panel/tray-menu.cjs +40 -0
  135. package/resources/panel/tray.png +0 -0
  136. package/scripts/_utils.py +403 -54
  137. package/scripts/annual_report.py +4 -2
  138. package/scripts/article_notes.py +423 -0
  139. package/scripts/backfill_articles.py +438 -0
  140. package/scripts/biz_daily.py +590 -105
  141. package/scripts/chat_notes.py +470 -0
  142. package/scripts/classify_daily.py +10 -18
  143. package/scripts/compile_wiki.py +1059 -46
  144. package/scripts/create_reading_notes.py +199 -14
  145. package/scripts/decide.py +253 -0
  146. package/scripts/draft_reply.py +671 -0
  147. package/scripts/export_chat_html.py +35 -33
  148. package/scripts/extract_todos.py +31 -5
  149. package/scripts/fav_notes.py +318 -0
  150. package/scripts/fix_topics.py +2 -1
  151. package/scripts/generate_ai_report.py +214 -9
  152. package/scripts/generate_html.py +18 -3
  153. package/scripts/generate_review.py +2 -2
  154. package/scripts/jev_client.py +294 -0
  155. package/scripts/jev_probe.py +440 -0
  156. package/scripts/nt_common.py +82 -0
  157. package/scripts/nt_decrypt.py +1755 -1460
  158. package/scripts/pipeline.py +27 -2
  159. package/scripts/promote_ideas.py +19 -4
  160. package/scripts/quality_eval.py +707 -0
  161. package/scripts/rag_chat.py +4 -4
  162. package/scripts/read_image.py +187 -0
  163. package/scripts/reply_debt.py +488 -0
  164. package/scripts/route_cards.py +453 -0
  165. package/scripts/semantic_search.py +208 -49
  166. package/scripts/user_notes.py +315 -0
  167. package/scripts/vault_rag.py +13 -7
  168. package/scripts/vault_search.py +21 -4
  169. package/scripts/wiki_lint.py +397 -0
  170. package/src/core/appMsgFormat.ts +117 -0
  171. package/src/core/ntCore.ts +27 -2
  172. package/src/core/sqlcipherCore.ts +5 -61
  173. package/src/core/wcdbCore.ts +6 -2
  174. package/src/panel/client.ts +49 -0
  175. package/src/panel/endpoint.ts +144 -0
  176. package/src/panel/launch.ts +101 -0
  177. package/src/panel/server.ts +424 -0
  178. package/src/panel/userId.ts +72 -0
  179. package/src/services/assistantDaemon.ts +57 -9
  180. package/src/services/assistantEval.ts +611 -0
  181. package/src/services/assistantMemory.ts +413 -57
  182. package/src/services/assistantPrivacy.ts +18 -4
  183. package/src/services/assistantRouter.ts +191 -0
  184. package/src/services/assistantService.ts +671 -74
  185. package/src/services/assistantTools.ts +1118 -57
  186. package/src/services/assistantTrace.ts +175 -0
  187. package/src/services/chatService.ts +6 -2
  188. package/src/services/configService.ts +403 -385
  189. package/src/services/pythonBridge.ts +139 -0
  190. package/src/services/syncService.ts +37 -9
  191. package/src/services/wechatMessageService.ts +73 -7
  192. package/src/utils/atomicWrite.ts +56 -0
  193. package/src/utils/dateRange.ts +46 -0
  194. package/src/utils/text.ts +20 -0
@@ -0,0 +1,407 @@
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
+ body.busy #ball .face { background-image: url('/panel/mascot-focus.png'); }
182
+ body.offline #ball .face { background-image: url('/panel/mascot-sorry.png'); }
183
+ body.quota #ball .face { background-image: url('/panel/mascot-tired.png'); }
184
+
185
+ /* 被捏一下时的脸:眯眼笑。**硬切,不做淡入** —— 换表情本来就是一帧的事,淡入反而像
186
+ "图片正在加载"。两张图是用**同一套裁剪与缩放参数**从同一张源图生成的(实测差异只有
187
+ 3.5% 的像素,全部落在眼睛那一条上),所以切的时候脸不会跳、尺寸也不变。
188
+ 改 `background-image` 只此一处,`.face` 的基础规则仍在上面那条里。 */
189
+ body.ball-happy #ball .face {
190
+ background-image: url('/panel/mascot-happy.png');
191
+ }
192
+
193
+ /* 色相流动 —— 现在只在**忙碌**时跑(空闲时那一层根本不画)。
194
+ 8 秒一圈由 `body.busy` 那条给;色相本身只是被 `drift` 驱动的数值。 */
195
+ @property --hue { syntax: '<number>'; inherits: true; initial-value: 224; }
196
+ #ball { --hue: 224; }
197
+ @keyframes drift {
198
+ 0% { --hue: 224; } /* 蓝 */
199
+ 25% { --hue: 268; } /* 紫 */
200
+ 50% { --hue: 316; } /* 品红 */
201
+ 75% { --hue: 190; } /* 青 */
202
+ 100% { --hue: 224; }
203
+ }
204
+
205
+ /* **空闲时球后面什么都没有**(2026-09-26 改)。
206
+
207
+ 这一层原来一直亮着,那是用户当初要的"会变动的背景"。但两个代价在用过之后才显出来:
208
+ 球背后永远有一圈彩色圆盘,**"这张图是透明的"这件事根本看不出来**;而它同时兼着状态灯,
209
+ 一直亮着就等于没有状态灯——离线、忙碌、额度用完,球长得都一样。
210
+
211
+ 现在:**静止 = 只有猫,亮起来 = 有话说**。这是那条需求的**范围缩小,不是取消**——
212
+ 色相流动还在,只是改在忙碌时发生(下面那条)。 */
213
+
214
+ /* ---------------------------------------------------------------- 状态 */
215
+
216
+ /* 正在想事情:光晕变亮、转得快,并且轻轻呼吸。
217
+ 这一条的用处不只是好看——球收起来的时候,此前**完全看不出它在干活**。 */
218
+ body.busy #ball .glow {
219
+ /* 8 秒转一圈色相(不是 3 秒):呼吸的节奏交给 1.5s,色相再快就变成闪了 */
220
+ animation: drift 8s linear infinite, breathe 1.5s ease-in-out infinite;
221
+ }
222
+ @keyframes breathe {
223
+ 0%, 100% { transform: scale(1); opacity: 1; }
224
+ 50% { transform: scale(1.08); opacity: .82; }
225
+ }
226
+
227
+ /* 连不上 / 出错:换成暖色并停住流动——不动的东西才会被注意到 */
228
+ body.offline #ball { --hue: 12; }
229
+ body.offline #ball .glow { animation: none; opacity: .9; }
230
+
231
+ /* 今天的额度用完了:琥珀色 */
232
+ body.quota #ball { --hue: 38; }
233
+ body.quota #ball .glow { animation: none; opacity: .85; }
234
+
235
+ /* 系统说"减少动态效果"就照办:停掉所有流动,只留静止的光晕。
236
+ 展开/收起那两段也一起停(下面那几条,**写在原来那条之后**——测试盯的是这一段开头
237
+ 240 个字符里就有 `animation: none`,把新规则插在前面会把它挤出窗口)。 */
238
+ @media (prefers-reduced-motion: reduce) {
239
+ #ball .glow,
240
+ body.busy #ball .glow { animation: none; }
241
+ body.mode-chat #bubble { animation: none; }
242
+ body.mode-chat.closing #bubble { animation: none; opacity: 0; }
243
+ #ball { transition: none; }
244
+ /* 形变也要停。只关 transition 的话,悬停/按下会**瞬间**跳到放大/压扁的形状,
245
+ 那看起来更像故障而不是"少动一点"。 */
246
+ #ball:hover, #ball:active { transform: none; }
247
+ }
248
+
249
+ /* 悬停:微微放大 + 阴影加深 + 提亮 —— 三样一起才够"这里能点"。
250
+ **放大只敢到 1.02**:出图按"实心像素最远点 ≤ 0.98 × 半径"缩放(见 `.face` 那段),
251
+ 1.02 倍时圆外只剩 1 个像素(实测),再大就开始削掉猫的外缘。 */
252
+ #ball:hover { transform: scale(1.02); filter: drop-shadow(0 8px 16px rgba(0, 0, 0, .58)) brightness(1.08); }
253
+ /* 按下:压扁。**只往小里做** —— 放大或位移会把内容顶出窗口、被窗口边裁掉。 */
254
+ #ball:active { transform: scale(.92); }
255
+
256
+ /* 球形态:把气泡整个藏掉,窗口缩回球那么大(96x96,主进程那边做)。
257
+ 底色早就不是 body 的了(见上面 `body.shell` 那两条)——气泡自己画,窗口是透明的。 */
258
+ body.mode-ball #bubble { display: none; }
259
+ /* 球形态下不许出现滚动条:busy 的呼吸动画会 scale(1.08),
260
+ 溢出的那点内容会在球那 96x96 的窗口里长出滚动条(实测:右侧冒出上下箭头)。 */
261
+ body.mode-ball { overflow: hidden; }
262
+
263
+ /* 对话形态(有外壳时)才显示"收起" */
264
+ body.mode-chat #collapse { display: inline-block; }
265
+
266
+ /* header 顶在气泡的圆角上,得自己把上面两个角修圆(气泡那层没开 overflow: hidden,
267
+ 开了会把尾巴剪掉)。只有它需要:下面几层的底边不是气泡的底边(footer 才是,而它没有底色)。 */
268
+ body.shell header { border-radius: var(--bubble-radius) var(--bubble-radius) 0 0; }
269
+
270
+ /* 展开时气泡淡进来。放在**稳定态那个类**上(不是临时的过渡类):只要页面变成对话形态
271
+ 就播一次——第一次打开、从球展开、托盘展开都算,它本来就是"气泡出现了"这件事。
272
+ **这段动画是全部动效**:窗口那边主进程是一次到位的,不逐帧改大小——透明窗口每 resize
273
+ 一次 DWM 就要重新合成一次,用户实测"闪好多才弹出"就是这么来的。这里只动 opacity 与
274
+ transform,合成器直接做,不重新布局、不改窗口。
275
+ `transform-origin` 取球那一侧:气泡看起来是从球那儿"长出来"的,而不是凭空出现。
276
+ **不给 `main` 加 transform**:`#log` 是滚动容器,加位移会多出一个包含块。 */
277
+ /* 原点取球心那一侧:球贴窗口底时球心离底 38px(球 76 高),贴顶时离顶 38px */
278
+ body.mode-chat #bubble {
279
+ transform-origin: right calc(100% - 38px);
280
+ animation: bubble-in .2s cubic-bezier(.2, .8, .2, 1) both;
281
+ }
282
+ body.mode-chat.bubble-right #bubble { transform-origin: left calc(100% - 38px); }
283
+ body.mode-chat.anchor-top #bubble { transform-origin: right 38px; }
284
+ body.mode-chat.bubble-right.anchor-top #bubble { transform-origin: left 38px; }
285
+ @keyframes bubble-in {
286
+ from { opacity: 0; transform: scale(.94); }
287
+ to { opacity: 1; transform: none; }
288
+ }
289
+
290
+ /* 收起:先把气泡淡掉(**窗口这会儿还是大的**,所以这 130ms 真看得见),主进程等这段时间
291
+ 过去再缩窗口,缩完才通知页面把气泡摘掉(`display: none`)。顺序不能反:提前摘掉就成了
292
+ "啪"地消失,晚摘掉就会在球那 96x96 的窗口里露出一条气泡的边。
293
+ **这里用 animation 而不是 transition**:上面那条 `bubble-in` 带着 `both` 填充,而动画在
294
+ 层叠里**压过**过渡——写 `opacity: 0` 会被它一直覆盖着,收起时看着就像没反应。
295
+ 两条都是动画时,靠"更靠后的选择器 + 更高的特指度"(多一个 `.closing`)取胜。 */
296
+ body.mode-chat.closing #bubble { animation: bubble-out .13s ease both; }
297
+ @keyframes bubble-out {
298
+ from { opacity: 1; }
299
+ to { opacity: 0; }
300
+ }
301
+
302
+ .ghost {
303
+ margin-left: auto;
304
+ background: transparent;
305
+ color: var(--dim);
306
+ border: 1px solid var(--line);
307
+ padding: 2px 10px;
308
+ font-weight: 400;
309
+ -webkit-app-region: no-drag;
310
+ }
311
+ .ghost:hover { color: var(--text); }
312
+
313
+ main {
314
+ flex: 1;
315
+ overflow-y: auto;
316
+ padding: 12px 14px;
317
+ display: flex;
318
+ flex-direction: column;
319
+ gap: 10px;
320
+ }
321
+
322
+ .turn { max-width: 92%; white-space: pre-wrap; word-break: break-word; }
323
+ .turn.me { align-self: flex-end; background: var(--accent); color: #fff; padding: 7px 11px; border-radius: 12px 12px 2px 12px; }
324
+ .turn.it { align-self: flex-start; background: var(--panel); padding: 7px 11px; border-radius: 12px 12px 12px 2px; }
325
+ .turn.sys { align-self: center; color: var(--dim); font-size: 12px; text-align: center; }
326
+ .turn.err { align-self: stretch; border: 1px solid var(--err); color: var(--err); padding: 7px 11px; border-radius: 10px; }
327
+ .turn.pending { color: var(--dim); font-style: italic; }
328
+
329
+ /* 助手回答下的复制按钮。**常显**而不是 hover 才出现:这是个常驻小窗,
330
+ 鼠标常常不在它上面,hover 等于没有。做得很淡,不跟正文抢注意力。 */
331
+ .turn .copy {
332
+ display: block;
333
+ margin-top: 6px;
334
+ padding: 1px 7px;
335
+ font: inherit;
336
+ font-size: 11px;
337
+ color: var(--dim);
338
+ background: transparent;
339
+ border: 1px solid var(--line);
340
+ border-radius: 999px;
341
+ cursor: pointer;
342
+ }
343
+ .turn .copy:hover { color: var(--text); border-color: var(--dim); }
344
+
345
+ /* ---------------------------------------------------------------- 还没聊过时的开头 */
346
+
347
+ .empty {
348
+ margin: auto 0; /* 垂直居中一点,不要贴着顶 */
349
+ display: flex;
350
+ flex-direction: column;
351
+ align-items: flex-start;
352
+ gap: 8px;
353
+ padding: 8px 2px;
354
+ }
355
+ .empty-title { font-weight: 600; }
356
+ .empty-sub { color: var(--dim); font-size: 12px; margin-bottom: 6px; }
357
+
358
+ /* 例子做成扁平的胶囊按钮:全局 button 那套(蓝底、大内边距)在这儿太重了 */
359
+ .chip {
360
+ background: transparent;
361
+ border: 1px solid var(--line);
362
+ color: var(--text);
363
+ font-weight: 400;
364
+ font-size: 13px;
365
+ padding: 5px 12px;
366
+ border-radius: 999px;
367
+ }
368
+ .chip:hover { border-color: var(--accent); color: #fff; }
369
+
370
+ form {
371
+ display: flex;
372
+ gap: 8px;
373
+ padding: 10px 12px;
374
+ border-top: 1px solid var(--line);
375
+ background: var(--panel);
376
+ }
377
+
378
+ textarea {
379
+ flex: 1;
380
+ resize: none;
381
+ padding: 8px 10px;
382
+ border: 1px solid var(--line);
383
+ border-radius: 8px;
384
+ background: var(--bg);
385
+ color: var(--text);
386
+ font: inherit;
387
+ }
388
+ textarea:focus { outline: none; border-color: var(--accent); }
389
+
390
+ button {
391
+ padding: 0 16px;
392
+ border: 0;
393
+ border-radius: 8px;
394
+ background: var(--accent);
395
+ color: #fff;
396
+ font: inherit;
397
+ font-weight: 600;
398
+ cursor: pointer;
399
+ }
400
+ button:disabled { opacity: .45; cursor: default; }
401
+
402
+ footer {
403
+ padding: 6px 14px 10px;
404
+ color: var(--dim);
405
+ font-size: 12px;
406
+ }
407
+ footer .warn { color: var(--warn); }
@@ -0,0 +1,72 @@
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
+ * 主进程把窗口切成了球形态/气泡形态时通知页面。**通道名是常量,不由调用方给**。
52
+ *
53
+ * 载荷在这里**收窄**过再交出去:`mode` 只认 'ball'/'chat',`side` 只认 'left'/'right',
54
+ * `anchorY` 只认 'top'/'bottom',`bubbleHeight` 夹进 1..10000,`fadeMs` 夹进 0..2000,
55
+ * `done` 只认布尔。跟通道名同一个道理——不把主进程给的东西原样透传。
56
+ */
57
+ onMode: (cb) => {
58
+ if (typeof cb !== 'function') return
59
+ const clampInt = (value, max, min = 0) => {
60
+ const n = Math.round(Number(value))
61
+ return Number.isFinite(n) ? Math.min(Math.max(n, min), max) : min
62
+ }
63
+ ipcRenderer.on('panel:mode', (_event, payload) => cb({
64
+ mode: payload && payload.mode === 'ball' ? 'ball' : 'chat',
65
+ side: payload && payload.side === 'right' ? 'right' : 'left',
66
+ anchorY: payload && payload.anchorY === 'top' ? 'top' : 'bottom',
67
+ bubbleHeight: clampInt(payload && payload.bubbleHeight, 10000, 1),
68
+ fadeMs: clampInt(payload && payload.fadeMs, 2000),
69
+ done: !!(payload && payload.done),
70
+ }))
71
+ },
72
+ })
@@ -0,0 +1,197 @@
1
+ /**
2
+ * 右键菜单的**内容**:纯数据 + 回调,不 require electron。
3
+ *
4
+ * 为什么单独一个文件:菜单里的点击在 CI 里点不了,但**"有哪几项、点了各自该干什么"是我们的代码**,
5
+ * 而 `Menu.buildFromTemplate` + `popup` 那一步才是 Electron 的。拆开之后这个函数能被测试直接
6
+ * require 进来、把每个 `click` 真的调一遍——比拿正则去匹配源码文本结实得多(同 `tray-menu.cjs`)。
7
+ *
8
+ * **为什么球形态下用原生菜单而不是页内那个**:原生菜单画在**窗口外面**,所以球那 76x76 的
9
+ * 窗口不用先展开成对话窗——用户的原话是"右键应该算是快速功能",而"顺手把窗口弹出来"恰好
10
+ * 是他要避免的那一下。页内菜单留给浏览器降级那条路(那条路没有 Electron,也就没有原生菜单,
11
+ * 而它的窗口是正常的浏览器窗,画得下)。
12
+ *
13
+ * 菜单有两段:**起草回复**(联系人名单)与**快捷功能**(固定动作)。第二段是后加的,
14
+ * 加它的判据只有一条:**这个动作能不能用一句固定的话做完**。要打字才能用的(搜索、导出、
15
+ * 指定某条消息)做不成一键,不许往这里塞——塞进来只会做出一个点了没反应的菜单项。
16
+ *
17
+ * 一条不能松的语义:**代价写在菜单里**,而且**逐项说清**(见 `costNote()`)。点一下 = 一次
18
+ * 出境这件事必须在菜单里看得见,而不是点完才知道;反过来,"这几项其实只读本地"也该看得见,
19
+ * 否则用户会不敢点本地那几项。
20
+ *
21
+ * 最后一项是**「关闭悬浮球」**,它是这个菜单里唯一不碰 AI 的一项。三条取舍:
22
+ * - 它是 `win.hide()`(收起来),**不是退出**。退出面板 / 退出并停止助手属于"退出"那一类,
23
+ * 在托盘菜单里(`tray-menu.cjs` 把它们和"显示 / 展开"分隔开了),这里塞进来就把两类混了。
24
+ * - 所以它**可逆**,而收回来的路(托盘图标)必须写在标签里——球藏起来以后任务栏没有它的位置
25
+ * (`setSkipTaskbar(true)`),不写清楚就成了"点一下它永远消失了"。
26
+ * - **任何情况下它都在**:名单没配过、快捷功能全不可用时,用户照样得有个办法把球收起来。
27
+ */
28
+ 'use strict'
29
+
30
+ /** 菜单最多列几个人。窗口小、名单可能很长,超出部分不该把菜单撑成一整屏 */
31
+ const MAX_ITEMS = 20
32
+
33
+ /** 名单为空时菜单显示的两行:说清没配,并给出该跑的那条命令 */
34
+ const EMPTY_HINT = [
35
+ '还没配名单',
36
+ 'weflow-cli config set quickReplyContacts "某人,另一人"',
37
+ ]
38
+
39
+ /**
40
+ * 收起来那一项的标签。括号里那句**不是凑字数的**:球藏起来之后任务栏里没有它
41
+ * (`setSkipTaskbar(true)`),不写出"托盘图标能再打开",这一项看上去就像"永久关掉"。
42
+ */
43
+ const CLOSE_LABEL = '关闭悬浮球(托盘图标能再打开)'
44
+
45
+ /** 两段的段头。**不可点**:它们是分组,不是按钮 */
46
+ const REPLY_SECTION = '起草回复'
47
+ const ACTION_SECTION = '快捷功能'
48
+
49
+ /**
50
+ * 快捷功能。每一项 = 一句固定的话 + 一句给人看的标签。
51
+ *
52
+ * `prompt` **随菜单项一起回传**(经 `panel:quickMenu` 的返回值到页面),页面不另存一份
53
+ * id→话术的映射:两份映射一定会漂移,而漂移的那种失败是"菜单写着甲、点下去做了乙",
54
+ * 不报错、只有读代码才发现。
55
+ *
56
+ * `cloud` 只用来生成提示(`costNote()`),不影响行为——但这些话术**要么点了就调模型**,
57
+ * 要么**纯本地**,没有中间态,所以一个布尔值够用。
58
+ *
59
+ * 措辞里点名工具("用 who_owes_reply 看一遍")是必要的:助手是在 19 个工具里自己选,
60
+ * 不点名时"谁在等我回话"这种话它可能去调 `search_chats` 凑答案。**这仍是靠模型选对工具**,
61
+ * 不是硬绑定——真出现选错的情况再谈加参数。
62
+ */
63
+ const ACTIONS = [
64
+ {
65
+ id: 'who_owes_reply',
66
+ label: '谁在等我回话',
67
+ cloud: true,
68
+ prompt: '用 who_owes_reply 逐会话看一遍:谁在等我回话?只列真的欠着回复的,每条写清是对方和该回什么。',
69
+ },
70
+ {
71
+ id: 'get_todos',
72
+ label: '我的待办',
73
+ cloud: false,
74
+ prompt: '用 get_todos 把我的待办列出来,按紧急程度排。',
75
+ },
76
+ {
77
+ id: 'get_daily_report',
78
+ label: '今日日报',
79
+ cloud: false,
80
+ prompt: '用 get_daily_report 说说最新一期日报里有什么。',
81
+ },
82
+ {
83
+ id: 'get_stats',
84
+ label: '最近统计',
85
+ cloud: false,
86
+ prompt: '用 get_stats 说说我最近的聊天统计。',
87
+ },
88
+ {
89
+ id: 'get_reading_stats',
90
+ label: '阅读统计',
91
+ cloud: false,
92
+ prompt: '用 get_reading_stats 说说我的阅读情况。',
93
+ },
94
+ {
95
+ id: 'list_sessions',
96
+ label: '最近会话',
97
+ cloud: false,
98
+ prompt: '用 list_sessions 列出我最近的会话。',
99
+ },
100
+ {
101
+ id: 'get_sns',
102
+ label: '朋友圈',
103
+ cloud: false,
104
+ prompt: '用 get_sns 说说我朋友圈最近有什么。',
105
+ },
106
+ {
107
+ id: 'get_weread',
108
+ label: '微信读书',
109
+ cloud: false,
110
+ prompt: '用 get_weread 说说我微信读书的最近情况。',
111
+ },
112
+ ]
113
+
114
+ /** 把名单规整成菜单项用的数组:只要非空字符串、去重、有上限 */
115
+ function normalizeNames(labels) {
116
+ const seen = new Set()
117
+ const names = []
118
+ for (const raw of Array.isArray(labels) ? labels : []) {
119
+ if (typeof raw !== 'string') continue
120
+ const name = raw.trim()
121
+ if (!name || seen.has(name)) continue
122
+ seen.add(name)
123
+ names.push(name)
124
+ if (names.length >= MAX_ITEMS) break
125
+ }
126
+ return names
127
+ }
128
+
129
+ /**
130
+ * 代价那一行。**从 ACTIONS 派生,不写死**——写死的那种失败是"菜单加了功能、提示还停在旧话上",
131
+ * 而不准确的代价提示比没有更糟:它让人以为已经被告知了。
132
+ *
133
+ * 起草回复(联系人名单)也要算进去:它是这个菜单里唯一**会把聊天正文发出去**的一项。
134
+ */
135
+ function costNote(hasContacts) {
136
+ const cloud = []
137
+ if (hasContacts) cloud.push(REPLY_SECTION)
138
+ for (const action of ACTIONS) {
139
+ if (action.cloud) cloud.push(action.label)
140
+ }
141
+ if (!cloud.length) return '这些项都不调云端模型,只读本地'
142
+ return `会调云端模型的只有:${cloud.join('、')};其余只读本地,不花钱`
143
+ }
144
+
145
+ /**
146
+ * @param {unknown} labels 名单(联系人名)
147
+ * @param {{
148
+ * pick: (item: {kind: 'contact', name: string} | {kind: 'action', label: string, prompt: string}) => void,
149
+ * close: () => void,
150
+ * }} actions 选中了哪一项 / 选了"收起来"。
151
+ * 关掉菜单这件事由调用方的 `popup({ callback })` 负责,这里不管。
152
+ *
153
+ * 两项都必填:少给一个就当场抛,而不是弹出一个点了没反应的菜单。
154
+ */
155
+ function quickMenuTemplate(labels, actions) {
156
+ if (typeof actions?.pick !== 'function') {
157
+ throw new Error('quickMenuTemplate 需要一个 pick 回调')
158
+ }
159
+ if (typeof actions?.close !== 'function') {
160
+ throw new Error('quickMenuTemplate 需要一个 close 回调')
161
+ }
162
+ const names = normalizeNames(labels)
163
+ const closeItem = { label: CLOSE_LABEL, click: () => actions.close() }
164
+
165
+ const items = []
166
+ if (names.length) {
167
+ items.push({ label: REPLY_SECTION, enabled: false })
168
+ for (const name of names) {
169
+ items.push({ label: name, click: () => actions.pick({ kind: 'contact', name }) })
170
+ }
171
+ } else {
172
+ // 没配名单**不等于这个菜单没用**:下面那八个快捷功能照常给出,所以这里只是少了一段,
173
+ // 不是"空框"(第一版没有快捷功能时,这一段之后就直接是收起来,那才叫空框)。
174
+ items.push(...EMPTY_HINT.map(label => ({ label, enabled: false })))
175
+ }
176
+
177
+ items.push({ type: 'separator' }, { label: ACTION_SECTION, enabled: false })
178
+ for (const action of ACTIONS) {
179
+ items.push({
180
+ label: action.label,
181
+ click: () => actions.pick({ kind: 'action', label: action.label, prompt: action.prompt }),
182
+ })
183
+ }
184
+
185
+ items.push(
186
+ { type: 'separator' },
187
+ { label: costNote(names.length > 0), enabled: false },
188
+ { type: 'separator' },
189
+ closeItem,
190
+ )
191
+ return items
192
+ }
193
+
194
+ module.exports = {
195
+ MAX_ITEMS, EMPTY_HINT, CLOSE_LABEL, REPLY_SECTION, ACTION_SECTION,
196
+ ACTIONS, costNote, normalizeNames, quickMenuTemplate,
197
+ }