weflow-cli 1.16.0 → 1.17.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/CHANGELOG.md +42 -0
- package/README.md +16 -4
- package/bin/weflow-cli.ts +348 -6
- package/dist/bin/weflow-cli.js +381 -8
- package/dist/bin/weflow-cli.js.map +1 -1
- package/dist/src/core/ntCore.d.ts +47 -0
- package/dist/src/core/ntCore.d.ts.map +1 -1
- package/dist/src/core/ntCore.js +24 -0
- package/dist/src/core/ntCore.js.map +1 -1
- package/dist/src/services/assistantService.d.ts +8 -1
- package/dist/src/services/assistantService.d.ts.map +1 -1
- package/dist/src/services/assistantService.js +47 -8
- package/dist/src/services/assistantService.js.map +1 -1
- package/dist/src/services/assistantTools.d.ts +8 -0
- package/dist/src/services/assistantTools.d.ts.map +1 -1
- package/dist/src/services/assistantTools.js +67 -11
- package/dist/src/services/assistantTools.js.map +1 -1
- package/dist/src/services/configService.d.ts.map +1 -1
- package/dist/src/services/configService.js +3 -2
- package/dist/src/services/configService.js.map +1 -1
- package/dist/src/services/cursorProbe.d.ts +44 -0
- package/dist/src/services/cursorProbe.d.ts.map +1 -0
- package/dist/src/services/cursorProbe.js +183 -0
- package/dist/src/services/cursorProbe.js.map +1 -0
- package/dist/src/services/qchatImport.d.ts +89 -0
- package/dist/src/services/qchatImport.d.ts.map +1 -0
- package/dist/src/services/qchatImport.js +273 -0
- package/dist/src/services/qchatImport.js.map +1 -0
- package/dist/src/services/setupVenv.d.ts +43 -0
- package/dist/src/services/setupVenv.d.ts.map +1 -0
- package/dist/src/services/setupVenv.js +198 -0
- package/dist/src/services/setupVenv.js.map +1 -0
- package/dist/src/utils/python.d.ts +10 -0
- package/dist/src/utils/python.d.ts.map +1 -1
- package/dist/src/utils/python.js +27 -4
- package/dist/src/utils/python.js.map +1 -1
- package/docs/DATA_CONTRACT.md +25 -0
- package/docs/DECISIONS.md +92 -30
- package/docs/FUNCTIONAL-IMPROVEMENT-SPEC.md +365 -365
- package/docs/HEALTH-CHECK.md +104 -104
- package/docs/JEV/345/272/224/347/224/250/346/226/275/345/267/245/346/226/207/346/241/243.md +310 -310
- package/docs/NEXT_ACTIONS.md +236 -236
- package/docs/NPM-PUBLISH.md +130 -130
- package/docs/POSITIONING.md +68 -68
- package/docs/PROJECT_STATE.md +6 -3
- package/docs/REPLAY.md +169 -0
- package/docs/SYNC_CONTRACT.md +197 -180
- package/package.json +89 -88
- package/requirements-3x.txt +4 -4
- package/requirements-voice.txt +15 -15
- package/resources/js/graph3d/NOTICE.txt +35 -35
- package/resources/js/graph3d/d3-binarytree.min.js +2 -2
- package/resources/js/graph3d/d3-dispatch.min.js +2 -2
- package/resources/js/graph3d/d3-force-3d.min.js +2 -2
- package/resources/js/graph3d/d3-octree.min.js +2 -2
- package/resources/js/graph3d/d3-quadtree.min.js +2 -2
- package/resources/js/graph3d/d3-timer.min.js +2 -2
- package/resources/js/graph3d/three.min.js +6 -6
- package/resources/js/graph3d/viewer2d.js +550 -448
- package/resources/panel/quick-menu.cjs +197 -197
- package/scripts/_utils.py +1338 -1338
- package/scripts/article_notes.py +423 -423
- package/scripts/chat_notes.py +86 -3
- package/scripts/check_readme_commands.py +427 -426
- package/scripts/contact_schema.py +339 -339
- package/scripts/create_reading_notes.py +11 -11
- package/scripts/daily_task.ps1 +213 -213
- package/scripts/fav_notes.py +321 -321
- package/scripts/graph_2d.py +156 -151
- package/scripts/graph_3d.py +672 -646
- package/scripts/health_check.py +473 -473
- package/scripts/imported_chats.py +68 -0
- package/scripts/install_tasks.ps1 +105 -105
- package/scripts/leads.py +179 -179
- package/scripts/network.py +206 -206
- package/scripts/nt_decrypt.py +135 -1
- package/scripts/on_this_day.py +178 -178
- package/scripts/opinion_timeline.py +138 -138
- package/scripts/promises.py +185 -185
- package/scripts/replay.py +803 -0
- package/scripts/run_hidden.vbs +25 -0
- package/scripts/semantic_search.py +73 -5
- package/scripts/user_notes.py +315 -315
- package/scripts/voice_transcribe.py +139 -139
- package/scripts/watch_issues.py +212 -212
- package/scripts/watch_mail.py +131 -131
- package/src/core/ntCore.ts +59 -0
- package/src/services/assistantService.ts +48 -8
- package/src/services/assistantTools.ts +73 -10
- package/src/services/configService.ts +3 -2
- package/src/services/cursorProbe.ts +222 -0
- package/src/services/qchatImport.ts +327 -0
- package/src/services/setupVenv.ts +238 -0
- package/src/services/syncService.ts +363 -363
- package/src/services/syncState.ts +275 -275
- package/src/services/wechatMedia.ts +166 -166
- package/src/utils/python.ts +33 -4
|
@@ -1,197 +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
|
-
}
|
|
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
|
+
}
|