@wendongfly/myhi 1.3.53 → 1.3.55
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/builtin-skills/e2e.md +14 -0
- package/dist/builtin-skills/pw-install.md +14 -0
- package/dist/builtin-skills/seed.md +21 -0
- package/dist/chat.html +143 -14
- package/dist/index.js +14 -14
- package/dist/index.min.js +3 -2
- package/package.json +1 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# E2E 端到端测试(Playwright)
|
|
2
|
+
|
|
3
|
+
使用 Playwright 对指定功能或页面进行自动化端到端测试。
|
|
4
|
+
|
|
5
|
+
执行步骤:
|
|
6
|
+
1. 确认 Playwright 已安装,若未安装先完成安装(检查 @playwright/test + 浏览器)
|
|
7
|
+
2. 了解测试目标:应用 URL、模块名称、核心用户流程
|
|
8
|
+
3. 分析页面结构(优先用 getByRole / getByLabel / getByText 定位,避免脆弱 CSS 选择器)
|
|
9
|
+
4. 编写测试用例,覆盖:主流程(happy path)+ 关键边界(表单校验、权限拦截、空状态)
|
|
10
|
+
5. 运行测试:npx playwright test --reporter=list
|
|
11
|
+
6. 失败时:查看截图和 trace,分析根因,修复后重跑
|
|
12
|
+
7. 输出测试报告:通过/失败数量 + 覆盖的功能点列表
|
|
13
|
+
|
|
14
|
+
目标模块或说明:$ARGUMENTS
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# 检查并安装 Playwright
|
|
2
|
+
|
|
3
|
+
检测当前项目的 Playwright 安装状态,按需完成安装和配置。
|
|
4
|
+
|
|
5
|
+
执行步骤:
|
|
6
|
+
1. 识别包管理器(检查 pnpm-lock.yaml / yarn.lock / package-lock.json)
|
|
7
|
+
2. 检查 `@playwright/test` 是否已在 devDependencies 中
|
|
8
|
+
3. 若未安装:用检测到的包管理器安装 `@playwright/test`
|
|
9
|
+
4. 检查浏览器:运行 `npx playwright install --dry-run`,若缺少则安装 chromium
|
|
10
|
+
5. 检查是否有 `playwright.config.{ts,js}`,若无则根据项目框架(Vite/Next/Nuxt 等)生成合适的配置
|
|
11
|
+
6. 验证安装:运行一个最简示例测试确认环境正常
|
|
12
|
+
7. 输出可用的测试命令清单
|
|
13
|
+
|
|
14
|
+
$ARGUMENTS
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# 插入拟真测试数据
|
|
2
|
+
|
|
3
|
+
通过 Playwright UI 操作或直接 API 调用,为指定模块批量写入符合真实场景的测试数据。
|
|
4
|
+
|
|
5
|
+
数据生成规范(中国用户习惯):
|
|
6
|
+
- 姓名:常用汉字姓氏 + 名(2-3字,自然感强)
|
|
7
|
+
- 手机:1xx 开头,用 130-139 / 150-159 / 180-189 等真实号段
|
|
8
|
+
- 邮箱:拼音+数字 @ qq.com / 163.com / gmail.com
|
|
9
|
+
- 地址:省市区+街道+门牌,层级完整真实
|
|
10
|
+
- 身份证:区域码正确+真实生日+合法顺序码(仅测试用途)
|
|
11
|
+
- 金额:符合业务量级(几十到几千元,非整数)
|
|
12
|
+
- 日期:最近 1 年内随机,格式与系统保持一致
|
|
13
|
+
|
|
14
|
+
执行步骤:
|
|
15
|
+
1. 分析目标模块的字段要求(查看表单 / API 接口 / 数据库 schema)
|
|
16
|
+
2. 生成 15-20 条完整拟真记录(多样性:不同年龄段、城市、金额区间)
|
|
17
|
+
3. 选择最高效写入方式:API 直调(首选)→ Playwright UI 填表 → 直接 SQL
|
|
18
|
+
4. 批量执行写入,遇错记录并继续(不中断整批)
|
|
19
|
+
5. 验证:查询条数与关键字段,确认数据已成功入库
|
|
20
|
+
|
|
21
|
+
目标模块:$ARGUMENTS
|
package/dist/chat.html
CHANGED
|
@@ -350,6 +350,7 @@
|
|
|
350
350
|
<button class="sk sk-claude" onclick="openGitSheet()" style="color:#3fb950">提交</button>
|
|
351
351
|
<button class="sk sk-claude" onclick="showMemory()">记忆</button>
|
|
352
352
|
<button class="sk sk-claude" onclick="openSkillSheet()" style="color:#9d5cf5">技能</button>
|
|
353
|
+
<span id="sk-builtin-skills"></span>
|
|
353
354
|
<span id="sk-custom-skills"></span>
|
|
354
355
|
</div>
|
|
355
356
|
</div>
|
|
@@ -664,7 +665,12 @@
|
|
|
664
665
|
let outputBuffer = '';
|
|
665
666
|
let outputTimer = null;
|
|
666
667
|
const OUTPUT_DELAY = 600; // 输出合并延迟(ms),越大越少DOM操作
|
|
667
|
-
const MAX_MESSAGES =
|
|
668
|
+
const MAX_MESSAGES = 500; // 聊天区最大消息数;用户主动向上翻历史时临时禁用 trim
|
|
669
|
+
// 历史分页加载(jsonl 是真相源;500 内存上限和 150 DOM 上限都不够长会话回顾)
|
|
670
|
+
let _historyOffset = 0; // 已加载的最老消息距最新的偏移(含初次 agent:history 回放的)
|
|
671
|
+
let _historyHasMore = true; // 后端还有更老消息
|
|
672
|
+
let _historyLoading = false; // 防抖锁
|
|
673
|
+
let _userScrolledUp = false; // 用户已主动往上翻 → 关掉 trim 防被自动删
|
|
668
674
|
|
|
669
675
|
// 命令历史
|
|
670
676
|
const cmdHistory = [];
|
|
@@ -1442,6 +1448,11 @@
|
|
|
1442
1448
|
chatArea.innerHTML = '';
|
|
1443
1449
|
endStream(); endToolGroup(); removeThinking();
|
|
1444
1450
|
|
|
1451
|
+
// 重置历史分页状态:从最新一批开始算偏移
|
|
1452
|
+
_historyOffset = history.length;
|
|
1453
|
+
_historyHasMore = true;
|
|
1454
|
+
_userScrolledUp = false;
|
|
1455
|
+
|
|
1445
1456
|
for (const msg of history) {
|
|
1446
1457
|
endStream(); // 每条历史消息都是独立的,不要流式合并
|
|
1447
1458
|
if (msg.type === 'user' && msg.content) {
|
|
@@ -1489,6 +1500,96 @@
|
|
|
1489
1500
|
scrollToBottom();
|
|
1490
1501
|
});
|
|
1491
1502
|
|
|
1503
|
+
// ── 历史分页:向上滚动到顶部触发加载更老批次 ──
|
|
1504
|
+
async function loadOlderHistory() {
|
|
1505
|
+
if (_historyLoading || !_historyHasMore) return;
|
|
1506
|
+
_historyLoading = true;
|
|
1507
|
+
// 顶部插入"加载中"提示
|
|
1508
|
+
const indicator = document.createElement('div');
|
|
1509
|
+
indicator.className = 'msg msg-status';
|
|
1510
|
+
indicator.id = '_hist_loading_indicator';
|
|
1511
|
+
indicator.textContent = '— 加载更早的历史… —';
|
|
1512
|
+
chatArea.insertBefore(indicator, chatArea.firstChild);
|
|
1513
|
+
// 记录滚动锚点(保持视觉位置)
|
|
1514
|
+
const prevScrollHeight = chatArea.scrollHeight;
|
|
1515
|
+
const prevScrollTop = chatArea.scrollTop;
|
|
1516
|
+
try {
|
|
1517
|
+
const r = await fetch(`/api/agent/history/${SESSION_ID}?offset=${_historyOffset}&limit=50`);
|
|
1518
|
+
const d = await r.json();
|
|
1519
|
+
indicator.remove();
|
|
1520
|
+
if (!d.ok || !d.messages?.length) {
|
|
1521
|
+
_historyHasMore = false;
|
|
1522
|
+
const end = document.createElement('div');
|
|
1523
|
+
end.className = 'msg msg-status';
|
|
1524
|
+
end.textContent = '— 已到最早 —';
|
|
1525
|
+
chatArea.insertBefore(end, chatArea.firstChild);
|
|
1526
|
+
return;
|
|
1527
|
+
}
|
|
1528
|
+
// 准备 fragment,按 history 渲染逻辑创建 DOM,然后整体 prepend
|
|
1529
|
+
const frag = document.createDocumentFragment();
|
|
1530
|
+
for (const msg of d.messages) {
|
|
1531
|
+
if (msg.type === 'user' && msg.content) {
|
|
1532
|
+
const el = document.createElement('div');
|
|
1533
|
+
el.className = 'msg msg-input';
|
|
1534
|
+
el.innerHTML = `<div><div class="bubble">${escHtml(msg.content)}</div><div class="meta">${msg.timestamp ? new Date(msg.timestamp).toLocaleString('zh-CN') : ''}</div></div>`;
|
|
1535
|
+
frag.appendChild(el);
|
|
1536
|
+
} else if (msg.type === 'assistant' && msg.message?.content) {
|
|
1537
|
+
const texts = [], tools = [];
|
|
1538
|
+
for (const block of msg.message.content) {
|
|
1539
|
+
if (block.type === 'text' && block.text) texts.push(block.text);
|
|
1540
|
+
else if (block.type === 'tool_use') tools.push({ name: block.name || '工具', input: block.input || {} });
|
|
1541
|
+
}
|
|
1542
|
+
if (texts.length) {
|
|
1543
|
+
const el = document.createElement('div');
|
|
1544
|
+
el.className = 'msg msg-assistant';
|
|
1545
|
+
const content = document.createElement('div');
|
|
1546
|
+
content.className = 'content';
|
|
1547
|
+
content.innerHTML = renderMarkdown(texts.join('\n\n'));
|
|
1548
|
+
el.appendChild(content);
|
|
1549
|
+
frag.appendChild(el);
|
|
1550
|
+
}
|
|
1551
|
+
// 历史里工具卡简化为状态行:用户翻历史主要看对话文本,不需要交互
|
|
1552
|
+
for (const t of tools) {
|
|
1553
|
+
const el = document.createElement('div');
|
|
1554
|
+
el.className = 'msg msg-status';
|
|
1555
|
+
el.textContent = '— 🔧 ' + t.name + ' —';
|
|
1556
|
+
frag.appendChild(el);
|
|
1557
|
+
}
|
|
1558
|
+
} else if (msg.type === 'result') {
|
|
1559
|
+
const el = document.createElement('div');
|
|
1560
|
+
el.className = 'msg msg-status';
|
|
1561
|
+
const cost = msg.total_cost_usd ? ` ($${msg.total_cost_usd.toFixed(4)})` : '';
|
|
1562
|
+
el.textContent = '— 完成' + cost + ' —';
|
|
1563
|
+
frag.appendChild(el);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
chatArea.insertBefore(frag, chatArea.firstChild);
|
|
1567
|
+
_historyOffset += d.messages.length;
|
|
1568
|
+
_historyHasMore = d.hasMore;
|
|
1569
|
+
// 保持滚动位置:新内容在顶部插入后,scrollTop 加上新增高度
|
|
1570
|
+
chatArea.scrollTop = prevScrollTop + (chatArea.scrollHeight - prevScrollHeight);
|
|
1571
|
+
} catch (e) {
|
|
1572
|
+
indicator.textContent = '— 加载失败: ' + e.message + ' —';
|
|
1573
|
+
setTimeout(() => indicator.remove(), 3000);
|
|
1574
|
+
} finally {
|
|
1575
|
+
_historyLoading = false;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
chatArea.addEventListener('scroll', () => {
|
|
1580
|
+
// 用户主动往上翻 → 禁用自动 trim,避免被删
|
|
1581
|
+
if (chatArea.scrollTop < chatArea.scrollHeight - chatArea.clientHeight - 100) {
|
|
1582
|
+
_userScrolledUp = true;
|
|
1583
|
+
} else if (chatArea.scrollTop > chatArea.scrollHeight - chatArea.clientHeight - 20) {
|
|
1584
|
+
// 回到接近底部 → 恢复 trim
|
|
1585
|
+
_userScrolledUp = false;
|
|
1586
|
+
}
|
|
1587
|
+
// 接近顶部 100px → 拉更老历史
|
|
1588
|
+
if (chatArea.scrollTop < 100 && _historyHasMore && !_historyLoading) {
|
|
1589
|
+
loadOlderHistory();
|
|
1590
|
+
}
|
|
1591
|
+
});
|
|
1592
|
+
|
|
1492
1593
|
socket.on('agent:busy', (busy) => {
|
|
1493
1594
|
if (busy) { showThinking(); setWorkState('thinking'); }
|
|
1494
1595
|
else { removeThinking(); setWorkState('idle'); }
|
|
@@ -1952,6 +2053,7 @@
|
|
|
1952
2053
|
window.goBack = function() { window.location.href = '/'; };
|
|
1953
2054
|
function scrollToBottom() { requestAnimationFrame(() => { chatArea.scrollTop = chatArea.scrollHeight; }); }
|
|
1954
2055
|
function trimMessages() {
|
|
2056
|
+
if (_userScrolledUp) return; // 用户在往上翻历史,禁用自动删
|
|
1955
2057
|
const msgs = chatArea.querySelectorAll('.msg');
|
|
1956
2058
|
if (msgs.length > MAX_MESSAGES) {
|
|
1957
2059
|
const remove = msgs.length - MAX_MESSAGES;
|
|
@@ -2302,16 +2404,23 @@
|
|
|
2302
2404
|
try {
|
|
2303
2405
|
const resp = await fetch(`/api/skills?sessionId=${SESSION_ID}`);
|
|
2304
2406
|
const data = await resp.json();
|
|
2305
|
-
const container = document.getElementById('sk-custom-skills');
|
|
2306
2407
|
_customSkills = data.skills || [];
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2408
|
+
const builtins = _customSkills.filter(s => s.source === 'builtin');
|
|
2409
|
+
const customs = _customSkills.filter(s => s.source !== 'builtin');
|
|
2410
|
+
// 内置技能:始终逐个显示(青绿色)
|
|
2411
|
+
const builtinEl = document.getElementById('sk-builtin-skills');
|
|
2412
|
+
builtinEl.innerHTML = builtins.map(s =>
|
|
2413
|
+
`<button class="sk sk-claude" onclick="runSkill('${escHtml(s.name)}')" title="${escHtml(s.desc)}" style="color:#26a69a">/${escHtml(s.name)}</button>`
|
|
2414
|
+
).join('');
|
|
2415
|
+
// 用户/项目技能:≤3 显示按钮;>3 折叠
|
|
2416
|
+
const customEl = document.getElementById('sk-custom-skills');
|
|
2417
|
+
if (!customs.length) { customEl.innerHTML = ''; return; }
|
|
2418
|
+
if (customs.length <= 3) {
|
|
2419
|
+
customEl.innerHTML = customs.map(s =>
|
|
2311
2420
|
`<button class="sk sk-claude" onclick="runSkill('${escHtml(s.name)}')" title="${escHtml(s.desc)}" style="color:#9d5cf5">/${escHtml(s.name)}</button>`
|
|
2312
2421
|
).join('');
|
|
2313
2422
|
} else {
|
|
2314
|
-
|
|
2423
|
+
customEl.innerHTML = `<button class="sk sk-claude" onclick="openCmdSheet()" style="color:#9d5cf5">/命令 (${customs.length})</button>`;
|
|
2315
2424
|
}
|
|
2316
2425
|
} catch {}
|
|
2317
2426
|
}
|
|
@@ -2343,15 +2452,33 @@
|
|
|
2343
2452
|
document.getElementById('skill-run-sheet').classList.remove('open');
|
|
2344
2453
|
_pendingSkillName = null;
|
|
2345
2454
|
};
|
|
2346
|
-
window.submitSkillRun = function() {
|
|
2455
|
+
window.submitSkillRun = async function() {
|
|
2347
2456
|
if (!_pendingSkillName) return;
|
|
2348
2457
|
const args = document.getElementById('skill-run-input').value.trim();
|
|
2349
2458
|
const name = _pendingSkillName;
|
|
2459
|
+
const skill = _customSkills.find(s => s.name === name);
|
|
2350
2460
|
closeSkillRunSheet();
|
|
2351
2461
|
if (!isController && canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID });
|
|
2352
|
-
const
|
|
2353
|
-
|
|
2354
|
-
|
|
2462
|
+
const displayCmd = args ? `/${name} ${args}` : `/${name}`;
|
|
2463
|
+
if (skill?.source === 'builtin') {
|
|
2464
|
+
// 内置技能不在 .claude/commands/ 里,需拉取内容后直接发送为 prompt
|
|
2465
|
+
try {
|
|
2466
|
+
const resp = await fetch(`/api/skills/content?name=${encodeURIComponent(name)}&sessionId=${SESSION_ID}`);
|
|
2467
|
+
const data = await resp.json();
|
|
2468
|
+
let prompt = data.content || displayCmd;
|
|
2469
|
+
if (args) {
|
|
2470
|
+
prompt = prompt.replace(/\$ARGUMENTS/g, args);
|
|
2471
|
+
} else {
|
|
2472
|
+
prompt = prompt.replace(/[^\n]*\$ARGUMENTS[^\n]*/g, '').replace(/\n{3,}/g, '\n\n').trim();
|
|
2473
|
+
}
|
|
2474
|
+
socket.emit('agent:query', { prompt });
|
|
2475
|
+
} catch {
|
|
2476
|
+
socket.emit('agent:query', { prompt: displayCmd });
|
|
2477
|
+
}
|
|
2478
|
+
} else {
|
|
2479
|
+
socket.emit('agent:query', { prompt: displayCmd });
|
|
2480
|
+
}
|
|
2481
|
+
addInputMessage(displayCmd);
|
|
2355
2482
|
showThinking();
|
|
2356
2483
|
};
|
|
2357
2484
|
window.openCmdSheet = function() {
|
|
@@ -2361,8 +2488,9 @@
|
|
|
2361
2488
|
filterInp.value = '';
|
|
2362
2489
|
const render = (keyword = '') => {
|
|
2363
2490
|
const kw = keyword.toLowerCase();
|
|
2491
|
+
// 命令面板只列用户/项目技能(内置技能始终在快捷栏直接显示)
|
|
2364
2492
|
const filtered = _customSkills.filter(s =>
|
|
2365
|
-
!kw || s.name.toLowerCase().includes(kw) || (s.desc || '').toLowerCase().includes(kw));
|
|
2493
|
+
s.source !== 'builtin' && (!kw || s.name.toLowerCase().includes(kw) || (s.desc || '').toLowerCase().includes(kw)));
|
|
2366
2494
|
listEl.innerHTML = filtered.length
|
|
2367
2495
|
? filtered.map(s => `<div class="action-sheet-item" onclick="runSkillAndClose('${escHtml(s.name)}')">
|
|
2368
2496
|
<div><div style="color:#d2a8ff">/${escHtml(s.name)}</div>${s.desc ? `<div class="desc">${escHtml(s.desc)}</div>` : ''}</div>
|
|
@@ -2404,11 +2532,12 @@
|
|
|
2404
2532
|
try {
|
|
2405
2533
|
const resp = await fetch(`/api/skills?sessionId=${SESSION_ID}&detail=1`);
|
|
2406
2534
|
const data = await resp.json();
|
|
2407
|
-
|
|
2535
|
+
const editable = (data.skills || []).filter(s => s.source !== 'builtin');
|
|
2536
|
+
if (!editable.length) {
|
|
2408
2537
|
listEl.innerHTML = '<div style="text-align:center;color:#8b949e;padding:2rem 0">暂无技能,点击右上角 + 新建</div>';
|
|
2409
2538
|
return;
|
|
2410
2539
|
}
|
|
2411
|
-
listEl.innerHTML =
|
|
2540
|
+
listEl.innerHTML = editable.map(s => `
|
|
2412
2541
|
<div class="action-sheet-item" style="gap:0.5rem">
|
|
2413
2542
|
<div style="min-width:0;flex:1;cursor:pointer" onclick="editSkill('${escHtml(s.name)}','${escHtml(s.source)}')">
|
|
2414
2543
|
<div style="color:#9d5cf5;font-weight:500">/${escHtml(s.name)}</div>
|