cc-viewer 1.7.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/App-gUSmU6Ny.js +2 -0
- package/dist/assets/{MdxEditorPanel-C5Q6uUyH.js → MdxEditorPanel-DAR39A3S.js} +1 -1
- package/dist/assets/{Mobile-MoBp9c8k.js → Mobile-qfRsRth0.js} +1 -1
- package/dist/assets/index-B51Ci0GL.js +2 -0
- package/dist/assets/{seqResourceLoaders-CxuD1CYl.css → seqResourceLoaders-DFrHmgnK.css} +1 -1
- package/dist/assets/seqResourceLoaders-DZJxMMaR.js +2 -0
- package/dist/index.html +1 -1
- package/package.json +2 -1
- package/server/lib/log-watcher.js +29 -3
- package/server/lib/v2/adapter.js +71 -6
- package/server/lib/v2/live-feed.js +110 -3
- package/server/lib/v2/meta-rows.js +360 -0
- 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/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/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,194 @@
|
|
|
1
|
+
import analystSvg from '../img/teammates/analyst.svg?raw';
|
|
2
|
+
import auditorSvg from '../img/teammates/auditor.svg?raw';
|
|
3
|
+
import builderSvg from '../img/teammates/builder.svg?raw';
|
|
4
|
+
import defaultSvg from '../img/teammates/default.svg?raw';
|
|
5
|
+
import designerSvg from '../img/teammates/designer.svg?raw';
|
|
6
|
+
import executorSvg from '../img/teammates/executor.svg?raw';
|
|
7
|
+
import expertSvg from '../img/teammates/expert.svg?raw';
|
|
8
|
+
import explorerSvg from '../img/teammates/explorer.svg?raw';
|
|
9
|
+
import implementerSvg from '../img/teammates/implementer.svg?raw';
|
|
10
|
+
import investigatorSvg from '../img/teammates/investigator.svg?raw';
|
|
11
|
+
import researcherSvg from '../img/teammates/researcher.svg?raw';
|
|
12
|
+
import reviewerSvg from '../img/teammates/reviewer.svg?raw';
|
|
13
|
+
import scannerSvg from '../img/teammates/scanner.svg?raw';
|
|
14
|
+
import securitySvg from '../img/teammates/security.svg?raw';
|
|
15
|
+
import tracerSvg from '../img/teammates/tracer.svg?raw';
|
|
16
|
+
import translatorSvg from '../img/teammates/translator.svg?raw';
|
|
17
|
+
import workerSvg from '../img/teammates/worker.svg?raw';
|
|
18
|
+
|
|
19
|
+
export const ROLE_MAP = {
|
|
20
|
+
worker: { svg: workerSvg },
|
|
21
|
+
reviewer: { svg: reviewerSvg },
|
|
22
|
+
researcher: { svg: researcherSvg },
|
|
23
|
+
explorer: { svg: explorerSvg },
|
|
24
|
+
analyst: { svg: analystSvg },
|
|
25
|
+
tracer: { svg: tracerSvg },
|
|
26
|
+
investigator: { svg: investigatorSvg },
|
|
27
|
+
builder: { svg: builderSvg },
|
|
28
|
+
implementer: { svg: implementerSvg },
|
|
29
|
+
auditor: { svg: auditorSvg },
|
|
30
|
+
translator: { svg: translatorSvg },
|
|
31
|
+
security: { svg: securitySvg },
|
|
32
|
+
scanner: { svg: scannerSvg },
|
|
33
|
+
expert: { svg: expertSvg },
|
|
34
|
+
executor: { svg: executorSvg },
|
|
35
|
+
designer: { svg: designerSvg },
|
|
36
|
+
default: { svg: defaultSvg },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const PREFIX_RULES = [
|
|
40
|
+
{ prefix: 'worker-', role: 'worker' },
|
|
41
|
+
{ prefix: 'reviewer-', role: 'reviewer' },
|
|
42
|
+
{ prefix: 'researcher-', role: 'researcher' },
|
|
43
|
+
{ prefix: 'explorer-', role: 'explorer' },
|
|
44
|
+
{ prefix: 'explore-', role: 'explorer' },
|
|
45
|
+
{ prefix: 'translator-', role: 'translator' },
|
|
46
|
+
{ prefix: 'svg-creator-', role: 'designer' },
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
const SUFFIX_RULES = [
|
|
50
|
+
{ suffix: '-reviewer', role: 'reviewer' },
|
|
51
|
+
{ suffix: '-analyst', role: 'analyst' },
|
|
52
|
+
{ suffix: '-tracer', role: 'tracer' },
|
|
53
|
+
{ suffix: '-investigator', role: 'investigator' },
|
|
54
|
+
{ suffix: '-builder', role: 'builder' },
|
|
55
|
+
{ suffix: '-impl', role: 'implementer' },
|
|
56
|
+
{ suffix: '-auditor', role: 'auditor' },
|
|
57
|
+
{ suffix: '-scanner', role: 'scanner' },
|
|
58
|
+
{ suffix: '-expert', role: 'expert' },
|
|
59
|
+
{ suffix: '-executor', role: 'executor' },
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const CONTAINS_RULES = [
|
|
63
|
+
{ keyword: 'security', role: 'security' },
|
|
64
|
+
{ keyword: 'implementer', role: 'implementer' },
|
|
65
|
+
{ keyword: 'review', role: 'reviewer' },
|
|
66
|
+
{ keyword: 'explor', role: 'explorer' },
|
|
67
|
+
{ keyword: 'research', role: 'researcher' },
|
|
68
|
+
{ keyword: 'analy', role: 'analyst' },
|
|
69
|
+
{ keyword: 'trac', role: 'tracer' },
|
|
70
|
+
{ keyword: 'investigat', role: 'investigator' },
|
|
71
|
+
{ keyword: 'build', role: 'builder' },
|
|
72
|
+
{ keyword: 'audit', role: 'auditor' },
|
|
73
|
+
{ keyword: 'translat', role: 'translator' },
|
|
74
|
+
{ keyword: 'scan', role: 'scanner' },
|
|
75
|
+
{ keyword: 'expert', role: 'expert' },
|
|
76
|
+
{ keyword: 'execut', role: 'executor' },
|
|
77
|
+
{ keyword: 'design', role: 'designer' },
|
|
78
|
+
{ keyword: 'work', role: 'worker' },
|
|
79
|
+
];
|
|
80
|
+
|
|
81
|
+
const ABBREV_PREFIX_RULES = [
|
|
82
|
+
{ prefix: 'cr-', role: 'reviewer' },
|
|
83
|
+
{ prefix: 'r-', role: 'reviewer' },
|
|
84
|
+
{ prefix: 'ui-', role: 'reviewer' },
|
|
85
|
+
{ prefix: 'ux-', role: 'reviewer' },
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
// Hash-based fallback: deterministic role from name (visual variety for unmatched names)
|
|
89
|
+
const ROLE_KEYS = Object.keys(ROLE_MAP).filter(k => k !== 'default');
|
|
90
|
+
|
|
91
|
+
function resolveRole(name) {
|
|
92
|
+
const lower = name.toLowerCase();
|
|
93
|
+
|
|
94
|
+
for (const { prefix, role } of PREFIX_RULES) {
|
|
95
|
+
if (lower.startsWith(prefix)) return role;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const { suffix, role } of SUFFIX_RULES) {
|
|
99
|
+
if (lower.endsWith(suffix)) return role;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const { keyword, role } of CONTAINS_RULES) {
|
|
103
|
+
if (lower.includes(keyword)) return role;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const { prefix, role } of ABBREV_PREFIX_RULES) {
|
|
107
|
+
if (lower.startsWith(prefix)) return role;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Hash fallback: same name always maps to same role
|
|
111
|
+
if (lower.length > 0) {
|
|
112
|
+
let hash = 0;
|
|
113
|
+
for (let i = 0; i < lower.length; i++) hash = lower.charCodeAt(i) + ((hash << 5) - hash);
|
|
114
|
+
return ROLE_KEYS[((hash % ROLE_KEYS.length) + ROLE_KEYS.length) % ROLE_KEYS.length];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return 'default';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 20 个 CSS 变量引用:背景色由 global.css 按当前主题(曜石黑 / 雪山白)决定。
|
|
121
|
+
// 运行时不做主题判定,避免初次渲染时 data-theme 未就绪导致的误判。
|
|
122
|
+
// 索引对应 `--avatar-bg-0..19`,顺序由 nameToColorIndex 决定。
|
|
123
|
+
const AVATAR_BG_VAR_COUNT = 20;
|
|
124
|
+
|
|
125
|
+
function nameToColorIndex(name) {
|
|
126
|
+
let hash = 0;
|
|
127
|
+
for (let i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
|
128
|
+
return ((hash % AVATAR_BG_VAR_COUNT) + AVATAR_BG_VAR_COUNT) % AVATAR_BG_VAR_COUNT;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Removes SMIL <animate> elements from an avatar SVG string. DESIGN.md's
|
|
132
|
+
// degradation rule guarantees the static markup IS the finished portrait
|
|
133
|
+
// (all animate elements are self-closing and hold their hidden states inside
|
|
134
|
+
// the animate itself), so the stripped string renders the complete image.
|
|
135
|
+
// The narrow regex (only self-closing <animate>) is intentionally coupled to
|
|
136
|
+
// test/teammate-svg-assets.test.js, whose element allowlist and self-closing
|
|
137
|
+
// assertion forbid animateTransform/animateMotion/<set> and open-form
|
|
138
|
+
// <animate> — keep both in sync if that allowlist ever relaxes.
|
|
139
|
+
export function stripSvgAnimations(svg) {
|
|
140
|
+
return svg.replace(/<animate\b[^>]*\/>/g, '');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Static (animation-stripped) variant per role, computed once so the strip
|
|
144
|
+
// regex does not rerun on every render of every old row.
|
|
145
|
+
const _staticSvgCache = new Map();
|
|
146
|
+
|
|
147
|
+
function getStaticSvg(role, svg) {
|
|
148
|
+
let cached = _staticSvgCache.get(role);
|
|
149
|
+
if (cached === undefined) {
|
|
150
|
+
cached = stripSvgAnimations(svg);
|
|
151
|
+
_staticSvgCache.set(role, cached);
|
|
152
|
+
}
|
|
153
|
+
return cached;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Policy: a teammate avatar plays its one-shot draw-in only when its message
|
|
157
|
+
// timestamp is within windowMs of the newest item's timestamp. Missing or
|
|
158
|
+
// unparseable timestamps animate (today's behavior). latestTs may be an ISO
|
|
159
|
+
// string or epoch ms.
|
|
160
|
+
export function shouldAnimateTeammateAvatar(msgTs, latestTs, windowMs = 60000) {
|
|
161
|
+
const msgMs = typeof msgTs === 'number' ? msgTs : Date.parse(msgTs);
|
|
162
|
+
const latestMs = typeof latestTs === 'number' ? latestTs : Date.parse(latestTs);
|
|
163
|
+
if (Number.isNaN(msgMs) || Number.isNaN(latestMs)) return true;
|
|
164
|
+
return latestMs - msgMs <= windowMs;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Scan helper for ChatView.buildAllItems: given [{ts, isTeammateAvatar}] in
|
|
168
|
+
// render order, returns the max parseable timestamp (epoch ms; NaN entries
|
|
169
|
+
// skipped) and the index of the last teammate-avatar row (-1 if none).
|
|
170
|
+
export function pickAvatarAnimationTargets(entries) {
|
|
171
|
+
let latestMs = NaN;
|
|
172
|
+
let newestTeammateIdx = -1;
|
|
173
|
+
for (let i = 0; i < entries.length; i++) {
|
|
174
|
+
const { ts, isTeammateAvatar } = entries[i];
|
|
175
|
+
const ms = typeof ts === 'number' ? ts : Date.parse(ts);
|
|
176
|
+
if (!Number.isNaN(ms) && (Number.isNaN(latestMs) || ms > latestMs)) latestMs = ms;
|
|
177
|
+
if (isTeammateAvatar) newestTeammateIdx = i;
|
|
178
|
+
}
|
|
179
|
+
return { latestMs, newestTeammateIdx };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function getTeammateAvatar(name, { animated = true } = {}) {
|
|
183
|
+
let clean = (name || '').trim();
|
|
184
|
+
// Strip "Teammate: " prefix (from formatTeammateLabel)
|
|
185
|
+
clean = clean.replace(/^Teammate:\s*/i, '');
|
|
186
|
+
// Strip trailing "(model-info)" suffix
|
|
187
|
+
clean = clean.replace(/\([^)]*\)\s*$/, '').trim();
|
|
188
|
+
const role = resolveRole(clean);
|
|
189
|
+
const entry = ROLE_MAP[role];
|
|
190
|
+
// SVG 表达角色,CSS 变量引用表达个体身份——浏览器按当前 [data-theme] 选值
|
|
191
|
+
const color = `var(--avatar-bg-${nameToColorIndex(clean)})`;
|
|
192
|
+
const svg = animated ? entry.svg : getStaticSvg(role, entry.svg);
|
|
193
|
+
return { svg, color, role };
|
|
194
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native Teammate 检测器
|
|
3
|
+
*
|
|
4
|
+
* 识别 Claude Code 同进程内通过 Agent 工具启动的 native teammate。
|
|
5
|
+
* 与外部进程 teammate(通过 --agent-name 参数启动)不同,native teammate
|
|
6
|
+
* 的 API 请求没有 req.teammate 字段,需要通过 system prompt 特征检测。
|
|
7
|
+
*
|
|
8
|
+
* 特征:system prompt 中包含 "You are a Claude agent" 标记
|
|
9
|
+
* (主 agent 的 system prompt 是 "You are Claude Code")
|
|
10
|
+
*
|
|
11
|
+
* 版本兼容:
|
|
12
|
+
* - Claude Code v2.1.81+: system block[1] = "You are a Claude agent, built on Anthropic's Claude Agent SDK."
|
|
13
|
+
* - 未来版本如果特征变化,在此文件中添加新的检测规则即可
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// 内联 getSystemText 避免与 contentFilter 循环依赖
|
|
17
|
+
function getSystemText(body) {
|
|
18
|
+
const system = body?.system;
|
|
19
|
+
if (typeof system === 'string') return system;
|
|
20
|
+
if (Array.isArray(system)) return system.map(s => (s && s.text) || '').join('');
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Native teammate 特征:system prompt 包含 "You are a Claude agent"
|
|
25
|
+
// 注意区分 "You are Claude Code"(主 agent)
|
|
26
|
+
// 但 "You are a Claude agent" 对所有 SDK agent(含普通 subagent)都匹配,
|
|
27
|
+
// 单这一条判据会把 Agent tool 启动的普通 subagent 误判为 teammate。
|
|
28
|
+
// 真正区分:teammate 之间通过 SendMessage tool 通信,subagent 不会被授予该工具。
|
|
29
|
+
const NATIVE_TEAMMATE_RE = /You are a Claude agent/i;
|
|
30
|
+
|
|
31
|
+
// WeakMap cache 避免重复检测
|
|
32
|
+
const _cache = new WeakMap();
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 判断请求是否为 native teammate(同进程内的 Agent 子代理)
|
|
36
|
+
* @param {object} req - 请求对象
|
|
37
|
+
* @returns {boolean}
|
|
38
|
+
*/
|
|
39
|
+
export function isNativeTeammate(req) {
|
|
40
|
+
if (!req) return false;
|
|
41
|
+
const cached = _cache.get(req);
|
|
42
|
+
if (cached !== undefined) return cached;
|
|
43
|
+
|
|
44
|
+
// 已有 teammate 字段(外部进程 teammate)→ 不是 native teammate
|
|
45
|
+
if (req.teammate) {
|
|
46
|
+
_cache.set(req, false);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const sysText = getSystemText(req.body || {});
|
|
51
|
+
if (!NATIVE_TEAMMATE_RE.test(sysText)) {
|
|
52
|
+
_cache.set(req, false);
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// SendMessage tool 是 teammate 间通信必需工具,普通 subagent 不会被授予。
|
|
57
|
+
// 命中正则但没 SendMessage → 是通过 Agent tool 启动的 subagent,不是 teammate。
|
|
58
|
+
const tools = req.body?.tools;
|
|
59
|
+
const hasSendMessage = Array.isArray(tools) && tools.some(t => t && t.name === 'SendMessage');
|
|
60
|
+
const result = hasSendMessage;
|
|
61
|
+
_cache.set(req, result);
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 从 native teammate 请求中提取名字
|
|
67
|
+
* 优先从首条 user message 中匹配 "You are XXX" 模式
|
|
68
|
+
* @param {object} req - 请求对象
|
|
69
|
+
* @returns {string|null}
|
|
70
|
+
*/
|
|
71
|
+
export function extractNativeTeammateName(req) {
|
|
72
|
+
if (!req?.body) return null;
|
|
73
|
+
|
|
74
|
+
const msgs = req.body.messages || [];
|
|
75
|
+
if (msgs.length === 0) return null;
|
|
76
|
+
|
|
77
|
+
// 搜索所有 user 消息(上下文压缩后 hook 可能不在 msg[0] 中)
|
|
78
|
+
for (const m of msgs) {
|
|
79
|
+
if (m.role !== 'user') continue;
|
|
80
|
+
const content = m.content;
|
|
81
|
+
let text = '';
|
|
82
|
+
if (typeof content === 'string') {
|
|
83
|
+
text = content;
|
|
84
|
+
} else if (Array.isArray(content)) {
|
|
85
|
+
text = content
|
|
86
|
+
.filter(c => c && c.type === 'text')
|
|
87
|
+
.map(c => c.text || '')
|
|
88
|
+
.join(' ');
|
|
89
|
+
}
|
|
90
|
+
if (!text) continue;
|
|
91
|
+
|
|
92
|
+
// 匹配名字模式(按优先级尝试)
|
|
93
|
+
const nameMatch =
|
|
94
|
+
// OMC hook: "Agent oh-my-claudecode:code-reviewer started"
|
|
95
|
+
text.match(/Agent\s+(?:oh-my-claudecode:)?(\S+)\s+started/i)
|
|
96
|
+
// 任务提示: "You are CRer2, ..."
|
|
97
|
+
|| text.match(/You are (\w+)[,.]/)
|
|
98
|
+
// 显式名字: "name: CRer2"
|
|
99
|
+
|| text.match(/name[::]\s*["']?(\w+)/i);
|
|
100
|
+
if (nameMatch) return nameMatch[1];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 从 billing header 中提取 Claude Code 版本号
|
|
108
|
+
* v2.1.90+ system prompt 中包含 x-anthropic-billing-header: cc_version=X.Y.Z
|
|
109
|
+
* @param {object} req - 请求对象
|
|
110
|
+
* @returns {string|null} 版本号如 "2.1.90" 或 null
|
|
111
|
+
*/
|
|
112
|
+
export function extractCcVersion(req) {
|
|
113
|
+
const system = req?.body?.system;
|
|
114
|
+
if (!Array.isArray(system)) return null;
|
|
115
|
+
for (const block of system) {
|
|
116
|
+
if (!block?.text) continue;
|
|
117
|
+
const m = block.text.match(/cc_version=([\d.]+[a-fA-F0-9]*)/);
|
|
118
|
+
if (m) return m[1];
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* termDiag —— 终端渲染管线最小可观测性(Windows 卡死排查回报通道)。
|
|
3
|
+
*
|
|
4
|
+
* 纯本地、仅数值计数器/水位,不含任何命令内容、无任何上报。
|
|
5
|
+
* 使用方式(用户侧):
|
|
6
|
+
* - 控制台执行 `__ccvTermDiag()` 获取快照;
|
|
7
|
+
* - `localStorage.ccv_term_diag = '1'` 后每 5s console.warn 一行
|
|
8
|
+
* `[ccv-diag] {...}`(沿既有 [cc-viewer] 英文诊断日志惯例,非 UI 文案,
|
|
9
|
+
* 不走 i18n)。
|
|
10
|
+
*
|
|
11
|
+
* 指标:
|
|
12
|
+
* trimCount writeQueue 积压丢弃次数(2MB 高水位触发)
|
|
13
|
+
* resyncCount 服务端 data-resync 次数(behind→resume 振荡的标尺)
|
|
14
|
+
* longtaskCount 主线程 ≥50ms 长任务数(PerformanceObserver,仅计数不降级)
|
|
15
|
+
* writeQPendingBytes / chunkSize / cbLatencyEwma 喂入闭环水位(P2 自适应接线)
|
|
16
|
+
* detect* promptDetect 耗时统计(>50ms 计 overrun——仅监测计数,无熔断降级)
|
|
17
|
+
*
|
|
18
|
+
* 纯 JS、浏览器 API 全部能力守卫(node:test 可直接 import 测纯逻辑)。
|
|
19
|
+
*/
|
|
20
|
+
import { getPromptDetectStats } from './promptDetect.js';
|
|
21
|
+
|
|
22
|
+
const counters = {
|
|
23
|
+
trimCount: 0,
|
|
24
|
+
resyncCount: 0,
|
|
25
|
+
longtaskCount: 0,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const gauges = {
|
|
29
|
+
writeQPendingBytes: 0,
|
|
30
|
+
chunkSize: 0,
|
|
31
|
+
cbLatencyEwma: 0,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function diagCount(name, n = 1) {
|
|
35
|
+
if (typeof counters[name] === 'number') counters[name] += n;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function diagSet(name, value) {
|
|
39
|
+
if (typeof gauges[name] === 'number' && typeof value === 'number') gauges[name] = value;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 指数加权滑动平均(alpha 默认 0.2),返回更新后的值 */
|
|
43
|
+
export function diagEwma(name, sample, alpha = 0.2) {
|
|
44
|
+
if (typeof gauges[name] !== 'number' || typeof sample !== 'number') return 0;
|
|
45
|
+
gauges[name] = gauges[name] === 0 ? sample : gauges[name] * (1 - alpha) + sample * alpha;
|
|
46
|
+
return gauges[name];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getTermDiagSnapshot() {
|
|
50
|
+
const detect = getPromptDetectStats();
|
|
51
|
+
return {
|
|
52
|
+
...counters,
|
|
53
|
+
writeQPendingBytes: Math.round(gauges.writeQPendingBytes),
|
|
54
|
+
chunkSize: Math.round(gauges.chunkSize),
|
|
55
|
+
cbLatencyEwma: Math.round(gauges.cbLatencyEwma * 10) / 10,
|
|
56
|
+
detectCalls: detect.calls,
|
|
57
|
+
detectLastMs: Math.round(detect.lastMs * 10) / 10,
|
|
58
|
+
detectMaxMs: Math.round(detect.maxMs * 10) / 10,
|
|
59
|
+
detectOverruns: detect.overruns,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 仅测试用:清零本模块计数(promptDetect 的统计不归此处管) */
|
|
64
|
+
export function _resetTermDiagForTest() {
|
|
65
|
+
for (const k of Object.keys(counters)) counters[k] = 0;
|
|
66
|
+
for (const k of Object.keys(gauges)) gauges[k] = 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let _installed = false;
|
|
70
|
+
let _logTimer = null;
|
|
71
|
+
let _ltObserver = null;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 浏览器侧安装(幂等):window.__ccvTermDiag + longtask 计数 + 可选周期日志。
|
|
75
|
+
* 在 node 环境(无 window)下为 no-op。
|
|
76
|
+
*/
|
|
77
|
+
export function installTermDiag() {
|
|
78
|
+
if (_installed || typeof window === 'undefined') return;
|
|
79
|
+
_installed = true;
|
|
80
|
+
|
|
81
|
+
window.__ccvTermDiag = getTermDiagSnapshot;
|
|
82
|
+
|
|
83
|
+
// 长任务仅计数不降级(与 TerminalPanel WebGL 守卫的能力检测同模式)
|
|
84
|
+
try {
|
|
85
|
+
if (typeof PerformanceObserver !== 'undefined'
|
|
86
|
+
&& PerformanceObserver.supportedEntryTypes?.includes('longtask')) {
|
|
87
|
+
_ltObserver = new PerformanceObserver((list) => {
|
|
88
|
+
diagCount('longtaskCount', list.getEntries().length);
|
|
89
|
+
});
|
|
90
|
+
_ltObserver.observe({ entryTypes: ['longtask'] });
|
|
91
|
+
}
|
|
92
|
+
} catch {}
|
|
93
|
+
|
|
94
|
+
// 周期日志:localStorage 开关,运行期改动即时生效(每 tick 重查)
|
|
95
|
+
try {
|
|
96
|
+
_logTimer = setInterval(() => {
|
|
97
|
+
try {
|
|
98
|
+
if (localStorage.getItem('ccv_term_diag') === '1') {
|
|
99
|
+
console.warn('[ccv-diag] ' + JSON.stringify(getTermDiagSnapshot()));
|
|
100
|
+
}
|
|
101
|
+
} catch {}
|
|
102
|
+
}, 5000);
|
|
103
|
+
} catch {}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** 仅测试/热重载用:卸载 observer 与定时器 */
|
|
107
|
+
export function uninstallTermDiag() {
|
|
108
|
+
if (_ltObserver) { try { _ltObserver.disconnect(); } catch {} _ltObserver = null; }
|
|
109
|
+
if (_logTimer) { clearInterval(_logTimer); _logTimer = null; }
|
|
110
|
+
if (typeof window !== 'undefined' && window.__ccvTermDiag === getTermDiagSnapshot) {
|
|
111
|
+
try { delete window.__ccvTermDiag; } catch {}
|
|
112
|
+
}
|
|
113
|
+
_installed = false;
|
|
114
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// 终端复制/粘贴键位判定 + 剪贴板写入(共用于 TerminalPanel / ScratchTerminal)
|
|
3
|
+
// ----------------------------------------------------------------------------
|
|
4
|
+
// 背景:xterm 6 的键盘处理把 Ctrl+C / Ctrl+V 当控制字符(\x03 / \x16)发往 PTY,
|
|
5
|
+
// 并对该 keydown 调用 preventDefault(CoreBrowserTerminal._keyDown 末尾的 cancel)。
|
|
6
|
+
// 后果(仅 Win/Linux,Mac 用 Cmd+C/V 走 metaKey 不受影响):
|
|
7
|
+
// - Ctrl+V:浏览器原生 paste 默认动作被压掉 → 无法粘贴(只有 Ctrl+Shift+V 能用,
|
|
8
|
+
// 因为带 Shift 不进 ctrl-letter 控制字符分支,xterm 不 cancel,原生 paste 照常)。
|
|
9
|
+
// - Ctrl+C:恒发 SIGINT,永远不会复制。
|
|
10
|
+
// 这里把 Ctrl+C/Ctrl+V 还原成"智能复制/粘贴"(对齐 Windows Terminal / VS Code 终端):
|
|
11
|
+
// 在 attachCustomKeyEventHandler(最先执行、且返回 false 时 xterm 不会 cancel/preventDefault)
|
|
12
|
+
// 里判定动作,由调用方主动读剪贴板并走安全送出路径。
|
|
13
|
+
// ============================================================================
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 判断键盘事件是否应触发终端的复制/粘贴。
|
|
17
|
+
* 仅在 keydown、Ctrl 按下(且非 Shift/Alt/Meta)、非 Mac、物理键为 C/V 时命中。
|
|
18
|
+
* 纯函数,不读取任何全局状态(isMac 由调用方传入),便于单测。
|
|
19
|
+
*
|
|
20
|
+
* @param {KeyboardEvent} e
|
|
21
|
+
* @param {{ isMac?: boolean }} [opts]
|
|
22
|
+
* @returns {'copy'|'paste'|null}
|
|
23
|
+
*/
|
|
24
|
+
export function clipboardKeyAction(e, opts = {}) {
|
|
25
|
+
// Mac:Ctrl+C/V 维持终端原义(SIGINT / 字面插入),复制粘贴走 Cmd(metaKey),不在此接管。
|
|
26
|
+
if (opts.isMac) return null;
|
|
27
|
+
if (!e || e.type !== 'keydown') return null;
|
|
28
|
+
if (!e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) return null;
|
|
29
|
+
// e.code 是物理键位(布局无关);同时兼容只有 e.key 的场景。
|
|
30
|
+
const key = typeof e.key === 'string' ? e.key.toLowerCase() : '';
|
|
31
|
+
if (e.code === 'KeyC' || key === 'c') return 'copy';
|
|
32
|
+
if (e.code === 'KeyV' || key === 'v') return 'paste';
|
|
33
|
+
// 刻意不接管 Ctrl+X:终端里它是合法控制字符(\x18,如 emacs/nano 前缀键),剪切语义不适用,交回 xterm。
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 计算粘贴时应发送给 PTY 的字节串:bracketed-paste 包裹 + 注入消毒决策。
|
|
39
|
+
* 抽出原 _handlePaste / _pasteText(及 scratch 对应)中重复的判定,便于单测覆盖这段最易回归的逻辑。
|
|
40
|
+
*
|
|
41
|
+
* @param {string} text 剪贴板文本
|
|
42
|
+
* @param {{ bracketedPasteMode?: boolean, active?: boolean, sanitize?: (s:string)=>string }} [opts]
|
|
43
|
+
* - active=false(原生 paste 路径):bracketedPasteMode 且无注入时返回 null(交 xterm 自动包裹);
|
|
44
|
+
* 单行且无包裹模式也返回 null(交浏览器原生插入);其余包裹。
|
|
45
|
+
* - active=true(主动读剪贴板,无原生 paste 事件):bracketedPasteMode 时也自行包裹;单行原样返回。
|
|
46
|
+
* @returns {string|null} 要 ws.send 的 data;null 表示此处不发送(交由 xterm / 原生处理)
|
|
47
|
+
*/
|
|
48
|
+
export function planPasteSend(text, opts = {}) {
|
|
49
|
+
if (typeof text !== 'string' || text.length === 0) return null;
|
|
50
|
+
const { bracketedPasteMode = false, active = false, sanitize = (s) => s } = opts;
|
|
51
|
+
const hasInjection = /\x1b\[20[01]~/.test(text);
|
|
52
|
+
// 原生路径下:shell 已开 bracketed paste 且无注入序列 → xterm 会自动包裹,无需干预
|
|
53
|
+
if (!active && bracketedPasteMode && !hasInjection) return null;
|
|
54
|
+
const isMultiline = text.includes('\n') || text.includes('\r');
|
|
55
|
+
const needWrap = active ? (bracketedPasteMode || hasInjection || isMultiline)
|
|
56
|
+
: (hasInjection || isMultiline);
|
|
57
|
+
if (needWrap) return `\x1b[200~${sanitize(text)}\x1b[201~`;
|
|
58
|
+
// active:单行无包裹模式 → 原样发送;原生路径:交浏览器默认插入(不发送)
|
|
59
|
+
return active ? text : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 将文本写入剪贴板:安全上下文优先 navigator.clipboard.writeText,
|
|
64
|
+
* 否则退回隐藏 textarea + document.execCommand('copy')(覆盖 LAN HTTP 等非安全上下文),
|
|
65
|
+
* 并在结束后还原原焦点,避免抢走终端焦点。
|
|
66
|
+
*
|
|
67
|
+
* @param {string} text
|
|
68
|
+
* @returns {Promise<boolean>} 是否复制成功
|
|
69
|
+
*/
|
|
70
|
+
export async function copyTextToClipboard(text) {
|
|
71
|
+
if (typeof text !== 'string' || text.length === 0) return false;
|
|
72
|
+
const clip = typeof navigator !== 'undefined' ? navigator.clipboard : null;
|
|
73
|
+
if (clip && typeof clip.writeText === 'function') {
|
|
74
|
+
try {
|
|
75
|
+
await clip.writeText(text);
|
|
76
|
+
return true;
|
|
77
|
+
} catch {
|
|
78
|
+
// 安全上下文但权限/焦点等原因失败 → 落到 execCommand 兜底
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return execCommandCopy(text);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function execCommandCopy(text) {
|
|
85
|
+
if (typeof document === 'undefined' || !document.body) return false;
|
|
86
|
+
const prevActive = document.activeElement;
|
|
87
|
+
const ta = document.createElement('textarea');
|
|
88
|
+
ta.value = text;
|
|
89
|
+
ta.setAttribute('readonly', '');
|
|
90
|
+
ta.style.position = 'fixed';
|
|
91
|
+
ta.style.top = '-9999px';
|
|
92
|
+
ta.style.left = '-9999px';
|
|
93
|
+
ta.style.opacity = '0';
|
|
94
|
+
document.body.appendChild(ta);
|
|
95
|
+
let ok = false;
|
|
96
|
+
try {
|
|
97
|
+
ta.focus();
|
|
98
|
+
ta.select();
|
|
99
|
+
ok = document.execCommand('copy');
|
|
100
|
+
} catch {
|
|
101
|
+
ok = false;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
document.body.removeChild(ta);
|
|
105
|
+
} catch {
|
|
106
|
+
/* noop */
|
|
107
|
+
}
|
|
108
|
+
// 还原焦点(execCommand 复制需要短暂聚焦临时 textarea,复制后交还原元素)
|
|
109
|
+
if (prevActive && typeof prevActive.focus === 'function') {
|
|
110
|
+
try {
|
|
111
|
+
prevActive.focus();
|
|
112
|
+
} catch {
|
|
113
|
+
/* noop */
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return ok;
|
|
117
|
+
}
|