@wendongfly/myhi 1.3.194 → 1.3.195

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/chat.html CHANGED
@@ -454,6 +454,66 @@
454
454
  .dr-btn.take { background: var(--green); color: #fff; font-weight: 600; }
455
455
  .dr-btn:active { opacity: 0.75; }
456
456
  .dr-src { color: var(--blue2); }
457
+ /* 技能面板 */
458
+ .skl-box { max-width: 640px !important; max-height: min(88vh, 820px); min-height: min(460px, 80vh); display: flex; flex-direction: column; padding-top: 0.6rem !important; }
459
+ @media (min-width: 769px) { #skill-sheet.open { align-items: center; } #skill-sheet .skl-box { border-radius: 14px; } }
460
+ .skl-head { display: flex; align-items: center; gap: 0.5rem; padding: 0.1rem 0.1rem 0.55rem; border-bottom: 1px solid var(--surface2); }
461
+ .skl-head b { font-size: 0.92rem; }
462
+ .skl-back { background: none; border: none; color: var(--text2); font-size: 1.3rem; line-height: 1; cursor: pointer; padding: 0 0.3rem; border-radius: 6px; }
463
+ .skl-back:hover { background: var(--surface2); }
464
+ .skl-view { flex: 1; min-height: 0; overflow-y: auto; scrollbar-width: thin; padding: 0.55rem 0.1rem 0.2rem; }
465
+ .skl-tools { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-bottom: 0.5rem; }
466
+ .skl-tool { border: 1px solid var(--border); background: var(--surface2); color: var(--text2); border-radius: 8px; padding: 0.3rem 0.6rem; font-size: 0.74rem; cursor: pointer; white-space: nowrap; position: relative; }
467
+ .skl-tool.pri { background: var(--purple); border-color: var(--purple); color: #fff; font-weight: 600; }
468
+ .skl-badge { position: absolute; top: -6px; right: -6px; background: var(--green); color: #fff; border-radius: 8px; font-size: 0.6rem; padding: 0 0.3rem; font-weight: 700; }
469
+ .skl-banner { background: rgba(46,160,67,0.1); color: var(--green); border-radius: 8px; padding: 0.4rem 0.6rem; font-size: 0.74rem; cursor: pointer; margin-bottom: 0.4rem; }
470
+ .skl-grp { font-size: 0.68rem; color: var(--muted); font-weight: 600; padding: 0.5rem 0.2rem 0.2rem; }
471
+ .skl-item { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.3rem; border-bottom: 1px solid var(--surface2); border-radius: 6px; }
472
+ .skl-item:hover { background: var(--surface2); }
473
+ .skl-main { flex: 1; min-width: 0; cursor: pointer; }
474
+ .skl-name { color: var(--purple2); font-weight: 600; font-size: 0.84rem; }
475
+ .skl-desc { font-size: 0.72rem; color: var(--muted); line-height: 1.45; margin-top: 0.1rem; word-break: break-word; }
476
+ .skl-tag { display: inline-block; margin-left: 0.35rem; font-size: 0.6rem; padding: 0.02rem 0.35rem; border-radius: 8px; border: 1px solid var(--border); color: var(--muted); vertical-align: 1px; }
477
+ .skl-tag.skill { border-color: var(--green); color: var(--green); }
478
+ .skl-tag.command { border-color: var(--purple2); color: var(--purple2); }
479
+ .skl-acts { display: flex; gap: 0.25rem; flex-shrink: 0; }
480
+ .skl-empty { text-align: center; color: var(--muted); padding: 1.6rem 0.5rem; font-size: 0.8rem; line-height: 1.7; }
481
+ .skl-foot { font-size: 0.68rem; color: var(--muted); line-height: 1.55; padding: 0.6rem 0.2rem 0; }
482
+ .skl-row { display: flex; gap: 0.4rem; align-items: center; margin-bottom: 0.35rem; flex-wrap: wrap; }
483
+ .skl-sel { flex: 1; min-width: 9rem; background: var(--bg); color: var(--text2); border: 1px solid var(--border); border-radius: 8px; padding: 0.45rem; font-size: 0.8rem; }
484
+ .skl-hint { font-size: 0.72rem; color: var(--muted); line-height: 1.6; }
485
+ .skl-hint code { background: var(--surface2); padding: 0 0.25rem; border-radius: 3px; }
486
+ .skl-lbl { font-size: 0.72rem; color: var(--text2); font-weight: 600; margin: 0.6rem 0 0.3rem; }
487
+ .skl-radios { display: flex; gap: 0.4rem; flex-wrap: wrap; }
488
+ .skl-radios label { flex: 1; min-width: 10rem; display: flex; gap: 0.4rem; align-items: flex-start; border: 1px solid var(--border); border-radius: 8px; padding: 0.45rem 0.55rem; cursor: pointer; font-size: 0.78rem; }
489
+ .skl-radios label b { white-space: nowrap; }
490
+ .skl-radios label small { display: block; color: var(--muted); font-size: 0.68rem; margin-top: 0.1rem; }
491
+ .skl-radios input { margin-top: 0.2rem; }
492
+ .skl-card { border: 1px solid var(--border); border-radius: 9px; padding: 0.5rem 0.6rem; margin-bottom: 0.45rem; font-size: 0.8rem; }
493
+ .skl-share-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.45rem; }
494
+ @media (max-width: 480px) { .skl-share-grid { grid-template-columns: 1fr; } }
495
+ .skl-big { text-align: left; border: 1px solid var(--border); background: var(--bg); color: var(--text); border-radius: 10px; padding: 0.55rem 0.65rem; cursor: pointer; display: grid; grid-template-columns: 1.6rem 1fr; column-gap: 0.3rem; align-items: center; }
496
+ .skl-big:hover { border-color: var(--purple); }
497
+ .skl-big span { grid-row: span 2; font-size: 1.1rem; }
498
+ .skl-big b { font-size: 0.8rem; }
499
+ .skl-big small { color: var(--muted); font-size: 0.66rem; line-height: 1.4; }
500
+ .skl-out { margin-top: 0.5rem; font-size: 0.76rem; color: var(--text2); line-height: 1.6; background: var(--surface2); border-radius: 8px; padding: 0.5rem 0.6rem; }
501
+ .skl-warn { background: rgba(248,81,73,0.1); color: var(--red); border-radius: 8px; padding: 0.45rem 0.6rem; font-size: 0.74rem; line-height: 1.55; margin: 0.4rem 0; }
502
+ .skl-ok { color: var(--green); font-size: 0.74rem; margin: 0.35rem 0; }
503
+ .skl-findings { margin: 0.35rem 0; display: flex; flex-direction: column; gap: 0.25rem; }
504
+ .skl-f { font-size: 0.72rem; border-radius: 6px; padding: 0.3rem 0.5rem; line-height: 1.5; }
505
+ .skl-f.danger { background: rgba(248,81,73,0.1); color: var(--red); }
506
+ .skl-f.warn { background: rgba(210,153,34,0.1); color: var(--yellow); }
507
+ .skl-f small { color: var(--muted); }
508
+ .skl-f code { display: block; margin-top: 0.15rem; font-size: 0.68rem; color: var(--text2); white-space: pre-wrap; word-break: break-all; }
509
+ .skl-files { margin: 0.4rem 0; font-size: 0.74rem; }
510
+ .skl-files summary { cursor: pointer; color: var(--blue2); }
511
+ .skl-files pre { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 0.45rem; font-size: 0.7rem; max-height: 14rem; overflow: auto; white-space: pre-wrap; word-break: break-word; }
512
+ .skl-fname { font-weight: 600; margin-top: 0.4rem; color: var(--text2); }
513
+ .skl-confirm { display: flex; gap: 0.4rem; align-items: center; font-size: 0.74rem; color: var(--text2); margin-top: 0.4rem; cursor: pointer; }
514
+ .skl-pick { display: flex; gap: 0.5rem; align-items: flex-start; padding: 0.45rem 0.2rem; border-bottom: 1px solid var(--surface2); cursor: pointer; }
515
+ .skl-pick input { margin-top: 0.2rem; }
516
+ .sk.skl-dot::after { content: ''; display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--green); margin-left: 0.25rem; vertical-align: 2px; }
457
517
  /* 草稿面板(队列) */
458
518
  .ib-box { max-width: 640px !important; max-height: min(86vh, 760px); min-height: min(420px, 80vh); display: flex; flex-direction: column; padding-top: 0.6rem !important; }
459
519
  @media (min-width: 769px) { #inbox-sheet.open { align-items: center; } #inbox-sheet .ib-box { border-radius: 14px; } }
@@ -467,8 +527,15 @@
467
527
  .ib-pane { flex: 1; min-height: 0; display: flex; flex-direction: column; }
468
528
  .ib-bar { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.1rem 0.35rem; flex-wrap: wrap; }
469
529
  .ib-hint { flex: 1; min-width: 8em; font-size: 0.68rem; color: var(--muted); line-height: 1.45; }
470
- .ib-autoall { display: flex; align-items: center; gap: 0.3rem; font-size: 0.72rem; color: var(--text2); cursor: pointer; white-space: nowrap; }
471
- .ib-autoall input { width: 0.95rem; height: 0.95rem; }
530
+ .ib-autoall { display: flex; align-items: center; gap: 0.4rem; position: relative; font-size: 0.72rem; color: var(--text2); cursor: pointer; white-space: nowrap; }
531
+ /* 开关样式:比「复选框 + ▶」直观,一眼看出开 / 关 */
532
+ .ib-autoall input { position: absolute; opacity: 0; width: 0; height: 0; }
533
+ .ib-sw { position: relative; width: 1.9rem; height: 1.05rem; border-radius: 1rem; background: var(--border2, var(--border)); transition: background 0.15s; flex-shrink: 0; }
534
+ .ib-sw::after { content: ''; position: absolute; top: 0.125rem; left: 0.125rem; width: 0.8rem; height: 0.8rem; border-radius: 50%; background: #fff; box-shadow: 0 1px 2px rgba(0,0,0,0.25); transition: transform 0.15s; }
535
+ .ib-autoall input:checked + .ib-sw { background: var(--green); }
536
+ .ib-autoall input:checked + .ib-sw::after { transform: translateX(0.85rem); }
537
+ .ib-autoall input:indeterminate + .ib-sw { background: rgba(46,160,67,0.45); }
538
+ .ib-autoall input:focus-visible + .ib-sw { outline: 2px solid var(--green); outline-offset: 2px; }
472
539
  .ib-paused { display: flex; align-items: center; gap: 0.5rem; font-size: 0.7rem; color: var(--yellow); background: rgba(210,153,34,0.10); border-radius: 8px; padding: 0.35rem 0.55rem; margin-bottom: 0.3rem; }
473
540
  .ib-paused span { flex: 1; }
474
541
  .ib-list { flex: 1; min-height: 3rem; overflow-y: auto; scrollbar-width: thin; }
@@ -989,8 +1056,8 @@
989
1056
  <div class="ib-bar">
990
1057
  <span id="ib-hint" class="ib-hint"></span>
991
1058
  <button id="ib-sel-btn" class="dr-btn" onclick="ibToggleSelect()" title="勾选几条后合并、一起取回或删除">☑ 多选</button>
992
- <label class="ib-autoall" title="勾上后:每干完一轮,自动按顺序发出下一条(每条都是一轮真实的模型调用)。上一轮被中断或出错时会暂停,等你确认再继续。">
993
- <input type="checkbox" id="ib-auto-all" onchange="ibAutoAll(this.checked)"> ▶ 依次自动发送
1059
+ <label id="ib-autoall" class="ib-autoall" title="打开后:每干完一轮,自动按顺序发出下一条(每条都是一轮真实的模型调用)。上一轮被中断或出错时会暂停,等你确认再继续。">
1060
+ <input type="checkbox" id="ib-auto-all" onchange="ibAutoAll(this.checked)"><span class="ib-sw"></span>依次自动发送
994
1061
  </label>
995
1062
  </div>
996
1063
  <div id="ib-paused" class="ib-paused" style="display:none">
@@ -1199,59 +1266,113 @@
1199
1266
 
1200
1267
  <div id="skill-sheet" class="action-sheet">
1201
1268
  <div class="action-sheet-backdrop" onclick="closeSkillSheet()"></div>
1202
- <div class="action-sheet-box" style="max-height:85vh;display:flex;flex-direction:column">
1203
- <div class="action-sheet-title" style="display:flex;align-items:center;justify-content:space-between;text-align:left">
1204
- <span id="skill-sheet-title">技能管理</span>
1205
- <div style="display:flex;gap:0.4rem">
1206
- <button onclick="showSkillMarket()" style="background:var(--surface2);color:var(--text2);border:1px solid var(--border);border-radius:6px;padding:0.3rem 0.7rem;font-size:0.75rem;cursor:pointer">市场</button>
1207
- <button onclick="showSkillImport()" style="background:var(--surface2);color:var(--text2);border:1px solid var(--border);border-radius:6px;padding:0.3rem 0.7rem;font-size:0.75rem;cursor:pointer">Git</button>
1208
- <button onclick="showSkillEditor()" style="background:var(--purple);color:#fff;border:none;border-radius:6px;padding:0.3rem 0.7rem;font-size:0.75rem;cursor:pointer">+ 新建</button>
1209
- </div>
1269
+ <div class="action-sheet-box skl-box">
1270
+ <div class="skl-head">
1271
+ <button id="skl-back" class="skl-back" onclick="sklView('list')" style="display:none" title="返回">‹</button>
1272
+ <b id="skill-sheet-title">技能</b>
1273
+ <button class="ib-x" onclick="closeSkillSheet()" title="关闭">✕</button>
1210
1274
  </div>
1211
- <!-- 技能列表视图 -->
1212
- <div id="skill-list-view" style="flex:1;overflow-y:auto;scrollbar-width:thin">
1213
- <div style="text-align:center;color:var(--muted);padding:2rem 0">加载中...</div>
1275
+ <!-- 列表 -->
1276
+ <div id="skl-v-list" class="skl-view">
1277
+ <div class="skl-tools">
1278
+ <button class="skl-tool pri" onclick="sklView('gen')" title="把本次任务里的做法提炼成技能,以后一点就能复用">✨ 从本次任务生成</button>
1279
+ <button class="skl-tool" onclick="sklView('import')" title="粘贴分享码 / 分享链接,或选择技能文件">📥 导入</button>
1280
+ <button class="skl-tool" onclick="sklView('team')" title="团队技能仓库:订阅、检查更新">👥 团队仓库<span id="skl-upd-badge" class="skl-badge" style="display:none"></span></button>
1281
+ <button class="skl-tool" onclick="showSkillMarket()">🛒 市场</button>
1282
+ <button class="skl-tool" onclick="showSkillEditor()">+ 手写</button>
1283
+ </div>
1284
+ <div id="skl-upd-banner" class="skl-banner" style="display:none" onclick="sklView('team')"></div>
1285
+ <div id="skill-list-view" class="skl-list"></div>
1286
+ <div class="skl-foot">技能 = 预先写好的一段做事方法。<b>手动技能</b>点快捷栏按钮(或输入 /名称)运行;<b>自动技能</b>不用点,AI 遇到合适的场景会自己用。</div>
1214
1287
  </div>
1215
- <!-- 技能编辑视图 -->
1216
- <div id="skill-edit-view" style="display:none;flex:1;overflow-y:auto;scrollbar-width:thin;padding:0.3rem 0.2rem">
1217
- <div style="display:flex;flex-direction:column;gap:0.4rem">
1218
- <input id="skill-name" class="slash-inp" placeholder="技能名称 (如 mosaic-names)" autocomplete="off">
1219
- <select id="skill-scope" class="slash-inp" style="background:var(--bg);color:var(--text2);border:1px solid var(--border);border-radius:8px;padding:0.5rem;font-size:0.82rem">
1220
- <option value="user">用户级 (~/.claude/commands/)</option>
1221
- <option value="project">项目级 (.claude/commands/)</option>
1288
+ <!-- 编辑 -->
1289
+ <div id="skl-v-edit" class="skl-view" style="display:none">
1290
+ <div class="skl-row">
1291
+ <input id="skill-name" class="slash-inp" placeholder="名称(英文,如 deploy-test)" autocomplete="off" style="flex:1">
1292
+ </div>
1293
+ <div class="skl-row">
1294
+ <select id="skill-kind" class="slash-inp skl-sel" onchange="sklKindChanged()">
1295
+ <option value="command">手动技能(/命令,点按钮运行)</option>
1296
+ <option value="skill">自动技能(AI 遇到合适场景自己用)</option>
1297
+ </select>
1298
+ <select id="skill-scope" class="slash-inp skl-sel">
1299
+ <option value="project">只在本项目</option>
1300
+ <option value="user">本机所有项目</option>
1222
1301
  </select>
1223
- <textarea id="skill-content" class="slash-inp" placeholder="# 技能标题&#10;&#10;技能描述和指令内容..." rows="10" style="resize:vertical;font-family:monospace;font-size:0.78rem;line-height:1.5"></textarea>
1224
1302
  </div>
1225
- <button class="action-sheet-cancel" style="background:var(--purple);color:#fff;font-weight:600;margin-top:0.5rem;margin-bottom:0.4rem" onclick="saveSkill()">保存</button>
1226
- <button class="action-sheet-cancel" onclick="backToSkillList()">返回列表</button>
1303
+ <textarea id="skill-content" class="slash-inp" rows="14" style="resize:vertical;font-family:monospace;font-size:0.78rem;line-height:1.5;width:100%;box-sizing:border-box"></textarea>
1304
+ <div id="skill-edit-hint" class="skl-hint"></div>
1305
+ <button class="action-sheet-cancel" style="background:var(--purple);color:#fff;font-weight:600;margin-top:0.5rem" onclick="saveSkill()">保存</button>
1227
1306
  </div>
1228
- <!-- Git 安装视图 -->
1229
- <div id="skill-import-view" style="display:none;flex:1;overflow-y:auto;scrollbar-width:thin;padding:0.3rem 0.2rem">
1230
- <div style="display:flex;flex-direction:column;gap:0.4rem">
1231
- <div style="display:flex;gap:0.4rem">
1232
- <input id="skill-git-url" class="slash-inp" placeholder="Git 仓库地址 (https:// 或 git@...)" autocomplete="off" style="flex:1">
1233
- <button onclick="scanGitSkills()" style="background:var(--purple);color:#fff;border:none;border-radius:8px;padding:0.5rem 0.8rem;font-size:0.82rem;cursor:pointer;white-space:nowrap">扫描</button>
1234
- </div>
1235
- <div id="skill-git-auth" style="display:flex;gap:0.4rem">
1236
- <input id="skill-git-user" class="slash-inp" placeholder="用户名 (可选)" autocomplete="off" style="flex:1">
1237
- <input id="skill-git-pass" class="slash-inp" placeholder="密码/Token (可选)" type="password" autocomplete="off" style="flex:1">
1307
+ <!-- 从本次任务生成 -->
1308
+ <div id="skl-v-gen" class="skl-view" style="display:none">
1309
+ <div class="skl-hint" style="margin-bottom:0.5rem">让正在干活的 AI 把<b>本次任务里的做法</b>提炼成可复用的技能:项目特有的路径、名字会改成参数,不会写进密码等敏感信息。AI <b>先出草稿给你确认</b>,确认后才保存。</div>
1310
+ <textarea id="skl-gen-hint" class="slash-inp" rows="3" placeholder="想沉淀成什么技能?例如「把今天这套部署到测试环境的流程做成技能」。可以留空,让 AI 自己判断" style="width:100%;box-sizing:border-box;resize:vertical"></textarea>
1311
+ <div class="skl-lbl">技能类型</div>
1312
+ <div class="skl-radios">
1313
+ <label><input type="radio" name="skl-gen-kind" value="command" checked> <b>手动技能</b><small>点按钮 / 输入 /名称 才运行,最直观</small></label>
1314
+ <label><input type="radio" name="skl-gen-kind" value="skill"> <b>自动技能</b><small>AI 遇到合适场景自己用,可带辅助脚本</small></label>
1315
+ </div>
1316
+ <div class="skl-lbl">保存到</div>
1317
+ <div class="skl-radios">
1318
+ <label><input type="radio" name="skl-gen-scope" value="project" checked> <b>只在本项目</b><small>跟着项目目录走</small></label>
1319
+ <label><input type="radio" name="skl-gen-scope" value="user"> <b>本机所有项目</b><small>这台电脑上的任务都能用</small></label>
1320
+ </div>
1321
+ <button class="action-sheet-cancel" style="background:var(--purple);color:#fff;font-weight:600;margin-top:0.6rem" onclick="genSkillFromTask()">✨ 让 AI 起草</button>
1322
+ </div>
1323
+ <!-- 分享 -->
1324
+ <div id="skl-v-share" class="skl-view" style="display:none">
1325
+ <div id="skl-share-info" class="skl-card"></div>
1326
+ <div id="skl-share-warn" class="skl-warn" style="display:none"></div>
1327
+ <div class="skl-share-grid">
1328
+ <button class="skl-big" onclick="sklShareDownload()"><span>⬇</span><b>下载技能文件</b><small>微信 / 邮件发给对方,对方「📥 导入」</small></button>
1329
+ <button class="skl-big" onclick="sklShareCode()"><span>📋</span><b>复制分享码</b><small>一段文本,粘贴给对方导入</small></button>
1330
+ <button class="skl-big" onclick="sklShareLink()"><span>🔗</span><b>生成分享链接</b><small>同一局域网 / Tailscale 的 myhi 直接导入,24 小时有效</small></button>
1331
+ <button class="skl-big" onclick="sklSharePublishForm()"><span>👥</span><b>发布到团队仓库</b><small>推到 Git 仓库,订阅的人会收到更新</small></button>
1332
+ </div>
1333
+ <div id="skl-share-out" class="skl-out" style="display:none"></div>
1334
+ <div id="skl-pub-form" style="display:none;margin-top:0.5rem">
1335
+ <input id="skl-pub-url" class="slash-inp" placeholder="仓库地址(https://… 或 git@…)" autocomplete="off" style="width:100%;box-sizing:border-box;margin-bottom:0.35rem">
1336
+ <div class="skl-row">
1337
+ <input id="skl-pub-user" class="slash-inp" placeholder="用户名(可选)" autocomplete="off" style="flex:1">
1338
+ <input id="skl-pub-pass" class="slash-inp" type="password" placeholder="Token(需要写权限)" autocomplete="off" style="flex:1">
1339
+ <input id="skl-pub-branch" class="slash-inp" placeholder="分支(默认)" autocomplete="off" style="width:6.5rem">
1238
1340
  </div>
1239
- <select id="skill-import-scope" class="slash-inp" style="background:var(--bg);color:var(--text2);border:1px solid var(--border);border-radius:8px;padding:0.5rem;font-size:0.82rem">
1240
- <option value="user">安装到用户级 (~/.claude/commands/)</option>
1241
- <option value="project">安装到项目级 (.claude/commands/)</option>
1341
+ <button class="action-sheet-cancel" style="background:var(--purple);color:#fff;font-weight:600;margin-top:0.4rem" onclick="sklPublish()">发布</button>
1342
+ </div>
1343
+ </div>
1344
+ <!-- 导入 -->
1345
+ <div id="skl-v-import" class="skl-view" style="display:none">
1346
+ <textarea id="skl-imp-text" class="slash-inp" rows="3" placeholder="粘贴分享码(MYHI-SKILL:…)或分享链接(…/s/skill/…)" style="width:100%;box-sizing:border-box;resize:vertical"></textarea>
1347
+ <div class="skl-row" style="margin-top:0.35rem">
1348
+ <button class="dr-btn" onclick="document.getElementById('skl-imp-file').click()">📄 选择技能文件</button>
1349
+ <input type="file" id="skl-imp-file" accept=".myhiskill,.json,.md" style="display:none" onchange="sklImportFile(this.files[0]); this.value='';">
1350
+ <button class="dr-btn take" style="margin-left:auto" onclick="sklImportPreview()">预览</button>
1351
+ </div>
1352
+ <div id="skl-imp-preview"></div>
1353
+ </div>
1354
+ <!-- 团队仓库 -->
1355
+ <div id="skl-v-team" class="skl-view" style="display:none">
1356
+ <div id="skl-subs"></div>
1357
+ <div class="skl-lbl" style="margin-top:0.6rem">添加团队仓库</div>
1358
+ <input id="skill-git-url" class="slash-inp" placeholder="Git 仓库地址(https://… 或 git@…)" autocomplete="off" style="width:100%;box-sizing:border-box;margin-bottom:0.35rem">
1359
+ <div class="skl-row">
1360
+ <input id="skill-git-user" class="slash-inp" placeholder="用户名(可选)" autocomplete="off" style="flex:1">
1361
+ <input id="skill-git-pass" class="slash-inp" type="password" placeholder="密码 / Token(可选)" autocomplete="off" style="flex:1">
1362
+ </div>
1363
+ <div class="skl-row">
1364
+ <select id="skill-import-scope" class="slash-inp skl-sel">
1365
+ <option value="user">装到本机所有项目</option>
1366
+ <option value="project">只装到本项目</option>
1242
1367
  </select>
1368
+ <button class="dr-btn take" onclick="scanGitSkills()">扫描</button>
1243
1369
  </div>
1244
- <div id="skill-import-preview" style="display:none;margin-top:0.5rem;padding:0.5rem;background:var(--bg);border:1px solid var(--border);border-radius:8px;font-size:0.78rem;max-height:30vh;overflow-y:auto;scrollbar-width:thin"></div>
1245
- <div id="skill-import-scan-btn"></div>
1246
- <button id="skill-import-install-btn" class="action-sheet-cancel" style="display:none;background:var(--green);color:#000;font-weight:600;margin-bottom:0.4rem" onclick="installGitSkills()">安装选中的技能</button>
1247
- <button class="action-sheet-cancel" onclick="backToSkillList()">返回列表</button>
1370
+ <div id="skill-import-preview"></div>
1248
1371
  </div>
1249
- <!-- 技能市场视图 -->
1250
- <div id="skill-market-view" style="display:none;flex:1;overflow-y:auto;scrollbar-width:thin;padding:0.3rem 0">
1372
+ <!-- 市场 -->
1373
+ <div id="skl-v-market" class="skl-view" style="display:none">
1251
1374
  <div id="skill-market-list"></div>
1252
- <button class="action-sheet-cancel" onclick="backToSkillList()" style="margin-top:0.5rem">返回列表</button>
1253
1375
  </div>
1254
- <button id="skill-close-btn" class="action-sheet-cancel" onclick="closeSkillSheet()">关闭</button>
1255
1376
  </div>
1256
1377
  </div>
1257
1378
 
@@ -2689,6 +2810,7 @@
2689
2810
  try { updateInjectBtn(); } catch {}
2690
2811
  // 用 try 包住:这两个绑定声明在文件后段,初始化早期调用会撞 TDZ
2691
2812
  try { if (!busy) _draftTipDismissed = false; renderDraftTip(); } catch {}
2813
+ try { if (!busy && _sklGenPending) { const sc = _sklGenPending; _sklGenPending = false; loadSkills(); fetch('/api/skills/changed', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ sessionId: SESSION_ID, scope: sc }) }).catch(() => {}); } } catch {}
2692
2814
  try { if (!busy) _ibIdleSince = Date.now(); if (document.getElementById('inbox-sheet').classList.contains('open')) renderInbox(); } catch {}
2693
2815
  try { if (!busy) checkGuide(); else renderGuideTip(); } catch {}
2694
2816
  }
@@ -3821,7 +3943,7 @@
3821
3943
  followup: ['🔄 跟进', '长任务交出去后,AI 每说完一轮,隔一段时间自动替你问进度、让它接着干;判断全部完成就停(最多 5 次,每次都算一轮模型调用)。'],
3822
3944
  git: ['提交', '查看改动并 git 提交 / 推送。'],
3823
3945
  memory: ['记忆', '查看、编辑「项目说明」和 AI 的长期记忆,每次新对话都会带上;长期项目可一键让 AI 生成项目说明。'],
3824
- skills: ['技能', '管理常用指令模板(/命令),一点就发;可自建。'],
3946
+ skills: ['技能', '把做事方法存成技能,以后一点就能复用:可从本次任务一键生成,分享给其他 myhi(文件 / 分享码 / 链接 / 团队仓库),导入前自动做安全检查。'],
3825
3947
  cost: ['费用', '查看本次会话的 token 用量与花费。'],
3826
3948
  };
3827
3949
  document.querySelectorAll('.sk[data-sk]').forEach(b => { const h = SK_HELP[b.dataset.sk]; if (h && !b.title) b.title = h[1]; });
@@ -5303,6 +5425,8 @@
5303
5425
  const selBtn = document.getElementById('ib-sel-btn');
5304
5426
  selBtn.textContent = _ibSelMode ? '完成' : '☑ 多选';
5305
5427
  selBtn.style.display = mine.length > 1 || _ibSelMode ? '' : 'none';
5428
+ // 多选时只管勾选和批量操作,自动发送开关先藏起来;没草稿时也不显示
5429
+ document.getElementById('ib-autoall').style.display = _ibSelMode || !mine.length ? 'none' : '';
5306
5430
 
5307
5431
  // 暂停提示:有待自动发送的草稿,会话却已空闲一阵(上一轮被中断 / 出错时不会自动往下发)
5308
5432
  clearTimeout(_ibPauseTimer);
@@ -5922,6 +6046,7 @@
5922
6046
  const customs = _customSkills.filter(s => s.source !== 'builtin');
5923
6047
  // 内置技能:始终逐个显示(青绿色)
5924
6048
  const builtinEl = document.getElementById('sk-builtin-skills');
6049
+ loadSubs(); // 团队仓库有更新时快捷栏「技能」按钮带个绿点
5925
6050
  builtinEl.innerHTML = builtins.map(s =>
5926
6051
  `<button class="sk sk-claude" onclick="runSkill('${escHtml(s.name)}')" title="${escHtml(s.desc)}" style="color:var(--teal)">/${escHtml(s.name)}</button>`
5927
6052
  ).join('');
@@ -5930,14 +6055,15 @@
5930
6055
  if (!customs.length) { customEl.innerHTML = ''; return; }
5931
6056
  if (customs.length <= 3) {
5932
6057
  customEl.innerHTML = customs.map(s =>
5933
- `<button class="sk sk-claude" onclick="runSkill('${escHtml(s.name)}')" title="${escHtml(s.desc)}" style="color:var(--purple2)">/${escHtml(s.name)}</button>`
6058
+ `<button class="sk sk-claude" onclick="runSkill('${escHtml(s.name)}', false, '${s.kind || 'command'}')" title="${escHtml(s.desc)}${s.kind === 'skill' ? '(自动技能:AI 也会在合适时自己用)' : ''}" style="color:var(--purple2)">${s.kind === 'skill' ? '🧠' : '/'}${escHtml(s.name)}</button>`
5934
6059
  ).join('');
5935
6060
  } else {
5936
6061
  customEl.innerHTML = `<button class="sk sk-claude" onclick="openCmdSheet()" style="color:var(--purple2)">/命令 (${customs.length})</button>`;
5937
6062
  }
5938
6063
  } catch {}
5939
6064
  }
5940
- window.runSkill = function(name, skipPrompt) {
6065
+ window.runSkill = function(name, skipPrompt, kind) {
6066
+ _pendingSkillKind = kind || _customSkills.find(s => s.name === name)?.kind || 'command';
5941
6067
  // 直接运行(从弹窗确认后调用 skipPrompt=true)
5942
6068
  if (skipPrompt) {
5943
6069
  if (!isController && canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID });
@@ -5948,10 +6074,10 @@
5948
6074
  return;
5949
6075
  }
5950
6076
  // 先弹窗让用户补充要求
5951
- const skill = _customSkills.find(s => s.name === name);
6077
+ const skill = _customSkills.find(s => s.name === name && (s.kind || 'command') === _pendingSkillKind);
5952
6078
  openSkillRunSheet(name, skill?.desc);
5953
6079
  };
5954
- let _pendingSkillName = null;
6080
+ let _pendingSkillName = null, _pendingSkillKind = 'command';
5955
6081
  window.openSkillRunSheet = function(name, desc) {
5956
6082
  _pendingSkillName = name;
5957
6083
  const sheet = document.getElementById('skill-run-sheet');
@@ -5973,6 +6099,13 @@
5973
6099
  closeSkillRunSheet();
5974
6100
  if (!isController && canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID });
5975
6101
  const displayCmd = args ? `/${name} ${args}` : `/${name}`;
6102
+ if (_pendingSkillKind === 'skill') {
6103
+ // 自动技能(Agent 技能)没有 /命令:明确请 AI 用它
6104
+ socket.emit('agent:query', { prompt: `请使用「${name}」技能${args ? '完成:' + args : '。'}` });
6105
+ addInputMessage(`🧠 ${name}${args ? ' ' + args : ''}`);
6106
+ showThinking();
6107
+ return;
6108
+ }
5976
6109
  if (skill?.source === 'builtin') {
5977
6110
  // 内置技能不在 .claude/commands/ 里,需拉取内容后直接发送为 prompt
5978
6111
  try {
@@ -6005,8 +6138,8 @@
6005
6138
  const filtered = _customSkills.filter(s =>
6006
6139
  s.source !== 'builtin' && (!kw || s.name.toLowerCase().includes(kw) || (s.desc || '').toLowerCase().includes(kw)));
6007
6140
  listEl.innerHTML = filtered.length
6008
- ? filtered.map(s => `<div class="action-sheet-item" onclick="runSkillAndClose('${escHtml(s.name)}')">
6009
- <div><div style="color:var(--purple3)">/${escHtml(s.name)}</div>${s.desc ? `<div class="desc">${escHtml(s.desc)}</div>` : ''}</div>
6141
+ ? filtered.map(s => `<div class="action-sheet-item" onclick="runSkillAndClose('${escHtml(s.name)}', '${s.kind || 'command'}')">
6142
+ <div><div style="color:var(--purple3)">${s.kind === 'skill' ? '🧠 ' : '/'}${escHtml(s.name)}</div>${s.desc ? `<div class="desc">${escHtml(s.desc)}</div>` : ''}</div>
6010
6143
  </div>`).join('')
6011
6144
  : '<div style="text-align:center;color:var(--muted);padding:1.5rem 0;font-size:0.82rem">无匹配命令</div>';
6012
6145
  };
@@ -6018,9 +6151,9 @@
6018
6151
  window.closeCmdSheet = function() {
6019
6152
  document.getElementById('cmd-sheet').classList.remove('open');
6020
6153
  };
6021
- window.runSkillAndClose = function(name) {
6154
+ window.runSkillAndClose = function(name, kind) {
6022
6155
  closeCmdSheet();
6023
- runSkill(name);
6156
+ runSkill(name, false, kind);
6024
6157
  };
6025
6158
  // 加入会话后加载 skill
6026
6159
  socket.on('joined', () => {
@@ -6032,252 +6165,440 @@
6032
6165
  if (document.getElementById('inbox-sheet')?.classList.contains('open')) renderInbox();
6033
6166
  });
6034
6167
 
6035
- // ── 技能管理 ──────────────────────────────────
6036
- let _editingSkill = null; // 编辑中的技能名,null 表示新建
6168
+ // ── 技能管理(见 src/skills.js)────────────────────────
6169
+ // 手动技能 = Claude 的斜杠命令(.claude/commands/名.md);自动技能 = Agent 技能(.claude/skills/名/SKILL.md,AI 自己调用)。
6170
+ // ✨ 从本次任务生成(AI 起草、确认后保存)、📤 分享(文件 / 分享码 / 链接 / 团队仓库)、📥 导入(先预览 + 安全扫描)、👥 团队仓库订阅。
6171
+ let _editingSkill = null; // 编辑中的 { name, source, kind },null = 新建
6172
+ let _sklAll = [], _sklDirs = null, _sklShare = null, _sklImp = null, _gitSkills = [], _sklSubs = [];
6173
+ const SKL_TITLES = { list: '技能', edit: '编辑技能', gen: '✨ 从本次任务生成技能', share: '📤 分享技能', import: '📥 导入技能', team: '👥 团队技能仓库', market: '🛒 技能市场' };
6174
+ const sklEsc = (s) => escHtml(String(s ?? '')).replace(/"/g, '&quot;').replace(/'/g, '&#39;');
6175
+ const sklKindLabel = (k) => k === 'skill' ? '自动' : '手动';
6176
+ const sklScopeLabel = (s) => s === 'project' ? '本项目' : s === 'user' ? '本机' : '内置';
6177
+ async function sklJson(url, opts) {
6178
+ const r = await fetch(url, opts);
6179
+ const d = await r.json().catch(() => ({}));
6180
+ if (!r.ok || d.ok === false || d.error) { const e = new Error(d.error || ('HTTP ' + r.status)); e.data = d; throw e; }
6181
+ return d;
6182
+ }
6183
+ const sklPost = (url, body) => sklJson(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body || {}) });
6037
6184
 
6185
+ window.sklView = function(v) {
6186
+ for (const k of Object.keys(SKL_TITLES)) { const el = document.getElementById('skl-v-' + k); if (el) el.style.display = k === v ? '' : 'none'; }
6187
+ document.getElementById('skill-sheet-title').textContent = SKL_TITLES[v] || '技能';
6188
+ document.getElementById('skl-back').style.display = v === 'list' ? 'none' : '';
6189
+ if (v === 'list') refreshSkillList();
6190
+ if (v === 'team') renderSubs();
6191
+ if (v === 'import') { document.getElementById('skl-imp-preview').innerHTML = ''; setTimeout(() => document.getElementById('skl-imp-text').focus(), 80); }
6192
+ };
6038
6193
  window.openSkillSheet = async function() {
6039
6194
  document.getElementById('skill-sheet').classList.add('open');
6040
- document.getElementById('skill-list-view').style.display = '';
6041
- document.getElementById('skill-edit-view').style.display = 'none';
6042
- document.getElementById('skill-close-btn').style.display = '';
6043
- await refreshSkillList();
6044
- };
6045
- window.closeSkillSheet = function() {
6046
- document.getElementById('skill-sheet').classList.remove('open');
6195
+ sklView('list');
6047
6196
  };
6197
+ window.closeSkillSheet = function() { document.getElementById('skill-sheet').classList.remove('open'); };
6198
+ window.backToSkillList = function() { sklView('list'); };
6048
6199
 
6049
6200
  async function refreshSkillList() {
6050
6201
  const listEl = document.getElementById('skill-list-view');
6051
- listEl.innerHTML = '<div style="text-align:center;color:var(--muted);padding:2rem 0">加载中...</div>';
6202
+ listEl.innerHTML = '<div class="skl-empty">加载中…</div>';
6052
6203
  try {
6053
- const resp = await fetch(`/api/skills?sessionId=${SESSION_ID}&detail=1`);
6054
- const data = await resp.json();
6055
- const editable = (data.skills || []).filter(s => s.source !== 'builtin');
6056
- if (!editable.length) {
6057
- listEl.innerHTML = '<div style="text-align:center;color:var(--muted);padding:2rem 0">暂无技能,点击右上角 + 新建</div>';
6058
- return;
6059
- }
6060
- listEl.innerHTML = editable.map(s => `
6061
- <div class="action-sheet-item" style="gap:0.5rem">
6062
- <div style="min-width:0;flex:1;cursor:pointer" onclick="editSkill('${escHtml(s.name)}','${escHtml(s.source)}')">
6063
- <div style="color:var(--purple2);font-weight:500">/${escHtml(s.name)}</div>
6064
- <div class="desc">${escHtml(s.desc)} · ${s.source === 'project' ? '项目级' : '用户级'}</div>
6065
- </div>
6066
- <button onclick="deleteSkill('${escHtml(s.name)}','${escHtml(s.source)}')" style="background:none;border:none;color:var(--red);font-size:0.78rem;cursor:pointer;padding:0.3rem 0.5rem;white-space:nowrap">删除</button>
6067
- </div>
6068
- `).join('');
6204
+ const data = await sklJson(`/api/skills?sessionId=${SESSION_ID}`);
6205
+ _sklAll = data.skills || []; _sklDirs = data.dirs || null;
6206
+ const own = _sklAll.filter(s => s.source !== 'builtin');
6207
+ const group = (src, title) => {
6208
+ const items = own.filter(s => s.source === src);
6209
+ if (!items.length) return '';
6210
+ return `<div class="skl-grp">${title}</div>` + items.map(s => `
6211
+ <div class="skl-item">
6212
+ <div class="skl-main" onclick="editSkill('${sklEsc(s.name)}','${s.source}','${s.kind}')" title="点击编辑">
6213
+ <div><span class="skl-name">${s.kind === 'skill' ? '🧠 ' : '/'}${escHtml(s.name)}</span><span class="skl-tag ${s.kind}">${sklKindLabel(s.kind)}</span>${s.fileCount > 1 ? `<span class="skl-tag">${s.fileCount} 个文件</span>` : ''}</div>
6214
+ <div class="skl-desc">${escHtml(s.desc || '(没有说明)')}</div>
6215
+ </div>
6216
+ <div class="skl-acts">
6217
+ <button class="ib-ic" onclick="openSkillShare('${sklEsc(s.name)}','${s.source}','${s.kind}')" title="分享给其他 myhi">📤</button>
6218
+ <button class="ib-ic" onclick="deleteSkill('${sklEsc(s.name)}','${s.source}','${s.kind}')" title="删除">🗑</button>
6219
+ </div>
6220
+ </div>`).join('');
6221
+ };
6222
+ listEl.innerHTML = own.length ? group('project', '本项目') + group('user', '本机所有项目')
6223
+ : '<div class="skl-empty">还没有技能。<br>做完一件以后还会再做的事,点上面「✨ 从本次任务生成」,下次一点就能复用。</div>';
6069
6224
  } catch (e) {
6070
- listEl.innerHTML = `<div style="text-align:center;color:var(--red);padding:2rem 0">加载失败: ${e.message}</div>`;
6071
- }
6072
- }
6073
-
6074
- window.showSkillEditor = function(name, scope, content) {
6075
- _editingSkill = name || null;
6076
- document.getElementById('skill-list-view').style.display = 'none';
6077
- document.getElementById('skill-edit-view').style.display = '';
6078
- document.getElementById('skill-close-btn').style.display = 'none';
6079
- document.getElementById('skill-sheet-title').textContent = name ? `编辑 /${name}` : '新建技能';
6080
- document.getElementById('skill-name').value = name || '';
6081
- document.getElementById('skill-name').disabled = !!name;
6082
- document.getElementById('skill-scope').value = scope || 'user';
6083
- document.getElementById('skill-content').value = content || '# 技能标题\n\n在此输入技能指令...\n';
6084
- };
6085
-
6086
- window.editSkill = async function(name, source) {
6225
+ listEl.innerHTML = `<div class="skl-empty" style="color:var(--red)">加载失败:${escHtml(e.message)}</div>`;
6226
+ }
6227
+ sklCheckSubsQuietly();
6228
+ }
6229
+
6230
+ // ── 编辑 / 手写 ──
6231
+ const SKL_TPL = {
6232
+ command: '# 技能标题(一句话说明做什么)\n\n## 目标\n\n## 步骤\n1. \n2. \n\n## 注意事项\n\n(运行时用户补充的内容会替换下面的 $ARGUMENTS)\n$ARGUMENTS\n',
6233
+ skill: '---\nname: {{name}}\ndescription: 什么时候该用这个技能(写清楚场景,AI 据此判断要不要用)\n---\n\n# 技能标题\n\n## 步骤\n1. \n2. \n\n## 注意事项\n',
6234
+ };
6235
+ window.sklKindChanged = function() {
6236
+ const kind = document.getElementById('skill-kind').value;
6237
+ const ta = document.getElementById('skill-content');
6238
+ if (!_editingSkill && (!ta.value.trim() || Object.values(SKL_TPL).some(t => ta.value === t.replace('{{name}}', document.getElementById('skill-name').value.trim() || 'my-skill')))) {
6239
+ ta.value = SKL_TPL[kind].replace('{{name}}', document.getElementById('skill-name').value.trim() || 'my-skill');
6240
+ }
6241
+ document.getElementById('skill-edit-hint').innerHTML = (kind === 'skill'
6242
+ ? '自动技能:名字只能用<b>小写字母、数字和连字符</b>(如 weekly-report);开头 <code>description</code> 写清楚<b>什么时候用</b>,AI 遇到这类场景会自己调用。辅助脚本等其他文件放在同目录(可让 AI 帮你加)。'
6243
+ : '手动技能:名字用英文、数字、- _ .(a:b 可分组);第一行标题会显示成说明;<code>$ARGUMENTS</code> 会换成运行时你补充的内容。') + '<div id="skl-name-fix"></div>';
6244
+ sklNameCheck();
6245
+ };
6246
+ // 自动技能名字不合规:实时提示并给出改好的名字(一点就用)
6247
+ const sklSlug = (n) => (String(n || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').replace(/anthropic|claude/g, 'ai').slice(0, 64).replace(/-+$/, '') || 'my-skill');
6248
+ window.sklNameCheck = function() {
6249
+ const box = document.getElementById('skl-name-fix');
6250
+ if (!box) return;
6251
+ const n = document.getElementById('skill-name').value.trim();
6252
+ const kind = document.getElementById('skill-kind').value;
6253
+ const bad = kind === 'skill' && n && (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(n) || n.length > 64 || /anthropic|claude/.test(n));
6254
+ box.innerHTML = bad ? `<span style="color:var(--yellow)">⚠️ 这个名字 Claude 可能认不出来,</span><a style="color:var(--blue2);cursor:pointer" onclick="document.getElementById('skill-name').value='${sklSlug(n)}';sklNameCheck()">改成 ${sklSlug(n)}</a>` : '';
6255
+ };
6256
+ document.getElementById('skill-name')?.addEventListener('input', () => sklNameCheck());
6257
+ window.showSkillEditor = function(name, scope, content, kind) {
6258
+ _editingSkill = name ? { name, source: scope, kind: kind || 'command' } : null;
6259
+ sklView('edit');
6260
+ document.getElementById('skill-sheet-title').textContent = name ? `编辑 ${kind === 'skill' ? '🧠 ' : '/'}${name}` : '手写新技能';
6261
+ const n = document.getElementById('skill-name'); n.value = name || ''; n.disabled = !!name;
6262
+ document.getElementById('skill-kind').value = kind || 'command';
6263
+ document.getElementById('skill-kind').disabled = !!name;
6264
+ document.getElementById('skill-scope').value = scope || 'project';
6265
+ document.getElementById('skill-scope').disabled = !!name;
6266
+ document.getElementById('skill-content').value = content || '';
6267
+ sklKindChanged();
6268
+ };
6269
+ window.editSkill = async function(name, source, kind) {
6087
6270
  try {
6088
- const resp = await fetch(`/api/skills?sessionId=${SESSION_ID}&detail=1`);
6089
- const data = await resp.json();
6090
- const skill = data.skills?.find(s => s.name === name && s.source === source);
6091
- showSkillEditor(name, source, skill?.content || '');
6092
- } catch {}
6271
+ const d = await sklJson(`/api/skills/content?sessionId=${SESSION_ID}&name=${encodeURIComponent(name)}&source=${source}&kind=${kind}`);
6272
+ showSkillEditor(name, source, d.content || '', kind);
6273
+ } catch (e) { addStatusMessage('打开失败:' + e.message); }
6093
6274
  };
6094
-
6095
- window.backToSkillList = function() {
6096
- document.getElementById('skill-list-view').style.display = '';
6097
- document.getElementById('skill-edit-view').style.display = 'none';
6098
- document.getElementById('skill-import-view').style.display = 'none';
6099
- document.getElementById('skill-market-view').style.display = 'none';
6100
- document.getElementById('skill-close-btn').style.display = '';
6101
- document.getElementById('skill-sheet-title').textContent = '技能管理';
6102
- refreshSkillList();
6103
- };
6104
-
6105
6275
  window.saveSkill = async function() {
6106
6276
  const name = document.getElementById('skill-name').value.trim();
6107
6277
  const scope = document.getElementById('skill-scope').value;
6278
+ const kind = document.getElementById('skill-kind').value;
6108
6279
  const content = document.getElementById('skill-content').value;
6109
6280
  if (!name) { addStatusMessage('请输入技能名称'); return; }
6110
6281
  if (!content.trim()) { addStatusMessage('请输入技能内容'); return; }
6111
6282
  try {
6112
- const resp = await fetch(`/api/skills?sessionId=${SESSION_ID}`, {
6113
- method: 'POST',
6114
- headers: { 'Content-Type': 'application/json' },
6115
- body: JSON.stringify({ name, scope, content }),
6116
- });
6117
- const data = await resp.json();
6118
- if (data.error) { addStatusMessage('保存失败: ' + data.error); return; }
6119
- addStatusMessage(`技能 /${name} 已保存`);
6120
- backToSkillList();
6121
- loadSkills(); // 刷新快捷栏按钮
6122
- } catch (e) {
6123
- addStatusMessage('保存失败: ' + e.message);
6124
- }
6283
+ await sklPost(`/api/skills?sessionId=${SESSION_ID}`, { name, scope, content, kind });
6284
+ addStatusMessage(`技能 ${kind === 'skill' ? '🧠 ' : '/'}${name} 已保存`);
6285
+ sklView('list'); loadSkills();
6286
+ } catch (e) { addStatusMessage('保存失败:' + e.message); }
6287
+ };
6288
+ window.deleteSkill = async function(name, source, kind) {
6289
+ if (!confirm(`确定删除技能「${name}」?`)) return;
6290
+ try {
6291
+ await sklJson(`/api/skills?sessionId=${SESSION_ID}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, scope: source, kind }) });
6292
+ addStatusMessage(`技能「${name}」已删除`);
6293
+ refreshSkillList(); loadSkills();
6294
+ } catch (e) { addStatusMessage('删除失败:' + e.message); }
6295
+ };
6296
+
6297
+ // ── ✨ 从本次任务生成:交给当前任务的 AI(它最清楚刚才怎么做成的),先出草稿、确认后才写文件 ──
6298
+ window.genSkillFromTask = function() {
6299
+ if (currentSession?.mode !== 'agent') return addStatusMessage('只有 AI 任务里可以生成技能');
6300
+ if (workState !== 'idle') return addStatusMessage('AI 正在干活,等这一轮结束再生成技能');
6301
+ const hint = document.getElementById('skl-gen-hint').value.trim();
6302
+ const kind = document.querySelector('input[name=skl-gen-kind]:checked').value;
6303
+ const scope = document.querySelector('input[name=skl-gen-scope]:checked').value;
6304
+ const dirs = _sklDirs?.[scope];
6305
+ const dir = dirs ? (kind === 'skill' ? dirs.skill : dirs.command) : (scope === 'project' ? (kind === 'skill' ? '.claude/skills' : '.claude/commands') : (kind === 'skill' ? '~/.claude/skills' : '~/.claude/commands'));
6306
+ const fmt = kind === 'skill'
6307
+ ? `类型:Agent 技能(AI 遇到合适场景会自己调用)。在 ${dir}/<名称>/ 下建 SKILL.md:开头 YAML frontmatter 写 name(同目录名)和 description(用中文写清楚「什么场景该用这个技能」,AI 靠它判断);正文写目标、分步骤做法、注意事项、完成标准。需要辅助脚本就放在同一目录,并在 SKILL.md 里写明怎么用。`
6308
+ : `类型:斜杠命令(用户点按钮或输入 /名称 运行)。文件 ${dir}/<名称>.md:第一行是「# 中文标题」(myhi 拿它当说明),正文写目标、前提、分步骤做法、注意事项、完成标准;每次运行需要用户补充的内容用 $ARGUMENTS 表示。`;
6309
+ const prompt = [
6310
+ `请把本次会话里我们完成的工作提炼成一个可以复用的 Claude Code 技能${hint ? ',我的想法:' + hint : '(你判断最值得沉淀的那套做法)'}。`,
6311
+ fmt,
6312
+ kind === 'skill' ? '名称只能用小写字母、数字和连字符(如 deploy-test,不超过 64 个字符,不能含 anthropic / claude),目录名和 SKILL.md 里的 name 要一致。' : '名称用小写英文和连字符(如 deploy-test)。',
6313
+ '把本项目特有的路径、名字、账号改成参数或说明,让它在别的项目、别的电脑上也能用;不要写入任何密码、token、密钥或客户的真实敏感信息。',
6314
+ '先把完整草稿(含名称和每个文件的内容)贴出来问我是否保存(可以直接保存 / 我要修改);我确认之后再写入文件。在我确认之前不要创建或修改任何文件。如果同名技能已经存在,先告诉我。',
6315
+ ].join('\n');
6316
+ closeSkillSheet();
6317
+ if (!isController && canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID });
6318
+ const display = `✨ 把本次任务的做法生成${kind === 'skill' ? '自动' : '手动'}技能(${scope === 'project' ? '本项目' : '本机所有项目'})${hint ? ':' + hint : ''}`;
6319
+ addInputMessage(display);
6320
+ socket.emit('agent:query', { prompt, display });
6321
+ showThinking();
6322
+ _sklGenPending = scope;
6323
+ };
6324
+ let _sklGenPending = false; // 生成中(存范围):这轮结束刷新快捷栏的技能按钮,并让相关任务重新加载技能
6325
+
6326
+ // ── 📤 分享 ──
6327
+ window.openSkillShare = function(name, source, kind) {
6328
+ _sklShare = { name, source, kind, force: false };
6329
+ sklView('share');
6330
+ document.getElementById('skl-share-info').innerHTML = `<b>${kind === 'skill' ? '🧠 ' : '/'}${escHtml(name)}</b> <span class="skl-tag ${kind}">${sklKindLabel(kind)}</span><span class="skl-tag">${sklScopeLabel(source)}</span>`;
6331
+ document.getElementById('skl-share-warn').style.display = 'none';
6332
+ document.getElementById('skl-share-out').style.display = 'none';
6333
+ document.getElementById('skl-pub-form').style.display = 'none';
6334
+ };
6335
+ function sklShareFail(e, retry) {
6336
+ const w = document.getElementById('skl-share-warn');
6337
+ if (e.data?.secrets) {
6338
+ w.innerHTML = `⚠️ ${escHtml(e.message)}<div style="margin-top:0.35rem;display:flex;gap:0.4rem"><button class="dr-btn" onclick="editSkill('${sklEsc(_sklShare.name)}','${_sklShare.source}','${_sklShare.kind}')">去编辑</button><button class="dr-btn" id="skl-force-btn">确认没有敏感信息,继续</button></div>`;
6339
+ w.style.display = '';
6340
+ document.getElementById('skl-force-btn').onclick = () => { _sklShare.force = true; w.style.display = 'none'; retry(); };
6341
+ } else addStatusMessage('分享失败:' + e.message);
6342
+ }
6343
+ const sklShareBody = () => ({ sessionId: SESSION_ID, name: _sklShare.name, source: _sklShare.source, kind: _sklShare.kind, force: _sklShare.force });
6344
+ function sklOut(html) { const o = document.getElementById('skl-share-out'); o.innerHTML = html; o.style.display = ''; }
6345
+ async function sklCopy(text, ok) {
6346
+ try { await navigator.clipboard.writeText(text); addStatusMessage(ok); }
6347
+ catch { const ta = document.createElement('textarea'); ta.value = text; document.body.appendChild(ta); ta.select(); try { document.execCommand('copy'); addStatusMessage(ok); } catch {} ta.remove(); }
6348
+ }
6349
+ window.sklShareDownload = async function() {
6350
+ try {
6351
+ const d = await sklPost('/api/skills/export', sklShareBody());
6352
+ const blob = new Blob([JSON.stringify(d.pkg, null, 2)], { type: 'application/json' });
6353
+ const a = document.createElement('a');
6354
+ a.href = URL.createObjectURL(blob); a.download = d.pkg.name.replace(/:/g, '-') + '.myhiskill';
6355
+ document.body.appendChild(a); a.click(); a.remove(); setTimeout(() => URL.revokeObjectURL(a.href), 2000);
6356
+ sklOut('✓ 已下载 <b>' + escHtml(a.download) + '</b>。发给对方后,在对方 myhi 的「技能 → 📥 导入」里选择这个文件。');
6357
+ } catch (e) { sklShareFail(e, sklShareDownload); }
6358
+ };
6359
+ window.sklShareCode = async function() {
6360
+ try {
6361
+ const d = await sklPost('/api/skills/export', sklShareBody());
6362
+ await sklCopy(d.code, '分享码已复制');
6363
+ sklOut(`✓ 分享码已复制(${d.code.length} 字符)。对方在「技能 → 📥 导入」里粘贴即可。<textarea class="slash-inp" rows="3" readonly style="width:100%;box-sizing:border-box;margin-top:0.35rem;font-size:0.7rem" onclick="this.select()">${escHtml(d.code)}</textarea>`);
6364
+ } catch (e) { sklShareFail(e, sklShareCode); }
6365
+ };
6366
+ window.sklShareLink = async function() {
6367
+ try {
6368
+ const d = await sklPost('/api/skills/share-link', sklShareBody());
6369
+ await sklCopy(d.url, '分享链接已复制');
6370
+ sklOut(`✓ 链接已复制,<b>${new Date(d.expires).toLocaleString()}</b> 前有效。对方在「📥 导入」里粘贴(两台 myhi 要能互相访问):<input class="slash-inp" readonly value="${sklEsc(d.url)}" style="width:100%;box-sizing:border-box;margin-top:0.35rem;font-size:0.72rem" onclick="this.select()">`);
6371
+ } catch (e) { sklShareFail(e, sklShareLink); }
6372
+ };
6373
+ window.sklSharePublishForm = function() {
6374
+ const f = document.getElementById('skl-pub-form');
6375
+ f.style.display = f.style.display === 'none' ? '' : 'none';
6376
+ const last = JSON.parse(localStorage.getItem('myhi_skl_pub') || 'null') || (_sklSubs[0] ? { url: _sklSubs[0].url, username: _sklSubs[0].username, branch: _sklSubs[0].branch } : {});
6377
+ if (!document.getElementById('skl-pub-url').value) {
6378
+ document.getElementById('skl-pub-url').value = last.url || '';
6379
+ document.getElementById('skl-pub-user').value = last.username || '';
6380
+ document.getElementById('skl-pub-branch').value = last.branch || '';
6381
+ }
6382
+ };
6383
+ window.sklPublish = async function() {
6384
+ const url = document.getElementById('skl-pub-url').value.trim();
6385
+ if (!url) { addStatusMessage('请填写团队仓库地址'); return; }
6386
+ const body = { ...sklShareBody(), url, username: document.getElementById('skl-pub-user').value.trim(), password: document.getElementById('skl-pub-pass').value, branch: document.getElementById('skl-pub-branch').value.trim() };
6387
+ sklOut('发布中…(克隆仓库、提交、推送)');
6388
+ try {
6389
+ const d = await sklPost('/api/skills/publish', body);
6390
+ localStorage.setItem('myhi_skl_pub', JSON.stringify({ url, username: body.username, branch: body.branch }));
6391
+ sklOut(d.unchanged ? '仓库里已经是这个版本,不用重复发布。' : '✓ 已发布到团队仓库。订阅了这个仓库的 myhi「检查更新」时会看到。');
6392
+ } catch (e) { document.getElementById('skl-share-out').style.display = 'none'; sklShareFail(e, sklPublish); }
6125
6393
  };
6126
6394
 
6127
- window.deleteSkill = async function(name, source) {
6128
- if (!confirm(`确定删除技能 /${name}?`)) return;
6395
+ // ── 📥 导入:分享码 / 分享链接 / 文件 → 预览 + 安全扫描 → 选范围安装 ──
6396
+ window.sklImportFile = async function(file) {
6397
+ if (!file) return;
6398
+ const text = await file.text();
6399
+ let body;
6400
+ if (/\.md$/i.test(file.name)) {
6401
+ // 直接给了一个 .md:按手动技能处理,名称取文件名
6402
+ const name = file.name.replace(/\.md$/i, '').replace(/[^\w.-]+/g, '-');
6403
+ body = { pkg: { myhiSkill: 1, kind: 'command', name, files: [{ path: name + '.md', content: text }] } };
6404
+ } else if (text.trim().startsWith('MYHI-SKILL:')) body = { code: text };
6405
+ else body = { pkg: text };
6406
+ sklImportPreview(body);
6407
+ };
6408
+ window.sklImportPreview = async function(body) {
6409
+ const box = document.getElementById('skl-imp-preview');
6410
+ if (!body) {
6411
+ const t = document.getElementById('skl-imp-text').value.trim();
6412
+ if (!t) { addStatusMessage('先粘贴分享码 / 分享链接,或选择技能文件'); return; }
6413
+ body = /^https?:\/\/\S+\/s\/skill\/[\w-]+/.test(t) ? { url: t.match(/https?:\/\/\S+\/s\/skill\/[\w-]+/)[0] } : { code: t };
6414
+ }
6415
+ box.innerHTML = '<div class="skl-empty">读取中…</div>';
6129
6416
  try {
6130
- const resp = await fetch(`/api/skills?sessionId=${SESSION_ID}`, {
6131
- method: 'DELETE',
6132
- headers: { 'Content-Type': 'application/json' },
6133
- body: JSON.stringify({ name, scope: source }),
6134
- });
6135
- const data = await resp.json();
6136
- if (data.error) { addStatusMessage('删除失败: ' + data.error); return; }
6137
- addStatusMessage(`技能 /${name} 已删除`);
6138
- refreshSkillList();
6139
- loadSkills();
6417
+ const d = await sklPost('/api/skills/preview', { sessionId: SESSION_ID, ...body });
6418
+ _sklImp = d;
6419
+ const p = d.pkg;
6420
+ const danger = d.findings.filter(f => f.level === 'danger'), warn = d.findings.filter(f => f.level === 'warn');
6421
+ box.innerHTML = `
6422
+ <div class="skl-card">
6423
+ <div><b>${p.kind === 'skill' ? '🧠 ' : '/'}${escHtml(p.name)}</b> <span class="skl-tag ${p.kind}">${sklKindLabel(p.kind)}</span>${p.from ? `<span class="skl-tag">来自 ${escHtml(p.from)}</span>` : ''}</div>
6424
+ <div class="skl-desc" style="margin-top:0.2rem">${escHtml(p.desc || '(没有说明)')}</div>
6425
+ ${p.renamedFrom ? `<div class="skl-hint" style="color:var(--yellow)">原名「${escHtml(p.renamedFrom)}」不符合自动技能的命名规范,安装时改成「${escHtml(p.name)}」</div>` : ''}
6426
+ </div>
6427
+ ${sklFindingsHtml(d.findings)}
6428
+ <details class="skl-files"><summary>查看全部内容(${p.files.length} 个文件)——安装前建议过一遍</summary>
6429
+ ${p.files.map(f => `<div class="skl-fname">${escHtml(f.path)}</div><pre>${escHtml(f.content)}</pre>`).join('')}
6430
+ </details>
6431
+ <div class="skl-row" style="margin-top:0.5rem">
6432
+ <select id="skl-imp-scope" class="slash-inp skl-sel" onchange="sklImpExists()">
6433
+ <option value="project">只装到本项目</option>
6434
+ <option value="user">装到本机所有项目</option>
6435
+ </select>
6436
+ </div>
6437
+ <div id="skl-imp-exists" class="skl-hint"></div>
6438
+ ${danger.length ? '<label class="skl-confirm"><input type="checkbox" id="skl-imp-ack"> 我已看过上面标红的内容,确认这个技能来源可信</label>' : ''}
6439
+ <button class="action-sheet-cancel" style="background:${danger.length ? 'var(--red)' : 'var(--green)'};color:#fff;font-weight:600;margin-top:0.4rem" onclick="sklInstall()">${danger.length ? '⚠️ 仍然安装' : '安装'}</button>`;
6440
+ // 带脚本的技能默认只装本项目
6441
+ document.getElementById('skl-imp-scope').value = p.kind === 'skill' && p.files.length > 1 ? 'project' : (warn.length || danger.length ? 'project' : 'user');
6442
+ sklImpExists();
6140
6443
  } catch (e) {
6141
- addStatusMessage('删除失败: ' + e.message);
6444
+ box.innerHTML = `<div class="skl-warn">⚠️ ${escHtml(e.message)}</div>`;
6142
6445
  }
6143
6446
  };
6144
-
6145
- // ── Git 安装技能 ──────────────────────────────
6146
- let _gitSkills = []; // 扫描到的技能列表
6147
-
6148
- window.showSkillImport = function() {
6149
- document.getElementById('skill-list-view').style.display = 'none';
6150
- document.getElementById('skill-edit-view').style.display = 'none';
6151
- document.getElementById('skill-import-view').style.display = '';
6152
- document.getElementById('skill-close-btn').style.display = 'none';
6153
- document.getElementById('skill-sheet-title').textContent = '从 Git 安装技能';
6154
- document.getElementById('skill-import-preview').style.display = 'none';
6155
- document.getElementById('skill-import-install-btn').style.display = 'none';
6156
- document.getElementById('skill-import-scan-btn').innerHTML = '';
6157
- document.getElementById('skill-git-url').value = '';
6158
- _gitSkills = [];
6159
- setTimeout(() => document.getElementById('skill-git-url').focus(), 100);
6447
+ function sklFindingsHtml(findings) {
6448
+ if (!findings?.length) return '<div class="skl-ok">✓ 安全检查没发现可疑写法</div>';
6449
+ return '<div class="skl-findings">' + findings.map(f => `<div class="skl-f ${f.level}">${f.level === 'danger' ? '⛔' : '⚠️'} ${escHtml(f.why)}${f.file ? ` <small>${escHtml(f.file)}${f.line ? ' 第 ' + f.line + ' 行' : ''}</small>` : ''}${f.text ? `<code>${escHtml(f.text)}</code>` : ''}</div>`).join('') + '</div>';
6450
+ }
6451
+ window.sklImpExists = function() {
6452
+ if (!_sklImp) return;
6453
+ const sc = document.getElementById('skl-imp-scope').value;
6454
+ document.getElementById('skl-imp-exists').textContent = _sklImp.exists?.[sc] ? `⚠️ ${sc === 'project' ? '本项目' : '本机'}已经有同名技能,安装会覆盖它` : '';
6160
6455
  };
6161
-
6162
- document.getElementById('skill-git-url')?.addEventListener('keydown', (e) => {
6163
- if (e.key === 'Enter') { e.preventDefault(); scanGitSkills(); }
6164
- });
6165
-
6456
+ window.sklInstall = async function() {
6457
+ if (!_sklImp) return;
6458
+ const ack = document.getElementById('skl-imp-ack');
6459
+ if (ack && !ack.checked) { addStatusMessage('这个技能里有标红的可疑内容:确认来源可信后先勾选确认'); return; }
6460
+ const scope = document.getElementById('skl-imp-scope').value;
6461
+ try {
6462
+ await sklPost('/api/skills/install', { sessionId: SESSION_ID, pkg: _sklImp.pkg, scope });
6463
+ addStatusMessage(`已安装技能「${_sklImp.pkg.name}」(${scope === 'project' ? '本项目' : '本机所有项目'}),下一轮 AI 就能用`);
6464
+ _sklImp = null; document.getElementById('skl-imp-text').value = '';
6465
+ sklView('list'); loadSkills();
6466
+ } catch (e) { addStatusMessage('安装失败:' + e.message); }
6467
+ };
6468
+
6469
+ // ── 👥 团队仓库:订阅列表 / 检查更新 / 扫描安装 ──
6470
+ async function loadSubs() { try { _sklSubs = (await sklJson('/api/skills/subs')).subs || []; } catch { _sklSubs = []; } updateSubBadge(); return _sklSubs; }
6471
+ function updateSubBadge() {
6472
+ const n = _sklSubs.reduce((a, s) => a + (s.updates?.length || 0), 0);
6473
+ const b = document.getElementById('skl-upd-badge'), ban = document.getElementById('skl-upd-banner');
6474
+ if (b) { b.textContent = n; b.style.display = n ? '' : 'none'; }
6475
+ if (ban) { ban.textContent = `👥 团队仓库有 ${n} 个技能更新 / 新技能 · 点这里查看`; ban.style.display = n ? '' : 'none'; }
6476
+ const skBtn = document.querySelector('#sk-claude-agent .sk[data-sk="skills"]');
6477
+ if (skBtn) skBtn.classList.toggle('skl-dot', n > 0);
6478
+ }
6479
+ let _sklAutoChecked = false;
6480
+ async function sklCheckSubsQuietly() {
6481
+ await loadSubs();
6482
+ if (_sklAutoChecked) return;
6483
+ _sklAutoChecked = true;
6484
+ for (const s of _sklSubs) {
6485
+ if (Date.now() - (s.lastCheck || 0) < 6 * 3600 * 1000) continue; // 6 小时内查过就不再查
6486
+ try { await sklPost(`/api/skills/subs/${s.id}/check`); } catch {}
6487
+ }
6488
+ await loadSubs();
6489
+ }
6490
+ const maskUrl = (u) => String(u || '').replace(/\/\/[^@/]+@/, '//');
6491
+ async function renderSubs() {
6492
+ const box = document.getElementById('skl-subs');
6493
+ await loadSubs();
6494
+ box.innerHTML = _sklSubs.length ? '<div class="skl-lbl">已订阅</div>' + _sklSubs.map(s => `
6495
+ <div class="skl-card skl-sub">
6496
+ <div style="display:flex;align-items:center;gap:0.4rem">
6497
+ <div style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${sklEsc(maskUrl(s.url))}">${escHtml(maskUrl(s.url).replace(/^https?:\/\//, ''))}</div>
6498
+ <span class="skl-tag">${s.scope === 'project' ? '本项目' : '本机'}</span>
6499
+ </div>
6500
+ <div class="skl-desc">已装 ${Object.keys(s.installed || {}).length} 个 · ${s.lastCheck ? '上次检查 ' + new Date(s.lastCheck).toLocaleString() : '还没检查过'}${s.updates?.length ? ` · <b style="color:var(--green)">${s.updates.length} 个更新</b>` : ''}</div>
6501
+ <div class="skl-row" style="margin-top:0.35rem">
6502
+ <button class="dr-btn take" onclick="sklSubCheck('${s.id}')">检查更新</button>
6503
+ <button class="dr-btn" style="margin-left:auto" onclick="sklSubRemove('${s.id}')">取消订阅</button>
6504
+ </div>
6505
+ <div id="skl-sub-out-${s.id}"></div>
6506
+ </div>`).join('') : '<div class="skl-hint">还没有订阅团队仓库。团队把技能放在一个 Git 仓库里(commands/、skills/ 目录),大家订阅后就能一起用、收到更新;也可以在技能的「📤 分享」里发布到仓库。</div>';
6507
+ }
6508
+ function sklPackListHtml(list, withStatus) {
6509
+ return list.map((s, i) => `
6510
+ <label class="skl-pick">
6511
+ <input type="checkbox" ${s.findings?.some(f => f.level === 'danger') ? '' : 'checked'} data-key="${sklEsc(s.kind + ':' + s.name)}">
6512
+ <div style="min-width:0;flex:1">
6513
+ <div><span class="skl-name">${s.kind === 'skill' ? '🧠 ' : '/'}${escHtml(s.name)}</span><span class="skl-tag ${s.kind}">${sklKindLabel(s.kind)}</span>${withStatus ? `<span class="skl-tag ${s.status === 'new' ? 'skill' : ''}">${s.status === 'new' ? '新技能' : '有更新'}</span>` : ''}</div>
6514
+ <div class="skl-desc">${escHtml(s.desc || '')}</div>
6515
+ ${s.findings?.length ? sklFindingsHtml(s.findings) : ''}
6516
+ </div>
6517
+ </label>`).join('');
6518
+ }
6519
+ window.sklSubCheck = async function(id) {
6520
+ const out = document.getElementById('skl-sub-out-' + id);
6521
+ out.innerHTML = '<div class="skl-empty">检查中…</div>';
6522
+ try {
6523
+ const d = await sklPost(`/api/skills/subs/${id}/check`);
6524
+ await loadSubs();
6525
+ if (!d.updates.length) { out.innerHTML = '<div class="skl-ok">✓ 已是最新</div>'; return; }
6526
+ out.innerHTML = sklPackListHtml(d.updates, true) + `<button class="action-sheet-cancel" style="background:var(--green);color:#fff;font-weight:600;margin-top:0.4rem" onclick="sklSubUpdate('${id}')">更新 / 安装选中的</button>`;
6527
+ } catch (e) { out.innerHTML = `<div class="skl-warn">⚠️ ${escHtml(e.message)}</div>`; }
6528
+ };
6529
+ window.sklSubUpdate = async function(id) {
6530
+ const names = [...document.querySelectorAll(`#skl-sub-out-${id} input[type=checkbox]:checked`)].map(c => c.dataset.key);
6531
+ if (!names.length) { addStatusMessage('请至少选一个'); return; }
6532
+ try {
6533
+ const d = await sklPost('/api/skills/repo/install', { sessionId: SESSION_ID, subId: id, names });
6534
+ addStatusMessage(`已更新 ${d.installed.length} 个技能`);
6535
+ loadSkills(); renderSubs();
6536
+ } catch (e) { addStatusMessage('更新失败:' + e.message); }
6537
+ };
6538
+ window.sklSubRemove = async function(id) {
6539
+ if (!confirm('取消订阅这个仓库?已经装好的技能会保留。')) return;
6540
+ try { await sklJson(`/api/skills/subs/${id}`, { method: 'DELETE' }); } catch {}
6541
+ renderSubs();
6542
+ };
6543
+ document.getElementById('skill-git-url')?.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); scanGitSkills(); } });
6166
6544
  window.scanGitSkills = async function() {
6167
6545
  const url = document.getElementById('skill-git-url').value.trim();
6168
6546
  if (!url) { addStatusMessage('请输入 Git 仓库地址'); return; }
6169
- const statusEl = document.getElementById('skill-import-scan-btn');
6170
- statusEl.innerHTML = '<div style="text-align:center;color:var(--muted);padding:0.5rem 0">正在克隆并扫描...</div>';
6171
- document.getElementById('skill-import-preview').style.display = 'none';
6172
- document.getElementById('skill-import-install-btn').style.display = 'none';
6547
+ const box = document.getElementById('skill-import-preview');
6548
+ box.innerHTML = '<div class="skl-empty">正在克隆并扫描…</div>';
6173
6549
  try {
6174
- const username = document.getElementById('skill-git-user').value.trim();
6175
- const password = document.getElementById('skill-git-pass').value;
6176
- const resp = await fetch('/api/skills/scan-git', {
6177
- method: 'POST',
6178
- headers: { 'Content-Type': 'application/json' },
6179
- body: JSON.stringify({ url, username: username || undefined, password: password || undefined }),
6180
- });
6181
- const data = await resp.json();
6182
- statusEl.innerHTML = '';
6183
- if (data.error) { addStatusMessage(data.error); return; }
6184
- _gitSkills = data.skills || [];
6185
- if (!_gitSkills.length) {
6186
- addStatusMessage(data.message || '未找到技能文件');
6187
- return;
6188
- }
6189
- const preview = document.getElementById('skill-import-preview');
6190
- preview.innerHTML = _gitSkills.map((s, i) => `
6191
- <label style="display:flex;align-items:flex-start;gap:0.5rem;padding:0.4rem 0;border-bottom:1px solid var(--surface2);cursor:pointer">
6192
- <input type="checkbox" checked data-skill-idx="${i}" style="margin-top:0.15rem">
6193
- <div style="min-width:0;flex:1">
6194
- <div style="color:var(--purple2);font-weight:500">/${escHtml(s.name)}</div>
6195
- <div style="color:var(--muted);font-size:0.72rem">${escHtml(s.desc)}</div>
6196
- </div>
6197
- </label>
6198
- `).join('');
6199
- preview.style.display = '';
6200
- document.getElementById('skill-import-install-btn').style.display = '';
6201
- } catch (e) {
6202
- addStatusMessage('扫描失败: ' + e.message);
6203
- statusEl.innerHTML = '';
6204
- }
6550
+ const d = await sklPost('/api/skills/repo/scan', { url, username: document.getElementById('skill-git-user').value.trim() || undefined, password: document.getElementById('skill-git-pass').value || undefined });
6551
+ _gitSkills = d.skills || [];
6552
+ if (!_gitSkills.length) { box.innerHTML = '<div class="skl-hint">这个仓库里没找到技能(commands/*.md、skills/*/SKILL.md)</div>'; return; }
6553
+ box.innerHTML = sklPackListHtml(_gitSkills, false) +
6554
+ '<label class="skl-confirm"><input type="checkbox" id="skl-git-sub" checked> 订阅这个仓库:以后有更新 / 新技能时提示我</label>' +
6555
+ '<button class="action-sheet-cancel" style="background:var(--green);color:#fff;font-weight:600;margin-top:0.4rem" onclick="installGitSkills()">安装选中的技能</button>';
6556
+ } catch (e) { box.innerHTML = `<div class="skl-warn">⚠️ ${escHtml(e.message)}</div>`; }
6205
6557
  };
6206
-
6207
6558
  window.installGitSkills = async function() {
6208
- const scope = document.getElementById('skill-import-scope').value;
6209
- const checkboxes = document.querySelectorAll('#skill-import-preview input[type=checkbox]');
6210
- const selected = [];
6211
- checkboxes.forEach(cb => {
6212
- if (cb.checked) selected.push(_gitSkills[parseInt(cb.dataset.skillIdx)]);
6213
- });
6214
- if (!selected.length) { addStatusMessage('请至少选择一个技能'); return; }
6215
- const btn = document.getElementById('skill-import-install-btn');
6216
- btn.textContent = '安装中...';
6217
- btn.disabled = true;
6218
- let ok = 0, fail = 0;
6219
- for (const s of selected) {
6220
- try {
6221
- const resp = await fetch(`/api/skills?sessionId=${SESSION_ID}`, {
6222
- method: 'POST',
6223
- headers: { 'Content-Type': 'application/json' },
6224
- body: JSON.stringify({ name: s.name, scope, content: s.content }),
6225
- });
6226
- const data = await resp.json();
6227
- if (data.error) fail++;
6228
- else ok++;
6229
- } catch { fail++; }
6230
- }
6231
- addStatusMessage(`安装完成: ${ok} 个成功${fail ? `, ${fail} 个失败` : ''}`);
6232
- btn.textContent = '安装选中的技能';
6233
- btn.disabled = false;
6234
- backToSkillList();
6235
- loadSkills();
6559
+ const names = [...document.querySelectorAll('#skill-import-preview input[data-key]:checked')].map(c => c.dataset.key);
6560
+ if (!names.length) { addStatusMessage('请至少选择一个技能'); return; }
6561
+ const dangerSel = _gitSkills.filter(s => names.includes(s.kind + ':' + s.name) && s.findings?.some(f => f.level === 'danger'));
6562
+ if (dangerSel.length && !confirm(`选中的技能里有 ${dangerSel.length} 个带标红的可疑内容(${dangerSel.map(s => s.name).join('、')})。确认来源可信、仍然安装?`)) return;
6563
+ try {
6564
+ const d = await sklPost('/api/skills/repo/install', {
6565
+ sessionId: SESSION_ID, names, scope: document.getElementById('skill-import-scope').value,
6566
+ url: document.getElementById('skill-git-url').value.trim(), username: document.getElementById('skill-git-user').value.trim() || undefined,
6567
+ password: document.getElementById('skill-git-pass').value || undefined, subscribe: document.getElementById('skl-git-sub')?.checked,
6568
+ });
6569
+ addStatusMessage(`已安装 ${d.installed.length} 个技能${d.sub ? ',并订阅了这个仓库' : ''}`);
6570
+ document.getElementById('skill-import-preview').innerHTML = '';
6571
+ loadSkills(); renderSubs();
6572
+ } catch (e) { addStatusMessage('安装失败:' + e.message); }
6236
6573
  };
6237
6574
 
6238
- // ── 技能市场 ──────────────────────────────────
6575
+ // ── 🛒 技能市场(社区仓库,走团队仓库的扫描安装,默认不订阅)──
6239
6576
  const SKILL_MARKET = [
6240
- { name: 'Claude Command Suite', desc: '216+ 命令, 12 技能, 54 AI Agent — 全面的软件工程工作流', repo: 'qdhenry/Claude-Command-Suite', stars: '1k+', path: 'commands' },
6241
- { name: 'Production Commands', desc: '全栈开发、安全、数据/ML、DevOps 命令集', repo: 'wshobson/commands', stars: '500+', path: '' },
6242
- { name: 'Claude Slash Commands', desc: '轻量级常用命令集合,直接可用', repo: 'artemgetmann/claude-slash-commands', stars: '200+', path: '' },
6243
- { name: 'Claude Commands', desc: '自定义命令合集,附完整文档', repo: 'david-t-martel/claude-commands', stars: '100+', path: '' },
6244
- { name: 'Claude Code Skills', desc: '专业技能市场,生产级 Skill 合集', repo: 'daymade/claude-code-skills', stars: '100+', path: '' },
6245
- { name: 'My Claude Code', desc: '日常使用的精选命令和工作流', repo: 'vincenthopf/My-Claude-Code', stars: '50+', path: '' },
6577
+ { name: 'Claude Command Suite', desc: '216+ 命令, 12 技能, 54 AI Agent — 全面的软件工程工作流', repo: 'qdhenry/Claude-Command-Suite', stars: '1k+' },
6578
+ { name: 'Production Commands', desc: '全栈开发、安全、数据/ML、DevOps 命令集', repo: 'wshobson/commands', stars: '500+' },
6579
+ { name: 'Claude Slash Commands', desc: '轻量级常用命令集合,直接可用', repo: 'artemgetmann/claude-slash-commands', stars: '200+' },
6580
+ { name: 'Claude Commands', desc: '自定义命令合集,附完整文档', repo: 'david-t-martel/claude-commands', stars: '100+' },
6581
+ { name: 'Claude Code Skills', desc: '专业技能市场,生产级 Skill 合集', repo: 'daymade/claude-code-skills', stars: '100+' },
6582
+ { name: 'My Claude Code', desc: '日常使用的精选命令和工作流', repo: 'vincenthopf/My-Claude-Code', stars: '50+' },
6246
6583
  ];
6247
-
6248
6584
  window.showSkillMarket = function() {
6249
- document.getElementById('skill-list-view').style.display = 'none';
6250
- document.getElementById('skill-edit-view').style.display = 'none';
6251
- document.getElementById('skill-import-view').style.display = 'none';
6252
- document.getElementById('skill-market-view').style.display = '';
6253
- document.getElementById('skill-close-btn').style.display = 'none';
6254
- document.getElementById('skill-sheet-title').textContent = '技能市场';
6255
-
6256
- const list = document.getElementById('skill-market-list');
6257
- list.innerHTML = '<div style="font-size:0.72rem;color:var(--muted);padding:0.3rem 0.5rem;border-bottom:1px solid var(--surface2)">热门社区技能仓库 · 点击安装</div>' +
6585
+ sklView('market');
6586
+ document.getElementById('skill-market-list').innerHTML = '<div class="skl-hint">热门社区技能仓库 · 点一个扫描,选择要装的(安装前会做安全检查)</div>' +
6258
6587
  SKILL_MARKET.map((m, i) => `
6259
- <div class="action-sheet-item" style="gap:0.5rem">
6260
- <div style="min-width:0;flex:1;cursor:pointer" onclick="installFromMarket(${i})">
6588
+ <div class="skl-item" style="cursor:pointer" onclick="installFromMarket(${i})">
6589
+ <div class="skl-main">
6261
6590
  <div style="font-weight:500">${escHtml(m.name)} <span style="color:var(--muted);font-size:0.7rem">★ ${m.stars}</span></div>
6262
- <div class="desc">${escHtml(m.desc)}</div>
6591
+ <div class="skl-desc">${escHtml(m.desc)}</div>
6263
6592
  <div style="color:var(--blue);font-size:0.68rem">${escHtml(m.repo)}</div>
6264
6593
  </div>
6265
- </div>
6266
- `).join('') +
6267
- '<div style="padding:0.6rem 0.5rem;font-size:0.72rem;color:var(--muted);border-top:1px solid var(--surface2)">' +
6268
- '更多: <a href="https://github.com/hesreallyhim/awesome-claude-code" target="_blank" style="color:var(--blue)">awesome-claude-code</a>' +
6269
- ' · <a href="https://skillsmp.com" target="_blank" style="color:var(--blue)">skillsmp.com</a>' +
6270
- ' · <a href="https://www.skillhub.club" target="_blank" style="color:var(--blue)">skillhub.club</a></div>';
6594
+ </div>`).join('') +
6595
+ '<div class="skl-hint" style="margin-top:0.5rem">更多:<a href="https://github.com/hesreallyhim/awesome-claude-code" target="_blank" style="color:var(--blue)">awesome-claude-code</a></div>';
6271
6596
  };
6272
-
6273
6597
  window.installFromMarket = async function(idx) {
6274
- const m = SKILL_MARKET[idx];
6275
- const url = `https://github.com/${m.repo}.git`;
6276
- // 复用 Git 安装流程
6277
- showSkillImport();
6278
- document.getElementById('skill-git-url').value = url;
6279
- // 自动触发扫描
6598
+ sklView('team');
6599
+ document.getElementById('skill-git-url').value = `https://github.com/${SKILL_MARKET[idx].repo}.git`;
6280
6600
  await scanGitSkills();
6601
+ const sub = document.getElementById('skl-git-sub'); if (sub) sub.checked = false;
6281
6602
  };
6282
6603
 
6283
6604
  // ── Git 提交推送 ──────────────────────────────
@@ -6500,7 +6821,7 @@
6500
6821
  ].join('');
6501
6822
  if (!isController && canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID });
6502
6823
  addInputMessage('📘 让 AI 生成项目说明');
6503
- socket.emit('agent:query', { prompt });
6824
+ socket.emit('agent:query', { prompt, display: '📘 让 AI 生成项目说明' });
6504
6825
  showThinking();
6505
6826
  };
6506
6827
 
@@ -6699,7 +7020,7 @@
6699
7020
  if (currentSession?.mode === 'agent') {
6700
7021
  if (!isController && canTakeControl()) socket.emit('take-control', { sessionId: SESSION_ID });
6701
7022
  addInputMessage('🤖 让 AI 过一遍记忆(找过时/重复/废弃)');
6702
- socket.emit('agent:query', { prompt });
7023
+ socket.emit('agent:query', { prompt, display: '🤖 让 AI 过一遍记忆(找过时/重复/废弃)' });
6703
7024
  showThinking();
6704
7025
  } else {
6705
7026
  addStatusMessage('该功能仅在 agent(Claude) 会话可用');