cc-viewer 1.7.0 → 1.7.2
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/dist/assets/App-B224r-8M.js +2 -0
- package/dist/assets/{MdxEditorPanel-C5Q6uUyH.js → MdxEditorPanel-BUubiE6Y.js} +1 -1
- package/dist/assets/{Mobile-MoBp9c8k.js → Mobile-CKmfRRvo.js} +1 -1
- package/dist/assets/index-CjsQEQc2.js +2 -0
- package/dist/assets/seqResourceLoaders-C-T_EMiO.js +2 -0
- package/dist/assets/{seqResourceLoaders-CxuD1CYl.css → seqResourceLoaders-DFrHmgnK.css} +1 -1
- package/dist/index.html +1 -1
- package/package.json +2 -1
- package/server/lib/log-management.js +1 -0
- package/server/lib/log-watcher.js +29 -3
- package/server/lib/stats-worker.js +21 -2
- package/server/lib/v2/adapter.js +123 -18
- package/server/lib/v2/journal.js +9 -5
- package/server/lib/v2/jsonl-read.js +127 -0
- package/server/lib/v2/live-feed.js +222 -20
- package/server/lib/v2/meta-rows.js +361 -0
- package/server/lib/v2/replay.js +5 -5
- package/server/lib/v2/session-select.js +69 -2
- package/server/lib/v2/v2-writer.js +15 -0
- package/server/lib/wire-compress.js +167 -0
- package/server/routes/events.js +96 -26
- package/server/routes/im.js +1 -1
- package/server/routes/logs.js +15 -12
- package/server/routes/v2.js +60 -0
- package/server/routes/workspaces.js +7 -6
- package/server/server.js +23 -1
- package/server/workspace-registry.js +5 -0
- package/src/utils/apiUrl.js +26 -0
- package/src/utils/askFallback.js +21 -0
- package/src/utils/askOptionDesc.js +55 -0
- package/src/utils/askPortalMatcher.js +54 -0
- package/src/utils/autoApproveOptions.js +27 -0
- package/src/utils/avatarAnimationPostPass.js +49 -0
- package/src/utils/builtinPresets.js +18 -0
- package/src/utils/clearCheckpoint.js +7 -0
- package/src/utils/commandValidator.js +45 -0
- package/src/utils/contentFilter.js +620 -0
- package/src/utils/contextRaw.js +18 -0
- package/src/utils/contextSidebarNavigation.js +16 -0
- package/src/utils/contextTurns.js +117 -0
- package/src/utils/displayScaleHelper.js +47 -0
- package/src/utils/effectiveModel.js +13 -0
- package/src/utils/entry-slim.js +563 -0
- package/src/utils/entryCache.js +144 -0
- package/src/utils/errorReport.js +6 -0
- package/src/utils/fileExpandedPathsStorage.js +70 -0
- package/src/utils/fileIcons.jsx +36 -0
- package/src/utils/fileOpen.js +37 -0
- package/src/utils/formatters.js +48 -0
- package/src/utils/gitApi.js +50 -0
- package/src/utils/gitTreeBuilder.js +19 -0
- package/src/utils/helpers.js +846 -0
- package/src/utils/identityHeal.js +99 -0
- package/src/utils/imConnState.js +59 -0
- package/src/utils/imOrigin.js +24 -0
- package/src/utils/imTr.js +7 -0
- package/src/utils/imageCompress.js +83 -0
- package/src/utils/imageDownscale.js +28 -0
- package/src/utils/imageResize.js +112 -0
- package/src/utils/ingestPipeline.js +47 -0
- package/src/utils/lazyWithReload.js +53 -0
- package/src/utils/markdown.js +30 -0
- package/src/utils/markdownIncremental.js +39 -0
- package/src/utils/markdownProfiler.js +113 -0
- package/src/utils/mdExtensionDetect.js +64 -0
- package/src/utils/memoryLinkParser.js +44 -0
- package/src/utils/modalMask.js +15 -0
- package/src/utils/monotime.js +6 -0
- package/src/utils/pinnedMenu.js +55 -0
- package/src/utils/presetShortcuts.js +14 -0
- package/src/utils/projectAlias.js +135 -0
- package/src/utils/promptClassifier.js +112 -0
- package/src/utils/promptDetect.js +196 -0
- package/src/utils/promptNav.js +60 -0
- package/src/utils/ptyChunkBuilder.js +251 -0
- package/src/utils/quickMenuHoverIntent.js +35 -0
- package/src/utils/rateLimitParser.js +119 -0
- package/src/utils/readResultPool.js +106 -0
- package/src/utils/refreshCachedItemProp.js +30 -0
- package/src/utils/requestType.js +208 -0
- package/src/utils/resizeCalc.js +21 -0
- package/src/utils/resolveLocalized.js +17 -0
- package/src/utils/searchApi.js +47 -0
- package/src/utils/searchReplace.js +51 -0
- package/src/utils/seqResourceLoaders.js +79 -0
- package/src/utils/sessionManager.js +471 -0
- package/src/utils/sessionMerge.js +180 -0
- package/src/utils/skillModalController.js +111 -0
- package/src/utils/skillsParser.js +116 -0
- package/src/utils/slashCommandLabels.js +80 -0
- package/src/utils/splitDragCalc.js +73 -0
- package/src/utils/stickyBottomController.js +643 -0
- package/src/utils/svgSanitize.js +48 -0
- package/src/utils/systemTags.js +47 -0
- package/src/utils/tClaude.js +20 -0
- package/src/utils/teamModalBuilder.js +351 -0
- package/src/utils/teamSessionParser.js +190 -0
- package/src/utils/teammateAvatars.js +194 -0
- package/src/utils/teammateDetector.js +121 -0
- package/src/utils/termDiag.js +114 -0
- package/src/utils/terminalClipboard.js +117 -0
- package/src/utils/terminalWriteQueue.js +398 -0
- package/src/utils/thinkingMerge.js +49 -0
- package/src/utils/toolCatalog.js +29 -0
- package/src/utils/toolResultBuilder.js +393 -0
- package/src/utils/toolResultClassifier.js +16 -0
- package/src/utils/toolResultCore.js +167 -0
- package/src/utils/toolsDiff.js +44 -0
- package/src/utils/toolsXmlFormatter.js +3 -0
- package/src/utils/ultraplanController.js +109 -0
- package/src/utils/ultraplanExperts.js +86 -0
- package/src/utils/ultraplanTemplates.js +154 -0
- package/src/utils/userImageRefs.js +49 -0
- package/src/utils/v3Assembler.js +112 -0
- package/src/utils/v3Rows.js +48 -0
- package/src/utils/voicePackPlayer.js +233 -0
- package/src/utils/webSearchGrouping.js +111 -0
- package/src/utils/workflowFormat.js +52 -0
- package/src/utils/workflowRuns.js +118 -0
- package/src/utils/workflowStore.js +94 -0
- package/dist/assets/App-DTkacfRW.js +0 -2
- package/dist/assets/index-Ac8vnV3v.js +0 -2
- package/dist/assets/seqResourceLoaders-Cd_TAXTb.js +0 -2
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
// 内容分类与过滤规则
|
|
2
|
+
// ChatView(对话模式)和 AppHeader(用户 Prompt 弹窗)共用此模块,确保过滤逻辑一致。
|
|
3
|
+
// MainAgent / Teammate 判断也收敛于此,供全局统一调用。
|
|
4
|
+
|
|
5
|
+
// ============== 请求体辅助 ==============
|
|
6
|
+
|
|
7
|
+
const SUBAGENT_SYSTEM_RE = /command execution specialist|file search specialist|planning specialist|general-purpose agent|security monitor|performing a web search/i;
|
|
8
|
+
|
|
9
|
+
// cc_version 2.1.181+:CLI 在 billing header 显式标注子代理(cc_is_subagent=true);真·主代理省略此字段(从不为 =false)。
|
|
10
|
+
// 这类子代理继承完整 "You are Claude Code" prompt + Edit/Bash/Agent 工具,会误中轻量 MainAgent 启发式,故须显式排除。
|
|
11
|
+
// 结尾 \b 锚定:仅匹配 `=true`(其后为 `;` / 空白 / 串尾),避免 `=truex` 之类误匹配。
|
|
12
|
+
const SUBAGENT_BILLING_RE = /cc_is_subagent=true\b/;
|
|
13
|
+
|
|
14
|
+
// Teammate 检测:system prompt 中包含 Agent Teammate Communication 标记(外部进程 teammate)
|
|
15
|
+
const TEAMMATE_SYSTEM_RE = /running as an agent in a team|Agent Teammate Communication/i;
|
|
16
|
+
|
|
17
|
+
// Native teammate 检测(同进程内 Agent 子代理),独立模块便于版本兼容
|
|
18
|
+
// Extensioned for server-side reuse (see requestType.js header note).
|
|
19
|
+
import { isNativeTeammate, extractNativeTeammateName } from './teammateDetector.js';
|
|
20
|
+
|
|
21
|
+
// ============== 跨会话 / teammate「协议通知」识别 ==============
|
|
22
|
+
// harness 把跨会话 / teammate 通知作为 role=user 文本注入主会话。既有逻辑只认 <teammate-message>
|
|
23
|
+
// 包裹形态与 "Another Claude session sent a message:" 前缀;这里补「裸协议 JSON」形态 + 新版 caveat 文案,
|
|
24
|
+
// 统一归类为 teammate 状态气泡(非用户手输)。type 白名单与 ChatMessage 的 ui.teammate.${type} 渲染一致。
|
|
25
|
+
export const INTER_SESSION_NOTIFICATION_TYPES = new Set([
|
|
26
|
+
'idle_notification', 'shutdown_request', 'shutdown_response',
|
|
27
|
+
'shutdown_approved', 'teammate_terminated',
|
|
28
|
+
'plan_approval_request', 'plan_approval_response',
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
// harness 注入的「跨会话包裹文本」标记(英文固定)。
|
|
32
|
+
const INTER_SESSION_LEAD_RE = /^Another Claude session sent a message:/i;
|
|
33
|
+
// 尾部 caveat(新旧两种措辞)。刻意不用 /m:`(^|\n)` 提供行首锚定,`$` 表整串结尾——多行 caveat 会一并
|
|
34
|
+
// 剥到空行 / 串尾,不会因 lazy + /m 只剥首行;行首锚定避免误伤用户正文中段引用此句。
|
|
35
|
+
const INTER_SESSION_CAVEAT_RES = [
|
|
36
|
+
/(^|\n)This came from another Claude session[\s\S]*?(?=\n\n|$)/i,
|
|
37
|
+
/(^|\n)IMPORTANT: This is NOT from your user[\s\S]*?(?=\n\n|$)/i,
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
// 花括号配对扫描出顶层 {...} 候选 JSON 段(跳过字符串字面量内的花括号 / 转义),返回 { raw, start, end }
|
|
41
|
+
// 区间,供调用方一次性区间拼接剔除(不逐个 replace,避免 O(n²))。用配对而非 [^{}]* 正则以正确处理嵌套。
|
|
42
|
+
function scanTopLevelJsonObjects(s) {
|
|
43
|
+
if (typeof s !== 'string' || s.indexOf('{') === -1) return [];
|
|
44
|
+
const out = [];
|
|
45
|
+
let depth = 0, start = -1, inStr = false, esc = false;
|
|
46
|
+
for (let i = 0; i < s.length; i++) {
|
|
47
|
+
const ch = s[i];
|
|
48
|
+
if (inStr) {
|
|
49
|
+
if (esc) esc = false;
|
|
50
|
+
else if (ch === '\\') esc = true;
|
|
51
|
+
else if (ch === '"') inStr = false;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (ch === '"') inStr = true;
|
|
55
|
+
else if (ch === '{') { if (depth === 0) start = i; depth++; }
|
|
56
|
+
else if (ch === '}' && depth > 0) { depth--; if (depth === 0 && start >= 0) { out.push({ raw: s.slice(start, i + 1), start, end: i + 1 }); start = -1; } }
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 识别 s 中的白名单协议通知 JSON:返回 { statuses:[{type,from}], rest },rest = 剔除这些 JSON 后的剩余文本。
|
|
62
|
+
// 单次扫描 + 区间拼接(O(n)):避免对每个命中 JSON 做整串 replace 造成 O(n²)(评审 S1),同时统一原先
|
|
63
|
+
// replace/split-join 两套剔除写法。非白名单 / 解析失败的 {...} 原样保留在 rest 中。
|
|
64
|
+
function extractProtocolNotifications(s) {
|
|
65
|
+
const statuses = [];
|
|
66
|
+
let rest = '', cursor = 0;
|
|
67
|
+
for (const { raw, start, end } of scanTopLevelJsonObjects(s)) {
|
|
68
|
+
let j;
|
|
69
|
+
try { j = JSON.parse(raw); } catch { continue; }
|
|
70
|
+
if (j && typeof j.type === 'string' && INTER_SESSION_NOTIFICATION_TYPES.has(j.type)) {
|
|
71
|
+
statuses.push({ type: j.type, from: (typeof j.from === 'string' && j.from) ? j.from : null });
|
|
72
|
+
rest += s.slice(cursor, start);
|
|
73
|
+
cursor = end;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
rest += s.slice(cursor);
|
|
77
|
+
return { statuses, rest };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 解析「裸协议通知」文本块(不含 <teammate-message> 包裹——包裹形态由 classifyUserContent 主路径处理)。
|
|
81
|
+
// 返回 { statuses:[{type,from}], rest } 或 null。必须带 harness 标记(前缀 / caveat)才认定为通知(见下)。
|
|
82
|
+
export function parseInterSessionNotification(text) {
|
|
83
|
+
if (typeof text !== 'string') return null;
|
|
84
|
+
let body = text.trim();
|
|
85
|
+
if (!body) return null;
|
|
86
|
+
// 去 <teammate-message> 包裹,避免与 classifyUserContent 主路径重复计入
|
|
87
|
+
body = body.replace(/<teammate-message[\s\S]*?<\/teammate-message>/gi, '').trim();
|
|
88
|
+
if (!body) return null;
|
|
89
|
+
|
|
90
|
+
const hadLead = INTER_SESSION_LEAD_RE.test(body);
|
|
91
|
+
let work = hadLead ? body.replace(INTER_SESSION_LEAD_RE, '') : body;
|
|
92
|
+
let hadCaveat = false;
|
|
93
|
+
for (const cr of INTER_SESSION_CAVEAT_RES) {
|
|
94
|
+
if (cr.test(work)) { hadCaveat = true; work = work.replace(cr, ''); }
|
|
95
|
+
}
|
|
96
|
+
// 必须带 harness 标记("Another Claude session…" 前缀 或 caveat 段)才认定为通知。真实跨会话通知一定
|
|
97
|
+
// 带其一(裸 <teammate-message> 包裹形态由 classifyUserContent 主路径单独处理);据此,用户「整段粘贴一坨
|
|
98
|
+
// 协议形 JSON」绝不会被误判隐藏——彻底消除 over-filter 向量(评审 S2/F2,对齐用户「别过滤正常请求」诉求)。
|
|
99
|
+
if (!hadLead && !hadCaveat) return null;
|
|
100
|
+
|
|
101
|
+
const { statuses, rest } = extractProtocolNotifications(work);
|
|
102
|
+
if (statuses.length === 0) return null;
|
|
103
|
+
return { statuses, rest: rest.trim() };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 提取请求体中的 system prompt 文本
|
|
108
|
+
*/
|
|
109
|
+
export function getSystemText(body) {
|
|
110
|
+
const system = body?.system;
|
|
111
|
+
if (typeof system === 'string') return system;
|
|
112
|
+
if (Array.isArray(system)) {
|
|
113
|
+
return system.map(s => (s && s.text) || '').join('');
|
|
114
|
+
}
|
|
115
|
+
return '';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// WeakMap cache for isTeammate — avoids redundant getSystemText + regex per request
|
|
119
|
+
const _isTeammateCache = new WeakMap();
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 判断请求是否为 Teammate 子进程的请求。
|
|
123
|
+
* 支持两种检测:interceptor ���式(req.teammate 字段)和 proxy 模式(system prompt 标记)。
|
|
124
|
+
* 全局唯一入口,与 isMainAgent 同级。
|
|
125
|
+
*/
|
|
126
|
+
export function isTeammate(req) {
|
|
127
|
+
if (!req) return false;
|
|
128
|
+
const cached = _isTeammateCache.get(req);
|
|
129
|
+
if (cached !== undefined) return cached;
|
|
130
|
+
// interceptor 模式:通过 process.argv 写入的 teammate 字段
|
|
131
|
+
if (req.teammate) { _isTeammateCache.set(req, true); return true; }
|
|
132
|
+
// native teammate:同进程内 Agent 子代理(system prompt 包含 "You are a Claude agent")
|
|
133
|
+
if (isNativeTeammate(req)) {
|
|
134
|
+
// 注入 teammate 字段供下游 requestType.js 的 formatTeammateLabel 使用
|
|
135
|
+
if (!req.teammate) {
|
|
136
|
+
req.teammate = extractNativeTeammateName(req) || null;
|
|
137
|
+
}
|
|
138
|
+
_isTeammateCache.set(req, true);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
// proxy 模式:通过 system prompt 检测(外部进程 teammate)
|
|
142
|
+
const sysText = getSystemText(req.body || {});
|
|
143
|
+
const result = TEAMMATE_SYSTEM_RE.test(sysText);
|
|
144
|
+
_isTeammateCache.set(req, result);
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// WeakMap cache for isMainAgent — avoids redundant regex/array work across call sites
|
|
149
|
+
const _isMainAgentCache = new WeakMap();
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 判断请求是否为 MainAgent 请求。
|
|
153
|
+
* 包含 interceptor 标记校验 + 新旧架构检测,全局唯一入口。
|
|
154
|
+
*/
|
|
155
|
+
export function isMainAgent(req) {
|
|
156
|
+
if (!req) return false;
|
|
157
|
+
const cached = _isMainAgentCache.get(req);
|
|
158
|
+
if (cached !== undefined) return cached;
|
|
159
|
+
const result = _isMainAgentImpl(req);
|
|
160
|
+
_isMainAgentCache.set(req, result);
|
|
161
|
+
return result;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function _isMainAgentImpl(req) {
|
|
165
|
+
if (!req) return false;
|
|
166
|
+
|
|
167
|
+
// Teammate 子进程的请求不是 MainAgent,避免污染主会话视图
|
|
168
|
+
if (isTeammate(req)) return false;
|
|
169
|
+
|
|
170
|
+
// cc_is_subagent=true ⇒ 子代理,绝非 MainAgent(cc_version 2.1.181+)。须早于下方 req.mainAgent 短路,
|
|
171
|
+
// 以覆盖「已落盘旧日志」里被服务端标成 mainAgent=true 的记录(向后兼容:旧日志/真·主代理无此 token)。
|
|
172
|
+
if (SUBAGENT_BILLING_RE.test(getSystemText(req.body || {}))) return false;
|
|
173
|
+
|
|
174
|
+
if (req.mainAgent) {
|
|
175
|
+
// 排除被误标记的 SubAgent(旧日志兼容)
|
|
176
|
+
const sysText = getSystemText(req.body || {});
|
|
177
|
+
if (SUBAGENT_SYSTEM_RE.test(sysText)) return false;
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 统一检测逻辑:支持新旧架构
|
|
182
|
+
const body = req.body || {};
|
|
183
|
+
if (!body.system || !Array.isArray(body.tools)) return false;
|
|
184
|
+
|
|
185
|
+
const sysText = getSystemText(body);
|
|
186
|
+
|
|
187
|
+
// 必须包含 MainAgent 身份标识
|
|
188
|
+
if (!sysText.includes('You are Claude Code')) return false;
|
|
189
|
+
|
|
190
|
+
// 排除 SubAgent
|
|
191
|
+
if (SUBAGENT_SYSTEM_RE.test(sysText)) return false;
|
|
192
|
+
|
|
193
|
+
// 新架构检测(v2.1.69+):延迟工具加载机制
|
|
194
|
+
const isSystemArray = Array.isArray(body.system);
|
|
195
|
+
const hasToolSearch = body.tools.some(t => t.name === 'ToolSearch');
|
|
196
|
+
|
|
197
|
+
if (isSystemArray && hasToolSearch) {
|
|
198
|
+
// 检查第一条消息是否包含 <available-deferred-tools>
|
|
199
|
+
const messages = body.messages || [];
|
|
200
|
+
const firstMsgContent = messages.length > 0 ?
|
|
201
|
+
(typeof messages[0].content === 'string' ? messages[0].content :
|
|
202
|
+
Array.isArray(messages[0].content) ? messages[0].content.map(c => c.text || '').join('') : '') : '';
|
|
203
|
+
if (firstMsgContent.includes('<available-deferred-tools>')) {
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// v2.1.81+: 轻量 MainAgent 初始请求工具数可能 < 10,降低阈值兼容
|
|
209
|
+
if (body.tools.length > 5) {
|
|
210
|
+
const hasEdit = body.tools.some(t => t.name === 'Edit');
|
|
211
|
+
const hasShell = body.tools.some(t => t.name === 'Bash' || t.name === 'PowerShell');
|
|
212
|
+
const hasTaskOrAgent = body.tools.some(t => t.name === 'Task' || t.name === 'Agent');
|
|
213
|
+
if (hasEdit && hasShell && hasTaskOrAgent) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// /clear checkpoint 检测:抽到独立无依赖模块,便于 node --test 直接 import。
|
|
222
|
+
export { isPostClearCheckpoint, isCompactContinuation, isSessionBoundary } from './clearCheckpoint.js';
|
|
223
|
+
|
|
224
|
+
// ============== 文本内容过滤 ==============
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 判断文本是否为 Skill 加载内容
|
|
228
|
+
*/
|
|
229
|
+
export function isSkillText(text) {
|
|
230
|
+
if (!text) return false;
|
|
231
|
+
return /^Base directory for this skill:/i.test(text.trim());
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* 判断文本是否为系统注入文本(不应作为用户消息展示)
|
|
236
|
+
*/
|
|
237
|
+
/**
|
|
238
|
+
* Strip known system/command tags from a text block, returning only user-authored content.
|
|
239
|
+
* Used to extract user input embedded in system-reminder-wrapped blocks (e.g., /ultraplan).
|
|
240
|
+
*/
|
|
241
|
+
function stripSystemTags(text) {
|
|
242
|
+
if (!text) return '';
|
|
243
|
+
let out = text
|
|
244
|
+
.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/gi, '')
|
|
245
|
+
.replace(/<local-command-caveat>[\s\S]*?<\/local-command-caveat>/gi, '')
|
|
246
|
+
.replace(/<local-command-stdout>[\s\S]*?<\/local-command-stdout>/gi, '')
|
|
247
|
+
.replace(/<command-name>[\s\S]*?<\/command-name>/gi, '')
|
|
248
|
+
.replace(/<command-message>[\s\S]*?<\/command-message>/gi, '')
|
|
249
|
+
.replace(/<command-args>[\s\S]*?<\/command-args>/gi, '')
|
|
250
|
+
.replace(/<teammate-message[\s\S]*?<\/teammate-message>/gi, '')
|
|
251
|
+
.replace(/<task-notification>[\s\S]*?<\/task-notification>/gi, '')
|
|
252
|
+
// harness 注入队友消息轮的包裹文本:前缀行 + 尾部 IMPORTANT 免责段。
|
|
253
|
+
// 尾段用 ^...m 多行锚定——段落必须起行才剥,用户正文中段引用该句不受影响;
|
|
254
|
+
// 只锚定句首短语、不绑定其后的破折号/措辞(harness 微调标点不致尾段泄漏成 user 气泡)
|
|
255
|
+
.replace(/^Another Claude session sent a message:\s*/i, '')
|
|
256
|
+
.replace(/^IMPORTANT: This is NOT from your user\b[\s\S]*?(?=\n\n|$)/im, '');
|
|
257
|
+
// 新版跨会话 caveat(多行安全:行首锚定,剥到空行 / 串尾)
|
|
258
|
+
out = out.replace(/(^|\n)This came from another Claude session[\s\S]*?(?=\n\n|$)/i, '');
|
|
259
|
+
// 裸协议通知 JSON(idle / shutdown_* / teammate_terminated / plan_approval_*,含嵌套)——单次扫描剔除
|
|
260
|
+
// (O(n),评审 S1)。与 <teammate-message> 包裹的协议 JSON 同类,剥离后二次回收只剩用户追加正文(无则空)
|
|
261
|
+
out = extractProtocolNotifications(out).rest;
|
|
262
|
+
return out.trim();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Claude Code 内部合成 prompt 白名单(CLI 在主会话里合成的 recap/title/compact/topic/summary 查询,
|
|
266
|
+
// HTTP 层 role=user 但不是用户手输)。与 requestType.js 的 Synthetic 分类共用同一白名单,
|
|
267
|
+
// 在 isSystemText 里统一过滤 → ChatView / Mobile / DetailPanel / teamModalBuilder 全链路隐藏。
|
|
268
|
+
// 匹配 last user message 的起首(`^` 锚定 + trim),避免误伤用户引用原文。
|
|
269
|
+
// KEEP IN SYNC: test/synthetic-classification.test.js 有 inline 副本。
|
|
270
|
+
export const SYNTHETIC_PROMPTS = [
|
|
271
|
+
{ subType: 'Recap', pattern: /^The user stepped away and is coming back\. Recap in under/i },
|
|
272
|
+
{ subType: 'Title', pattern: /^(Based on the above conversation, generate a|Please write a)\s+(short|concise)\s+title/i },
|
|
273
|
+
{ subType: 'Compact', pattern: /^(Your task is to create a detailed summary of the conversation|This session is being continued from a previous conversation)/i },
|
|
274
|
+
{ subType: 'Topic', pattern: /^Analyze if this message indicates a new/i },
|
|
275
|
+
{ subType: 'Summary', pattern: /^Summarize this coding session/i },
|
|
276
|
+
];
|
|
277
|
+
|
|
278
|
+
export function isSyntheticPromptText(text) {
|
|
279
|
+
if (!text || typeof text !== 'string') return false;
|
|
280
|
+
const trimmed = text.trim();
|
|
281
|
+
if (!trimmed) return false;
|
|
282
|
+
for (const { pattern } of SYNTHETIC_PROMPTS) {
|
|
283
|
+
if (pattern.test(trimmed)) return true;
|
|
284
|
+
}
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function isSystemText(text) {
|
|
289
|
+
if (!text) return true;
|
|
290
|
+
const trimmed = text.trim();
|
|
291
|
+
if (!trimmed) return true;
|
|
292
|
+
// 包含 plan 内容的文本块不应被过滤(即使开头有系统标签)
|
|
293
|
+
if (/Implement the following plan:/i.test(trimmed)) return false;
|
|
294
|
+
if (/^<[a-zA-Z_][\w-]*[\s>]/i.test(trimmed)) return true;
|
|
295
|
+
if (/^\[SUGGESTION MODE:/i.test(trimmed)) return true;
|
|
296
|
+
// Claude Code 输出截断时注入的系统消息
|
|
297
|
+
if (/^Your response was cut off because it exceeded the output token limit/i.test(trimmed)) return true;
|
|
298
|
+
// Skill 加载的文档内容
|
|
299
|
+
if (/^Base directory for this skill:/i.test(trimmed)) return true;
|
|
300
|
+
// CLI 内部合成 prompt(Recap/Title/Compact/Topic/Summary)
|
|
301
|
+
if (isSyntheticPromptText(trimmed)) return true;
|
|
302
|
+
// harness 注入的队友消息轮:包裹文本(前缀 + 尾部 IMPORTANT 段)非用户手输,
|
|
303
|
+
// teammate 内容本身经 classifyUserContent 提取为 teammateBlocks 独立渲染
|
|
304
|
+
if (/^Another Claude session sent a message:/i.test(trimmed)) return true;
|
|
305
|
+
// 裸协议通知(直接以协议 JSON 起头、无 "Another Claude session" 前缀):必须 parseInterSessionNotification
|
|
306
|
+
// 命中白名单协议 JSON 才算系统文本——粘贴非协议 JSON / 含追加正文不会被误吞。caveat 是尾部 chrome
|
|
307
|
+
// (真实形态总是 JSON 在前、caveat 在后),故「起头即 caveat」的块视为用户正文,防整段消失(评审 F1);
|
|
308
|
+
// 其 caveat chrome 在确为通知的块内由 parse / stripSystemTags 处理。
|
|
309
|
+
if (trimmed.startsWith('{') && parseInterSessionNotification(trimmed)) return true;
|
|
310
|
+
// 用户拒绝 tool / 中断 Claude 时 CLI 注入的占位 user message —— 与上方 "✗ 已拒绝" badge 语义重复
|
|
311
|
+
// 涵盖历史变体:"[Request interrupted by user for tool use]"、"[Request interrupted by user]"、"[Request interrupted...]"
|
|
312
|
+
if (/^\[Request interrupted/i.test(trimmed)) return true;
|
|
313
|
+
return false;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// 字符串型 user/assistant message 的「可显示正文」。忠实镜像 classifyUserContent 的两段语义
|
|
317
|
+
// (classifyUserContent 内「首过滤 + stripSystemTags 二次回收」两步),只是作用于字符串:
|
|
318
|
+
// Pass1:非系统文本 → 原样返回(保留用户正文中段引用的成对标签,与当前行为逐字一致,零回归);
|
|
319
|
+
// Pass2:系统块(以 chrome 标签起首等被 isSystemText 判真)→ 剥掉已知 chrome 后再判,仍是真实正文则
|
|
320
|
+
// 返回剥离后正文,否则 ''(应隐藏)。
|
|
321
|
+
// 解决「系统标签起首 + 真实正文」字符串被 isSystemText 整条吞掉(数组路径有此回收,字符串路径原先没有)。
|
|
322
|
+
// 注意:用户手打未闭合 <system-reminder>(无配对)仍判系统文本而隐藏——沿用当前行为,本函数不改变它。
|
|
323
|
+
export function extractDisplayText(str) {
|
|
324
|
+
if (typeof str !== 'string' || !str.trim()) return '';
|
|
325
|
+
if (!isSystemText(str)) return str; // Pass1:已是用户文本,原样
|
|
326
|
+
const recovered = stripSystemTags(str); // Pass2:二次回收
|
|
327
|
+
return (recovered && !isSystemText(recovered)) ? recovered : '';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* 从 user message 的 content 数组中分类提取各类文本块。
|
|
332
|
+
* @param {Array} content — message.content 数组
|
|
333
|
+
* @returns {{ commands: string[], textBlocks: Array, skillBlocks: Array, teammateBlocks: Array, taskNotificationBlocks: Array }}
|
|
334
|
+
* commands — 提取到的 slash command 名称(如 "/clear")
|
|
335
|
+
* textBlocks — 过滤后的普通用户文本块(不含系统文本、command 块、skill 块)
|
|
336
|
+
* skillBlocks — skill 加载的文本块
|
|
337
|
+
* teammateBlocks — teammate-message 解析块
|
|
338
|
+
* taskNotificationBlocks — task-notification 解析块
|
|
339
|
+
*/
|
|
340
|
+
export function classifyUserContent(content) {
|
|
341
|
+
if (!Array.isArray(content)) return { commands: [], textBlocks: [], skillBlocks: [], teammateBlocks: [], taskNotificationBlocks: [] };
|
|
342
|
+
|
|
343
|
+
// Extract <teammate-message> blocks from user content
|
|
344
|
+
const teammateBlocks = [];
|
|
345
|
+
for (const b of content) {
|
|
346
|
+
if (b.type !== 'text') continue;
|
|
347
|
+
const text = b.text || '';
|
|
348
|
+
const re = /<teammate-message\s+([^>]*)>([\s\S]*?)<\/teammate-message>/gi;
|
|
349
|
+
let match;
|
|
350
|
+
while ((match = re.exec(text)) !== null) {
|
|
351
|
+
const attrs = match[1];
|
|
352
|
+
const body = match[2].trim();
|
|
353
|
+
const idMatch = attrs.match(/teammate_id="([^"]*)"/);
|
|
354
|
+
const colorMatch = attrs.match(/color="([^"]*)"/);
|
|
355
|
+
const summaryMatch = attrs.match(/summary="([^"]*)"/);
|
|
356
|
+
const tmId = idMatch ? idMatch[1] : 'teammate';
|
|
357
|
+
const tmColor = colorMatch ? colorMatch[1] : null;
|
|
358
|
+
// JSON lifecycle signals → compact status bubble
|
|
359
|
+
if (body.startsWith('{')) {
|
|
360
|
+
try {
|
|
361
|
+
const j = JSON.parse(body);
|
|
362
|
+
if (j && j.type) {
|
|
363
|
+
teammateBlocks.push({
|
|
364
|
+
id: tmId, color: tmColor, summary: null,
|
|
365
|
+
content: null, status: j.type, statusFrom: j.from || tmId,
|
|
366
|
+
});
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
} catch {}
|
|
370
|
+
}
|
|
371
|
+
teammateBlocks.push({
|
|
372
|
+
id: tmId, color: tmColor,
|
|
373
|
+
summary: summaryMatch ? summaryMatch[1] : null,
|
|
374
|
+
content: body, status: null,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 裸协议通知(未包 <teammate-message>):harness 注入的 idle / shutdown_* / teammate_terminated /
|
|
380
|
+
// plan_approval_* 等,提取为 teammate 状态气泡(与包裹形态同渲染)。按 status|from 去重,避免与上面
|
|
381
|
+
// 包裹形态在「同块既有包裹又有裸 JSON」的极端场景下重复出气泡。
|
|
382
|
+
const seenStatus = new Set(teammateBlocks.filter(t => t.status).map(t => `${t.status}|${t.statusFrom}`));
|
|
383
|
+
for (const b of content) {
|
|
384
|
+
if (b.type !== 'text') continue;
|
|
385
|
+
const txt = b.text || '';
|
|
386
|
+
if (!txt.includes('"type"')) continue; // 廉价早退:协议通知必含 JSON 的 "type"
|
|
387
|
+
// 仅对「通知起头」的块出状态气泡(与 isSystemText 的隐藏条件对齐:前缀 lead 或 裸 JSON 起头)。
|
|
388
|
+
// 用户在正文里引用 / 转贴整条通知(caveat 起头、prose 起头)→ 该块仍是 user 气泡,不再额外塞一个
|
|
389
|
+
// 幽灵状态气泡,也不会双重渲染(评审 qa-A / auditor-F1)。
|
|
390
|
+
const head = txt.trimStart();
|
|
391
|
+
if (!head.startsWith('{') && !INTER_SESSION_LEAD_RE.test(head)) continue;
|
|
392
|
+
const note = parseInterSessionNotification(txt);
|
|
393
|
+
if (!note) continue;
|
|
394
|
+
for (const s of note.statuses) {
|
|
395
|
+
const from = s.from || 'teammate';
|
|
396
|
+
const k = `${s.type}|${from}`;
|
|
397
|
+
if (seenStatus.has(k)) continue;
|
|
398
|
+
seenStatus.add(k);
|
|
399
|
+
teammateBlocks.push({ id: from, color: null, summary: null, content: null, status: s.type, statusFrom: from });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Extract <task-notification> blocks from user content (early exit if none)
|
|
404
|
+
const taskNotificationBlocks = [];
|
|
405
|
+
const hasTaskNotification = content.some(b => b.type === 'text' && /<task-notification>/i.test(b.text || ''));
|
|
406
|
+
if (hasTaskNotification) {
|
|
407
|
+
for (const b of content) {
|
|
408
|
+
if (b.type !== 'text') continue;
|
|
409
|
+
const text = b.text || '';
|
|
410
|
+
const tnRe = /<task-notification>([\s\S]*?)<\/task-notification>/gi;
|
|
411
|
+
let tnMatch;
|
|
412
|
+
while ((tnMatch = tnRe.exec(text)) !== null) {
|
|
413
|
+
const inner = tnMatch[1];
|
|
414
|
+
const field = (tag) => { const m = inner.match(new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`, 'i')); return m ? m[1].trim() : null; };
|
|
415
|
+
const usageBlock = inner.match(/<usage>([\s\S]*?)<\/usage>/i);
|
|
416
|
+
let usage = null;
|
|
417
|
+
if (usageBlock) {
|
|
418
|
+
const uf = (tag) => { const m = usageBlock[1].match(new RegExp(`<${tag}>([\\s\\S]*?)<\\/${tag}>`, 'i')); return m ? m[1].trim() : null; };
|
|
419
|
+
usage = { totalTokens: Number(uf('total_tokens') || 0), toolUses: Number(uf('tool_uses') || 0), durationMs: Number(uf('duration_ms') || 0) };
|
|
420
|
+
}
|
|
421
|
+
taskNotificationBlocks.push({
|
|
422
|
+
taskId: field('task-id'),
|
|
423
|
+
status: field('status'),
|
|
424
|
+
summary: field('summary'),
|
|
425
|
+
result: field('result'),
|
|
426
|
+
usage,
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const hasCommand = content.some(b => b.type === 'text' && /<command-message>/i.test(b.text || ''));
|
|
433
|
+
|
|
434
|
+
// 提取 slash command 名称
|
|
435
|
+
const commands = [];
|
|
436
|
+
if (hasCommand) {
|
|
437
|
+
for (const b of content) {
|
|
438
|
+
if (b.type !== 'text') continue;
|
|
439
|
+
const m = (b.text || '').match(/<command-name>\s*([^<]*)<\/command-name>/i);
|
|
440
|
+
if (m) {
|
|
441
|
+
const cmd = m[1].trim();
|
|
442
|
+
commands.push(cmd.startsWith('/') ? cmd : `/${cmd}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// 过滤出非系统文本块
|
|
448
|
+
let textBlocks = content.filter(b => b.type === 'text' && !isSystemText(b.text));
|
|
449
|
+
|
|
450
|
+
// 二次提取:从被过滤的系统块中提取嵌入的用户文本
|
|
451
|
+
// (e.g., /ultraplan 将 skill 指令和用户输入合并在同一 <system-reminder> 块中)
|
|
452
|
+
// stripSystemTags 后再过一次 isSystemText —— 避免对 [Request interrupted ...] 这种纯标记
|
|
453
|
+
// 文本(无可剥离 XML)误回收成用户气泡
|
|
454
|
+
for (const b of content) {
|
|
455
|
+
if (b.type !== 'text' || !isSystemText(b.text)) continue;
|
|
456
|
+
const userText = stripSystemTags(b.text);
|
|
457
|
+
if (userText && !isSystemText(userText)) {
|
|
458
|
+
textBlocks.push({ ...b, text: userText });
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// 过滤掉 command 相关块
|
|
463
|
+
if (hasCommand) {
|
|
464
|
+
textBlocks = textBlocks.filter(b => !/<command-message>/i.test(b.text || ''));
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// skill 文本(isSkillText)必然先被 isSystemText 的同一正则("Base directory for this skill:")
|
|
468
|
+
// 过滤,textBlocks 两条进入路径(初次过滤/二次回收)都要求 !isSystemText,故 skill 块不可能
|
|
469
|
+
// 出现在 textBlocks 中;保留 skillBlocks 键以维持返回 shape(ChatView/ImConversationModal 消费)。
|
|
470
|
+
const skillBlocks = [];
|
|
471
|
+
|
|
472
|
+
return { commands, textBlocks, skillBlocks, teammateBlocks, taskNotificationBlocks };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* 从 teammate 请求的 messages 中提取名字。
|
|
477
|
+
* 扫描 SendMessage 的 tool_result,查找 routing.sender 字段。
|
|
478
|
+
*/
|
|
479
|
+
export function extractTeammateName(body) {
|
|
480
|
+
const msgs = body?.messages;
|
|
481
|
+
if (!Array.isArray(msgs)) return null;
|
|
482
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
483
|
+
const content = msgs[i].content;
|
|
484
|
+
if (!Array.isArray(content)) continue;
|
|
485
|
+
for (const block of content) {
|
|
486
|
+
if (block.type !== 'tool_result') continue;
|
|
487
|
+
const items = Array.isArray(block.content) ? block.content : [block];
|
|
488
|
+
for (const item of items) {
|
|
489
|
+
const text = item.text || (typeof item.content === 'string' ? item.content : '');
|
|
490
|
+
if (!text || !text.includes('"sender"')) continue;
|
|
491
|
+
try {
|
|
492
|
+
const parsed = JSON.parse(text);
|
|
493
|
+
if (parsed?.routing?.sender) return parsed.routing.sender;
|
|
494
|
+
} catch { /* not JSON, skip */ }
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ============== Teammate 名称解析(prompt 内容匹配)==============
|
|
502
|
+
|
|
503
|
+
// 持久化注册表:Agent tool_use prompt 前缀 → teammate name
|
|
504
|
+
const _promptRegistry = new Map();
|
|
505
|
+
// Requests whose response has been scanned for Agent tool_use blocks. A request
|
|
506
|
+
// is only added once its response is present, so a spawn turn that completes
|
|
507
|
+
// LATE (it was in-flight and therefore excluded from the filtered array, then
|
|
508
|
+
// INSERTED mid-array on completion) still gets scanned — the old positional
|
|
509
|
+
// cursor skipped it forever. WeakSet cannot be cleared, so it is recreated on
|
|
510
|
+
// session switch.
|
|
511
|
+
let _registryScanned = new WeakSet();
|
|
512
|
+
// 用首条请求的 timestamp 标识会话,切换时自动 reset
|
|
513
|
+
let _registrySessionKey = null;
|
|
514
|
+
|
|
515
|
+
const PROMPT_PREFIX_LEN = 60;
|
|
516
|
+
const TM_TAG_RE = /<teammate-message[^>]*>/;
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* 从 teammate 首条 user message 中提取 <teammate-message> 后的 prompt 内容。
|
|
520
|
+
*/
|
|
521
|
+
function _extractSpawnPrompt(req) {
|
|
522
|
+
const msgs = req.body?.messages;
|
|
523
|
+
if (!Array.isArray(msgs) || msgs.length === 0) return '';
|
|
524
|
+
const first = msgs[0];
|
|
525
|
+
const content = first.content;
|
|
526
|
+
if (typeof content === 'string') {
|
|
527
|
+
const m = TM_TAG_RE.exec(content);
|
|
528
|
+
if (!m) return '';
|
|
529
|
+
return content.slice(m.index + m[0].length).trimStart();
|
|
530
|
+
}
|
|
531
|
+
if (Array.isArray(content)) {
|
|
532
|
+
for (const b of content) {
|
|
533
|
+
if (b.type !== 'text' || !b.text) continue;
|
|
534
|
+
const m = TM_TAG_RE.exec(b.text);
|
|
535
|
+
if (!m) continue;
|
|
536
|
+
return b.text.slice(m.index + m[0].length).trimStart();
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return '';
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* v2.1.90+ Agent 模式:native teammate 的首条 user message 是 raw prompt(无 <teammate-message> 包装)。
|
|
544
|
+
* 直接提取首条 user message 文本内容用于 prompt prefix 匹配。
|
|
545
|
+
*/
|
|
546
|
+
function _extractRawPrompt(req) {
|
|
547
|
+
const msgs = req.body?.messages;
|
|
548
|
+
if (!Array.isArray(msgs) || msgs.length === 0) return '';
|
|
549
|
+
const first = msgs[0];
|
|
550
|
+
const content = first.content;
|
|
551
|
+
if (typeof content === 'string') return content.trimStart();
|
|
552
|
+
if (Array.isArray(content)) {
|
|
553
|
+
for (const b of content) {
|
|
554
|
+
if (b.type === 'text' && b.text) return b.text.trimStart();
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return '';
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* 预扫描 requests,通过匹配 MainAgent 的 Agent tool_use prompt
|
|
562
|
+
* 与 native teammate 的首条消息内容,注入 req.teammate 名字。
|
|
563
|
+
*
|
|
564
|
+
* 必须在 classifyRequest 之前调用(classifyRequest 结果有 WeakMap 缓存)。
|
|
565
|
+
* 版本兼容:已有 req.teammate(interceptor 模式)的请求不受影响。
|
|
566
|
+
*/
|
|
567
|
+
export function resolveTeammateNames(requests) {
|
|
568
|
+
if (!Array.isArray(requests) || requests.length === 0) return;
|
|
569
|
+
|
|
570
|
+
// 通过首条请求的 timestamp 检测会话切换,自动 reset
|
|
571
|
+
const sessionKey = requests[0]?.timestamp || null;
|
|
572
|
+
if (sessionKey !== _registrySessionKey) {
|
|
573
|
+
_promptRegistry.clear();
|
|
574
|
+
_registryScanned = new WeakSet();
|
|
575
|
+
_registrySessionKey = sessionKey;
|
|
576
|
+
}
|
|
577
|
+
// Step 1: scan MainAgent responses for Agent tool_use blocks, building the
|
|
578
|
+
// prompt-prefix → name map. Full walk with O(1) WeakSet skips; a request is
|
|
579
|
+
// marked scanned ONLY when its response exists, so it is re-visited (cheap,
|
|
580
|
+
// two property reads) until the response arrives, then scanned exactly once.
|
|
581
|
+
// Map.set overwrites, so a re-scan is idempotent anyway.
|
|
582
|
+
for (const req of requests) {
|
|
583
|
+
if (_registryScanned.has(req)) continue;
|
|
584
|
+
if (!req.mainAgent) { _registryScanned.add(req); continue; }
|
|
585
|
+
const content = req.response?.body?.content;
|
|
586
|
+
if (!req.response) continue;
|
|
587
|
+
if (Array.isArray(content)) {
|
|
588
|
+
for (const block of content) {
|
|
589
|
+
if (block.type !== 'tool_use' || block.name !== 'Agent') continue;
|
|
590
|
+
const inp = block.input;
|
|
591
|
+
if (!inp || !inp.name || !inp.prompt) continue;
|
|
592
|
+
const prefix = inp.prompt.trimStart().slice(0, PROMPT_PREFIX_LEN);
|
|
593
|
+
if (prefix) _promptRegistry.set(prefix, inp.name);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
_registryScanned.add(req);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
if (_promptRegistry.size === 0) return;
|
|
600
|
+
|
|
601
|
+
// Step 2: 为缺少名字的 native/proxy teammate 注入 req.teammate
|
|
602
|
+
for (const req of requests) {
|
|
603
|
+
if (req.teammate) continue;
|
|
604
|
+
if (!isNativeTeammate(req) && !TEAMMATE_SYSTEM_RE.test(getSystemText(req.body || {}))) continue;
|
|
605
|
+
|
|
606
|
+
let prompt = _extractSpawnPrompt(req);
|
|
607
|
+
// v2.1.90+ Agent 模式 fallback:无 <teammate-message> 时尝试 raw prompt
|
|
608
|
+
if (!prompt && isNativeTeammate(req)) prompt = _extractRawPrompt(req);
|
|
609
|
+
if (!prompt) continue;
|
|
610
|
+
const prefix = prompt.slice(0, PROMPT_PREFIX_LEN);
|
|
611
|
+
|
|
612
|
+
// 精确前缀匹配
|
|
613
|
+
const name = _promptRegistry.get(prefix);
|
|
614
|
+
if (name) {
|
|
615
|
+
req.teammate = name;
|
|
616
|
+
// 清除可能已缓存的 classifyRequest 结果(subType 为 null 的旧缓存)
|
|
617
|
+
if (req._cachedTeammateName === null) req._cachedTeammateName = name;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { stripPrivateKeys } from './helpers';
|
|
2
|
+
|
|
3
|
+
// Context 标签页「原文」视图的文本组装(唯一实现,ContextTab 不内联第二份)。
|
|
4
|
+
// 口径与 DetailPanel Request/Response 标签 text 模式一致:stripPrivateKeys 剥掉
|
|
5
|
+
// 前端注入的 _ 前缀键(如 _timestamp)→ 字符串原样输出(不加引号包裹)、其余
|
|
6
|
+
// JSON.stringify 两空格缩进。
|
|
7
|
+
// turn 节点输出 [userMsg, ...systemMsgs, assistantMsg] 原始消息切片(rawSystem 为
|
|
8
|
+
// 折叠进本 turn 的 mid-conversation system 消息,按会话顺序位于 user 与 assistant 之间);
|
|
9
|
+
// 当前轮次 assistant 若由 response 覆盖(见 ContextTab turns useMemo),rawAssistant 即完整 response body。
|
|
10
|
+
export function buildContextItemRawText(item) {
|
|
11
|
+
if (!item) return '';
|
|
12
|
+
const raw = item.isTurn
|
|
13
|
+
? [item.rawUser, ...(Array.isArray(item.rawSystem) ? item.rawSystem : []), item.rawAssistant].filter(Boolean)
|
|
14
|
+
: item.raw;
|
|
15
|
+
if (raw == null) return '';
|
|
16
|
+
const clean = typeof raw === 'object' ? stripPrivateKeys(raw) : raw;
|
|
17
|
+
return typeof clean === 'string' ? clean : JSON.stringify(clean, null, 2);
|
|
18
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function getContextSidebarArrowNavigation({
|
|
2
|
+
currentId,
|
|
3
|
+
visibleIds = [],
|
|
4
|
+
key,
|
|
5
|
+
}) {
|
|
6
|
+
if (key !== 'ArrowUp' && key !== 'ArrowDown') return null;
|
|
7
|
+
|
|
8
|
+
const currentIndex = visibleIds.findIndex((id) => id === currentId);
|
|
9
|
+
const delta = key === 'ArrowUp' ? -1 : 1;
|
|
10
|
+
|
|
11
|
+
if (currentIndex >= 0) {
|
|
12
|
+
return visibleIds[currentIndex + delta] ?? null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return null;
|
|
16
|
+
}
|