@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.
@@ -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 = 150; // 聊天区最大消息数
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
- if (!_customSkills.length) { container.innerHTML = ''; return; }
2308
- // ≤3 个:直接显示按钮;>3 个:显示 "/命令 (N)" 入口
2309
- if (_customSkills.length <= 3) {
2310
- container.innerHTML = _customSkills.map(s =>
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
- container.innerHTML = `<button class="sk sk-claude" onclick="openCmdSheet()" style="color:#9d5cf5">/命令 (${_customSkills.length})</button>`;
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 cmd = args ? `/${name} ${args}` : `/${name}`;
2353
- socket.emit('agent:query', { prompt: cmd });
2354
- addInputMessage(cmd);
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
- if (!data.skills || !data.skills.length) {
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 = data.skills.map(s => `
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>