@wenbin_wb/dsh-bridge 2.8.7 → 2.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.
- package/CHANGELOG.md +295 -250
- package/README.en.md +408 -572
- package/README.md +430 -570
- package/client/client.js +3844 -3764
- package/client/index.js +140 -876
- package/client/mobile-styles.js +802 -0
- package/docs/fix-plan-202608.md +108 -0
- package/lib/auth/login-template.js +381 -381
- package/lib/auth/manager.js +531 -469
- package/lib/bridge-rpc.js +436 -511
- package/lib/cloudflared-manager.mjs +361 -345
- package/lib/compat.js +129 -0
- package/lib/feishu/index.js +225 -222
- package/lib/feishu/node.js +433 -409
- package/lib/index.js +1765 -1804
- package/lib/platform/base.js +147 -156
- package/lib/platform/commands.js +221 -0
- package/lib/platform/conversation-bridge.js +816 -1570
- package/lib/platform/dsh-storage.js +117 -0
- package/lib/platform/index.js +10 -10
- package/lib/platform/message-split.js +191 -0
- package/lib/platform/session-catalog.js +372 -0
- package/lib/platform/stream-slices.js +21 -0
- package/lib/qq/index.js +312 -309
- package/lib/qq/node.js +532 -533
- package/lib/telegram/index.js +215 -212
- package/lib/telegram/node.js +348 -350
- package/lib/tunnel-client.mjs +39 -15
- package/lib/wechat/gateway.js +973 -960
- package/lib/wechat/index.js +244 -241
- package/lib/wechat/media.js +285 -281
- package/lib/wechat/node.js +352 -350
- package/package.json +106 -102
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
// 会话目录:DSH 会话/工作区列表组织、渲染与格式化工具
|
|
2
|
+
// 自 conversation-bridge.js 拆出。私有存储兜底见 dsh-storage.js。
|
|
3
|
+
import { statSync } from 'node:fs'
|
|
4
|
+
import { normalize, resolve } from 'node:path'
|
|
5
|
+
import { isSafeWorkspacePath } from '../security/path-validator.js'
|
|
6
|
+
import { getArchivedSessionIds, getSessionProjCache, getRegisteredWorkspaces } from './dsh-storage.js'
|
|
7
|
+
|
|
8
|
+
export function sessionLabel(session) {
|
|
9
|
+
for (const event of session.events ?? []) {
|
|
10
|
+
if (event.type === 'user/message') {
|
|
11
|
+
const blocks = event.data.content ?? []
|
|
12
|
+
const text = blocks
|
|
13
|
+
.filter((block) => block.type === 'text')
|
|
14
|
+
.map((block) => block.text ?? '')
|
|
15
|
+
.join(' ')
|
|
16
|
+
.trim()
|
|
17
|
+
if (text) return text.length > 28 ? `${text.slice(0, 28)}…` : text
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return '新会话 (待输入)'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 校验工作区路径(防止路径遍历攻击)
|
|
24
|
+
export async function validateWorkspacePath(node, sel) {
|
|
25
|
+
const normalized = normalize(resolve(sel))
|
|
26
|
+
const workspaces = await listWorkspaces(node)
|
|
27
|
+
const allowedPaths = workspaces.map(w => normalize(resolve(w.path)))
|
|
28
|
+
|
|
29
|
+
if (!allowedPaths.includes(normalized)) {
|
|
30
|
+
const wsDisplay = allowedPaths.slice(0, 5).map((p, i) => `- \`[${i + 1}]\` \`${p}\``).join('\n')
|
|
31
|
+
const more = allowedPaths.length > 5 ? `\n- *…等共 ${allowedPaths.length} 个工作区*` : ''
|
|
32
|
+
return {
|
|
33
|
+
valid: false,
|
|
34
|
+
error: `❌ **路径不在已注册工作区列表中**:\`${normalized}\`\n\n**可用工作区:**\n${wsDisplay}${more}\n\n> 提示:发送 \`/workspaces\` 查看完整列表`
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 校验目录存在
|
|
39
|
+
let ok
|
|
40
|
+
try { ok = statSync(normalized).isDirectory() } catch { ok = false }
|
|
41
|
+
if (!ok) {
|
|
42
|
+
return {
|
|
43
|
+
valid: false,
|
|
44
|
+
error: `❌ **工作区目录不存在**:\`${normalized}\``
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { valid: true, path: normalized }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 从事件日志折叠会话标题(本地实现,等价 DSH foldSessionTitle):优先取最后的
|
|
52
|
+
// session/title 事件(DSH 生成的会话名),否则回退到第一条用户消息文本。
|
|
53
|
+
export function foldTitle(events) {
|
|
54
|
+
const evts = events ?? []
|
|
55
|
+
for (let i = evts.length - 1; i >= 0; i--) {
|
|
56
|
+
const e = evts[i]
|
|
57
|
+
if (e && e.type === 'session/title' && e.data?.title) return String(e.data.title)
|
|
58
|
+
}
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function isSubagentSession(cacheRow, liveSession) {
|
|
63
|
+
if (liveSession?.origin === 'subagent' || liveSession?.header?.origin === 'subagent') return true
|
|
64
|
+
const subVal = cacheRow?.rows?.subagent?.val
|
|
65
|
+
if (subVal && typeof subVal === 'object' && Object.keys(subVal).length > 0) return true
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function formatGoalTitle(raw) {
|
|
70
|
+
if (typeof raw === 'string' && raw) return raw
|
|
71
|
+
if (raw && typeof raw === 'object') {
|
|
72
|
+
const obj = raw.objective ?? raw.goal?.objective ?? raw.title
|
|
73
|
+
if (typeof obj === 'string' && obj) return obj
|
|
74
|
+
}
|
|
75
|
+
return ''
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 列出会话:严格对齐 DSH Web 端侧边栏会话树逻辑。
|
|
79
|
+
// 1. 过滤已归档会话 (archivedSessionIds)
|
|
80
|
+
// 2. 过滤未发起提问的空白会话 (blank: true)
|
|
81
|
+
// 3. 过滤子代理内部会话 (subagent origin)
|
|
82
|
+
// 4. 严格按工作区账本 (workspace.sessionIds) 组织
|
|
83
|
+
export async function listSessions(node) {
|
|
84
|
+
const archived = getArchivedSessionIds(node.ctx)
|
|
85
|
+
const projCache = getSessionProjCache(node.ctx)
|
|
86
|
+
const workspaces = await getRegisteredWorkspaces(node.ctx)
|
|
87
|
+
|
|
88
|
+
// 内存活跃会话(按 id 索引)
|
|
89
|
+
const liveList = [...(node.ctx.sessions?.list?.() ?? [])].filter(
|
|
90
|
+
(s) => s && s.id && !archived.has(s.id) && !s.archived && !s.header?.archived
|
|
91
|
+
)
|
|
92
|
+
const liveById = new Map(liveList.map((s) => [s.id, s]))
|
|
93
|
+
|
|
94
|
+
const accounted = new Set()
|
|
95
|
+
const result = []
|
|
96
|
+
|
|
97
|
+
// 1. 如果存在已注册的工作区,严格按工作区及其 sessionIds 账本组织(与 Web 端完全一致)
|
|
98
|
+
if (workspaces.length > 0) {
|
|
99
|
+
for (const ws of workspaces) {
|
|
100
|
+
const normWsPath = ws.path ? normalize(ws.path).toLowerCase() : ''
|
|
101
|
+
|
|
102
|
+
// 优先将当前工作区下新创建但在内存里的 live 会话追加到头部
|
|
103
|
+
for (const s of liveList) {
|
|
104
|
+
const sCwd = s.header?.cwd || s.cwd
|
|
105
|
+
if (sCwd && normalize(sCwd).toLowerCase() === normWsPath && !accounted.has(s.id)) {
|
|
106
|
+
if (isSubagentSession(projCache[s.id], s)) continue
|
|
107
|
+
accounted.add(s.id)
|
|
108
|
+
let title = s.title || (s.events ? foldTitle(s.events) : '')
|
|
109
|
+
if (!title) {
|
|
110
|
+
const cache = projCache[s.id]
|
|
111
|
+
title = cache?.rows?.title?.val || formatGoalTitle(cache?.rows?.goal?.val)
|
|
112
|
+
}
|
|
113
|
+
result.push({
|
|
114
|
+
id: s.id,
|
|
115
|
+
createdAt: s.header?.createdAt || Date.now(),
|
|
116
|
+
cwd: ws.path,
|
|
117
|
+
workspaceTitle: ws.title,
|
|
118
|
+
title: title || '新会话',
|
|
119
|
+
events: s.events,
|
|
120
|
+
seq: s.seq ?? 0,
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 按工作区账本存储的 sessionIds 顺序追加已记录会话
|
|
126
|
+
for (const sId of ws.sessionIds) {
|
|
127
|
+
if (archived.has(sId) || accounted.has(sId)) continue
|
|
128
|
+
accounted.add(sId)
|
|
129
|
+
|
|
130
|
+
const cache = projCache[sId]
|
|
131
|
+
const live = liveById.get(sId)
|
|
132
|
+
// 过滤空白草稿会话(非当前活动会话)
|
|
133
|
+
if (cache?.rows?.sessionListMetadata?.val?.blank === true && sId !== node.activeSessionId) {
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
// 过滤子代理内部会话
|
|
137
|
+
if (isSubagentSession(cache, live)) {
|
|
138
|
+
continue
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let title = cache?.rows?.title?.val || formatGoalTitle(cache?.rows?.goal?.val)
|
|
142
|
+
let createdAt = cache?.identity?.createdAt || 0
|
|
143
|
+
let cwd = ws.path
|
|
144
|
+
|
|
145
|
+
// 如果内存有该会话,提取最新数据
|
|
146
|
+
if (live) {
|
|
147
|
+
title = live.title || (live.events ? foldTitle(live.events) : '') || title
|
|
148
|
+
createdAt = live.header?.createdAt || createdAt
|
|
149
|
+
} else if (!title && node.ctx.sessionPersistence?.load) {
|
|
150
|
+
try {
|
|
151
|
+
const insp = await node.ctx.sessionPersistence.load(sId)
|
|
152
|
+
title = foldTitle(insp.events ?? []) ?? undefined
|
|
153
|
+
} catch {}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
result.push({
|
|
157
|
+
id: sId,
|
|
158
|
+
createdAt,
|
|
159
|
+
cwd,
|
|
160
|
+
workspaceTitle: ws.title,
|
|
161
|
+
title: title || '新会话',
|
|
162
|
+
events: live?.events,
|
|
163
|
+
seq: live?.seq ?? 0,
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 处理当前内存中处于活动状态但未绑定任何工作区的 live 会话
|
|
169
|
+
for (const s of liveList) {
|
|
170
|
+
if (accounted.has(s.id)) continue
|
|
171
|
+
if (isSubagentSession(projCache[s.id], s)) continue
|
|
172
|
+
accounted.add(s.id)
|
|
173
|
+
const title = s.title || (s.events ? foldTitle(s.events) : '') || '未分组会话'
|
|
174
|
+
result.push({
|
|
175
|
+
id: s.id,
|
|
176
|
+
createdAt: s.header?.createdAt || Date.now(),
|
|
177
|
+
cwd: s.header?.cwd || '(未指定)',
|
|
178
|
+
workspaceTitle: '未指定工作区',
|
|
179
|
+
title,
|
|
180
|
+
events: s.events,
|
|
181
|
+
seq: s.seq ?? 0,
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
} else {
|
|
185
|
+
// 2. 如果系统未注册任何工作区(如单目录/无工作区模式),降级读取内存及持久化会话
|
|
186
|
+
for (const s of liveList) {
|
|
187
|
+
accounted.add(s.id)
|
|
188
|
+
const title = s.title || (s.events ? foldTitle(s.events) : '') || '活跃会话'
|
|
189
|
+
result.push({
|
|
190
|
+
id: s.id,
|
|
191
|
+
createdAt: s.header?.createdAt || Date.now(),
|
|
192
|
+
cwd: s.header?.cwd || '(未指定)',
|
|
193
|
+
workspaceTitle: '未指定工作区',
|
|
194
|
+
title,
|
|
195
|
+
events: s.events,
|
|
196
|
+
seq: s.seq ?? 0,
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
if (node.ctx.sessionPersistence?.list) {
|
|
200
|
+
try {
|
|
201
|
+
const headers = await node.ctx.sessionPersistence.list()
|
|
202
|
+
if (Array.isArray(headers)) {
|
|
203
|
+
const coldHeaders = headers.filter((h) => h && h.id && !accounted.has(h.id) && !archived.has(h.id) && !h.archived)
|
|
204
|
+
for (const h of coldHeaders) {
|
|
205
|
+
accounted.add(h.id)
|
|
206
|
+
let title
|
|
207
|
+
try {
|
|
208
|
+
const insp = await node.ctx.sessionPersistence.load(h.id)
|
|
209
|
+
title = foldTitle(insp.events ?? []) ?? undefined
|
|
210
|
+
} catch {}
|
|
211
|
+
result.push({
|
|
212
|
+
id: h.id,
|
|
213
|
+
createdAt: h.createdAt ?? 0,
|
|
214
|
+
events: undefined,
|
|
215
|
+
seq: 0,
|
|
216
|
+
cwd: h.cwd || '(未指定)',
|
|
217
|
+
workspaceTitle: '未指定工作区',
|
|
218
|
+
title: title || '新会话',
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} catch {}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return result
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 列出可用工作区:使用 DSH 官方 workspaceRegistry。返回 [{title, path}]。
|
|
230
|
+
export async function listWorkspaces(node) {
|
|
231
|
+
try {
|
|
232
|
+
const list = await node.ctx.workspaceRegistry?.list?.() ?? []
|
|
233
|
+
const out = []
|
|
234
|
+
for (const ws of list) {
|
|
235
|
+
if (ws && ws.path) out.push({ title: ws.title ?? ws.path, path: ws.path })
|
|
236
|
+
}
|
|
237
|
+
return out.sort((a, b) => String(a.path).localeCompare(String(b.path)))
|
|
238
|
+
} catch {
|
|
239
|
+
return []
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function getWorkspaceBasename(cwd) {
|
|
244
|
+
if (!cwd || cwd === '(未指定)') return '(未指定)'
|
|
245
|
+
const norm = normalize(cwd).replace(/[\\/]+$/, '')
|
|
246
|
+
const parts = norm.split(/[\\/]/)
|
|
247
|
+
return parts[parts.length - 1] || cwd
|
|
248
|
+
}
|
|
249
|
+
export function describeTurnEnd(reason) {
|
|
250
|
+
const kind = reason?.kind ?? 'unknown'
|
|
251
|
+
switch (kind) {
|
|
252
|
+
case 'completed': return '✓ 已完成'
|
|
253
|
+
case 'error': return '❌ 出错'
|
|
254
|
+
case 'aborted': return '⏹ 已停止'
|
|
255
|
+
case 'blocked': return '⚠️ 已阻塞'
|
|
256
|
+
case 'max-tokens': return '⚠️ 输出截断'
|
|
257
|
+
case 'interrupted': return '⚡ 已中断'
|
|
258
|
+
default: return `[${kind}]`
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export async function renderSessions(node) {
|
|
263
|
+
const all = await listSessions(node)
|
|
264
|
+
if (all.length === 0) {
|
|
265
|
+
return `## 📋 会话列表\n\n> 暂无历史会话。发送 \`/new <提示词>\` 开始新会话。`
|
|
266
|
+
}
|
|
267
|
+
// 按工作区分组(保持 listSessions 中的工作区账本顺序)
|
|
268
|
+
const groups = new Map()
|
|
269
|
+
for (const s of all) {
|
|
270
|
+
const key = s.cwd || '(未指定)'
|
|
271
|
+
if (!groups.has(key)) {
|
|
272
|
+
groups.set(key, { title: s.workspaceTitle || getWorkspaceBasename(key), sessions: [] })
|
|
273
|
+
}
|
|
274
|
+
groups.get(key).sessions.push(s)
|
|
275
|
+
}
|
|
276
|
+
const parts = [
|
|
277
|
+
`## 📋 会话列表 (共 ${all.length} 个)`,
|
|
278
|
+
`> 切换会话:发送 \`/use 编号\` 或 \`/resume 编号\``,
|
|
279
|
+
'',
|
|
280
|
+
]
|
|
281
|
+
// 编号必须与 /use 的全量显示顺序(sessionsInDisplayOrder)一致:
|
|
282
|
+
// 表格里显示的 #N 就是 /use N 切换的会话(此前只对每组前 20 条编号,超长时编号错位)
|
|
283
|
+
const ordered = sessionsInDisplayOrder(all)
|
|
284
|
+
const displayIndex = new Map(ordered.map((s, i) => [s.id, i + 1]))
|
|
285
|
+
for (const [cwd, group] of groups) {
|
|
286
|
+
const groupName = cwd === '(未指定)' ? '📁 未指定工作区' : `📁 **${group.title || getWorkspaceBasename(cwd)}**`
|
|
287
|
+
parts.push(groupName)
|
|
288
|
+
parts.push('')
|
|
289
|
+
parts.push('| 序号 | 会话标题 / 摘要 | 时间 | 状态 |')
|
|
290
|
+
parts.push('| :--- | :--- | :--- | :--- |')
|
|
291
|
+
for (const session of group.sessions.slice(0, 20)) {
|
|
292
|
+
const displayNum = displayIndex.get(session.id) ?? 0
|
|
293
|
+
const isActive = session.id === node.activeSessionId
|
|
294
|
+
const statusTag = isActive ? '`[当前]`' : '-'
|
|
295
|
+
const rawTitle = session.title || (session.events ? sessionLabel(session) : '')
|
|
296
|
+
const titleText = formatGoalTitle(rawTitle) || (typeof rawTitle === 'string' ? rawTitle : '') || '新会话'
|
|
297
|
+
const safeTitle = String(titleText).replace(/\|/g, '|').replace(/\r?\n/g, ' ')
|
|
298
|
+
const when = session.createdAt ? fmtTime(session.createdAt) : '-'
|
|
299
|
+
parts.push(`| **#${displayNum}** | ${safeTitle} | ${when} | ${statusTag} |`)
|
|
300
|
+
}
|
|
301
|
+
if (group.sessions.length > 20) {
|
|
302
|
+
parts.push(`*…该工作区共 ${group.sessions.length} 个会话,仅显示前 20 个(编号保持全列表序号,可用 /use 直接切换)*`)
|
|
303
|
+
}
|
|
304
|
+
parts.push('')
|
|
305
|
+
}
|
|
306
|
+
if (all.length > 50) parts.push(`*…共 ${all.length} 个会话,仅显示前 50 个*`)
|
|
307
|
+
return parts.join('\n').trim()
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 与 renderSessions 完全一致的显示顺序:保持 listSessions 中的分组和顺序。
|
|
311
|
+
// /use N 用这个数组索引,保证显示的编号 N 与切换的会话一一对应。
|
|
312
|
+
export function sessionsInDisplayOrder(all) {
|
|
313
|
+
const groups = new Map()
|
|
314
|
+
for (const s of all) {
|
|
315
|
+
const key = s.cwd || '(未指定)'
|
|
316
|
+
if (!groups.has(key)) groups.set(key, [])
|
|
317
|
+
groups.get(key).push(s)
|
|
318
|
+
}
|
|
319
|
+
return [...groups.values()].flatMap((sessions) => sessions)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// 时间戳 → 简洁可读时间 (MM-DD HH:mm 或 YYYY-MM-DD HH:mm)
|
|
323
|
+
export function fmtTime(ms) {
|
|
324
|
+
try {
|
|
325
|
+
const d = new Date(ms)
|
|
326
|
+
if (isNaN(d.getTime())) return ''
|
|
327
|
+
const p = (n) => String(n).padStart(2, '0')
|
|
328
|
+
const now = new Date()
|
|
329
|
+
const isSameYear = d.getFullYear() === now.getFullYear()
|
|
330
|
+
const datePart = isSameYear ? `${p(d.getMonth() + 1)}-${p(d.getDate())}` : `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`
|
|
331
|
+
return `${datePart} ${p(d.getHours())}:${p(d.getMinutes())}`
|
|
332
|
+
} catch { return '' }
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// 提取精简会话短 ID(去除冗余 session- 前缀,保留 8 位短标识)
|
|
336
|
+
export function fmtSessionId(id) {
|
|
337
|
+
if (!id) return ''
|
|
338
|
+
const clean = String(id).replace(/^session-/, '')
|
|
339
|
+
return clean.length > 8 ? clean.slice(0, 8) : clean
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export function helpText() {
|
|
343
|
+
return [
|
|
344
|
+
'## 🤖 常用指令帮助',
|
|
345
|
+
'',
|
|
346
|
+
'### 💬 会话控制',
|
|
347
|
+
'| 指令 | 说明 | 示例 |',
|
|
348
|
+
'| :--- | :--- | :--- |',
|
|
349
|
+
'| `/sessions` | 查看所有会话表格列表 | `/sessions` 或 `/list` |',
|
|
350
|
+
'| `/use <编号>` | 切换到指定编号会话 | `/use 1` 或 `/resume 1` |',
|
|
351
|
+
'| `/new <提示词>` | 在当前工作区新建会话 | `/new 帮我写个脚本` |',
|
|
352
|
+
'| `/new <词> @N` | 在指定工作区新建会话 | `/new 帮我写个脚本 @1` |',
|
|
353
|
+
'| `/rename <新标题>` | 重命名当前活动会话 | `/rename 优化登录交互` |',
|
|
354
|
+
'| `/stop` | 中断停止当前正在执行的任务 | `/stop` |',
|
|
355
|
+
'| `/end` | 结束当前会话(回到空闲) | `/end` |',
|
|
356
|
+
'',
|
|
357
|
+
'### 📁 环境与状态',
|
|
358
|
+
'| 指令 | 说明 |',
|
|
359
|
+
'| :--- | :--- |',
|
|
360
|
+
'| `/workspaces` | 查看可用工作区表格列表 |',
|
|
361
|
+
'| `/addworkspace <路径>` | 注册添加新的电脑工作区目录 |',
|
|
362
|
+
'| `/status` | 查看 Agent 运行状态看板 |',
|
|
363
|
+
'| `/help` | 查看此帮助菜单 |',
|
|
364
|
+
'',
|
|
365
|
+
'### 🔐 权限确认',
|
|
366
|
+
'| 指令 | 快捷数字 | 说明 |',
|
|
367
|
+
'| :--- | :--- | :--- |',
|
|
368
|
+
'| `/yes` | `1` | 批准当前工具执行请求 |',
|
|
369
|
+
'| `/no` | `2` | 拒绝当前工具执行请求 |',
|
|
370
|
+
].join('\n')
|
|
371
|
+
}
|
|
372
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// 流式打字机输出的公共切片工具
|
|
2
|
+
//
|
|
3
|
+
// 各平台网关(qq/feishu/telegram)的流式输出都是"增量前缀"模式:
|
|
4
|
+
// 第 i 片内容 = 前 i 个基础分片的拼接(服务端逐片覆盖,消息逐渐变长)。
|
|
5
|
+
// 基础分块器因平台而异(QQ markdown 感知分块 / 飞书换行窗口 / Telegram splitForIM),
|
|
6
|
+
// 本模块只沉淀三者复制的"前缀累积"逻辑。
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 把基础分片转成增量前缀切片。
|
|
10
|
+
* @param {string[]} chunks 基础分片(非空内容应至少产生 1 片)
|
|
11
|
+
* @returns {string[]} 第 i 个元素 = chunks[0..i] 拼接
|
|
12
|
+
*/
|
|
13
|
+
export function cumulativeSlices(chunks) {
|
|
14
|
+
const slices = []
|
|
15
|
+
let acc = ''
|
|
16
|
+
for (const s of chunks) {
|
|
17
|
+
acc += s
|
|
18
|
+
slices.push(acc)
|
|
19
|
+
}
|
|
20
|
+
return slices
|
|
21
|
+
}
|