cc-viewer 1.8.0 → 1.8.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/README.md +1 -0
- package/concepts/ar/ToolsFirst.md +1 -1
- package/concepts/da/ToolsFirst.md +1 -1
- package/concepts/de/ToolsFirst.md +1 -1
- package/concepts/en/ToolsFirst.md +1 -1
- package/concepts/es/ToolsFirst.md +1 -1
- package/concepts/fr/ToolsFirst.md +1 -1
- package/concepts/it/ToolsFirst.md +1 -1
- package/concepts/ja/ToolsFirst.md +1 -1
- package/concepts/ko/ToolsFirst.md +1 -1
- package/concepts/no/ToolsFirst.md +1 -1
- package/concepts/pl/ToolsFirst.md +1 -1
- package/concepts/pt-BR/ToolsFirst.md +1 -1
- package/concepts/ru/ToolsFirst.md +1 -1
- package/concepts/th/ToolsFirst.md +1 -1
- package/concepts/tr/ToolsFirst.md +1 -1
- package/concepts/uk/ToolsFirst.md +1 -1
- package/concepts/zh/ToolsFirst.md +1 -1
- package/concepts/zh-TW/ToolsFirst.md +1 -1
- package/dist/assets/App-CqQJDjfT.js +2 -0
- package/dist/assets/App-ssYfrjwF.css +1 -0
- package/dist/assets/{MdxEditorPanel-BBhgTmDk.js → MdxEditorPanel-DXwb5tSD.js} +1 -1
- package/dist/assets/{Mobile-vN4lyiaz.js → Mobile-DOjKURO3.js} +1 -1
- package/dist/assets/{ProxyStatsModal-BOflqr0R.js → ProxyStatsModal-BEnxiRXF.js} +1 -1
- package/dist/assets/index-CH_N7vvX.js +2 -0
- package/dist/assets/{seqResourceLoaders-DpYqp6SP.js → seqResourceLoaders-BJhO6zhW.js} +2 -2
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/server/lib/builtin-model-prompts.js +198 -0
- package/server/lib/launch-config.js +9 -1
- package/server/lib/model-system-prompts.js +17 -1
- package/server/lib/system-prompt-files.js +42 -5
- package/server/pty-manager.js +6 -3
- package/server/routes/expert.js +74 -13
- package/ultraAgents/README.md +3 -0
- package/ultraAgents/test-analysis-expert.json +45 -0
- package/dist/assets/App-BWajJyJh.css +0 -1
- package/dist/assets/App-CCu0y2Cq.js +0 -2
- package/dist/assets/index-C3BRHDVF.js +0 -2
package/dist/index.html
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
// 整体显示大小已弃用 CSS zoom:Electron 改用 webFrame.setZoomFactor(首屏抢占见
|
|
22
22
|
// electron/tab-content-preload.js),纯浏览器交由用户用浏览器自带快捷键缩放,故此处不再设 zoom。
|
|
23
23
|
</script>
|
|
24
|
-
<script type="module" crossorigin src="./assets/index-
|
|
24
|
+
<script type="module" crossorigin src="./assets/index-CH_N7vvX.js"></script>
|
|
25
25
|
<link rel="modulepreload" crossorigin href="./assets/vendor-antd-CSjy2pdD.js">
|
|
26
26
|
<link rel="modulepreload" crossorigin href="./assets/vendor-codemirror-Clv6kvI5.js">
|
|
27
27
|
<link rel="modulepreload" crossorigin href="./assets/vendor-mdxeditor-CtujsSUV.js">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc-viewer",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.1",
|
|
4
4
|
"description": "Claude Code logging, visualization, and management toolkit — launch a web viewer alongside Claude Code with full request/response tracing, proxy, and mobile support",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "server.js",
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// 内置模型提示词层:让 packages/app/server/system-prompt-templates/presets/ 的 6 个预设
|
|
2
|
+
// 成为「默认生效」的模型 system prompt——spawn 时模型匹配且用户无对应文件(workspace >
|
|
3
|
+
// global 两级用户文件优先)时自动注入;用户可通过墓碑文件禁用某个内置条目。
|
|
4
|
+
//
|
|
5
|
+
// 数据源复用 system-prompt-presets.js 的 listSystemPromptPresets()(manifest 读取 +
|
|
6
|
+
// renderPresetTemplate 边界剥离都已在里面,且注释承诺不触发 createSystemPromptVariables/
|
|
7
|
+
// git 子进程)。本模块函数对非法入参会 throw(setBuiltinDisabled/materializeBuiltinPrompt),
|
|
8
|
+
// spawn 注入链路的安全由调用点整层 try-catch 保证(system-prompt-files.js 失败回落
|
|
9
|
+
// sentinel,注入链路永不 throw)。
|
|
10
|
+
//
|
|
11
|
+
// Built-in model prompt layer: the shipped presets act as default-effective model
|
|
12
|
+
// system prompts. User files (workspace > global) always win; a per-scope tombstone
|
|
13
|
+
// file disables individual built-in entries.
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
16
|
+
import { tmpdir } from 'node:os';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { expandModelIdVariants, normalizeModelName } from './model-system-prompts.js';
|
|
19
|
+
import { listSystemPromptPresets } from './system-prompt-presets.js';
|
|
20
|
+
import { renameSyncWithRetry } from './file-api.js';
|
|
21
|
+
import { reportSwallowed } from '@ccv/core/error-report';
|
|
22
|
+
|
|
23
|
+
// 墓碑文件名:放在对应 scope 的 modelPromptDir 里,内容是规范大写名的 JSON 数组。
|
|
24
|
+
// parseModelPromptFileName 对非 *_SYSTEM.md 文件名返回 null,天然不干扰条目列表。
|
|
25
|
+
// Tombstone file inside a scope's model-prompt dir: a JSON array of canonical names.
|
|
26
|
+
export const BUILTIN_DISABLED_FILE = '.builtin-disabled.json';
|
|
27
|
+
|
|
28
|
+
// 物化目录:preset 文本(边界已剥离、${...} 保持字面量,spawn 渲染管线再替换变量)
|
|
29
|
+
// 写成内容寻址的临时文件,供 --system-prompt-file 注入(文件对形式是快照钉扎的前提)。
|
|
30
|
+
// 惰性读取 env 覆盖(测试用):node --test 多进程并行时共享目录会被彼此的 GC 竞态误删。
|
|
31
|
+
// Materialized temp dir for boundary-stripped preset texts (content-addressed).
|
|
32
|
+
const materializeDir = () => process.env.CCV_BUILTIN_PROMPT_MATERIALIZE_DIR || join(tmpdir(), 'cc-viewer-builtin-prompts');
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 列出全部内置模型条目。name = manifest match 的大写规范化(与用户条目同名体系,
|
|
36
|
+
* 用户同名文件天然形成覆盖);text = renderPresetTemplate 输出(无边界标记)。
|
|
37
|
+
* List all built-in model entries derived from the preset manifest.
|
|
38
|
+
*
|
|
39
|
+
* @returns {Array<{ id: string, title: string, name: string, mode: 'override'|'append', matchLower: string, text: string }>}
|
|
40
|
+
*/
|
|
41
|
+
export function listBuiltinModelPrompts() {
|
|
42
|
+
let presets;
|
|
43
|
+
try {
|
|
44
|
+
presets = listSystemPromptPresets();
|
|
45
|
+
} catch {
|
|
46
|
+
return []; // manifest 损坏等:内置层整体缺席,调用方回落 sentinel
|
|
47
|
+
}
|
|
48
|
+
const out = [];
|
|
49
|
+
for (const p of presets) {
|
|
50
|
+
const name = normalizeModelName(typeof p?.match === 'string' ? p.match : '');
|
|
51
|
+
if (!name || typeof p.text !== 'string' || p.text.trim().length === 0) continue;
|
|
52
|
+
out.push({
|
|
53
|
+
id: p.id,
|
|
54
|
+
title: p.title || p.id,
|
|
55
|
+
name,
|
|
56
|
+
mode: p.defaultMode === 'override' ? 'override' : 'append',
|
|
57
|
+
matchLower: p.match.toLowerCase(),
|
|
58
|
+
text: p.text,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 按 modelId 匹配内置条目:别名展开(expandModelIdVariants,先 lowercase)后任一变体
|
|
66
|
+
* 包含 preset 的 match 即命中;多命中取 match 最长者、等长字典序(对齐 matchModelPrompt
|
|
67
|
+
* 的消歧规则)。
|
|
68
|
+
* Match a model id against built-in entries (alias-expanded substring, longest wins).
|
|
69
|
+
*
|
|
70
|
+
* @param {string|null} modelId
|
|
71
|
+
* @returns {{ id: string, name: string, mode: 'override'|'append', text: string } | null}
|
|
72
|
+
*/
|
|
73
|
+
export function matchBuiltinModelPrompt(modelId) {
|
|
74
|
+
const variants = expandModelIdVariants(modelId);
|
|
75
|
+
if (!variants.length) return null;
|
|
76
|
+
const hits = listBuiltinModelPrompts().filter((e) => variants.some((v) => v.includes(e.matchLower)));
|
|
77
|
+
if (!hits.length) return null;
|
|
78
|
+
hits.sort((a, b) => b.matchLower.length - a.matchLower.length || a.name.localeCompare(b.name));
|
|
79
|
+
const e = hits[0];
|
|
80
|
+
return { id: e.id, name: e.name, mode: e.mode, text: e.text };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 读某 scope 目录的墓碑名单。目录/文件缺失 → [](良性静默);文件存在但 JSON 损坏或
|
|
85
|
+
* 形状非法 → console.warn + reportSwallowed 后仍宽容返回 [](fail-open 是刻意的:
|
|
86
|
+
* 墓碑是辅助状态,宁可恢复注入也不因损坏阻断功能——但用户显式 opt-out 被静默逆转
|
|
87
|
+
* 必须有诊断痕迹,三方审查一致要求)。
|
|
88
|
+
* Read a scope dir's tombstone list. Missing dir/file → []; corrupt file → warn +
|
|
89
|
+
* reportSwallowed, still [] (deliberate fail-open, but never silently).
|
|
90
|
+
*
|
|
91
|
+
* @param {string|null|undefined} modelPromptDir
|
|
92
|
+
* @returns {string[]} 规范大写名数组(已排序)
|
|
93
|
+
*/
|
|
94
|
+
export function readBuiltinDisabled(modelPromptDir) {
|
|
95
|
+
if (!modelPromptDir) return [];
|
|
96
|
+
const target = join(modelPromptDir, BUILTIN_DISABLED_FILE);
|
|
97
|
+
if (!existsSync(target)) return [];
|
|
98
|
+
try {
|
|
99
|
+
const raw = readFileSync(target, 'utf-8');
|
|
100
|
+
const parsed = JSON.parse(raw);
|
|
101
|
+
if (!Array.isArray(parsed)) throw new Error('tombstone file is not a JSON array');
|
|
102
|
+
const names = parsed.map((n) => normalizeModelName(typeof n === 'string' ? n : '')).filter(Boolean);
|
|
103
|
+
return [...new Set(names)].sort();
|
|
104
|
+
} catch (err) {
|
|
105
|
+
console.warn(`[CC Viewer] built-in prompt tombstone ${target} unreadable (${err.message}); treating as no disables`);
|
|
106
|
+
reportSwallowed('builtin-model-prompts.readDisabled', err);
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 写墓碑:disabled=true 把 name 加入名单,false 移出。tmp+rename 原子写,数组去重排序。
|
|
113
|
+
* Add/remove a name in a scope dir's tombstone list (atomic tmp+rename write).
|
|
114
|
+
*
|
|
115
|
+
* @param {string} modelPromptDir 目标 scope 的 modelPromptDir(自动 mkdir -p)
|
|
116
|
+
* @param {string} name 条目名(经 normalizeModelName 规范化,非法 throw)
|
|
117
|
+
* @param {boolean} disabled
|
|
118
|
+
* @returns {{ name: string, disabled: boolean, list: string[] }}
|
|
119
|
+
*/
|
|
120
|
+
export function setBuiltinDisabled(modelPromptDir, name, disabled) {
|
|
121
|
+
if (!modelPromptDir) throw new Error('no target directory');
|
|
122
|
+
const canonical = normalizeModelName(name);
|
|
123
|
+
if (!canonical) throw new Error('invalid model prompt name');
|
|
124
|
+
const list = readBuiltinDisabled(modelPromptDir);
|
|
125
|
+
const next = disabled ? [...new Set([...list, canonical])].sort() : list.filter((n) => n !== canonical);
|
|
126
|
+
mkdirSync(modelPromptDir, { recursive: true });
|
|
127
|
+
const target = join(modelPromptDir, BUILTIN_DISABLED_FILE);
|
|
128
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
129
|
+
writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`, 'utf-8');
|
|
130
|
+
renameSyncWithRetry(tmp, target); // Windows 杀软/索引瞬时 EPERM 重试(与仓库写路径一致)
|
|
131
|
+
return { name: canonical, disabled: !!disabled, list: next };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 多 scope 合成判断:任一目录的墓碑名单含 name 即视为禁用(workspace 墓碑禁本工作区、
|
|
136
|
+
* global 墓碑全局禁用;对单次启动而言两者都是「该禁」)。
|
|
137
|
+
* Combine tombstones across scopes: disabled when ANY dir's list contains the name.
|
|
138
|
+
*
|
|
139
|
+
* @param {string} name 规范大写名
|
|
140
|
+
* @param {...(string|null|undefined)} modelPromptDirs
|
|
141
|
+
* @returns {boolean}
|
|
142
|
+
*/
|
|
143
|
+
export function isBuiltinDisabled(name, ...modelPromptDirs) {
|
|
144
|
+
const canonical = normalizeModelName(name);
|
|
145
|
+
if (!canonical) return false;
|
|
146
|
+
return modelPromptDirs.some((dir) => readBuiltinDisabled(dir).includes(canonical));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 把内置条目文本物化为内容寻址的临时文件(不存在才写,避并发截断;顺手清同 id
|
|
151
|
+
* 旧 hash 文件)。返回文件路径。
|
|
152
|
+
* Materialize a built-in entry's text into a content-addressed temp file.
|
|
153
|
+
*
|
|
154
|
+
* @param {string} id preset id(文件名安全字符校验)
|
|
155
|
+
* @param {string} text renderPresetTemplate 输出(边界已剥离、${...} 字面量保留)
|
|
156
|
+
* @returns {string} 物化文件绝对路径
|
|
157
|
+
*/
|
|
158
|
+
export function materializeBuiltinPrompt(id, text) {
|
|
159
|
+
if (typeof id !== 'string' || !/^[A-Za-z0-9._-]+$/.test(id)) throw new Error('invalid preset id');
|
|
160
|
+
if (typeof text !== 'string' || text.trim().length === 0) throw new Error('empty preset text');
|
|
161
|
+
const hash = createHash('sha256').update(text).digest('hex').slice(0, 8);
|
|
162
|
+
const fileName = `${id}-${hash}.md`;
|
|
163
|
+
const dir = materializeDir();
|
|
164
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 }); // 仅属主可读写:Linux 多用户 /tmp 下防跨用户预植
|
|
165
|
+
const target = join(dir, fileName);
|
|
166
|
+
// write-if-absent 必须回读校验内容 hash:可预测路径 + 预置恶意文件(hash 可算,
|
|
167
|
+
// preset 文本随包公开)会把注入内容掉包——不一致则覆盖。写入用 tmp+rename 原子落盘,
|
|
168
|
+
// 进程在写中途崩溃留下截断文件时 write-if-absent 永不愈合。
|
|
169
|
+
const writeAtomic = () => {
|
|
170
|
+
const tmp = `${target}.${process.pid}.tmp`;
|
|
171
|
+
writeFileSync(tmp, text, 'utf-8');
|
|
172
|
+
renameSyncWithRetry(tmp, target);
|
|
173
|
+
};
|
|
174
|
+
if (!existsSync(target)) {
|
|
175
|
+
writeAtomic();
|
|
176
|
+
} else {
|
|
177
|
+
try {
|
|
178
|
+
const existing = readFileSync(target, 'utf-8');
|
|
179
|
+
if (createHash('sha256').update(existing).digest('hex').slice(0, 8) !== hash) writeAtomic();
|
|
180
|
+
} catch {
|
|
181
|
+
writeAtomic(); // 读失败(截断/权限)→ 覆盖重写
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// best-effort GC:同 id 的旧 hash 文件(版本升级后残留)。精确匹配 8 位 hex hash 防
|
|
185
|
+
// id 前缀误删(如未来 kimi-k3-turbo);只清 mtime 足够旧的文件,避免跨版本并发
|
|
186
|
+
// (新旧实例同跑)时删掉旧实例正要读的文件。失败无碍。
|
|
187
|
+
try {
|
|
188
|
+
const idRe = new RegExp(`^${id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}-[0-9a-f]{8}\\.md$`);
|
|
189
|
+
for (const f of readdirSync(dir)) {
|
|
190
|
+
if (!idRe.test(f) || f === fileName) continue;
|
|
191
|
+
const p = join(dir, f);
|
|
192
|
+
try {
|
|
193
|
+
if (Date.now() - statSync(p).mtimeMs > 10 * 60_000) rmSync(p, { force: true });
|
|
194
|
+
} catch { /* ignore */ }
|
|
195
|
+
}
|
|
196
|
+
} catch { /* ignore */ }
|
|
197
|
+
return target;
|
|
198
|
+
}
|
|
@@ -108,6 +108,10 @@ export function suppressManuallyFlaggedPinned(entries, userArgs) {
|
|
|
108
108
|
// The F2 no-record notice only matters to users who HAVE injection configured right
|
|
109
109
|
// now (sentinel files or a model-prompt dir) — for everyone else a resume silently
|
|
110
110
|
// injecting nothing is exactly the status quo, and the line would be pure noise.
|
|
111
|
+
// Fidelity note (accepted gap): this check does NOT see the built-in preset layer —
|
|
112
|
+
// users whose only injection is a default-effective built-in (no dirs, no sentinels)
|
|
113
|
+
// get no F2 notice on resume either. Deliberate: making built-in hits count would turn
|
|
114
|
+
// the notice into near-constant noise for every built-in user resuming old sessions.
|
|
111
115
|
export function injectionConfigured(spawnDir, logDir = LOG_DIR) {
|
|
112
116
|
try {
|
|
113
117
|
return isNonEmptyFile(join(spawnDir, SYSTEM_PROMPT_FILE))
|
|
@@ -152,7 +156,7 @@ function _defaultModelReader(spawnDir, env, opts) {
|
|
|
152
156
|
* sysPrompt: {args: string[], loaded: string[], model: string|null, entries: object[], suppressed?: string, pinned?: boolean, noRecord?: boolean, noRecordNotice?: boolean},
|
|
153
157
|
* resume: object|null,
|
|
154
158
|
* resolvedModelId: string|null,
|
|
155
|
-
* diagnostic: null|'no-match'|'no-model',
|
|
159
|
+
* diagnostic: null|'no-match'|'no-model'|'builtin-disabled',
|
|
156
160
|
* }}
|
|
157
161
|
*/
|
|
158
162
|
export function resolveLaunchSystemPrompt(p) {
|
|
@@ -222,6 +226,10 @@ export function resolveLaunchSystemPrompt(p) {
|
|
|
222
226
|
});
|
|
223
227
|
if (suppressInjection) {
|
|
224
228
|
sysPrompt = { args: [], loaded: [], model: null, entries: [] };
|
|
229
|
+
} else if (sysPrompt.builtinDisabled) {
|
|
230
|
+
// The resolved model hit a built-in preset that the user tombstone-disabled —
|
|
231
|
+
// distinct from 'no-match' (a likely misnamed file): this is an intentional opt-out.
|
|
232
|
+
out.diagnostic = 'builtin-disabled';
|
|
225
233
|
} else if (resolvedModelId && !sysPrompt.model && !sysPrompt.suppressed
|
|
226
234
|
&& (existsSync(join(spawnDir, MODEL_PROMPT_DIR)) || existsSync(join(logDir, MODEL_PROMPT_DIR)))) {
|
|
227
235
|
// A system_prompt dir is configured but the resolved model matched no entry
|
|
@@ -261,10 +261,26 @@ function modelIdVariants(id) {
|
|
|
261
261
|
return MODEL_ID_ALIASES[id] || [id];
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
// 供内置预设匹配复用的导出包装:入参任意大小写,先剥 `[1m]` 类方括号后缀
|
|
265
|
+
// (spawn 渲染管线对 model.name 做同样的剥离;上游 resolver 正常会剥,这里兜底),
|
|
266
|
+
// 再 toLowerCase 展开别名——避免大写 env(如 K3)或带后缀的裸名(k3[1m])绕过别名表。
|
|
267
|
+
// 单一别名源,勿在别处复制 MODEL_ID_ALIASES。
|
|
268
|
+
// Exported wrapper for the built-in preset matcher: strips a trailing bracket suffix
|
|
269
|
+
// and lowercases before expanding, so uppercase ids (K3) or suffixed shorthands
|
|
270
|
+
// (k3[1m]) cannot bypass the alias table.
|
|
271
|
+
export function expandModelIdVariants(modelId) {
|
|
272
|
+
if (!modelId || typeof modelId !== 'string') return [];
|
|
273
|
+
const stripped = modelId.replace(/\s*\[[^\]]*\]$/, '').toLowerCase();
|
|
274
|
+
return modelIdVariants(stripped);
|
|
275
|
+
}
|
|
276
|
+
|
|
264
277
|
export function matchModelPrompt(modelId, candidates) {
|
|
265
278
|
if (!modelId || typeof modelId !== 'string') return null;
|
|
266
279
|
if (!Array.isArray(candidates)) return null;
|
|
267
|
-
|
|
280
|
+
// 与内置层(builtin-model-prompts.js)同一展开:剥 `[1m]` 类后缀 + lowercase + 别名。
|
|
281
|
+
// 若用户层不剥后缀而内置层剥(k3[1m] + 用户有 KIMI-K3_SYSTEM.md),用户文件会
|
|
282
|
+
// 静默 miss、内置反客为主 —— 违反「用户文件永远优先」,两层必须同语义。
|
|
283
|
+
const variants = expandModelIdVariants(modelId);
|
|
268
284
|
for (const cand of candidates) {
|
|
269
285
|
if (!cand || !cand.dir) continue;
|
|
270
286
|
const hits = listModelPrompts(cand.dir).filter((e) => {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, rmSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { MODEL_PROMPT_DIR, matchModelPrompt } from './model-system-prompts.js';
|
|
4
|
+
import { isBuiltinDisabled, matchBuiltinModelPrompt, materializeBuiltinPrompt } from './builtin-model-prompts.js';
|
|
4
5
|
import { isNonEmptyFile } from './file-api.js';
|
|
6
|
+
import { reportSwallowed } from '@ccv/core/error-report';
|
|
5
7
|
|
|
6
8
|
// isNonEmptyFile lives in file-api.js (shared leaf, cycle break:
|
|
7
9
|
// model-system-prompts.js must not import this module). Re-exported here so
|
|
@@ -42,22 +44,28 @@ export function hasArg(args, ...names) {
|
|
|
42
44
|
*
|
|
43
45
|
* 模型定制(opts.modelId 提供时):先在 <projectDir>/system_prompt/(工作区)与
|
|
44
46
|
* opts.globalModelDir(全局)里做模糊匹配;命中的条目「整体取代」上面两份默认 sentinel
|
|
45
|
-
* ——即便手动 flag 抑制了注入也不再回看默认文件(条目已取而代之)
|
|
46
|
-
*
|
|
47
|
+
* ——即便手动 flag 抑制了注入也不再回看默认文件(条目已取而代之)。未命中则进入内置层:
|
|
48
|
+
* 随包 presets(builtin-model-prompts.js)按同语义再匹配一次,命中且未被墓碑禁用时把
|
|
49
|
+
* preset 文本物化为临时文件注入;命中被禁用(builtinDisabled)或未命中才回落 sentinel。
|
|
47
50
|
*
|
|
48
51
|
* Decide whether to inject system-prompt file flags based on sentinel files in
|
|
49
52
|
* the launch directory. When opts.modelId is given, model-specific entries in
|
|
50
53
|
* the workspace/global system_prompt folders are matched first; a match fully
|
|
51
|
-
* supersedes the Default sentinels.
|
|
54
|
+
* supersedes the Default sentinels. Without a file match, shipped built-in presets
|
|
55
|
+
* act as the fallback layer (tombstone-disabled ones are skipped). Reads fs/env;
|
|
56
|
+
* writes materialized built-in temp files; the built-in layer never throws — any
|
|
57
|
+
* failure falls back to the sentinel logic via reportSwallowed.
|
|
52
58
|
*
|
|
53
59
|
* @param {string} projectDir 启动目录(绝对路径)
|
|
54
60
|
* @param {string[]} [existingArgs] 已有的 claude 参数(用于「手动优先」判断)
|
|
55
61
|
* @param {Object} [env] 环境变量(默认 process.env)
|
|
56
62
|
* @param {{ modelId?: string|null, globalModelDir?: string|null }} [opts]
|
|
57
|
-
* @returns {{ args: string[], loaded: string[], model: string|null, suppressed?: 'env'|'manual-flag'
|
|
63
|
+
* @returns {{ args: string[], loaded: string[], model: string|null, suppressed?: 'env'|'manual-flag',
|
|
64
|
+
* builtinDisabled?: string }}
|
|
58
65
|
* args: 待追加参数;loaded: 实际加载的文件(终端提示);model: 命中的条目名(未命中为 null);
|
|
59
66
|
* suppressed: 注入被有意跳过的原因(env 开关 / 手动同义 flag 抑制了已命中的模型条目)——
|
|
60
67
|
* 调用方(pty-manager)据此不再打「no matching entry」误导性告警。
|
|
68
|
+
* builtinDisabled 仅在内置层命中但被墓碑禁用时出现(增量字段,否则不存在)。
|
|
61
69
|
*/
|
|
62
70
|
export function buildSystemPromptFileArgs(projectDir, existingArgs = [], env = process.env, opts = {}) {
|
|
63
71
|
const out = { args: [], loaded: [], model: null };
|
|
@@ -81,8 +89,37 @@ export function buildSystemPromptFileArgs(projectDir, existingArgs = [], env = p
|
|
|
81
89
|
}
|
|
82
90
|
return out; // 命中即返回:默认 sentinel 不再参与(含手动 flag 抑制注入的情况)。
|
|
83
91
|
}
|
|
92
|
+
// 用户文件未命中 → 内置层(随包 presets):同语义匹配 + 墓碑检查;命中即物化注入并
|
|
93
|
+
// 提前返回(同样取代 sentinel)。整层 try-catch:任何失败(preset 缺失/tmp 不可写)经
|
|
94
|
+
// reportSwallowed 回落 sentinel——内置不可用绝不应连默认注入都拖垮。
|
|
95
|
+
// No file match → built-in fallback layer (shipped presets, tombstone-aware).
|
|
96
|
+
try {
|
|
97
|
+
const builtin = matchBuiltinModelPrompt(opts.modelId);
|
|
98
|
+
if (builtin) {
|
|
99
|
+
const flagPair = builtin.mode === 'override'
|
|
100
|
+
? ['--system-prompt', '--system-prompt-file']
|
|
101
|
+
: ['--append-system-prompt', '--append-system-prompt-file'];
|
|
102
|
+
if (hasArg(existingArgs, ...flagPair)) {
|
|
103
|
+
out.suppressed = 'manual-flag'; // 与文件命中同语义:有意跳过,非「无条目」
|
|
104
|
+
return out; // 手动抑制也提前返回(不物化、不回看 sentinel)
|
|
105
|
+
}
|
|
106
|
+
const workspaceModelDir = join(projectDir, MODEL_PROMPT_DIR);
|
|
107
|
+
if (isBuiltinDisabled(builtin.name, workspaceModelDir, opts.globalModelDir)) {
|
|
108
|
+
out.builtinDisabled = builtin.name; // 供 launch-config 区分「被禁用」与「无条目」
|
|
109
|
+
// 落回 sentinel(不 return)
|
|
110
|
+
} else {
|
|
111
|
+
const path = materializeBuiltinPrompt(builtin.id, builtin.text);
|
|
112
|
+
out.args.push(flagPair[1], path);
|
|
113
|
+
out.loaded.push(`builtin:${builtin.name}`); // 稳定标签,不打印 tmp 丑路径
|
|
114
|
+
out.model = builtin.name;
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch (err) {
|
|
119
|
+
reportSwallowed('system-prompt-files.builtin', err);
|
|
120
|
+
}
|
|
84
121
|
// No matching entry: fall through to the default sentinels. Diagnostics for
|
|
85
|
-
// this case live in the caller (pty-manager)
|
|
122
|
+
// this case live in the caller (launch-config/pty-manager).
|
|
86
123
|
}
|
|
87
124
|
|
|
88
125
|
// 整段替换:CC_SYSTEM.md → --system-prompt-file (用户已传 --system-prompt[-file] 则跳过)
|
package/server/pty-manager.js
CHANGED
|
@@ -390,8 +390,9 @@ async function _spawnClaudeImpl(proxyPort, cwd, extraArgs = [], claudePath = nul
|
|
|
390
390
|
// When the launch dir has a non-empty CC_SYSTEM.md / CC_APPEND_SYSTEM.md, auto-append
|
|
391
391
|
// --system-prompt-file / --append-system-prompt-file (each independent; skipped if the
|
|
392
392
|
// user already passed the synonymous flag). Model customization: fuzzy-match against
|
|
393
|
-
// <cwd>/system_prompt/ and <LOG_DIR>/system_prompt/ using
|
|
394
|
-
//
|
|
393
|
+
// <cwd>/system_prompt/ and <LOG_DIR>/system_prompt/ using the model id resolved from the
|
|
394
|
+
// ACTIVE configuration (proxy profile mapping > env > settings.json); a matched entry
|
|
395
|
+
// (user file first, then built-in presets) wholly replaces the two default sentinels above.
|
|
395
396
|
// Note: currentWorkspacePath is only assigned below, so the cwd param decides the launch
|
|
396
397
|
// dir here. Spawns inside LOG_DIR (IM worker working dir = <LOG_DIR>/IM_<id>/) skip model
|
|
397
398
|
// matching: the IM persona relies on the default sentinel CC_APPEND_SYSTEM.md injection,
|
|
@@ -425,7 +426,9 @@ async function _spawnClaudeImpl(proxyPort, cwd, extraArgs = [], claudePath = nul
|
|
|
425
426
|
suppressInjection: _systemPromptFileRejectedPaths.has(claudePath) || skipOnce,
|
|
426
427
|
});
|
|
427
428
|
sysPrompt = r.sysPrompt;
|
|
428
|
-
if (r.diagnostic === '
|
|
429
|
+
if (r.diagnostic === 'builtin-disabled') {
|
|
430
|
+
console.warn(`[CC Viewer] model-specific prompt: built-in prompt "${r.sysPrompt.builtinDisabled}" for modelId="${r.resolvedModelId}" is disabled via .builtin-disabled.json in the workspace or global ${MODEL_PROMPT_DIR}/ — falling back to defaults`);
|
|
431
|
+
} else if (r.diagnostic === 'no-match') {
|
|
429
432
|
console.warn(`[CC Viewer] model-specific prompt: modelId="${r.resolvedModelId}" resolved from active config but no matching entry found in workspace or global ${MODEL_PROMPT_DIR}/`);
|
|
430
433
|
} else if (r.diagnostic === 'no-model') {
|
|
431
434
|
console.warn(`[CC Viewer] model-specific prompt: no model id resolved from active config (--settings / env / settings.json / proxy profile) — entries in ${MODEL_PROMPT_DIR}/ skipped for this launch`);
|
package/server/routes/expert.js
CHANGED
|
@@ -15,6 +15,11 @@ import {
|
|
|
15
15
|
MODEL_PROMPT_DIR, normalizeModelName, listModelPrompts,
|
|
16
16
|
writeModelPrompt, deleteModelPrompt, matchModelPrompt,
|
|
17
17
|
} from '../lib/model-system-prompts.js';
|
|
18
|
+
import {
|
|
19
|
+
matchBuiltinModelPrompt, isBuiltinDisabled,
|
|
20
|
+
readBuiltinDisabled, setBuiltinDisabled, listBuiltinModelPrompts,
|
|
21
|
+
} from '../lib/builtin-model-prompts.js';
|
|
22
|
+
import { reportSwallowed } from '@ccv/core/error-report';
|
|
18
23
|
import { resolveSpawnModel } from '../lib/spawn-model-resolver.js';
|
|
19
24
|
import { listSystemPromptPresets, groupPresetsByCategory, getSystemPromptVariablesDoc } from '../lib/system-prompt-presets.js';
|
|
20
25
|
import { LOG_DIR } from '../../findcc.js';
|
|
@@ -41,12 +46,24 @@ function sendJson(res, code, obj) {
|
|
|
41
46
|
} catch { /* socket 已关闭:忽略 */ }
|
|
42
47
|
}
|
|
43
48
|
|
|
49
|
+
// scope → 目标 modelPromptDir 的统一解析(postModelPrompts 普通分支与墓碑分支共用):
|
|
50
|
+
// global 直取 LOG_DIR;workspace 需活动工作区(缺失返回 {error} 由调用方回 400)。
|
|
51
|
+
async function resolveScopeModelDir(deps, scope) {
|
|
52
|
+
if (scope === 'global') return { dir: join(LOG_DIR, MODEL_PROMPT_DIR) };
|
|
53
|
+
const dir = await resolveDir(deps);
|
|
54
|
+
if (!dir) return { error: 'no_active_workspace' };
|
|
55
|
+
return { dir: join(dir, MODEL_PROMPT_DIR) };
|
|
56
|
+
}
|
|
57
|
+
|
|
44
58
|
// Single source for "is a custom system prompt configured to inject" — mirrors the
|
|
45
59
|
// spawn-time injection semantics of buildSystemPromptFileArgs: the env kill switch wins;
|
|
46
60
|
// a matched model entry supersedes the Default sentinels for activation purposes.
|
|
47
61
|
// Fidelity note: spawn-only gates this helper cannot see (insideLogDir skip, manual
|
|
48
62
|
// --system-prompt-file flags, one-shot skip tokens) may rarely make "active" a false
|
|
49
|
-
// positive — acceptable for a UI hint.
|
|
63
|
+
// positive — acceptable for a UI hint. The built-in layer shares the same gates, and
|
|
64
|
+
// can also false-NEGATIVE the other way: when the spawn-time model signal comes from
|
|
65
|
+
// launchSettings (launcher-delivered ANTHROPIC_MODEL), this resolver may see no model
|
|
66
|
+
// while spawn still injects a built-in preset (entry stays hidden). Same acceptance.
|
|
50
67
|
function computeSystemPromptStatus(dir) {
|
|
51
68
|
if (process.env[DISABLE_AUTO_SYSTEM_PROMPT_ENV] === '1') {
|
|
52
69
|
return { active: false, modelId: null, matched: null, defaultActive: false };
|
|
@@ -61,8 +78,28 @@ function computeSystemPromptStatus(dir) {
|
|
|
61
78
|
{ dir: join(LOG_DIR, MODEL_PROMPT_DIR), scope: 'global' },
|
|
62
79
|
])
|
|
63
80
|
: null;
|
|
64
|
-
|
|
65
|
-
|
|
81
|
+
let matched = match ? { scope: match.scope, name: match.name, mode: match.mode } : null;
|
|
82
|
+
// 内置层镜像 spawn 语义:用户文件未命中时,内置 preset 命中(未禁用)同样构成注入;
|
|
83
|
+
// 命中被墓碑禁用则报 builtinDisabled(条件字段,仅此时出现,命名与 spawn 返回字段
|
|
84
|
+
// 对齐)——UI 据此让入口以「已禁用」态保活,点入可重启用,否则 chip 消失后无法找回禁用开关。
|
|
85
|
+
let builtinDisabledEntry;
|
|
86
|
+
if (!matched && modelId) {
|
|
87
|
+
try {
|
|
88
|
+
const builtin = matchBuiltinModelPrompt(modelId);
|
|
89
|
+
if (builtin) {
|
|
90
|
+
if (isBuiltinDisabled(builtin.name, dir ? join(dir, MODEL_PROMPT_DIR) : null, join(LOG_DIR, MODEL_PROMPT_DIR))) {
|
|
91
|
+
builtinDisabledEntry = { name: builtin.name };
|
|
92
|
+
} else {
|
|
93
|
+
matched = { scope: 'builtin', name: builtin.name, mode: builtin.mode };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} catch (err) {
|
|
97
|
+
reportSwallowed('expert.systemPromptStatus.builtin', err);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const out = { active: !!matched || defaultActive, modelId, matched, defaultActive };
|
|
101
|
+
if (builtinDisabledEntry) out.builtinDisabled = builtinDisabledEntry;
|
|
102
|
+
return out;
|
|
66
103
|
}
|
|
67
104
|
|
|
68
105
|
async function getSystemText(req, res, parsedUrl, isLocal, deps) {
|
|
@@ -124,12 +161,26 @@ async function getModelPrompts(req, res, parsedUrl, isLocal, deps) {
|
|
|
124
161
|
const dir = await resolveDir(deps);
|
|
125
162
|
const globalDir = join(LOG_DIR, MODEL_PROMPT_DIR);
|
|
126
163
|
const status = computeSystemPromptStatus(dir);
|
|
164
|
+
// 内置 preset 条目(默认生效层):text 为 renderPresetTemplate 输出(边界已剥离),
|
|
165
|
+
// disabled 双 scope 墓碑标志供弹窗渲染禁用态/重启用。
|
|
166
|
+
const workspaceModelDir = dir ? join(dir, MODEL_PROMPT_DIR) : null;
|
|
167
|
+
const disabledWs = readBuiltinDisabled(workspaceModelDir);
|
|
168
|
+
const disabledG = readBuiltinDisabled(globalDir);
|
|
169
|
+
const builtin = listBuiltinModelPrompts().map((e) => ({
|
|
170
|
+
id: e.id,
|
|
171
|
+
title: e.title,
|
|
172
|
+
name: e.name,
|
|
173
|
+
mode: e.mode,
|
|
174
|
+
text: e.text,
|
|
175
|
+
disabled: { workspace: disabledWs.includes(e.name), global: disabledG.includes(e.name) },
|
|
176
|
+
}));
|
|
127
177
|
sendJson(res, 200, {
|
|
128
178
|
workspaceDir: dir || null,
|
|
129
179
|
workspaceActive: !!dir,
|
|
130
180
|
globalDir,
|
|
131
181
|
workspace: dir ? collectModelEntries(join(dir, MODEL_PROMPT_DIR)) : [],
|
|
132
182
|
global: collectModelEntries(globalDir),
|
|
183
|
+
builtin,
|
|
133
184
|
// 当前生效配置解析出的模型 id 及其命中的条目(未命中为 null)——弹窗据此把默认页签
|
|
134
185
|
// 指向命中条目;matched.name 为规范化大写名,与页签 key 的构成一致。
|
|
135
186
|
modelId: status.modelId,
|
|
@@ -162,29 +213,39 @@ function postModelPrompts(req, res, parsedUrl, isLocal, deps) {
|
|
|
162
213
|
req.on('end', async () => {
|
|
163
214
|
if (truncated) return; // 超限已 destroy,socket 关闭,勿再解析/回包(对齐 postSystemText)
|
|
164
215
|
try {
|
|
165
|
-
const { scope, name, mode, text } = JSON.parse(body || '{}');
|
|
216
|
+
const { scope, name, mode, text, action } = JSON.parse(body || '{}');
|
|
166
217
|
if (scope !== 'workspace' && scope !== 'global') {
|
|
167
218
|
sendJson(res, 400, { error: 'bad_scope' });
|
|
168
219
|
return;
|
|
169
220
|
}
|
|
170
221
|
const canonical = normalizeModelName(name);
|
|
171
222
|
if (!canonical) { sendJson(res, 400, { error: 'bad_model_name' }); return; }
|
|
172
|
-
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
if (
|
|
178
|
-
|
|
223
|
+
const target = await resolveScopeModelDir(deps, scope);
|
|
224
|
+
if (target.error) { sendJson(res, 400, { error: target.error }); return; }
|
|
225
|
+
// 墓碑操作(禁用/启用内置 preset 条目)。action 存在但非法 → 400 兜底:
|
|
226
|
+
// 拼错的 action 绝不可 fall-through 到下面的「空 text = 删除用户条目」分支。
|
|
227
|
+
if (action !== undefined) {
|
|
228
|
+
if (action !== 'disable-builtin' && action !== 'enable-builtin') {
|
|
229
|
+
sendJson(res, 400, { error: 'bad_action' });
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (!listBuiltinModelPrompts().some((e) => e.name === canonical)) {
|
|
233
|
+
sendJson(res, 400, { error: 'unknown_builtin' });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const disabled = action === 'disable-builtin';
|
|
237
|
+
setBuiltinDisabled(target.dir, canonical, disabled);
|
|
238
|
+
sendJson(res, 200, { ok: true, scope, name: canonical, disabled });
|
|
239
|
+
return;
|
|
179
240
|
}
|
|
180
241
|
const raw = typeof text === 'string' ? text : '';
|
|
181
242
|
if (raw.trim().length === 0) {
|
|
182
243
|
// 空文本 = 删除条目(对齐 system-text 的「存空即禁用」约定;此时 mode 可缺省)。
|
|
183
|
-
deleteModelPrompt(
|
|
244
|
+
deleteModelPrompt(target.dir, canonical);
|
|
184
245
|
sendJson(res, 200, { ok: true, name: canonical, scope, cleared: true });
|
|
185
246
|
return;
|
|
186
247
|
}
|
|
187
|
-
const result = writeModelPrompt(
|
|
248
|
+
const result = writeModelPrompt(target.dir, canonical, mode === 'override' ? 'override' : 'append', raw);
|
|
188
249
|
sendJson(res, 200, { ok: true, scope, ...result });
|
|
189
250
|
} catch (e) {
|
|
190
251
|
console.error('[CC Viewer] expert model-prompts POST failed:', e.message);
|
package/ultraAgents/README.md
CHANGED
|
@@ -48,6 +48,8 @@ in `src/utils/ultraplanTemplates.js`). Preset `content` should therefore be writ
|
|
|
48
48
|
> `src/utils/ultraplanTemplates.js`'s `ULTRAPLAN_VARIANTS.codeExpert` / `researchExpert`,
|
|
49
49
|
> and is pinned byte-for-byte by `test/ultra-agents-api.test.js` — to change the body, edit that
|
|
50
50
|
> source file and regenerate the JSON in this directory; do not hand-write a second copy here.
|
|
51
|
+
> Other presets (e.g. `test-analysis-expert`) are authored standalone in this directory and have
|
|
52
|
+
> no template source.
|
|
51
53
|
|
|
52
54
|
## Validation and Limits
|
|
53
55
|
|
|
@@ -66,6 +68,7 @@ Each file undergoes defensive validation at load time. Invalid files are skipped
|
|
|
66
68
|
| --- | --- | --- | --- |
|
|
67
69
|
| `code-expert.json` | Code Expert / 代码专家 | Inline localized (all 18 languages) | `ULTRAPLAN_VARIANTS.codeExpert` |
|
|
68
70
|
| `research-expert.json` | Research Expert / 调研专家 | Inline localized (all 18 languages) | `ULTRAPLAN_VARIANTS.researchExpert` |
|
|
71
|
+
| `test-analysis-expert.json` | Test Analysis Expert / 测分专家 | Inline localized (all 18 languages) | standalone (authored here; UI-only Midscene.js YAML test-case generation, generation-only) |
|
|
69
72
|
|
|
70
73
|
To add a new preset: drop a new `*.json` file in this directory (file name should match `id`),
|
|
71
74
|
and restart/refresh to see it in the modal.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "test-analysis-expert",
|
|
3
|
+
"version": 1,
|
|
4
|
+
"title": {
|
|
5
|
+
"zh": "测分专家",
|
|
6
|
+
"en": "Test Analysis Expert",
|
|
7
|
+
"zh-TW": "測試分析專家",
|
|
8
|
+
"ko": "테스트 분석 전문가",
|
|
9
|
+
"ja": "テスト分析エキスパート",
|
|
10
|
+
"de": "Testanalyse-Experte",
|
|
11
|
+
"es": "Experto en análisis de pruebas",
|
|
12
|
+
"fr": "Expert en analyse de tests",
|
|
13
|
+
"it": "Esperto di analisi dei test",
|
|
14
|
+
"da": "Testanalyseekspert",
|
|
15
|
+
"pl": "Ekspert analizy testów",
|
|
16
|
+
"ru": "Эксперт по анализу тестирования",
|
|
17
|
+
"ar": "خبير تحليل الاختبارات",
|
|
18
|
+
"no": "Testanalyseekspert",
|
|
19
|
+
"pt-BR": "Especialista em análise de testes",
|
|
20
|
+
"th": "ผู้เชี่ยวชาญการวิเคราะห์การทดสอบ",
|
|
21
|
+
"tr": "Test analiz uzmanı",
|
|
22
|
+
"uk": "Експерт з аналізу тестування"
|
|
23
|
+
},
|
|
24
|
+
"description": {
|
|
25
|
+
"zh": "测试分析专家:多智能体素材分析 + Midscene YAML 用例生成,产出 UI 测试分析报告与自动化用例,只生成不执行。",
|
|
26
|
+
"en": "Test analysis expert: multi-agent materials analysis and Midscene.js YAML flow generation — produces a UI test-analysis report and automation cases; generation only, no execution.",
|
|
27
|
+
"zh-TW": "測試分析專家:多智能體素材分析 + Midscene YAML 用例產生,產出 UI 測試分析報告與自動化用例,只產生不執行。",
|
|
28
|
+
"ko": "테스트 분석 전문가: 멀티 에이전트 자료 분석 + Midscene YAML 케이스 생성 — UI 테스트 분석 보고서와 자동화 케이스를 산출하며, 실행 없이 생성만 합니다.",
|
|
29
|
+
"ja": "テスト分析エキスパート:マルチエージェント素材分析 + Midscene YAML フロー生成 — UI テスト分析レポートと自動化ケースを生成、実行は行いません。",
|
|
30
|
+
"de": "Testanalyse-Experte: Multi-Agenten-Materialanalyse und Midscene-YAML-Flow-Generierung – erstellt einen UI-Testanalysebericht und Automatisierungsfälle; nur Generierung, keine Ausführung.",
|
|
31
|
+
"es": "Experto en análisis de pruebas: análisis de materiales multiagente y generación de flujos YAML de Midscene.js — produce un informe de análisis de pruebas UI y casos de automatización; solo generación, sin ejecución.",
|
|
32
|
+
"fr": "Expert en analyse de tests : analyse des matériaux multi-agents et génération de flux YAML Midscene.js — produit un rapport d'analyse de tests UI et des cas d'automatisation ; génération uniquement, sans exécution.",
|
|
33
|
+
"it": "Esperto di analisi dei test: analisi dei materiali multi-agente e generazione di flussi YAML Midscene.js — produce un report di analisi dei test UI e casi di automazione; solo generazione, nessuna esecuzione.",
|
|
34
|
+
"da": "Testanalyseekspert: multi-agent-materialeanalyse og Midscene.js YAML-flow-generering — producerer en UI-testanalyserapport og automatiseringscases; kun generering, ingen eksekvering.",
|
|
35
|
+
"pl": "Ekspert analizy testów: wieloagentowa analiza materiałów i generowanie przepływów YAML Midscene.js — tworzy raport analizy testów UI i przypadki automatyzacji; tylko generowanie, bez wykonywania.",
|
|
36
|
+
"ru": "Эксперт по анализу тестирования: мультиагентный анализ материалов и генерация YAML-потоков Midscene.js — формирует отчёт по анализу UI-тестирования и автоматизированные кейсы; только генерация, без выполнения.",
|
|
37
|
+
"ar": "خبير تحليل الاختبارات: تحليل المواد متعدد الوكلاء وتوليد تدفقات YAML لـ Midscene.js — يُنتج تقرير تحليل اختبارات واجهة المستخدم وحالات أتمتة؛ توليد فقط دون تنفيذ.",
|
|
38
|
+
"no": "Testanalyseekspert: multi-agent materialeanalyse og Midscene.js YAML-flytgenerering — produserer en UI-testanalyserapport og automatiseringscaser; kun generering, ingen kjøring.",
|
|
39
|
+
"pt-BR": "Especialista em análise de testes: análise de materiais multiagente e geração de fluxos YAML do Midscene.js — produz um relatório de análise de testes de UI e casos de automação; apenas geração, sem execução.",
|
|
40
|
+
"th": "ผู้เชี่ยวชาญการวิเคราะห์การทดสอบ: วิเคราะห์เอกสารแบบหลายเอเจนต์และสร้างโฟลว์ YAML ของ Midscene.js — สร้างรายงานการวิเคราะห์การทดสอบ UI และกรณีทดสอบอัตโนมัติ สร้างอย่างเดียวไม่รัน",
|
|
41
|
+
"tr": "Test analiz uzmanı: çok aracılı malzeme analizi ve Midscene.js YAML akışı üretimi — UI test analiz raporu ve otomasyon senaryoları üretir; yalnızca üretim, çalıştırma yok.",
|
|
42
|
+
"uk": "Експерт з аналізу тестування: мультиагентний аналіз матеріалів і генерація YAML-потоків Midscene.js — створює звіт аналізу UI-тестування та кейси автоматизації; лише генерація, без виконання."
|
|
43
|
+
},
|
|
44
|
+
"content": "<system-reminder>\n[SCOPED INSTRUCTION] The following instructions apply only to the next 1–3 interactions. Once the task is complete, these instructions should gradually decrease in priority and no longer affect subsequent interactions. You should be adept at utilizing tools such as `AskUserQuestion`, `EnterPlanMode`, and `Agent`, rather than relying solely on plain text processing. Before execution, you must ensure that the `EnterPlanMode`, `ExitPlanMode`, `TaskCreate`, `TaskUpdate`, `TaskStop`, `TaskGet`, `TaskOutput` and `TaskList` tools are loaded.\n\nPre-requisite: Use `AskUserQuestion` to clarify the target UI scope (modules, pages, key flows), the case-text language, and the output directory whenever the request is ambiguous. Skip only if the intent is unambiguous.\n\nYou are a UI test-analysis expert. Your only deliverables are (a) a markdown test-analysis report (including validation notes) and (b) Midscene.js YAML automation flows (github.com/web-infra-dev/midscene). Hard rules:\n- UI layer only: do not produce API-level or unit-level test points or code.\n- Generation only: NEVER execute the YAML flows, a midscene runner, Playwright, or any browser automation; validation is by reading and cross-checking.\n- Case text inside YAML files follows the case-text language clarified at intake (default: the language of the source materials).\n\nLeverage a multi-agent exploration mechanism to formulate an exceptionally detailed test analysis.\n\nInstructions:\n1. Materials intake — the analysis requires these materials; confirm each given path actually exists and never fabricate contents:\n- REQUIRED: (a) system-analysis docs; (b) requirements docs; (c) the code repository of the system under test; (d) environment context (target URLs, accounts, viewport, midscene model configuration).\n- OPTIONAL: existing test assets (style reference + dedup), prototypes/design drafts or page snapshots, defect/risk history.\n- If any REQUIRED material is missing, use `AskUserQuestion` before continuing (offer: user supplies the path / proceed with assumptions recorded in the report / narrow the scope). Missing optional materials are noted in the report and do not block.\n\n2. Use the `Agent` tool to spawn parallel agents that analyze the materials from different angles:\n- Requirements deconstruction: testable behaviors, roles, preconditions, acceptance criteria; flag ambiguities and conflicts; assign each requirement item a stable requirement ID.\n- System-analysis mapping: pages, flows and state models (state x event x action x result); identify stateful lifecycles.\n- Code grounding: map requirement behaviors to concrete pages/routes/components in the repository; extract the exact visible UI copy/labels as assertion material; flag requirement-vs-code mismatches (when UI copy in code conflicts with the specification, the specification wins and the case is marked as a suspected bug); also flag behaviors found in code but absent from the specification (spec-silent), to be handled per the anti-oracle guardrail.\n- Environment completeness: verify the environment context fully specifies a midscene target (url, viewport, model configuration); list gaps; determine the login strategy per flow group: (a) `cookie` (path to a JSON cookie file) in the environment segment when provided, or (b) a first `task` acting as the login setup flow, or (c) login treated as an out-of-scope precondition recorded in the handoff notes.\n- (Only when test assets exist) Asset inventory: style conventions and a dedup list.\nYou may add other roles or deploy additional agents beyond the ones listed above; the maximum number of concurrently dispatched agents is 5.\n\n3. Synthesize the findings from all agents into a test-analysis report (markdown). The report must include:\n- A traceability matrix: requirement ID to test point(s).\n- The test-point list with fixed fields: ID | requirement ID(s) | page/flow | behavior under test | input/precondition (including entry state: login role, seed data) | expected result (grounded in the requirements) | design technique | priority | YAML file (backfilled in step 4).\n- The design technique for each test point, chosen by these decision rules (not definitions):\n pure input domains to equivalence partitioning + boundary values (template: min-1/min/min+1/max-1/max/max+1);\n multi-condition business rules to decision tables;\n lifecycle state machines (orders, sessions, wizards) to state-transition testing;\n more than 3 parameters that cannot be exhaustively combined to pairwise;\n tight time or broad scope to risk table, top-N first;\n default (display/copy/navigation points matching no technique): scenario-based, at least one happy-path case per requirement item, exception paths on demand.\n- Risk ranking (impact x probability) and a list of known non-goals.\nRe-read the report once against the intake materials, then submit it as the plan via `ExitPlanMode`. Once `ExitPlanMode` returns a result:\n- If approved: proceed to generate the YAML flows in this session.\n- If rejected: revise the report based on the feedback provided and call `ExitPlanMode` again.\n- If an error occurs (including receiving a \"Not in Plan Mode\" message): do **not** follow the suggestions provided in the error message; instead, prompt the user for further instructions.\n\n4. Generate the YAML flows (only after approval):\n- One YAML file per page/flow group; at least one `task` per test point; embed the test-point ID in the task name (e.g. `name: TP-012 login-empty-password`). A test point whose technique yields multiple attempts (BVA's six boundary values, decision-table rows, pairwise combinations) may either run several attempt -> assert -> reset cycles inside ONE task - each cycle re-establishing its own entry state via reload/navigation so no step depends on a previous attempt's end state - or split into suffixed tasks (`TP-012-1` ... `TP-012-n`); the convention used must be stated in the report.\n- File skeleton: an environment segment (`page:` is the recommended key, with `url`, `viewportWidth`/`viewportHeight` - default 1440x800 - plus `userAgent`/`cookie` (path to a JSON cookie file)/`output` only when the environment context requires them; `web:` remains supported as a compatibility entry but is discouraged for new files; `browser:` (multi-tab flows) and the optional `agent:` section (testId, reportFileName) are documented keys, acceptable when the environment context requires them; NEVER the deprecated `target:`; never mix `page`/`browser`/`web`/`target`) followed by `tasks:`. Structural rule: every task's steps live under that task's `flow:` key, and a task may only carry `name`/`continueOnError`/`flow`:\n tasks:\n - name: TP-012 login-empty-password\n flow:\n - aiTap: 'the blue Login button at the top right'\n- Flow granularity: each `-` step is ONE user-visible action in the case-text language; use instant actions (`aiTap`, `aiHover`, `aiInput`+`value`, `aiKeyboardPress`+`keyName`, `aiScroll`) for single-element operations and `ai`/`aiAct` only for multi-step or conditional actions; prompts describe elements visually (appearance + position, e.g. \"the blue Login button at the top right\"), never by DOM/selectors/xpath; YAML string hygiene: wrap every prompt/value/errorMessage string in single quotes - any string containing `: ` (colon+space), `#`, or leading/trailing spaces MUST be quoted or YAML parsing fails; `aiQuery` prompts must state the result format and carry a `name`; every flow ends with at least one `aiAssert` whose prompt states requirement-grounded visible text or state (optionally `errorMessage` carrying the test-point ID); prefer `aiWaitFor` (waits for a condition, default 30000ms, raise `timeout` when needed) for asynchronous UI and use `sleep` only for fixed-duration animation/throttle waits; `aiBoolean` may check a visible state mid-flow; `javascript` only for UI-state preconditions not reachable via `ai*` actions (e.g. seeding login state or localStorage), never for assertions.\n- Every flow is self-contained: it starts from a reachable entry page and never depends on the end state of a previous task or file; record the navigation chain in the test point's precondition.\n- Secrets and accounts are referenced as `${VAR}` placeholders documented in the handoff notes - never hardcode credentials.\n- Create one task per YAML file with `TaskCreate` (subject = relative path) and move it through in_progress to completed with `TaskUpdate` as generation and review fixes land; backfill the report's YAML-file column.\n- **Anti-oracle-compliance guardrail (most important):** expected results come from the requirements/system-analysis docs - NEVER derive an expectation from the implementation's current behavior. Because nothing is ever executed, 'implementation behavior' can only mean what the code-grounding agent read in the repository (UI copy, conditional rendering, validation rules) - there is no other source. When implementation conflicts with the specification, write the flow asserting the SPECIFIED behavior, mark it as a suspected-bug case, and place it in `suspected-bugs/` (isolated from the passing suite); never write a case that enshrines a buggy implementation. When the specification is SILENT about the expected result (no conflict, but no spec either), never derive the expectation from code: omit that particular `aiAssert` (the flow still ends with its spec-grounded assertion) and record the spec gap in the report's gap list.\n- Restating the hard rule: do NOT execute anything - no `midscene` CLI, no runner, no browser automation.\n\n5. Static validation - use the `Agent` tool to spawn 2-3 review agents that examine the deliverables by reading only (never execute):\n- Schema/structure: valid YAML; only documented midscene keys (`page`/`web`/`browser`, `agent`, `tasks`, `name`, `continueOnError`, `flow`, `ai`/`aiAct`, `aiTap`, `aiHover`, `aiInput`+`value`, `aiKeyboardPress`+`keyName`, `aiScroll`, `aiAssert`, `aiQuery`+`name`, `aiBoolean`, `aiWaitFor`, `sleep`, `javascript`, `recordToReport`); every flow step is a `-` array item; no mixed environment segments.\n- Assert locatability: every `aiAssert` targets visible copy/state derivable from the requirements or repository UI text; no selector/xpath assertions; prompts are visual and single-purpose.\n- Anti-oracle compliance: every `aiAssert`'s expected value is traceable to the requirements/system-analysis docs, not to the implementation's current behavior; suspected-bug flows are correctly isolated.\n- Flow self-containment & step reachability: every flow starts from a reachable entry page (the environment `url` or a navigation chain recorded in the test point's precondition); every step's target element is established by an earlier step in the same task - no cross-task/file dependency; every assertion on async content (per the code-grounding findings: fetch/SSE/lazy rendering) is preceded by `aiWaitFor` (explicit timeout) or `sleep`; every `${VAR}` referenced in a flow is documented in run-notes.md; every flow's login strategy (cookie path / first login task / out-of-scope precondition) matches the environment-completeness findings.\n- Traceability & dedup: every test point has at least one flow; every flow links back to a test-point ID (suffixed IDs like `TP-012-1` allowed); no duplicates of existing test assets.\nDistill findings into P0/P1/P2 items; fix P0 (and concrete low-risk P1) and re-review, at most 2 rounds; report anything left unfixed.\n\n6. Deliverables and closing report - write files under `test-analysis/` (confirm the location at intake):\n test-analysis/\n analysis-report.md # test-point matrix, techniques, traceability\n flows/<module>/<page-or-flow>.yaml\n suspected-bugs/*.yaml # isolated spec-vs-implementation cases\n validation-notes.md # static-review record + risk statement\n run-notes.md # offline execution handoff (see below)\nThe closing message must include: requirement x test-point x flow coverage summary; the gap list with reasons; a risk statement (known non-goals, suspected-bug list, visual-assertion uncertainty); and the offline-execution handoff (also written to run-notes.md): how to run (`midscene ./x.yaml`), the required `.env` variables (MIDSCENE_MODEL_NAME / MIDSCENE_MODEL_API_KEY / MIDSCENE_MODEL_BASE_URL / MIDSCENE_MODEL_FAMILY) and the `${VAR}` account placeholders, explicitly stating the flows were never executed.\n</system-reminder>"
|
|
45
|
+
}
|