aicq-openclaw-plugin 1.0.3 → 1.0.4

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/index.js CHANGED
@@ -9817,670 +9817,859 @@ var CSS = `
9817
9817
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9818
9818
  :root {
9819
9819
  --bg: #0f1117; --bg2: #1a1d27; --bg3: #242836; --bg4: #2e3347;
9820
- --text: #e4e6ef; --text2: #9499b3; --text3: #5c6080;
9821
- --accent: #e04040; --accent2: #ff5a5a; --ok: #34d399; --warn: #fbbf24;
9822
- --danger: #ef4444; --info: #60a5fa; --border: #2e3347; --radius: 8px;
9823
- --shadow: 0 2px 12px rgba(0,0,0,.3);
9820
+ --bg5: #353a50; --text: #e4e6ef; --text2: #9499b3; --text3: #5c6080;
9821
+ --accent: #6366f1; --accent2: #818cf8; --accent-bg: rgba(99,102,241,.12);
9822
+ --ok: #34d399; --ok-bg: rgba(52,211,153,.12); --warn: #fbbf24; --warn-bg: rgba(251,191,36,.12);
9823
+ --danger: #ef4444; --danger-bg: rgba(239,68,68,.12); --info: #60a5fa; --info-bg: rgba(96,165,250,.12);
9824
+ --border: #2e3347; --radius: 8px; --radius-lg: 12px; --shadow: 0 2px 12px rgba(0,0,0,.3);
9825
+ --sidebar-w: 240px; --header-h: 56px;
9826
+ --transition: .2s cubic-bezier(.4,0,.2,1);
9824
9827
  }
9825
- html, body { height: 100%; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; background: var(--bg); color: var(--text); font-size: 14px; line-height: 1.5; }
9828
+ html, body { height: 100%; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; background: var(--bg); color: var(--text); font-size: 14px; line-height: 1.6; overflow: hidden; }
9826
9829
  a { color: var(--info); text-decoration: none; }
9827
- button { font: inherit; cursor: pointer; border: none; border-radius: var(--radius); padding: 6px 14px; font-size: 13px; transition: background .15s, opacity .15s; }
9828
- button:disabled { opacity: .45; cursor: default; }
9829
- .btn { background: var(--bg3); color: var(--text); }
9830
- .btn:hover:not(:disabled) { background: var(--bg4); }
9831
- .btn-primary { background: var(--accent); color: #fff; }
9832
- .btn-primary:hover:not(:disabled) { background: var(--accent2); }
9833
- .btn-danger { background: #7f1d1d; color: #fca5a5; }
9834
- .btn-danger:hover:not(:disabled) { background: #991b1b; }
9835
- .btn-ok { background: #065f46; color: #6ee7b7; }
9836
- .btn-ok:hover:not(:disabled) { background: #064e3b; }
9837
- .btn-sm { padding: 3px 10px; font-size: 12px; }
9838
- .btn-ghost { background: transparent; color: var(--text2); }
9839
- .btn-ghost:hover:not(:disabled) { background: var(--bg3); color: var(--text); }
9840
-
9841
- input, select, textarea {
9842
- font: inherit; background: var(--bg); color: var(--text); border: 1px solid var(--border);
9843
- border-radius: var(--radius); padding: 7px 12px; width: 100%; outline: none;
9844
- transition: border-color .15s;
9830
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
9831
+ ::-webkit-scrollbar-track { background: transparent; }
9832
+ ::-webkit-scrollbar-thumb { background: var(--bg4); border-radius: 3px; }
9833
+ ::-webkit-scrollbar-thumb:hover { background: var(--bg5); }
9834
+
9835
+ /* Layout */
9836
+ .app { display: flex; height: 100vh; width: 100vw; overflow: hidden; }
9837
+
9838
+ /* Sidebar */
9839
+ .sidebar {
9840
+ width: var(--sidebar-w); min-width: var(--sidebar-w); height: 100vh;
9841
+ background: var(--bg2); border-right: 1px solid var(--border);
9842
+ display: flex; flex-direction: column; transition: width var(--transition), min-width var(--transition);
9843
+ z-index: 20; overflow: hidden;
9844
+ }
9845
+ .sidebar.collapsed { width: 60px; min-width: 60px; }
9846
+ .sidebar.collapsed .nav-label, .sidebar.collapsed .sidebar-header-text, .sidebar.collapsed .sidebar-footer-text { display: none; }
9847
+ .sidebar.collapsed .sidebar-header { justify-content: center; padding: 0 8px; }
9848
+ .sidebar.collapsed .nav-item { justify-content: center; padding: 10px 0; }
9849
+ .sidebar.collapsed .nav-item .nav-icon { margin-right: 0; }
9850
+
9851
+ .sidebar-header {
9852
+ display: flex; align-items: center; gap: 12px; padding: 16px 20px;
9853
+ border-bottom: 1px solid var(--border); min-height: var(--header-h);
9854
+ }
9855
+ .sidebar-logo {
9856
+ width: 32px; height: 32px; border-radius: 8px; background: linear-gradient(135deg, var(--accent), #a855f7);
9857
+ display: grid; place-items: center; font-size: 13px; font-weight: 800; color: #fff; flex-shrink: 0;
9858
+ }
9859
+ .sidebar-header-text h1 { font-size: 14px; font-weight: 700; line-height: 1.2; }
9860
+ .sidebar-header-text span { font-size: 11px; color: var(--text3); }
9861
+
9862
+ .sidebar-nav { flex: 1; overflow-y: auto; padding: 8px; }
9863
+ .nav-group { margin-bottom: 4px; }
9864
+ .nav-group-title { font-size: 10px; font-weight: 600; color: var(--text3); text-transform: uppercase; letter-spacing: .8px; padding: 12px 12px 6px; white-space: nowrap; }
9865
+ .nav-item {
9866
+ display: flex; align-items: center; padding: 9px 12px; border-radius: var(--radius);
9867
+ cursor: pointer; transition: all var(--transition); color: var(--text2); white-space: nowrap;
9868
+ position: relative; user-select: none;
9869
+ }
9870
+ .nav-item:hover { background: var(--bg3); color: var(--text); }
9871
+ .nav-item.active { background: var(--accent-bg); color: var(--accent2); }
9872
+ .nav-item.active::before {
9873
+ content: ''; position: absolute; left: 0; top: 50%; transform: translateY(-50%);
9874
+ width: 3px; height: 20px; background: var(--accent); border-radius: 0 2px 2px 0;
9875
+ }
9876
+ .nav-icon { width: 20px; text-align: center; margin-right: 10px; font-size: 15px; flex-shrink: 0; }
9877
+ .nav-label { font-size: 13px; font-weight: 500; }
9878
+ .nav-badge {
9879
+ margin-left: auto; background: var(--accent); color: #fff; font-size: 10px; font-weight: 600;
9880
+ padding: 1px 7px; border-radius: 10px; min-width: 18px; text-align: center;
9845
9881
  }
9846
- input:focus, select:focus, textarea:focus { border-color: var(--accent); }
9847
- input::placeholder { color: var(--text3); }
9848
- select { cursor: pointer; appearance: auto; }
9849
9882
 
9850
- .topbar {
9851
- display: flex; align-items: center; gap: 16px; padding: 12px 24px;
9883
+ .sidebar-footer {
9884
+ padding: 12px 16px; border-top: 1px solid var(--border); display: flex; align-items: center; gap: 8px;
9885
+ cursor: pointer; transition: background var(--transition); white-space: nowrap;
9886
+ }
9887
+ .sidebar-footer:hover { background: var(--bg3); }
9888
+ .sidebar-footer-text { font-size: 11px; color: var(--text3); }
9889
+
9890
+ /* Main area */
9891
+ .main { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; }
9892
+ .main-header {
9893
+ height: var(--header-h); min-height: var(--header-h);
9894
+ display: flex; align-items: center; gap: 16px; padding: 0 24px;
9852
9895
  background: var(--bg2); border-bottom: 1px solid var(--border);
9853
- position: sticky; top: 0; z-index: 10;
9854
9896
  }
9855
- .topbar h1 { font-size: 16px; font-weight: 600; display: flex; align-items: center; gap: 8px; }
9856
- .topbar h1 .logo { width: 24px; height: 24px; border-radius: 6px; background: var(--accent); display: grid; place-items: center; font-size: 12px; font-weight: 700; color: #fff; }
9857
- .topbar .status { margin-left: auto; display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text2); }
9858
- .topbar .dot { width: 8px; height: 8px; border-radius: 50%; }
9897
+ .toggle-btn {
9898
+ width: 32px; height: 32px; border-radius: 6px; background: var(--bg3);
9899
+ display: grid; place-items: center; cursor: pointer; color: var(--text2); border: none;
9900
+ font-size: 16px; transition: all var(--transition);
9901
+ }
9902
+ .toggle-btn:hover { background: var(--bg4); color: var(--text); }
9903
+ .main-header h2 { font-size: 16px; font-weight: 600; flex: 1; }
9904
+ .header-status { display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text2); }
9905
+ .dot { width: 8px; height: 8px; border-radius: 50%; }
9859
9906
  .dot-ok { background: var(--ok); box-shadow: 0 0 6px var(--ok); }
9860
9907
  .dot-err { background: var(--danger); box-shadow: 0 0 6px var(--danger); }
9861
-
9862
- .tabs {
9863
- display: flex; gap: 0; background: var(--bg2); border-bottom: 1px solid var(--border);
9864
- padding: 0 24px;
9908
+ .dot-warn { background: var(--warn); box-shadow: 0 0 6px var(--warn); }
9909
+ .header-actions { display: flex; gap: 8px; }
9910
+
9911
+ .main-content { flex: 1; overflow-y: auto; padding: 24px; }
9912
+ .page { display: none; }
9913
+ .page.active { display: block; animation: fadeIn .2s ease-out; }
9914
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
9915
+
9916
+ /* Components */
9917
+ .btn {
9918
+ font: inherit; cursor: pointer; border: none; border-radius: var(--radius);
9919
+ padding: 7px 16px; font-size: 13px; font-weight: 500; transition: all var(--transition);
9920
+ display: inline-flex; align-items: center; gap: 6px;
9865
9921
  }
9866
- .tab-btn {
9867
- padding: 10px 20px; font-size: 13px; font-weight: 500; color: var(--text2);
9868
- border-radius: 0; border-bottom: 2px solid transparent;
9869
- background: transparent; transition: all .15s;
9870
- }
9871
- .tab-btn:hover { color: var(--text); background: var(--bg); }
9872
- .tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); }
9922
+ .btn:disabled { opacity: .45; cursor: default; }
9923
+ .btn-default { background: var(--bg3); color: var(--text); }
9924
+ .btn-default:hover:not(:disabled) { background: var(--bg4); }
9925
+ .btn-primary { background: var(--accent); color: #fff; }
9926
+ .btn-primary:hover:not(:disabled) { background: var(--accent2); }
9927
+ .btn-danger { background: var(--danger-bg); color: #fca5a5; border: 1px solid rgba(239,68,68,.2); }
9928
+ .btn-danger:hover:not(:disabled) { background: rgba(239,68,68,.2); }
9929
+ .btn-ok { background: var(--ok-bg); color: #6ee7b7; border: 1px solid rgba(52,211,153,.2); }
9930
+ .btn-ok:hover:not(:disabled) { background: rgba(52,211,153,.2); }
9931
+ .btn-warn { background: var(--warn-bg); color: #fde68a; border: 1px solid rgba(251,191,36,.2); }
9932
+ .btn-warn:hover:not(:disabled) { background: rgba(251,191,36,.2); }
9933
+ .btn-ghost { background: transparent; color: var(--text2); }
9934
+ .btn-ghost:hover:not(:disabled) { background: var(--bg3); color: var(--text); }
9935
+ .btn-sm { padding: 4px 10px; font-size: 12px; }
9936
+ .btn-icon { width: 32px; height: 32px; padding: 0; justify-content: center; border-radius: 6px; }
9873
9937
 
9874
- .content { padding: 24px; max-width: 1200px; }
9875
- .content.hidden { display: none; }
9938
+ input, select, textarea {
9939
+ font: inherit; background: var(--bg); color: var(--text); border: 1px solid var(--border);
9940
+ border-radius: var(--radius); padding: 8px 12px; width: 100%; outline: none; transition: border-color var(--transition);
9941
+ }
9942
+ input:focus, select:focus, textarea:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-bg); }
9943
+ input::placeholder, textarea::placeholder { color: var(--text3); }
9944
+ select { cursor: pointer; }
9945
+ textarea { resize: vertical; min-height: 80px; }
9876
9946
 
9877
9947
  .card {
9878
- background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius);
9948
+ background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius-lg);
9879
9949
  padding: 20px; margin-bottom: 16px;
9880
9950
  }
9881
- .card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
9882
- .card-title { font-size: 15px; font-weight: 600; }
9951
+ .card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; flex-wrap: wrap; gap: 8px; }
9952
+ .card-title { font-size: 15px; font-weight: 600; display: flex; align-items: center; gap: 8px; }
9953
+ .card-desc { font-size: 12px; color: var(--text3); margin-top: 2px; }
9883
9954
 
9884
9955
  .toolbar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-bottom: 16px; }
9956
+ .search-box { position: relative; min-width: 220px; }
9957
+ .search-box input { padding-left: 34px; }
9958
+ .search-box::before { content: '\u{1F50D}'; position: absolute; left: 10px; top: 50%; transform: translateY(-50%); font-size: 13px; pointer-events: none; }
9959
+ .filter-group { display: flex; gap: 4px; }
9960
+ .filter-btn { padding: 4px 12px; font-size: 12px; border-radius: 20px; border: 1px solid var(--border); background: transparent; color: var(--text2); cursor: pointer; transition: all var(--transition); }
9961
+ .filter-btn.active, .filter-btn:hover { background: var(--accent-bg); color: var(--accent2); border-color: var(--accent); }
9885
9962
 
9886
9963
  table { width: 100%; border-collapse: collapse; font-size: 13px; }
9887
- thead th { text-align: left; padding: 8px 12px; color: var(--text2); font-weight: 500; font-size: 12px; text-transform: uppercase; letter-spacing: .5px; border-bottom: 1px solid var(--border); white-space: nowrap; }
9888
- tbody td { padding: 8px 12px; border-bottom: 1px solid var(--border); vertical-align: middle; }
9964
+ thead th {
9965
+ text-align: left; padding: 10px 14px; color: var(--text3); font-weight: 600; font-size: 11px;
9966
+ text-transform: uppercase; letter-spacing: .5px; border-bottom: 1px solid var(--border); white-space: nowrap;
9967
+ position: sticky; top: 0; background: var(--bg2); z-index: 1;
9968
+ }
9969
+ tbody td { padding: 10px 14px; border-bottom: 1px solid var(--border); vertical-align: middle; }
9970
+ tbody tr { transition: background var(--transition); }
9889
9971
  tbody tr:hover { background: var(--bg3); }
9890
- .mono { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; color: var(--text2); word-break: break-all; }
9891
- .badge { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 500; }
9892
- .badge-ok { background: #065f46; color: #6ee7b7; }
9893
- .badge-warn { background: #78350f; color: #fde68a; }
9894
- .badge-info { background: #1e3a5f; color: #93c5fd; }
9895
- .badge-danger { background: #7f1d1d; color: #fca5a5; }
9972
+ .mono { font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace; font-size: 12px; color: var(--text2); word-break: break-all; }
9973
+
9974
+ .badge { display: inline-flex; align-items: center; gap: 4px; padding: 2px 10px; border-radius: 20px; font-size: 11px; font-weight: 600; }
9975
+ .badge-ok { background: var(--ok-bg); color: var(--ok); }
9976
+ .badge-warn { background: var(--warn-bg); color: var(--warn); }
9977
+ .badge-danger { background: var(--danger-bg); color: var(--danger); }
9978
+ .badge-info { background: var(--info-bg); color: var(--info); }
9896
9979
  .badge-ghost { background: var(--bg3); color: var(--text2); }
9980
+ .badge-accent { background: var(--accent-bg); color: var(--accent2); }
9897
9981
 
9898
- .empty { text-align: center; padding: 48px 20px; color: var(--text3); }
9899
- .empty .icon { font-size: 40px; margin-bottom: 12px; opacity: .4; }
9900
- .empty p { font-size: 14px; }
9982
+ .tag { display: inline-flex; align-items: center; gap: 4px; background: var(--bg3); padding: 2px 8px; border-radius: 4px; font-size: 11px; color: var(--text2); }
9901
9983
 
9984
+ /* Stats */
9985
+ .stats-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; margin-bottom: 24px; }
9986
+ .stat-card { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 18px 20px; transition: border-color var(--transition); }
9987
+ .stat-card:hover { border-color: var(--accent); }
9988
+ .stat-icon { width: 36px; height: 36px; border-radius: 8px; display: grid; place-items: center; font-size: 16px; margin-bottom: 10px; }
9989
+ .stat-label { font-size: 11px; color: var(--text3); text-transform: uppercase; letter-spacing: .5px; font-weight: 600; }
9990
+ .stat-value { font-size: 24px; font-weight: 700; margin-top: 2px; line-height: 1.2; }
9991
+ .stat-sub { font-size: 11px; color: var(--text3); margin-top: 4px; }
9992
+
9993
+ /* Provider grid */
9994
+ .provider-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 12px; }
9995
+ .provider-card {
9996
+ background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius-lg);
9997
+ padding: 18px; transition: all var(--transition); cursor: pointer;
9998
+ }
9999
+ .provider-card:hover { border-color: var(--accent); transform: translateY(-1px); box-shadow: var(--shadow); }
10000
+ .provider-card .prov-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
10001
+ .provider-card .prov-name { font-weight: 600; font-size: 14px; display: flex; align-items: center; gap: 8px; }
10002
+ .provider-card .prov-desc { font-size: 12px; color: var(--text3); margin-bottom: 10px; }
10003
+ .provider-card .prov-model { font-size: 11px; color: var(--text2); background: var(--bg3); padding: 3px 8px; border-radius: 4px; display: inline-block; }
10004
+ .provider-card .prov-actions { margin-top: 12px; display: flex; gap: 6px; }
10005
+
10006
+ /* Modal */
9902
10007
  .modal-overlay {
9903
- position: fixed; inset: 0; background: rgba(0,0,0,.6); display: flex;
10008
+ position: fixed; inset: 0; background: rgba(0,0,0,.65); display: flex;
9904
10009
  align-items: center; justify-content: center; z-index: 100;
10010
+ animation: fadeIn .15s ease-out;
9905
10011
  }
9906
10012
  .modal-overlay.hidden { display: none; }
9907
10013
  .modal {
9908
- background: var(--bg2); border: 1px solid var(--border); border-radius: 12px;
9909
- padding: 24px; width: 90%; max-width: 480px; box-shadow: var(--shadow);
10014
+ background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius-lg);
10015
+ padding: 28px; width: 90%; max-width: 520px; box-shadow: 0 8px 32px rgba(0,0,0,.5);
10016
+ max-height: 85vh; overflow-y: auto; animation: modalIn .2s ease-out;
9910
10017
  }
9911
- .modal h3 { font-size: 16px; margin-bottom: 16px; }
9912
- .form-group { margin-bottom: 14px; }
9913
- .form-group label { display: block; font-size: 12px; font-weight: 500; color: var(--text2); margin-bottom: 4px; }
9914
- .form-group .hint { font-size: 11px; color: var(--text3); margin-top: 3px; }
9915
- .form-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 18px; }
9916
-
9917
- .perm-checks { display: flex; gap: 12px; }
9918
- .perm-checks label { display: flex; align-items: center; gap: 6px; cursor: pointer; font-size: 13px; }
10018
+ @keyframes modalIn { from { transform: scale(.95); opacity: 0; } to { transform: scale(1); opacity: 1; } }
10019
+ .modal-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 20px; }
10020
+ .modal-header h3 { font-size: 17px; font-weight: 600; display: flex; align-items: center; gap: 8px; }
10021
+ .modal-close { width: 28px; height: 28px; border-radius: 6px; background: var(--bg3); display: grid; place-items: center; cursor: pointer; border: none; color: var(--text2); font-size: 16px; }
10022
+ .modal-close:hover { background: var(--bg4); color: var(--text); }
10023
+
10024
+ .form-group { margin-bottom: 16px; }
10025
+ .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
10026
+ .form-group label { display: flex; align-items: center; gap: 6px; font-size: 12px; font-weight: 600; color: var(--text2); margin-bottom: 6px; text-transform: uppercase; letter-spacing: .3px; }
10027
+ .form-group .hint { font-size: 11px; color: var(--text3); margin-top: 4px; }
10028
+ .form-group .input-prefix { position: relative; }
10029
+ .form-group .input-prefix input { padding-left: 36px; }
10030
+ .form-group .input-prefix .prefix { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--text3); font-size: 12px; pointer-events: none; }
10031
+ .form-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 24px; padding-top: 16px; border-top: 1px solid var(--border); }
10032
+
10033
+ .perm-checks { display: flex; gap: 16px; }
10034
+ .perm-checks label { display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: 13px; color: var(--text); text-transform: none; letter-spacing: normal; font-weight: 400; }
9919
10035
  .perm-checks input[type=checkbox] { width: 16px; height: 16px; accent-color: var(--accent); }
9920
10036
 
9921
- .stats-row { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; margin-bottom: 20px; }
9922
- .stat-card { background: var(--bg3); border-radius: var(--radius); padding: 14px 18px; }
9923
- .stat-card .label { font-size: 11px; color: var(--text3); text-transform: uppercase; letter-spacing: .5px; }
9924
- .stat-card .value { font-size: 22px; font-weight: 700; margin-top: 2px; }
9925
-
9926
- .provider-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
9927
- .provider-card {
9928
- background: var(--bg3); border: 1px solid var(--border); border-radius: var(--radius);
9929
- padding: 16px; cursor: pointer; transition: border-color .15s;
9930
- }
9931
- .provider-card:hover { border-color: var(--accent); }
9932
- .provider-card .name { font-weight: 600; margin-bottom: 4px; }
9933
- .provider-card .desc { font-size: 12px; color: var(--text3); }
9934
- .provider-card .status-dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; margin-right: 6px; }
9935
-
9936
- .section-desc { font-size: 13px; color: var(--text2); margin-bottom: 16px; }
10037
+ /* Empty state */
10038
+ .empty { text-align: center; padding: 60px 24px; color: var(--text3); }
10039
+ .empty .icon { font-size: 48px; margin-bottom: 16px; opacity: .35; }
10040
+ .empty p { font-size: 14px; margin-bottom: 4px; }
10041
+ .empty .sub { font-size: 12px; color: var(--text3); margin-top: 8px; }
9937
10042
 
9938
- .tag { display: inline-flex; align-items: center; gap: 4px; background: var(--bg3); padding: 2px 8px; border-radius: 4px; font-size: 11px; color: var(--text2); }
9939
-
9940
- .loading { display: flex; align-items: center; justify-content: center; padding: 40px; color: var(--text3); }
9941
- .spinner { width: 20px; height: 20px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin .6s linear infinite; margin-right: 10px; }
10043
+ /* Loading */
10044
+ .loading-mask { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 60px 20px; color: var(--text3); }
10045
+ .spinner { width: 24px; height: 24px; border: 2.5px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin .6s linear infinite; margin-bottom: 12px; }
9942
10046
  @keyframes spin { to { transform: rotate(360deg); } }
9943
10047
 
10048
+ /* Toast */
10049
+ .toast-container { position: fixed; bottom: 20px; right: 20px; z-index: 200; display: flex; flex-direction: column; gap: 8px; }
9944
10050
  .toast {
9945
- position: fixed; bottom: 20px; right: 20px; padding: 12px 20px; border-radius: var(--radius);
9946
- color: #fff; font-size: 13px; z-index: 200; animation: slideIn .2s ease-out;
9947
- box-shadow: var(--shadow);
10051
+ padding: 12px 20px; border-radius: var(--radius); color: #fff; font-size: 13px;
10052
+ animation: slideIn .2s ease-out; box-shadow: var(--shadow); display: flex; align-items: center; gap: 8px;
10053
+ max-width: 400px;
9948
10054
  }
9949
10055
  .toast.hidden { display: none; }
9950
- .toast-ok { background: #065f46; }
9951
- .toast-err { background: #991b1b; }
9952
- .toast-info { background: #1e3a5f; }
9953
- @keyframes slideIn { from { transform: translateY(10px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
10056
+ .toast-ok { background: #065f46; border: 1px solid var(--ok); }
10057
+ .toast-err { background: #7f1d1d; border: 1px solid var(--danger); }
10058
+ .toast-info { background: #1e3a5f; border: 1px solid var(--info); }
10059
+ .toast-warn { background: #78350f; border: 1px solid var(--warn); }
10060
+ @keyframes slideIn { from { transform: translateX(20px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
9954
10061
 
10062
+ /* Actions cell */
9955
10063
  .actions-cell { display: flex; gap: 4px; }
9956
- .truncate { max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
10064
+ .truncate { max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
10065
+
10066
+ /* Detail panel */
10067
+ .detail-row { display: flex; gap: 12px; padding: 10px 0; border-bottom: 1px solid var(--border); }
10068
+ .detail-row:last-child { border-bottom: none; }
10069
+ .detail-key { width: 140px; flex-shrink: 0; font-size: 12px; color: var(--text3); font-weight: 500; padding-top: 2px; }
10070
+ .detail-val { flex: 1; font-size: 13px; word-break: break-all; }
9957
10071
 
10072
+ /* Section desc */
10073
+ .section-desc { font-size: 13px; color: var(--text2); margin-bottom: 20px; line-height: 1.6; }
10074
+
10075
+ /* Responsive */
9958
10076
  @media (max-width: 768px) {
9959
- .content { padding: 12px; }
9960
- .topbar, .tabs { padding-left: 12px; padding-right: 12px; }
9961
- table { font-size: 12px; }
9962
- .stats-row { grid-template-columns: repeat(2, 1fr); }
10077
+ .sidebar { position: fixed; left: -260px; z-index: 50; height: 100vh; transition: left var(--transition); }
10078
+ .sidebar.mobile-open { left: 0; }
10079
+ .main-content { padding: 16px; }
10080
+ .stats-grid { grid-template-columns: repeat(2, 1fr); }
9963
10081
  .provider-grid { grid-template-columns: 1fr; }
10082
+ .form-row { grid-template-columns: 1fr; }
9964
10083
  }
9965
10084
  `;
9966
10085
  var JS = `
9967
- const API = '/plugins/aicq-chat/api';
9968
-
9969
- let currentTab = 'agents';
9970
- let agentsData = null, friendsData = null, requestsData = null, sessionsData = null, identityData = null, modelsConfig = null, statusData = null;
9971
-
9972
- // \u2500\u2500 Utility \u2500\u2500
9973
- async function api(path, opts = {}) {
9974
- const res = await fetch(API + path, { headers: { 'Content-Type': 'application/json', ...opts.headers }, ...opts });
9975
- return res.json();
9976
- }
9977
-
9978
- function $(sel) { return document.querySelector(sel); }
9979
- function $$(sel) { return document.querySelectorAll(sel); }
9980
-
10086
+ // \u2500\u2500 Globals \u2500\u2500
10087
+ const API = '/api';
10088
+ let currentPage = 'dashboard';
10089
+ let refreshTimer = null;
10090
+
10091
+ // \u2500\u2500 jQuery-style helpers \u2500\u2500
10092
+ const $ = (sel, ctx) => (ctx || document).querySelector(sel);
10093
+ const $$ = (sel, ctx) => Array.from((ctx || document).querySelectorAll(sel));
10094
+ const html = (el, content) => { if (typeof el === 'string') el = $(el); if (el) el.innerHTML = content; return el; };
10095
+ const show = (el) => { if (typeof el === 'string') el = $(el); if (el) el.classList.remove('hidden'); return el; };
10096
+ const hide = (el) => { if (typeof el === 'string') el = $(el); if (el) el.classList.add('hidden'); return el; };
10097
+ const toggle = (el) => { if (typeof el === 'string') el = $(el); if (el) el.classList.toggle('hidden'); return el; };
10098
+
10099
+ // \u2500\u2500 Toast \u2500\u2500
9981
10100
  function toast(msg, type = 'info') {
9982
- const t = document.getElementById('toast');
9983
- t.textContent = msg;
10101
+ const container = $('#toast-container') || createToastContainer();
10102
+ const t = document.createElement('div');
10103
+ const icons = { ok: '\u2705', err: '\u274C', info: '\u2139\uFE0F', warn: '\u26A0\uFE0F' };
9984
10104
  t.className = 'toast toast-' + type;
9985
- t.classList.remove('hidden');
9986
- clearTimeout(t._t);
9987
- t._t = setTimeout(() => t.classList.add('hidden'), 3000);
10105
+ t.innerHTML = '<span>' + (icons[type] || '') + '</span><span>' + escHtml(msg) + '</span>';
10106
+ container.appendChild(t);
10107
+ setTimeout(() => { t.style.opacity = '0'; t.style.transform = 'translateX(20px)'; t.style.transition = '.2s'; setTimeout(() => t.remove(), 200); }, 3500);
10108
+ }
10109
+ function createToastContainer() {
10110
+ const c = document.createElement('div');
10111
+ c.id = 'toast-container';
10112
+ c.className = 'toast-container';
10113
+ document.body.appendChild(c);
10114
+ return c;
9988
10115
  }
9989
10116
 
9990
- function showModal(id) { $(id).classList.remove('hidden'); }
9991
- function hideModal(id) { $(id).classList.add('hidden'); }
9992
-
9993
- function escHtml(s) {
9994
- if (!s) return '';
9995
- const d = document.createElement('div');
9996
- d.textContent = s;
9997
- return d.innerHTML;
10117
+ // \u2500\u2500 API \u2500\u2500
10118
+ async function api(path, opts = {}) {
10119
+ try {
10120
+ const res = await fetch(API + path, { headers: { 'Content-Type': 'application/json', ...opts.headers }, ...opts });
10121
+ const data = await res.json();
10122
+ if (!res.ok && !data.error) data.error = 'HTTP ' + res.status;
10123
+ return data;
10124
+ } catch (e) { return { error: e.message }; }
9998
10125
  }
9999
10126
 
10127
+ // \u2500\u2500 Utilities \u2500\u2500
10128
+ function escHtml(s) { if (s == null) return ''; const d = document.createElement('div'); d.textContent = String(s); return d.innerHTML; }
10000
10129
  function timeAgo(iso) {
10001
- if (!iso) return '-';
10130
+ if (!iso) return '\u2014';
10002
10131
  const diff = Date.now() - new Date(iso).getTime();
10003
- const mins = Math.floor(diff / 60000);
10004
- if (mins < 1) return 'just now';
10005
- if (mins < 60) return mins + 'm ago';
10006
- const hrs = Math.floor(mins / 60);
10007
- if (hrs < 24) return hrs + 'h ago';
10008
- return Math.floor(hrs / 24) + 'd ago';
10132
+ if (diff < 0) return 'just now';
10133
+ const m = Math.floor(diff / 60000), h = Math.floor(m / 60), d = Math.floor(h / 24);
10134
+ if (m < 1) return 'just now'; if (m < 60) return m + ' min ago'; if (h < 24) return h + 'h ago'; if (d < 30) return d + 'd ago';
10135
+ return new Date(iso).toLocaleDateString();
10009
10136
  }
10010
-
10011
- function copyText(text) {
10012
- navigator.clipboard.writeText(text).then(() => toast('Copied!', 'ok')).catch(() => toast('Copy failed', 'err'));
10137
+ function maskKey(s) { if (!s || s.length < 12) return s || ''; return s.substring(0, 6) + '\u2022\u2022\u2022\u2022\u2022\u2022' + s.slice(-4); }
10138
+ function copyText(text) { navigator.clipboard.writeText(text).then(() => toast('Copied to clipboard', 'ok')).catch(() => toast('Copy failed', 'err')); }
10139
+
10140
+ // \u2500\u2500 Modal \u2500\u2500
10141
+ function showModal(id) { show(id); }
10142
+ function hideModal(id) { hide(id); }
10143
+
10144
+ // \u2500\u2500 Sidebar navigation \u2500\u2500
10145
+ function navigate(page) {
10146
+ currentPage = page;
10147
+ $$('.nav-item').forEach(n => n.classList.toggle('active', n.dataset.page === page));
10148
+ $$('.page').forEach(p => p.classList.toggle('active', p.id === 'page-' + page));
10149
+ $('#main-title').textContent = ($('.nav-item.active .nav-label') || {}).textContent || page;
10150
+ loadPage(page);
10151
+ // Close mobile sidebar
10152
+ $('.sidebar')?.classList.remove('mobile-open');
10013
10153
  }
10014
10154
 
10015
- // \u2500\u2500 Tab switching \u2500\u2500
10016
- function switchTab(tab) {
10017
- currentTab = tab;
10018
- $$('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === tab));
10019
- $$('.content').forEach(c => c.classList.toggle('hidden', c.id !== 'tab-' + tab));
10020
- if (tab === 'agents') loadAgents();
10021
- else if (tab === 'aicq') loadAICQ();
10022
- else if (tab === 'models') loadModels();
10155
+ function toggleSidebar() {
10156
+ const sb = $('.sidebar');
10157
+ if (window.innerWidth <= 768) { sb.classList.toggle('mobile-open'); }
10158
+ else { sb.classList.toggle('collapsed'); }
10023
10159
  }
10024
10160
 
10025
- // \u2500\u2500 Status \u2500\u2500
10026
- async function loadStatus() {
10027
- try {
10028
- statusData = await api('/status');
10029
- const dot = $('#status-dot');
10030
- const txt = $('#status-text');
10031
- if (statusData.connected) {
10032
- dot.className = 'dot dot-ok';
10033
- txt.textContent = 'Connected';
10034
- } else {
10035
- dot.className = 'dot dot-err';
10036
- txt.textContent = 'Disconnected';
10037
- }
10038
- } catch (e) {
10039
- $('#status-dot').className = 'dot dot-err';
10040
- $('#status-text').textContent = 'Error';
10161
+ function loadPage(page) {
10162
+ switch (page) {
10163
+ case 'dashboard': loadDashboard(); break;
10164
+ case 'agents': loadAgents(); break;
10165
+ case 'friends': loadFriends(); break;
10166
+ case 'models': loadModels(); break;
10167
+ case 'settings': loadSettings(); break;
10041
10168
  }
10042
10169
  }
10043
10170
 
10044
- // \u2500\u2500 TAB 1: Agent Management \u2500\u2500
10045
- async function loadAgents() {
10046
- setLoading('agents', true);
10047
- try {
10048
- const data = await api('/agents');
10049
- agentsData = data;
10050
- renderAgents(data);
10051
- } catch (e) {
10052
- toast('Failed to load agents: ' + e.message, 'err');
10053
- }
10054
- setLoading('agents', false);
10171
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
10172
+ // PAGE: Dashboard
10173
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
10174
+ async function loadDashboard() {
10175
+ const el = $('#dashboard-content');
10176
+ html(el, '<div class="loading-mask"><div class="spinner"></div>Loading dashboard...</div>');
10177
+ const [status, friends, identity] = await Promise.all([api('/status'), api('/friends'), api('/identity')]);
10178
+ if (status.error) { html(el, '<div class="empty"><div class="icon">\u26A0\uFE0F</div><p>Failed to connect to AICQ plugin</p></div>'); return; }
10179
+ const connCls = status.connected ? 'dot-ok' : 'dot-err';
10180
+ const connText = status.connected ? 'Connected' : 'Disconnected';
10181
+ const friendList = friends.friends || [];
10182
+ const aiFriends = friendList.filter(f => f.friendType === 'ai').length;
10183
+ const humanFriends = friendList.filter(f => f.friendType !== 'ai').length;
10184
+
10185
+ html(el, \\\`
10186
+ <div class="stats-grid">
10187
+ <div class="stat-card">
10188
+ <div class="stat-icon" style="background:var(--accent-bg)">\u{1F4E1}</div>
10189
+ <div class="stat-label">Server Status</div>
10190
+ <div class="stat-value" style="font-size:16px;display:flex;align-items:center;gap:8px">
10191
+ <span class="dot \${connCls}"></span> \${connText}
10192
+ </div>
10193
+ <div class="stat-sub">\${escHtml(status.serverUrl)}</div>
10194
+ </div>
10195
+ <div class="stat-card">
10196
+ <div class="stat-icon" style="background:var(--ok-bg)">\u{1F465}</div>
10197
+ <div class="stat-label">Total Friends</div>
10198
+ <div class="stat-value">\${friendList.length}</div>
10199
+ <div class="stat-sub">\${aiFriends} AI \xB7 \${humanFriends} Human</div>
10200
+ </div>
10201
+ <div class="stat-card">
10202
+ <div class="stat-icon" style="background:var(--info-bg)">\u{1F517}</div>
10203
+ <div class="stat-label">Active Sessions</div>
10204
+ <div class="stat-value">\${status.sessionCount || 0}</div>
10205
+ <div class="stat-sub">Encrypted sessions</div>
10206
+ </div>
10207
+ <div class="stat-card">
10208
+ <div class="stat-icon" style="background:var(--warn-bg)">\u{1F511}</div>
10209
+ <div class="stat-label">Agent ID</div>
10210
+ <div class="stat-value mono" style="font-size:13px">\${escHtml(status.agentId)}</div>
10211
+ <div class="stat-sub">Fingerprint: \${escHtml(status.fingerprint)}</div>
10212
+ </div>
10213
+ </div>
10214
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
10215
+ <div class="card">
10216
+ <div class="card-header"><div class="card-title">\u{1F4CB} Recent Friends</div><button class="btn btn-sm btn-ghost" onclick="navigate('friends')">View All \u2192</button></div>
10217
+ \${renderMiniFriendList(friendList.slice(0, 5))}
10218
+ </div>
10219
+ <div class="card">
10220
+ <div class="card-header"><div class="card-title">\u{1F916} Identity Info</div></div>
10221
+ <div class="detail-row"><div class="detail-key">Agent ID</div><div class="detail-val mono" style="cursor:pointer" onclick="copyText('\${identity.agentId}')">\${escHtml(identity.agentId)} \u{1F4CB}</div></div>
10222
+ <div class="detail-row"><div class="detail-key">Fingerprint</div><div class="detail-val mono">\${escHtml(identity.publicKeyFingerprint)}</div></div>
10223
+ <div class="detail-row"><div class="detail-key">Server URL</div><div class="detail-val mono" style="cursor:pointer" onclick="copyText('\${identity.serverUrl}')">\${escHtml(identity.serverUrl)} \u{1F4CB}</div></div>
10224
+ <div class="detail-row"><div class="detail-key">Connection</div><div class="detail-val"><span class="badge badge-\${identity.connected ? 'ok' : 'danger'}">\${identity.connected ? 'Online' : 'Offline'}</span></div></div>
10225
+ </div>
10226
+ </div>
10227
+ \\\`);
10228
+ }
10229
+
10230
+ function renderMiniFriendList(friends) {
10231
+ if (!friends.length) return '<div class="empty"><p>No friends yet</p></div>';
10232
+ let html = '';
10233
+ friends.forEach(f => {
10234
+ html += '<div class="detail-row"><div class="detail-key"><span class="badge badge-' + (f.friendType === 'ai' ? 'info' : 'ghost') + '">' + escHtml(f.friendType || '?') + '</span></div><div class="detail-val mono truncate" style="font-size:12px">' + escHtml(f.id) + '</div></div>';
10235
+ });
10236
+ return html;
10055
10237
  }
10056
10238
 
10057
- function renderAgents(data) {
10239
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
10240
+ // PAGE: Agent Management (from openclaw.json / stableclaw.json)
10241
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
10242
+ async function loadAgents() {
10058
10243
  const el = $('#agents-content');
10059
- const agents = data.agents || [];
10060
- const statsHtml = \`
10061
- <div class="stats-row">
10062
- <div class="stat-card"><div class="label">Total Agents</div><div class="value">\${agents.length}</div></div>
10063
- <div class="stat-card"><div class="label">Current Agent</div><div class="value" style="font-size:16px">\${escHtml(data.currentAgentId || '-')}</div></div>
10064
- <div class="stat-card"><div class="label">Fingerprint</div><div class="value mono" style="font-size:11px">\${escHtml(data.fingerprint || '-')}</div></div>
10065
- <div class="stat-card"><div class="label">Server</div><div class="value" style="font-size:13px">\${data.connected ? '\u{1F7E2}' : '\u{1F534}'} Online</div></div>
10066
- </div>\`;
10244
+ html(el, '<div class="loading-mask"><div class="spinner"></div>Loading agents...</div>');
10245
+ const data = await api('/agents');
10246
+ if (data.error) { html(el, '<div class="empty"><div class="icon">\u26A0\uFE0F</div><p>' + escHtml(data.error) + '</p></div>'); return; }
10067
10247
 
10068
- if (agents.length === 0) {
10069
- el.innerHTML = statsHtml + '<div class="empty"><div class="icon">\u{1F916}</div><p>No agents configured</p></div>';
10070
- return;
10071
- }
10248
+ const agents = data.agents || [];
10249
+ const configSource = data.configSource || 'unknown';
10072
10250
 
10073
10251
  let rows = '';
10074
- agents.forEach(a => {
10075
- const isCurrent = a.id === data.currentAgentId;
10076
- rows += \`<tr>
10077
- <td><span class="mono">\${escHtml(a.id)}</span> \${isCurrent ? '<span class="badge badge-ok">current</span>' : ''}</td>
10078
- <td>\${escHtml(a.name || a.aiName || '-')}</td>
10079
- <td><span class="tag">\${escHtml(a.friendType || '-')}</span></td>
10080
- <td>\${a.sessionCount != null ? '<span class="badge badge-info">' + a.sessionCount + ' sessions</span>' : '-'}</td>
10081
- <td class="mono truncate">\${escHtml(a.publicKeyFingerprint || '-')}</td>
10082
- <td>\${timeAgo(a.lastMessageAt)}</td>
10252
+ agents.forEach((a, i) => {
10253
+ const modelBadge = a.model ? '<span class="badge badge-accent">' + escHtml(a.model) + '</span>' : '<span class="badge badge-ghost">default</span>';
10254
+ const providerBadge = a.provider ? '<span class="tag">' + escHtml(a.provider) + '</span>' : '';
10255
+ const statusBadge = a.enabled !== false ? '<span class="badge badge-ok">active</span>' : '<span class="badge badge-warn">disabled</span>';
10256
+
10257
+ rows += \\\`<tr>
10258
+ <td>\${statusBadge}</td>
10259
+ <td><div style="font-weight:600">\${escHtml(a.name || a.id || 'Agent ' + (i + 1))}</div><div class="mono" style="font-size:11px;color:var(--text3)">\${escHtml(a.id || '\u2014')}</div></td>
10260
+ <td>\${modelBadge}</td>
10261
+ <td>\${providerBadge}</td>
10262
+ <td>\${escHtml(a.systemPrompt ? a.systemPrompt.substring(0, 60) + '...' : '\u2014')}</td>
10083
10263
  <td>
10084
10264
  <div class="actions-cell">
10085
- <button class="btn btn-sm btn-ghost" onclick="copyText('\${escHtml(a.id)}')" title="Copy ID">\u{1F4CB}</button>
10086
- \${!isCurrent ? '<button class="btn btn-sm btn-danger" onclick="deleteAgent(\\'' + escHtml(a.id) + '\\')">Delete</button>' : ''}
10265
+ <button class="btn btn-sm btn-ghost" onclick="viewAgent(\${i})" title="View">\u{1F441}\uFE0F</button>
10266
+ <button class="btn btn-sm btn-danger" onclick="deleteAgent(\${i})" title="Delete">\u{1F5D1}\uFE0F</button>
10087
10267
  </div>
10088
10268
  </td>
10089
- </tr>\`;
10269
+ </tr>\\\`;
10090
10270
  });
10091
10271
 
10092
- el.innerHTML = statsHtml + \`
10093
- <div class="card">
10094
- <div class="card-header">
10095
- <div class="card-title">Agent List</div>
10096
- <div style="display:flex;gap:8px">
10097
- <button class="btn btn-sm" onclick="loadAgents()">\u{1F504} Refresh</button>
10098
- </div>
10099
- </div>
10272
+ if (!agents.length) {
10273
+ html(el, \\\`
10274
+ <p class="section-desc">Reads agent configurations from <strong>\${escHtml(configSource)}</strong>. Configure your agents in the config file.</p>
10275
+ <div class="empty"><div class="icon">\u{1F916}</div><p>No agents configured</p><p class="sub">Add agents to your openclaw.json or stableclaw.json config file</p></div>
10276
+ \\\`);
10277
+ return;
10278
+ }
10279
+
10280
+ html(el, \\\`
10281
+ <div class="toolbar">
10282
+ <div class="search-box"><input type="text" placeholder="Search agents..." id="agent-search" oninput="filterAgentTable()"></div>
10283
+ <button class="btn btn-sm btn-default" onclick="loadAgents()">\u{1F504} Refresh</button>
10284
+ </div>
10285
+ <p class="section-desc">Agent list from <strong style="color:var(--accent2)">\${escHtml(configSource)}</strong>. Total: <strong>\${agents.length}</strong> agents configured.</p>
10286
+ <div class="card" style="padding:0;overflow:hidden">
10100
10287
  <div style="overflow-x:auto">
10101
10288
  <table>
10102
- <thead><tr><th>ID</th><th>Name</th><th>Type</th><th>Sessions</th><th>Fingerprint</th><th>Last Activity</th><th>Actions</th></tr></thead>
10103
- <tbody>\${rows}</tbody>
10289
+ <thead><tr><th style="width:60px">Status</th><th>Agent</th><th>Model</th><th>Provider</th><th>System Prompt</th><th style="width:90px">Actions</th></tr></thead>
10290
+ <tbody id="agent-table-body">\${rows}</tbody>
10104
10291
  </table>
10105
10292
  </div>
10106
- </div>\`;
10293
+ </div>
10294
+ \\\`);
10107
10295
  }
10108
10296
 
10109
- async function deleteAgent(id) {
10110
- if (!confirm('Delete agent ' + id + '? This cannot be undone.')) return;
10111
- try {
10112
- const r = await api('/agents/' + encodeURIComponent(id), { method: 'DELETE' });
10113
- if (r.success) { toast('Agent deleted', 'ok'); loadAgents(); }
10114
- else { toast(r.message || 'Delete failed', 'err'); }
10115
- } catch (e) { toast('Error: ' + e.message, 'err'); }
10297
+ function filterAgentTable() {
10298
+ const q = ($('#agent-search')?.value || '').toLowerCase();
10299
+ $$('#agent-table-body tr').forEach(tr => {
10300
+ tr.style.display = tr.textContent.toLowerCase().includes(q) ? '' : 'none';
10301
+ });
10116
10302
  }
10117
10303
 
10118
- // \u2500\u2500 TAB 2: AICQ Management \u2500\u2500
10119
- let aicqSubTab = 'friends';
10120
-
10121
- async function loadAICQ() {
10122
- setLoading('aicq', true);
10123
- try {
10124
- const [friends, requests, sessions, identity] = await Promise.all([
10125
- api('/friends'),
10126
- api('/friends/requests'),
10127
- api('/sessions'),
10128
- api('/identity')
10129
- ]);
10130
- friendsData = friends;
10131
- requestsData = requests;
10132
- sessionsData = sessions;
10133
- identityData = identity;
10134
- renderAICQSubTabs();
10135
- switchAICQSubTab(aicqSubTab);
10136
- } catch (e) {
10137
- toast('Failed to load AICQ data: ' + e.message, 'err');
10138
- }
10139
- setLoading('aicq', false);
10304
+ function viewAgent(index) {
10305
+ const agents = window._lastAgentsData?.agents || [];
10306
+ const a = agents[index];
10307
+ if (!a) return;
10308
+ let details = '';
10309
+ for (const [k, v] of Object.entries(a)) {
10310
+ if (v != null && v !== '') {
10311
+ const display = typeof v === 'string' && v.length > 200 ? escHtml(v.substring(0, 200)) + '...' : escHtml(String(v));
10312
+ details += '<div class="detail-row"><div class="detail-key">' + escHtml(k) + '</div><div class="detail-val mono" style="font-size:12px;cursor:pointer" onclick="copyText(decodeURIComponent(\\'' + encodeURIComponent(String(v)) + '\\'))">' + display + ' \u{1F4CB}</div></div>';
10313
+ }
10314
+ }
10315
+ html('#view-agent-body', details || '<div class="empty"><p>No data</p></div>');
10316
+ $('#view-agent-title').textContent = a.name || a.id || 'Agent';
10317
+ showModal('modal-view-agent');
10140
10318
  }
10141
10319
 
10142
- function renderAICQSubTabs() {
10143
- const friendCount = (friendsData?.friends || []).length;
10144
- const reqCount = (requestsData?.requests || []).length;
10145
- const sessCount = (sessionsData?.sessions || []).length;
10146
- $('#aicq-subtabs').innerHTML = \`
10147
- <button class="tab-btn \${aicqSubTab==='friends'?'active':''}" onclick="switchAICQSubTab('friends')">Friends (\${friendCount})</button>
10148
- <button class="tab-btn \${aicqSubTab==='requests'?'active':''}" onclick="switchAICQSubTab('requests')">Requests (\${reqCount})</button>
10149
- <button class="tab-btn \${aicqSubTab==='sessions'?'active':''}" onclick="switchAICQSubTab('sessions')">Sessions (\${sessCount})</button>
10150
- \`;
10320
+ async function deleteAgent(index) {
10321
+ if (!confirm('Are you sure you want to delete this agent?')) return;
10322
+ const r = await api('/agents/' + index, { method: 'DELETE' });
10323
+ if (r.success) { toast('Agent deleted', 'ok'); loadAgents(); }
10324
+ else { toast(r.message || r.error || 'Delete failed', 'err'); }
10151
10325
  }
10152
10326
 
10153
- function switchAICQSubTab(tab) {
10154
- aicqSubTab = tab;
10155
- renderAICQSubTabs();
10156
- $$('#aicq-content > div').forEach(d => d.classList.add('hidden'));
10157
- if (tab === 'friends') renderFriends();
10158
- else if (tab === 'requests') renderRequests();
10159
- else if (tab === 'sessions') renderSessions();
10327
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
10328
+ // PAGE: Friends Management
10329
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
10330
+ let friendsFilter = 'all';
10331
+
10332
+ async function loadFriends() {
10333
+ const el = $('#friends-content');
10334
+ html(el, '<div class="loading-mask"><div class="spinner"></div>Loading friends...</div>');
10335
+ const [friends, requests, sessions] = await Promise.all([api('/friends'), api('/friends/requests'), api('/sessions')]);
10336
+
10337
+ // Sub-tabs
10338
+ const friendCount = (friends.friends || []).length;
10339
+ const reqCount = (requests.requests || []).length;
10340
+ const sessCount = (sessions.sessions || []).length;
10341
+
10342
+ html('#friends-tabs', \\\`
10343
+ <button class="filter-btn \${friendsSubTab==='friends'?'active':''}" onclick="friendsSubTab='friends';loadFriends()">\u{1F465} Friends (<span id="fc">\${friendCount}</span>)</button>
10344
+ <button class="filter-btn \${friendsSubTab==='requests'?'active':''}" onclick="friendsSubTab='requests';loadFriends()">\u{1F4E8} Requests (<span id="rc">\${reqCount}</span>)</button>
10345
+ <button class="filter-btn \${friendsSubTab==='sessions'?'active':''}" onclick="friendsSubTab='sessions';loadFriends()">\u{1F517} Sessions (<span id="sc">\${sessCount}</span>)</button>
10346
+ \\\`);
10347
+
10348
+ window._friendsData = friends;
10349
+ window._requestsData = requests;
10350
+ window._sessionsData = sessions;
10351
+
10352
+ if (friendsSubTab === 'friends') renderFriendsList(friends.friends || []);
10353
+ else if (friendsSubTab === 'requests') renderRequestsList(requests.requests || []);
10354
+ else renderSessionsList(sessions.sessions || []);
10160
10355
  }
10356
+ window.friendsSubTab = 'friends';
10161
10357
 
10162
- function renderFriends() {
10163
- const el = $('#aicq-friends');
10164
- el.classList.remove('hidden');
10165
- const friends = friendsData?.friends || [];
10166
-
10167
- if (friends.length === 0) {
10168
- el.innerHTML = '<div class="empty"><div class="icon">\u{1F465}</div><p>No friends yet</p><p style="font-size:12px;margin-top:8px">Add a friend using their temp number or ID</p></div>';
10169
- return;
10170
- }
10171
-
10358
+ function renderFriendsList(friends) {
10359
+ const el = $('#friends-content');
10172
10360
  let rows = '';
10173
10361
  friends.forEach(f => {
10174
- const perms = (f.permissions || []).map(p => '<span class="badge badge-' + (p === 'exec' ? 'warn' : 'ok') + '">' + p + '</span>').join(' ');
10175
- rows += \`<tr>
10176
- <td class="mono">\${escHtml(f.id)}</td>
10177
- <td>\${escHtml(f.aiName || '-')}</td>
10178
- <td><span class="badge badge-\${f.friendType === 'ai' ? 'info' : 'ghost'}">\${escHtml(f.friendType || '?')}</span></td>
10362
+ const perms = (f.permissions || []).map(p => '<span class="badge badge-' + (p === 'exec' ? 'warn' : 'ok') + '">' + escHtml(p) + '</span>').join(' ');
10363
+ rows += \\\`<tr data-type="\${f.friendType || ''}" data-search="\${escHtml(f.id + ' ' + (f.aiName || ''))}">
10364
+ <td><span class="badge badge-\${f.friendType === 'ai' ? 'info' : 'ghost'}" style="font-size:10px">\${(f.friendType || 'unknown').toUpperCase()}</span></td>
10365
+ <td><div style="font-weight:500">\${escHtml(f.aiName || f.id?.substring(0, 12) || '\u2014')}</div><div class="mono" style="font-size:11px;color:var(--text3);cursor:pointer" onclick="copyText('\${escHtml(f.id)}')">\${escHtml(f.id)} \u{1F4CB}</div></td>
10179
10366
  <td>\${perms || '<span class="badge badge-ghost">none</span>'}</td>
10180
- <td class="mono" style="font-size:11px">\${escHtml(f.publicKeyFingerprint || '-')}</td>
10181
- <td>\${timeAgo(f.lastMessageAt)}</td>
10367
+ <td class="mono" style="font-size:11px">\${escHtml(f.publicKeyFingerprint || '\u2014')}</td>
10368
+ <td style="white-space:nowrap">\${timeAgo(f.lastMessageAt)}</td>
10182
10369
  <td>
10183
10370
  <div class="actions-cell">
10184
- <button class="btn btn-sm btn-ghost" onclick="editPermissions('\${escHtml(f.id)}', \${JSON.stringify(f.permissions || [])})">\u2699\uFE0F</button>
10185
- <button class="btn btn-sm btn-danger" onclick="removeFriend('\${escHtml(f.id)}')">\u{1F5D1}\uFE0F</button>
10186
- <button class="btn btn-sm btn-ghost" onclick="copyText('\${escHtml(f.id)}')" title="Copy ID">\u{1F4CB}</button>
10371
+ <button class="btn btn-sm btn-ghost" onclick="editFriendPerms('\${escHtml(f.id)}',\${JSON.stringify(f.permissions || [])})" title="Permissions">\u2699\uFE0F</button>
10372
+ <button class="btn btn-sm btn-danger" onclick="removeFriend('\${escHtml(f.id)}')" title="Remove">\u{1F5D1}\uFE0F</button>
10187
10373
  </div>
10188
10374
  </td>
10189
- </tr>\`;
10375
+ </tr>\\\`;
10190
10376
  });
10191
10377
 
10192
- el.innerHTML = \`
10193
- <div class="card">
10194
- <div class="card-header">
10195
- <div class="card-title">Friends (\${friends.length})</div>
10196
- <div style="display:flex;gap:8px">
10197
- <button class="btn btn-sm btn-primary" onclick="showAddFriend()">\u2795 Add Friend</button>
10198
- <button class="btn btn-sm" onclick="loadAICQ()">\u{1F504} Refresh</button>
10199
- </div>
10378
+ html(el, \\\`
10379
+ <div class="toolbar">
10380
+ <div class="search-box"><input type="text" placeholder="Search friends..." id="friend-search" oninput="filterFriendTable()"></div>
10381
+ <div class="filter-group">
10382
+ <button class="filter-btn \${friendsFilter==='all'?'active':''}" onclick="friendsFilter='all';filterFriendTable()">All</button>
10383
+ <button class="filter-btn \${friendsFilter==='ai'?'active':''}" onclick="friendsFilter='ai';filterFriendTable()">AI</button>
10384
+ <button class="filter-btn \${friendsFilter==='human'?'active':''}" onclick="friendsFilter='human';filterFriendTable()">Human</button>
10200
10385
  </div>
10201
- <div style="overflow-x:auto">
10386
+ <span style="flex:1"></span>
10387
+ <button class="btn btn-sm btn-primary" onclick="showAddFriendModal()">\u2795 Add Friend</button>
10388
+ <button class="btn btn-sm btn-default" onclick="loadFriends()">\u{1F504}</button>
10389
+ </div>
10390
+ <div class="card" style="padding:0;overflow:hidden">
10391
+ <div style="overflow-x:auto;max-height:calc(100vh - 280px);overflow-y:auto">
10202
10392
  <table>
10203
- <thead><tr><th>Node ID</th><th>Name</th><th>Type</th><th>Permissions</th><th>Fingerprint</th><th>Last Message</th><th>Actions</th></tr></thead>
10204
- <tbody>\${rows}</tbody>
10393
+ <thead><tr><th style="width:60px">Type</th><th>Friend</th><th>Permissions</th><th>Fingerprint</th><th>Last Message</th><th style="width:80px">Actions</th></tr></thead>
10394
+ <tbody id="friend-table-body">\${rows}</tbody>
10205
10395
  </table>
10206
10396
  </div>
10207
- </div>\`;
10397
+ \${!friends.length ? '<div class="empty"><div class="icon">\u{1F465}</div><p>No friends yet</p><p class="sub">Add a friend using their 6-digit temp number or node ID</p></div>' : ''}
10398
+ </div>
10399
+ \\\`);
10208
10400
  }
10209
10401
 
10210
- function renderRequests() {
10211
- const el = $('#aicq-requests');
10212
- el.classList.remove('hidden');
10213
- const reqs = requestsData?.requests || [];
10214
-
10215
- if (reqs.length === 0) {
10216
- el.innerHTML = '<div class="empty"><div class="icon">\u{1F4E8}</div><p>No pending friend requests</p></div>';
10217
- return;
10218
- }
10402
+ function filterFriendTable() {
10403
+ const q = ($('#friend-search')?.value || '').toLowerCase();
10404
+ $$('#friend-table-body tr').forEach(tr => {
10405
+ const matchSearch = tr.dataset.search?.toLowerCase().includes(q);
10406
+ const matchFilter = friendsFilter === 'all' || tr.dataset.type === friendsFilter;
10407
+ tr.style.display = matchSearch && matchFilter ? '' : 'none';
10408
+ });
10409
+ }
10219
10410
 
10411
+ function renderRequestsList(requests) {
10412
+ const el = $('#friends-content');
10220
10413
  let rows = '';
10221
- reqs.forEach(r => {
10222
- const statusBadge = r.status === 'pending' ? '<span class="badge badge-warn">pending</span>' :
10223
- r.status === 'accepted' ? '<span class="badge badge-ok">accepted</span>' :
10224
- '<span class="badge badge-ghost">' + escHtml(r.status) + '</span>';
10225
- rows += \`<tr>
10226
- <td class="mono">\${escHtml(r.id)}</td>
10227
- <td class="mono">\${escHtml(r.fromId || r.requesterId || '-')}</td>
10228
- <td>\${statusBadge}</td>
10414
+ requests.forEach(r => {
10415
+ const stCls = r.status === 'pending' ? 'warn' : r.status === 'accepted' ? 'ok' : 'ghost';
10416
+ rows += \\\`<tr>
10417
+ <td class="mono" style="font-size:11px">\${escHtml(r.id)}</td>
10418
+ <td class="mono" style="font-size:12px">\${escHtml(r.fromId || r.requesterId || '\u2014')}</td>
10419
+ <td><span class="badge badge-\${stCls}">\${escHtml(r.status)}</span></td>
10229
10420
  <td>\${timeAgo(r.createdAt)}</td>
10230
- <td>\${escHtml(r.message || '-')}</td>
10231
10421
  <td>
10232
- <div class="actions-cell">
10233
- \${r.status === 'pending' ? \`
10234
- <button class="btn btn-sm btn-ok" onclick="acceptRequest('\${escHtml(r.id)}')">\u2713 Accept</button>
10235
- <button class="btn btn-sm btn-danger" onclick="rejectRequest('\${escHtml(r.id)}')">\u2717 Reject</button>
10236
- \` : '-'}
10237
- </div>
10422
+ \${r.status === 'pending' ? '<div class="actions-cell"><button class="btn btn-sm btn-ok" onclick="acceptFriendReq(\\'' + escHtml(r.id) + '\\')">\u2713 Accept</button><button class="btn btn-sm btn-danger" onclick="rejectFriendReq(\\'' + escHtml(r.id) + '\\')">\u2717 Reject</button></div>' : '\u2014'}
10238
10423
  </td>
10239
- </tr>\`;
10424
+ </tr>\\\`;
10240
10425
  });
10241
-
10242
- el.innerHTML = \`
10243
- <div class="card">
10244
- <div class="card-header">
10245
- <div class="card-title">Friend Requests (\${reqs.length})</div>
10246
- <button class="btn btn-sm" onclick="loadAICQ()">\u{1F504} Refresh</button>
10247
- </div>
10248
- <div style="overflow-x:auto">
10249
- <table>
10250
- <thead><tr><th>Request ID</th><th>From</th><th>Status</th><th>Time</th><th>Message</th><th>Actions</th></tr></thead>
10251
- <tbody>\${rows}</tbody>
10252
- </table>
10253
- </div>
10254
- </div>\`;
10426
+ html(el, \\\`
10427
+ <div class="toolbar"><button class="btn btn-sm btn-default" onclick="loadFriends()">\u{1F504} Refresh</button></div>
10428
+ <div class="card" style="padding:0;overflow:hidden">
10429
+ <div style="overflow-x:auto"><table>
10430
+ <thead><tr><th>Request ID</th><th>From</th><th>Status</th><th>Time</th><th style="width:160px">Actions</th></tr></thead>
10431
+ <tbody>\${rows}</tbody>
10432
+ </table></div>
10433
+ \${!requests.length ? '<div class="empty"><div class="icon">\u{1F4E8}</div><p>No pending requests</p></div>' : ''}
10434
+ </div>
10435
+ \\\`);
10255
10436
  }
10256
10437
 
10257
- function renderSessions() {
10258
- const el = $('#aicq-sessions');
10259
- el.classList.remove('hidden');
10260
- const sessions = sessionsData?.sessions || [];
10261
-
10262
- if (sessions.length === 0) {
10263
- el.innerHTML = '<div class="empty"><div class="icon">\u{1F517}</div><p>No active encrypted sessions</p></div>';
10264
- return;
10265
- }
10266
-
10438
+ function renderSessionsList(sessions) {
10439
+ const el = $('#friends-content');
10267
10440
  let rows = '';
10268
10441
  sessions.forEach(s => {
10269
- rows += \`<tr>
10270
- <td class="mono">\${escHtml(s.peerId)}</td>
10442
+ rows += \\\`<tr>
10443
+ <td class="mono" style="font-size:12px;cursor:pointer" onclick="copyText('\${escHtml(s.peerId)}')">\${escHtml(s.peerId)} \u{1F4CB}</td>
10271
10444
  <td>\${timeAgo(s.createdAt)}</td>
10272
- <td><span class="badge badge-info">\${s.messageCount} msgs</span></td>
10273
- </tr>\`;
10445
+ <td><span class="badge badge-info">\${s.messageCount} messages</span></td>
10446
+ </tr>\\\`;
10274
10447
  });
10275
-
10276
- el.innerHTML = \`
10277
- <div class="card">
10278
- <div class="card-header">
10279
- <div class="card-title">Encrypted Sessions (\${sessions.length})</div>
10280
- <button class="btn btn-sm" onclick="loadAICQ()">\u{1F504} Refresh</button>
10281
- </div>
10282
- <div style="overflow-x:auto">
10283
- <table>
10284
- <thead><tr><th>Peer ID</th><th>Established</th><th>Messages</th></tr></thead>
10285
- <tbody>\${rows}</tbody>
10286
- </table>
10287
- </div>
10288
- </div>\`;
10289
- }
10290
-
10291
- async function showAddFriend() {
10292
- showModal('modal-add-friend');
10293
- $('#add-friend-target').value = '';
10294
- $('#add-friend-target').focus();
10448
+ html(el, \\\`
10449
+ <div class="toolbar"><button class="btn btn-sm btn-default" onclick="loadFriends()">\u{1F504} Refresh</button></div>
10450
+ <div class="card" style="padding:0;overflow:hidden">
10451
+ <div style="overflow-x:auto"><table>
10452
+ <thead><tr><th>Peer ID</th><th>Established</th><th>Messages</th></tr></thead>
10453
+ <tbody>\${rows}</tbody>
10454
+ </table></div>
10455
+ \${!sessions.length ? '<div class="empty"><div class="icon">\u{1F517}</div><p>No active sessions</p></div>' : ''}
10456
+ </div>
10457
+ \\\`);
10295
10458
  }
10296
10459
 
10460
+ function showAddFriendModal() { $('#add-friend-target').value = ''; showModal('modal-add-friend'); setTimeout(() => $('#add-friend-target')?.focus(), 100); }
10297
10461
  async function addFriend() {
10298
10462
  const target = $('#add-friend-target').value.trim();
10299
- if (!target) { toast('Enter a temp number or friend ID', 'err'); return; }
10463
+ if (!target) { toast('Enter a temp number or node ID', 'warn'); return; }
10300
10464
  hideModal('modal-add-friend');
10301
10465
  toast('Sending friend request...', 'info');
10302
- try {
10303
- const r = await api('/friends', { method: 'POST', body: JSON.stringify({ target }) });
10304
- if (r.success) { toast(r.message || 'Friend request sent!', 'ok'); loadAICQ(); }
10305
- else { toast(r.message || 'Failed to add friend', 'err'); }
10306
- } catch (e) { toast('Error: ' + e.message, 'err'); }
10466
+ const r = await api('/friends', { method: 'POST', body: JSON.stringify({ target }) });
10467
+ if (r.success) { toast(r.message || 'Friend request sent!', 'ok'); loadFriends(); }
10468
+ else { toast(r.message || r.error || 'Failed to add friend', 'err'); }
10307
10469
  }
10308
-
10309
10470
  async function removeFriend(id) {
10310
- if (!confirm('Remove friend ' + id + '? This will delete the encrypted session.')) return;
10311
- try {
10312
- const r = await api('/friends/' + encodeURIComponent(id), { method: 'DELETE' });
10313
- if (r.success) { toast('Friend removed', 'ok'); loadAICQ(); }
10314
- else { toast(r.message || 'Failed to remove', 'err'); }
10315
- } catch (e) { toast('Error: ' + e.message, 'err'); }
10471
+ if (!confirm('Remove friend ' + id + '?')) return;
10472
+ const r = await api('/friends/' + encodeURIComponent(id), { method: 'DELETE' });
10473
+ if (r.success) { toast('Friend removed', 'ok'); loadFriends(); }
10474
+ else { toast(r.message || r.error || 'Failed', 'err'); }
10316
10475
  }
10317
10476
 
10318
- let editingFriendId = null;
10319
- function editPermissions(id, perms) {
10320
- editingFriendId = id;
10321
- const chatChecked = perms.includes('chat') ? 'checked' : '';
10322
- const execChecked = perms.includes('exec') ? 'checked' : '';
10323
- $('#perm-chat').checked = chatChecked;
10324
- $('#perm-exec').checked = execChecked;
10477
+ let _editFriendId = null;
10478
+ function editFriendPerms(id, perms) {
10479
+ _editFriendId = id;
10480
+ $('#perm-chat').checked = (perms || []).includes('chat');
10481
+ $('#perm-exec').checked = (perms || []).includes('exec');
10325
10482
  showModal('modal-permissions');
10326
10483
  }
10327
-
10328
- async function savePermissions() {
10484
+ async function saveFriendPerms() {
10329
10485
  const perms = [];
10330
10486
  if ($('#perm-chat').checked) perms.push('chat');
10331
10487
  if ($('#perm-exec').checked) perms.push('exec');
10332
- try {
10333
- const r = await api('/friends/' + encodeURIComponent(editingFriendId) + '/permissions', {
10334
- method: 'PUT', body: JSON.stringify({ permissions: perms })
10335
- });
10336
- if (r.success) { toast('Permissions updated', 'ok'); hideModal('modal-permissions'); loadAICQ(); }
10337
- else { toast(r.message || 'Failed to update', 'err'); }
10338
- } catch (e) { toast('Error: ' + e.message, 'err'); }
10488
+ const r = await api('/friends/' + encodeURIComponent(_editFriendId) + '/permissions', { method: 'PUT', body: JSON.stringify({ permissions: perms }) });
10489
+ if (r.success) { toast('Permissions updated', 'ok'); hideModal('modal-permissions'); loadFriends(); }
10490
+ else { toast(r.message || r.error || 'Failed', 'err'); }
10339
10491
  }
10340
-
10341
- async function acceptRequest(id) {
10342
- try {
10343
- const r = await api('/friends/requests/' + encodeURIComponent(id) + '/accept', { method: 'POST', body: JSON.stringify({ permissions: ['chat'] }) });
10344
- if (r.success) { toast('Request accepted', 'ok'); loadAICQ(); }
10345
- else { toast(r.message || 'Failed', 'err'); }
10346
- } catch (e) { toast('Error: ' + e.message, 'err'); }
10492
+ async function acceptFriendReq(id) {
10493
+ const r = await api('/friends/requests/' + encodeURIComponent(id) + '/accept', { method: 'POST', body: JSON.stringify({ permissions: ['chat'] }) });
10494
+ if (r.success) { toast('Request accepted', 'ok'); loadFriends(); } else { toast(r.message || r.error || 'Failed', 'err'); }
10347
10495
  }
10348
-
10349
- async function rejectRequest(id) {
10350
- try {
10351
- const r = await api('/friends/requests/' + encodeURIComponent(id) + '/reject', { method: 'POST', body: JSON.stringify({}) });
10352
- if (r.success) { toast('Request rejected', 'ok'); loadAICQ(); }
10353
- else { toast(r.message || 'Failed', 'err'); }
10354
- } catch (e) { toast('Error: ' + e.message, 'err'); }
10496
+ async function rejectFriendReq(id) {
10497
+ const r = await api('/friends/requests/' + encodeURIComponent(id) + '/reject', { method: 'POST', body: JSON.stringify({}) });
10498
+ if (r.success) { toast('Request rejected', 'ok'); loadFriends(); } else { toast(r.message || r.error || 'Failed', 'err'); }
10355
10499
  }
10356
10500
 
10357
- // \u2500\u2500 TAB 3: Model Management \u2500\u2500
10501
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
10502
+ // PAGE: Model Management
10503
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
10504
+ let _modelProviders = null;
10505
+
10358
10506
  async function loadModels() {
10359
- setLoading('models', true);
10360
- try {
10361
- modelsConfig = await api('/models');
10362
- renderModels(modelsConfig);
10363
- } catch (e) {
10364
- toast('Failed to load models: ' + e.message, 'err');
10365
- }
10366
- setLoading('models', false);
10507
+ const el = $('#models-content');
10508
+ html(el, '<div class="loading-mask"><div class="spinner"></div>Loading model configuration...</div>');
10509
+ const data = await api('/models');
10510
+ if (data.error) { html(el, '<div class="empty"><div class="icon">\u26A0\uFE0F</div><p>' + escHtml(data.error) + '</p></div>'); return; }
10511
+ _modelProviders = data;
10512
+ renderModels(data);
10367
10513
  }
10368
10514
 
10369
10515
  function renderModels(data) {
10370
10516
  const el = $('#models-content');
10371
10517
  const providers = data.providers || [];
10372
- const configured = providers.filter(p => p.configured);
10518
+ const configured = providers.filter(p => p.configured).length;
10373
10519
 
10374
- let provCards = '';
10520
+ let cards = '';
10375
10521
  providers.forEach(p => {
10376
- const statusColor = p.configured ? 'var(--ok)' : 'var(--text3)';
10377
- const statusText = p.configured ? 'Configured' : 'Not configured';
10378
- provCards += \`
10379
- <div class="provider-card" onclick="showModelConfig('\${escHtml(p.id)}')">
10380
- <div class="name"><span class="status-dot" style="background:\${statusColor}"></span>\${escHtml(p.name)}</div>
10381
- <div class="desc">\${escHtml(p.description || '')}</div>
10382
- <div style="margin-top:8px;font-size:11px;color:var(--text3)">\${statusText}</div>
10383
- </div>\`;
10522
+ const icon = p.id === 'openai' ? '\u{1F7E2}' : p.id === 'anthropic' ? '\u{1F7E0}' : p.id === 'google' ? '\u{1F535}' : p.id === 'ollama' ? '\u{1F7E3}' : p.id === 'deepseek' ? '\u{1F537}' : p.id === 'groq' ? '\u26A1' : p.id === 'openrouter' ? '\u{1F310}' : '\u26AA';
10523
+ const statusBadge = p.configured
10524
+ ? '<span class="badge badge-ok">\u25CF Configured</span>'
10525
+ : '<span class="badge badge-ghost">Not set</span>';
10526
+ const currentModel = p.modelId ? '<span class="prov-model">' + escHtml(p.modelId) + '</span>' : '';
10527
+
10528
+ cards += \\\`
10529
+ <div class="provider-card" onclick="showModelConfigModal('\${escHtml(p.id)}')">
10530
+ <div class="prov-head">
10531
+ <div class="prov-name">\${icon} \${escHtml(p.name)}</div>
10532
+ \${statusBadge}
10533
+ </div>
10534
+ <div class="prov-desc">\${escHtml(p.description)}</div>
10535
+ \${currentModel}
10536
+ <div class="prov-actions">
10537
+ <button class="btn btn-sm btn-primary" onclick="event.stopPropagation();showModelConfigModal('\${escHtml(p.id)}')">Configure</button>
10538
+ </div>
10539
+ </div>\\\`;
10384
10540
  });
10385
10541
 
10386
- let currentModelsHtml = '';
10387
- if (data.currentModels && data.currentModels.length > 0) {
10542
+ let activeModelsSection = '';
10543
+ if (data.currentModels && data.currentModels.length) {
10388
10544
  let rows = '';
10389
10545
  data.currentModels.forEach(m => {
10390
- rows += \`<tr>
10391
- <td>\${escHtml(m.provider || '-')}</td>
10392
- <td class="mono">\${escHtml(m.modelId || '-')}</td>
10393
- <td>\${m.hasApiKey ? '<span class="badge badge-ok">\u25CF</span>' : '<span class="badge badge-danger">\u2717</span>'}</td>
10394
- <td class="mono">\${escHtml(m.baseUrl || '-')}</td>
10395
- <td><button class="btn btn-sm btn-ghost" onclick="showModelConfig('\${escHtml(m.providerId || m.provider || '')}')">Edit</button></td>
10396
- </tr>\`;
10546
+ rows += \\\`<tr>
10547
+ <td style="font-weight:500">\${escHtml(m.provider)}</td>
10548
+ <td class="mono">\${escHtml(m.modelId)}</td>
10549
+ <td><span class="badge badge-ok">\u25CF Key set</span></td>
10550
+ <td class="mono" style="font-size:11px">\${escHtml(m.baseUrl || 'default')}</td>
10551
+ <td><button class="btn btn-sm btn-ghost" onclick="showModelConfigModal('\${escHtml(m.providerId)}')">Edit</button></td>
10552
+ </tr>\\\`;
10397
10553
  });
10398
- currentModelsHtml = \`
10399
- <div class="card">
10400
- <div class="card-header">
10401
- <div class="card-title">Configured Models</div>
10402
- </div>
10403
- <div style="overflow-x:auto">
10404
- <table>
10405
- <thead><tr><th>Provider</th><th>Model ID</th><th>API Key</th><th>Base URL</th><th>Actions</th></tr></thead>
10406
- <tbody>\${rows}</tbody>
10407
- </table>
10408
- </div>
10409
- </div>\`;
10554
+ activeModelsSection = \\\`
10555
+ <div class="card" style="margin-top:20px">
10556
+ <div class="card-header"><div class="card-title">\u{1F4CA} Active Model Configurations</div></div>
10557
+ <div style="overflow-x:auto"><table>
10558
+ <thead><tr><th>Provider</th><th>Model</th><th>API Key</th><th>Base URL</th><th>Actions</th></tr></thead>
10559
+ <tbody>\${rows}</tbody>
10560
+ </table></div>
10561
+ </div>\\\`;
10410
10562
  }
10411
10563
 
10412
- el.innerHTML = \`
10413
- <p class="section-desc">Quickly configure LLM providers. Click a provider card to set up your API key and model.</p>
10414
- <div class="provider-grid">\${provCards}</div>
10415
- \${currentModelsHtml}
10416
- \`;
10564
+ html(el, \\\`
10565
+ <div class="stats-grid" style="margin-bottom:24px">
10566
+ <div class="stat-card">
10567
+ <div class="stat-icon" style="background:var(--accent-bg)">\u{1F9E0}</div>
10568
+ <div class="stat-label">Configured</div>
10569
+ <div class="stat-value">\${configured} / \${providers.length}</div>
10570
+ <div class="stat-sub">Providers with API keys</div>
10571
+ </div>
10572
+ </div>
10573
+ <p class="section-desc">Configure LLM providers for your agents. Click a provider card to set or update the API key, model, and base URL. Changes are saved directly to your config file.</p>
10574
+ <div class="provider-grid">\${cards}</div>
10575
+ \${activeModelsSection}
10576
+ \\\`);
10417
10577
  }
10418
10578
 
10419
- let editingProviderId = null;
10420
- function showModelConfig(providerId) {
10421
- editingProviderId = providerId;
10422
- const provider = (modelsConfig?.providers || []).find(p => p.id === providerId);
10423
- if (!provider) { toast('Provider not found', 'err'); return; }
10424
-
10425
- $('#model-provider-name').textContent = provider.name;
10426
- $('#model-api-key').value = provider.apiKey || '';
10427
- $('#model-api-key').placeholder = provider.apiKeyHint || 'Enter your API key';
10428
- $('#model-model-id').value = provider.modelId || '';
10429
- $('#model-model-id').placeholder = provider.modelHint || 'e.g. gpt-4o';
10430
- $('#model-base-url').value = provider.baseUrl || '';
10431
- $('#model-base-url').placeholder = provider.baseUrlHint || 'Default: provider endpoint';
10432
-
10579
+ let _editProviderId = null;
10580
+ function showModelConfigModal(id) {
10581
+ const p = (_modelProviders?.providers || []).find(x => x.id === id);
10582
+ if (!p) { toast('Provider not found', 'err'); return; }
10583
+ _editProviderId = id;
10584
+ $('#model-name').textContent = p.name;
10585
+ $('#model-icon').textContent = p.id === 'openai' ? '\u{1F7E2}' : p.id === 'anthropic' ? '\u{1F7E0}' : '\u{1F7E2}';
10586
+ $('#model-api-key').value = '';
10587
+ $('#model-api-key').placeholder = p.apiKeyHint || 'Enter API key';
10588
+ $('#model-model-id').value = p.modelId || '';
10589
+ $('#model-model-id').placeholder = p.modelHint || 'Model ID';
10590
+ $('#model-base-url').value = p.baseUrl || '';
10591
+ $('#model-base-url').placeholder = p.baseUrlHint || 'Default URL';
10592
+ $('#model-current-key').textContent = p.apiKeyHasValue ? 'Current: ' + p.apiKey : 'No API key configured';
10433
10593
  showModal('modal-model-config');
10434
10594
  }
10435
-
10436
10595
  async function saveModelConfig() {
10437
10596
  const apiKey = $('#model-api-key').value.trim();
10438
10597
  const modelId = $('#model-model-id').value.trim();
10439
10598
  const baseUrl = $('#model-base-url').value.trim();
10599
+ if (!apiKey && !modelId) { toast('Enter at least an API key or model ID', 'warn'); return; }
10600
+ hideModal('modal-model-config');
10601
+ toast('Saving configuration...', 'info');
10602
+ const r = await api('/models/' + encodeURIComponent(_editProviderId), { method: 'PUT', body: JSON.stringify({ apiKey, modelId, baseUrl }) });
10603
+ if (r.success) { toast(r.message || 'Configuration saved!', 'ok'); loadModels(); }
10604
+ else { toast(r.message || r.error || 'Failed to save', 'err'); }
10605
+ }
10606
+
10607
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
10608
+ // PAGE: Settings
10609
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
10610
+ async function loadSettings() {
10611
+ const el = $('#settings-content');
10612
+ html(el, '<div class="loading-mask"><div class="spinner"></div>Loading settings...</div>');
10613
+ const [status, identity, config] = await Promise.all([api('/status'), api('/identity'), api('/config')]);
10440
10614
 
10441
- if (!apiKey && !modelId) {
10442
- toast('Please enter at least an API key or model ID', 'err');
10615
+ if (config.error) {
10616
+ html(el, '<div class="empty"><div class="icon">\u26A0\uFE0F</div><p>' + escHtml(config.error) + '</p></div>');
10443
10617
  return;
10444
10618
  }
10445
10619
 
10446
- hideModal('modal-model-config');
10447
- toast('Saving model config...', 'info');
10620
+ html(el, \\\`
10621
+ <p class="section-desc">AICQ plugin runtime configuration and system information.</p>
10448
10622
 
10449
- try {
10450
- const r = await api('/models/' + encodeURIComponent(editingProviderId), {
10451
- method: 'PUT',
10452
- body: JSON.stringify({ apiKey, modelId, baseUrl })
10453
- });
10454
- if (r.success) { toast('Model config saved!', 'ok'); loadModels(); }
10455
- else { toast(r.message || 'Failed to save', 'err'); }
10456
- } catch (e) { toast('Error: ' + e.message, 'err'); }
10457
- }
10623
+ <div class="card">
10624
+ <div class="card-header"><div class="card-title">\u{1F50C} Connection Settings</div></div>
10625
+ <div class="detail-row"><div class="detail-key">Server URL</div><div class="detail-val mono" style="cursor:pointer" onclick="copyText('\${escHtml(status.serverUrl)}')">\${escHtml(status.serverUrl)} \u{1F4CB}</div></div>
10626
+ <div class="detail-row"><div class="detail-key">WebSocket Status</div><div class="detail-val"><span class="badge badge-\${status.connected ? 'ok' : 'danger'}">\${status.connected ? 'Connected' : 'Disconnected'}</span></div></div>
10627
+ </div>
10458
10628
 
10459
- // \u2500\u2500 Loading helpers \u2500\u2500
10460
- function setLoading(tab, loading) {
10461
- const el = $('#tab-' + tab);
10462
- if (!el) return;
10463
- const existing = el.querySelector('.loading-bar');
10464
- if (loading && !existing) {
10465
- const bar = document.createElement('div');
10466
- bar.className = 'loading-bar';
10467
- bar.innerHTML = '<div class="spinner"></div> Loading...';
10468
- bar.style.cssText = 'padding:12px;text-align:center;color:var(--text3);font-size:13px;display:flex;align-items:center;justify-content:center;gap:8px';
10469
- el.prepend(bar);
10470
- } else if (!loading && existing) {
10471
- existing.remove();
10472
- }
10629
+ <div class="card">
10630
+ <div class="card-header"><div class="card-title">\u{1F916} Agent Identity</div></div>
10631
+ <div class="detail-row"><div class="detail-key">Agent ID</div><div class="detail-val mono" style="cursor:pointer" onclick="copyText('\${escHtml(identity.agentId)}')">\${escHtml(identity.agentId)} \u{1F4CB}</div></div>
10632
+ <div class="detail-row"><div class="detail-key">Public Key Fingerprint</div><div class="detail-val mono">\${escHtml(identity.publicKeyFingerprint)}</div></div>
10633
+ </div>
10634
+
10635
+ <div class="card">
10636
+ <div class="card-header"><div class="card-title">\u{1F4C1} Config File</div></div>
10637
+ <div class="detail-row"><div class="detail-key">Source</div><div class="detail-val" style="cursor:pointer" onclick="copyText('\${escHtml(config.configPath || '')}')">\${escHtml(config.configPath || 'Not found')} \u{1F4CB}</div></div>
10638
+ <div class="detail-row"><div class="detail-key">Config Size</div><div class="detail-val">\${config.configSize || 0} bytes</div></div>
10639
+ </div>
10640
+
10641
+ <div class="card">
10642
+ <div class="card-header"><div class="card-title">\u{1F4CA} Statistics</div></div>
10643
+ <div class="detail-row"><div class="detail-key">Friends Count</div><div class="detail-val">\${status.friendCount || 0}</div></div>
10644
+ <div class="detail-row"><div class="detail-key">Active Sessions</div><div class="detail-val">\${status.sessionCount || 0}</div></div>
10645
+ <div class="detail-row"><div class="detail-key">Plugin Version</div><div class="detail-val">1.0.4</div></div>
10646
+ </div>
10647
+ \\\`);
10473
10648
  }
10474
10649
 
10475
- // \u2500\u2500 Init \u2500\u2500
10650
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
10651
+ // INIT
10652
+ // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
10476
10653
  document.addEventListener('DOMContentLoaded', () => {
10477
- $$('.tab-btn[data-tab]').forEach(btn => {
10478
- btn.addEventListener('click', () => switchTab(btn.dataset.tab));
10479
- });
10480
- loadStatus();
10481
- switchTab('agents');
10654
+ $$('.nav-item').forEach(n => n.addEventListener('click', () => navigate(n.dataset.page)));
10655
+ $('.toggle-btn')?.addEventListener('click', toggleSidebar);
10656
+
10657
+ // Load dashboard
10658
+ navigate('dashboard');
10659
+
10482
10660
  // Auto-refresh status every 30s
10483
- setInterval(loadStatus, 30000);
10661
+ refreshTimer = setInterval(() => {
10662
+ if (currentPage === 'dashboard') loadDashboard();
10663
+ // Update status dot
10664
+ api('/status').then(s => {
10665
+ if (!s.error) {
10666
+ const dot = $('#header-dot');
10667
+ if (dot) { dot.className = 'dot ' + (s.connected ? 'dot-ok' : 'dot-err'); }
10668
+ const txt = $('#header-status');
10669
+ if (txt) txt.textContent = s.connected ? 'Connected' : 'Disconnected';
10670
+ }
10671
+ });
10672
+ }, 30000);
10484
10673
  });
10485
10674
  `;
10486
10675
  var HTML = `<!DOCTYPE html>
@@ -10488,62 +10677,86 @@ var HTML = `<!DOCTYPE html>
10488
10677
  <head>
10489
10678
  <meta charset="UTF-8">
10490
10679
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
10491
- <title>AICQ Management</title>
10680
+ <title>AICQ Management Console</title>
10492
10681
  <style>${CSS}</style>
10493
10682
  </head>
10494
10683
  <body>
10495
- <div class="topbar">
10496
- <h1><span class="logo">AQ</span> AICQ Management</h1>
10497
- <div class="status">
10498
- <span id="status-dot" class="dot dot-err"></span>
10499
- <span id="status-text">Connecting...</span>
10500
- </div>
10501
- </div>
10684
+ <div class="app">
10685
+ <!-- Sidebar -->
10686
+ <aside class="sidebar">
10687
+ <div class="sidebar-header">
10688
+ <div class="sidebar-logo">AQ</div>
10689
+ <div class="sidebar-header-text"><h1>AICQ</h1><span>Management Console</span></div>
10690
+ </div>
10691
+ <nav class="sidebar-nav">
10692
+ <div class="nav-group">
10693
+ <div class="nav-group-title">Overview</div>
10694
+ <div class="nav-item active" data-page="dashboard"><span class="nav-icon">\u{1F4CA}</span><span class="nav-label">Dashboard</span></div>
10695
+ </div>
10696
+ <div class="nav-group">
10697
+ <div class="nav-group-title">Management</div>
10698
+ <div class="nav-item" data-page="agents"><span class="nav-icon">\u{1F916}</span><span class="nav-label">Agents</span></div>
10699
+ <div class="nav-item" data-page="friends"><span class="nav-icon">\u{1F465}</span><span class="nav-label">Friends</span><span class="nav-badge" id="friend-badge">0</span></div>
10700
+ <div class="nav-item" data-page="models"><span class="nav-icon">\u{1F9E0}</span><span class="nav-label">Models</span></div>
10701
+ </div>
10702
+ <div class="nav-group">
10703
+ <div class="nav-group-title">System</div>
10704
+ <div class="nav-item" data-page="settings"><span class="nav-icon">\u2699\uFE0F</span><span class="nav-label">Settings</span></div>
10705
+ </div>
10706
+ </nav>
10707
+ <div class="sidebar-footer" onclick="toggleSidebar()">
10708
+ <span>\u25C0</span><span class="sidebar-footer-text">Collapse sidebar</span>
10709
+ </div>
10710
+ </aside>
10711
+
10712
+ <!-- Main -->
10713
+ <main class="main">
10714
+ <header class="main-header">
10715
+ <button class="toggle-btn" onclick="toggleSidebar()">\u2630</button>
10716
+ <h2 id="main-title">Dashboard</h2>
10717
+ <div class="header-status">
10718
+ <span class="dot dot-err" id="header-dot"></span>
10719
+ <span id="header-status">Connecting...</span>
10720
+ </div>
10721
+ <div class="header-actions">
10722
+ <button class="btn btn-sm btn-default" onclick="loadPage(currentPage)">\u{1F504} Refresh</button>
10723
+ </div>
10724
+ </header>
10725
+ <div class="main-content">
10502
10726
 
10503
- <div class="tabs">
10504
- <button class="tab-btn active" data-tab="agents" onclick="switchTab('agents')">\u{1F916} Agent Management</button>
10505
- <button class="tab-btn" data-tab="aicq" onclick="switchTab('aicq')">\u{1F4AC} AICQ Management</button>
10506
- <button class="tab-btn" data-tab="models" onclick="switchTab('models')">\u{1F9E0} Model Management</button>
10507
- </div>
10727
+ <!-- Dashboard -->
10728
+ <div class="page active" id="page-dashboard"><div id="dashboard-content"><div class="loading-mask"><div class="spinner"></div>Loading...</div></div></div>
10508
10729
 
10509
- <!-- TAB 1: Agent Management -->
10510
- <div class="content" id="tab-agents">
10511
- <p class="section-desc">View and manage AICQ agent identities. Each agent has its own Ed25519 key pair and encrypted session state.</p>
10512
- <div id="agents-content">
10513
- <div class="loading"><div class="spinner"></div> Loading agents...</div>
10514
- </div>
10515
- </div>
10730
+ <!-- Agents -->
10731
+ <div class="page" id="page-agents"><div id="agents-content"></div></div>
10516
10732
 
10517
- <!-- TAB 2: AICQ Management -->
10518
- <div class="content hidden" id="tab-aicq">
10519
- <p class="section-desc">Manage encrypted friend connections, pending requests, and active sessions on the AICQ network.</p>
10520
- <div class="tabs" id="aicq-subtabs" style="padding:0;margin-bottom:16px;border:none"></div>
10521
- <div id="aicq-content">
10522
- <div id="aicq-friends" class="hidden"></div>
10523
- <div id="aicq-requests" class="hidden"></div>
10524
- <div id="aicq-sessions" class="hidden"></div>
10525
- </div>
10526
- </div>
10733
+ <!-- Friends -->
10734
+ <div class="page" id="page-friends">
10735
+ <div id="friends-tabs" style="display:flex;gap:6px;margin-bottom:16px"></div>
10736
+ <div id="friends-content"></div>
10737
+ </div>
10527
10738
 
10528
- <!-- TAB 3: Model Management -->
10529
- <div class="content hidden" id="tab-models">
10530
- <p class="section-desc">Quickly configure LLM providers for your agents. Select a provider and enter your API key to get started.</p>
10531
- <div id="models-content">
10532
- <div class="loading"><div class="spinner"></div> Loading model config...</div>
10533
- </div>
10739
+ <!-- Models -->
10740
+ <div class="page" id="page-models"><div id="models-content"></div></div>
10741
+
10742
+ <!-- Settings -->
10743
+ <div class="page" id="page-settings"><div id="settings-content"></div></div>
10744
+
10745
+ </div>
10746
+ </main>
10534
10747
  </div>
10535
10748
 
10536
10749
  <!-- Modal: Add Friend -->
10537
10750
  <div class="modal-overlay hidden" id="modal-add-friend" onclick="if(event.target===this)hideModal('modal-add-friend')">
10538
10751
  <div class="modal">
10539
- <h3>\u2795 Add Friend</h3>
10752
+ <div class="modal-header"><h3>\u2795 Add Friend</h3><button class="modal-close" onclick="hideModal('modal-add-friend')">\u2715</button></div>
10540
10753
  <div class="form-group">
10541
- <label>Temp Number or Friend ID</label>
10542
- <input id="add-friend-target" type="text" placeholder="Enter 6-digit temp number or node ID" onkeydown="if(event.key==='Enter')addFriend()">
10543
- <div class="hint">Enter the 6-digit temporary number shared by your friend, or their node ID directly.</div>
10754
+ <label>Temp Number or Node ID</label>
10755
+ <input id="add-friend-target" type="text" placeholder="6-digit number or node ID" onkeydown="if(event.key==='Enter')addFriend()">
10756
+ <div class="hint">Enter the 6-digit temporary number or the full node ID of your friend.</div>
10544
10757
  </div>
10545
10758
  <div class="form-actions">
10546
- <button class="btn" onclick="hideModal('modal-add-friend')">Cancel</button>
10759
+ <button class="btn btn-default" onclick="hideModal('modal-add-friend')">Cancel</button>
10547
10760
  <button class="btn btn-primary" onclick="addFriend()">Send Request</button>
10548
10761
  </div>
10549
10762
  </div>
@@ -10552,49 +10765,59 @@ var HTML = `<!DOCTYPE html>
10552
10765
  <!-- Modal: Edit Permissions -->
10553
10766
  <div class="modal-overlay hidden" id="modal-permissions" onclick="if(event.target===this)hideModal('modal-permissions')">
10554
10767
  <div class="modal">
10555
- <h3>\u2699\uFE0F Edit Permissions</h3>
10768
+ <div class="modal-header"><h3>\u2699\uFE0F Edit Permissions</h3><button class="modal-close" onclick="hideModal('modal-permissions')">\u2715</button></div>
10556
10769
  <div class="form-group">
10557
- <label>Permissions for this friend</label>
10558
- <div class="perm-checks" style="margin-top:8px">
10559
- <label><input type="checkbox" id="perm-chat" checked> Chat <span style="color:var(--text3);font-size:11px">(send/receive messages)</span></label>
10560
- <label><input type="checkbox" id="perm-exec"> Exec <span style="color:var(--text3);font-size:11px">(execute tools)</span></label>
10770
+ <label>Friend Permissions</label>
10771
+ <div class="perm-checks" style="margin-top:10px">
10772
+ <label><input type="checkbox" id="perm-chat" checked> \u{1F4AC} Chat <span style="color:var(--text3);font-size:11px">(send/receive messages)</span></label>
10773
+ <label><input type="checkbox" id="perm-exec"> \u{1F527} Exec <span style="color:var(--text3);font-size:11px">(execute tools/commands)</span></label>
10561
10774
  </div>
10562
10775
  </div>
10563
10776
  <div class="form-actions">
10564
- <button class="btn" onclick="hideModal('modal-permissions')">Cancel</button>
10565
- <button class="btn btn-primary" onclick="savePermissions()">Save</button>
10777
+ <button class="btn btn-default" onclick="hideModal('modal-permissions')">Cancel</button>
10778
+ <button class="btn btn-primary" onclick="saveFriendPerms()">Save Permissions</button>
10566
10779
  </div>
10567
10780
  </div>
10568
10781
  </div>
10569
10782
 
10570
10783
  <!-- Modal: Model Config -->
10571
10784
  <div class="modal-overlay hidden" id="modal-model-config" onclick="if(event.target===this)hideModal('modal-model-config')">
10572
- <div class="modal" style="max-width:520px">
10573
- <h3>\u{1F9E0} <span id="model-provider-name">Configure Provider</span></h3>
10785
+ <div class="modal" style="max-width:560px">
10786
+ <div class="modal-header"><h3><span id="model-icon">\u{1F7E2}</span> <span id="model-name">Provider</span></h3><button class="modal-close" onclick="hideModal('modal-model-config')">\u2715</button></div>
10787
+ <div style="margin-bottom:16px;font-size:12px;color:var(--text3)" id="model-current-key"></div>
10574
10788
  <div class="form-group">
10575
- <label>API Key</label>
10576
- <input id="model-api-key" type="password" placeholder="sk-...">
10577
- <div class="hint">Your provider API key. It will be stored securely in the OpenClaw config.</div>
10789
+ <label>\u{1F511} API Key</label>
10790
+ <div class="input-prefix"><span class="prefix">\u{1F511}</span><input id="model-api-key" type="password" placeholder="sk-..."></div>
10791
+ <div class="hint">Leave blank to keep the existing key. Enter a new key to replace it.</div>
10578
10792
  </div>
10579
10793
  <div class="form-group">
10580
- <label>Model ID</label>
10581
- <input id="model-model-id" type="text" placeholder="gpt-4o">
10582
- <div class="hint">The model identifier to use. Leave default for the provider's recommended model.</div>
10794
+ <label>\u{1F916} Model ID</label>
10795
+ <div class="input-prefix"><span class="prefix">\u{1F916}</span><input id="model-model-id" type="text" placeholder="gpt-4o"></div>
10796
+ <div class="hint">The model to use. E.g. gpt-4o, claude-sonnet-4-20250514, etc.</div>
10583
10797
  </div>
10584
10798
  <div class="form-group">
10585
- <label>Base URL <span style="color:var(--text3);font-size:11px">(optional)</span></label>
10586
- <input id="model-base-url" type="text" placeholder="https://api.openai.com/v1">
10587
- <div class="hint">Custom API endpoint. Only change this if using a compatible proxy or self-hosted model.</div>
10799
+ <label>\u{1F310} Base URL <span style="font-weight:400;text-transform:none;letter-spacing:0;color:var(--text3)">(optional)</span></label>
10800
+ <div class="input-prefix"><span class="prefix">\u{1F310}</span><input id="model-base-url" type="text" placeholder="https://..."></div>
10801
+ <div class="hint">Custom endpoint URL. Only needed for proxies or self-hosted models.</div>
10588
10802
  </div>
10589
10803
  <div class="form-actions">
10590
- <button class="btn" onclick="hideModal('modal-model-config')">Cancel</button>
10591
- <button class="btn btn-primary" onclick="saveModelConfig()">Save Configuration</button>
10804
+ <button class="btn btn-default" onclick="hideModal('modal-model-config')">Cancel</button>
10805
+ <button class="btn btn-primary" onclick="saveModelConfig()">\u{1F4BE} Save Configuration</button>
10592
10806
  </div>
10593
10807
  </div>
10594
10808
  </div>
10595
10809
 
10596
- <!-- Toast -->
10597
- <div class="toast hidden" id="toast"></div>
10810
+ <!-- Modal: View Agent -->
10811
+ <div class="modal-overlay hidden" id="modal-view-agent" onclick="if(event.target===this)hideModal('modal-view-agent')">
10812
+ <div class="modal">
10813
+ <div class="modal-header"><h3>\u{1F916} <span id="view-agent-title">Agent</span></h3><button class="modal-close" onclick="hideModal('modal-view-agent')">\u2715</button></div>
10814
+ <div id="view-agent-body"></div>
10815
+ <div class="form-actions"><button class="btn btn-default" onclick="hideModal('modal-view-agent')">Close</button></div>
10816
+ </div>
10817
+ </div>
10818
+
10819
+ <!-- Toast Container -->
10820
+ <div id="toast-container" class="toast-container"></div>
10598
10821
 
10599
10822
  <script>${JS}</script>
10600
10823
  </body>
@@ -10608,113 +10831,60 @@ import * as fs5 from "fs";
10608
10831
  import * as path5 from "path";
10609
10832
  import * as os from "os";
10610
10833
  var MODEL_PROVIDERS = [
10611
- {
10612
- id: "openai",
10613
- name: "OpenAI",
10614
- description: "GPT-4o, GPT-4, GPT-3.5 and more",
10615
- apiKeyHint: "sk-...",
10616
- modelHint: "gpt-4o",
10617
- baseUrlHint: "https://api.openai.com/v1",
10618
- configKey: "openai"
10619
- },
10620
- {
10621
- id: "anthropic",
10622
- name: "Anthropic",
10623
- description: "Claude 4, Claude 3.5 Sonnet, Haiku",
10624
- apiKeyHint: "sk-ant-...",
10625
- modelHint: "claude-sonnet-4-20250514",
10626
- baseUrlHint: "https://api.anthropic.com",
10627
- configKey: "anthropic"
10628
- },
10629
- {
10630
- id: "google",
10631
- name: "Google AI",
10632
- description: "Gemini 2.5 Pro, Gemini 2.5 Flash",
10633
- apiKeyHint: "AI...",
10634
- modelHint: "gemini-2.5-pro",
10635
- baseUrlHint: "",
10636
- configKey: "google"
10637
- },
10638
- {
10639
- id: "groq",
10640
- name: "Groq",
10641
- description: "Llama 3, Mixtral \u2014 ultra fast inference",
10642
- apiKeyHint: "gsk_...",
10643
- modelHint: "llama-3.3-70b-versatile",
10644
- baseUrlHint: "https://api.groq.com/openai/v1",
10645
- configKey: "groq"
10646
- },
10647
- {
10648
- id: "deepseek",
10649
- name: "DeepSeek",
10650
- description: "DeepSeek V3, DeepSeek R1",
10651
- apiKeyHint: "sk-...",
10652
- modelHint: "deepseek-chat",
10653
- baseUrlHint: "https://api.deepseek.com/v1",
10654
- configKey: "deepseek"
10655
- },
10656
- {
10657
- id: "ollama",
10658
- name: "Ollama (Local)",
10659
- description: "Run models locally on your machine",
10660
- apiKeyHint: "(no key needed)",
10661
- modelHint: "llama3",
10662
- baseUrlHint: "http://localhost:11434/v1",
10663
- configKey: "ollama"
10664
- },
10665
- {
10666
- id: "openrouter",
10667
- name: "OpenRouter",
10668
- description: "Unified API for 200+ models",
10669
- apiKeyHint: "sk-or-...",
10670
- modelHint: "openai/gpt-4o",
10671
- baseUrlHint: "https://openrouter.ai/api/v1",
10672
- configKey: "openrouter"
10673
- },
10674
- {
10675
- id: "mistral",
10676
- name: "Mistral AI",
10677
- description: "Mistral Large, Medium, Small",
10678
- apiKeyHint: "(your key)",
10679
- modelHint: "mistral-large-latest",
10680
- baseUrlHint: "https://api.mistral.ai/v1",
10681
- configKey: "mistral"
10682
- }
10834
+ { id: "openai", name: "OpenAI", description: "GPT-4o, GPT-4, GPT-3.5, o1, o3", apiKeyHint: "sk-...", modelHint: "gpt-4o", baseUrlHint: "https://api.openai.com/v1", configKey: "openai" },
10835
+ { id: "anthropic", name: "Anthropic", description: "Claude 4, Claude 3.5 Sonnet, Haiku, Opus", apiKeyHint: "sk-ant-...", modelHint: "claude-sonnet-4-20250514", baseUrlHint: "https://api.anthropic.com", configKey: "anthropic" },
10836
+ { id: "google", name: "Google AI", description: "Gemini 2.5 Pro, Gemini 2.5 Flash, Gemini Pro", apiKeyHint: "AI...", modelHint: "gemini-2.5-pro", baseUrlHint: "", configKey: "google" },
10837
+ { id: "groq", name: "Groq", description: "Llama 3.3, Mixtral \u2014 ultra fast inference", apiKeyHint: "gsk_...", modelHint: "llama-3.3-70b-versatile", baseUrlHint: "https://api.groq.com/openai/v1", configKey: "groq" },
10838
+ { id: "deepseek", name: "DeepSeek", description: "DeepSeek V3, DeepSeek R1 \u2014 strong reasoning", apiKeyHint: "sk-...", modelHint: "deepseek-chat", baseUrlHint: "https://api.deepseek.com/v1", configKey: "deepseek" },
10839
+ { id: "ollama", name: "Ollama (Local)", description: "Run models locally \u2014 Llama, Mistral, Phi, etc.", apiKeyHint: "(no key needed)", modelHint: "llama3", baseUrlHint: "http://localhost:11434/v1", configKey: "ollama" },
10840
+ { id: "openrouter", name: "OpenRouter", description: "Unified API for 200+ open-source and commercial models", apiKeyHint: "sk-or-...", modelHint: "openai/gpt-4o", baseUrlHint: "https://openrouter.ai/api/v1", configKey: "openrouter" },
10841
+ { id: "mistral", name: "Mistral AI", description: "Mistral Large, Medium, Small, Codestral", apiKeyHint: "(your key)", modelHint: "mistral-large-latest", baseUrlHint: "https://api.mistral.ai/v1", configKey: "mistral" },
10842
+ { id: "together", name: "Together AI", description: "Open-source models with fast inference", apiKeyHint: "...", modelHint: "meta-llama/Llama-3-70b-chat-hf", baseUrlHint: "https://api.together.xyz/v1", configKey: "together" },
10843
+ { id: "fireworks", name: "Fireworks AI", description: "Fast open-source model serving", apiKeyHint: "...", modelHint: "accounts/fireworks/models/llama-v3-70b-instruct", baseUrlHint: "https://api.fireworks.ai/inference/v1", configKey: "fireworks" }
10683
10844
  ];
10684
- function findOpenClawConfig() {
10685
- const candidates = [
10845
+ function findConfigPath() {
10846
+ const openclawPaths = [
10686
10847
  path5.join(process.cwd(), "openclaw.json"),
10687
- path5.join(process.cwd(), "stableclaw.json"),
10688
10848
  path5.join(os.homedir(), ".config", "openclaw", "openclaw.json"),
10689
- path5.join(os.homedir(), ".config", "stableclaw", "stableclaw.json"),
10690
10849
  path5.join(os.homedir(), ".openclaw", "openclaw.json"),
10850
+ path5.join(os.homedir(), "openclaw.json")
10851
+ ];
10852
+ for (const p of openclawPaths) {
10853
+ try {
10854
+ if (fs5.existsSync(p))
10855
+ return p;
10856
+ } catch {
10857
+ }
10858
+ }
10859
+ const stableclawPaths = [
10860
+ path5.join(process.cwd(), "stableclaw.json"),
10861
+ path5.join(os.homedir(), ".config", "stableclaw", "stableclaw.json"),
10691
10862
  path5.join(os.homedir(), ".stableclaw", "stableclaw.json"),
10692
- path5.join(os.homedir(), "openclaw.json"),
10693
10863
  path5.join(os.homedir(), "stableclaw.json")
10694
10864
  ];
10695
- for (const p of candidates) {
10865
+ for (const p of stableclawPaths) {
10696
10866
  try {
10697
- if (fs5.existsSync(p)) {
10867
+ if (fs5.existsSync(p))
10698
10868
  return p;
10699
- }
10700
10869
  } catch {
10701
10870
  }
10702
10871
  }
10703
10872
  return null;
10704
10873
  }
10705
- function readOpenClawConfig() {
10706
- const configPath = findOpenClawConfig();
10874
+ function readConfig() {
10875
+ const configPath = findConfigPath();
10707
10876
  if (!configPath)
10708
10877
  return null;
10709
10878
  try {
10710
10879
  const raw = fs5.readFileSync(configPath, "utf-8");
10711
- return JSON.parse(raw);
10880
+ const config2 = JSON.parse(raw);
10881
+ return { config: config2, configPath };
10712
10882
  } catch {
10713
10883
  return null;
10714
10884
  }
10715
10885
  }
10716
- function writeOpenClawConfig(config2) {
10717
- const configPath = findOpenClawConfig();
10886
+ function writeConfig(config2) {
10887
+ const configPath = findConfigPath();
10718
10888
  if (!configPath)
10719
10889
  return false;
10720
10890
  try {
@@ -10724,24 +10894,54 @@ function writeOpenClawConfig(config2) {
10724
10894
  return false;
10725
10895
  }
10726
10896
  }
10727
- function getModelConfig(config2) {
10897
+ function extractAgentsFromConfig(config2) {
10898
+ const agents = [];
10899
+ const agentsVal = config2.agents;
10900
+ if (Array.isArray(agentsVal)) {
10901
+ for (const a of agentsVal) {
10902
+ if (typeof a === "object" && a !== null) {
10903
+ agents.push(a);
10904
+ }
10905
+ }
10906
+ }
10907
+ const agentVal = config2.agent;
10908
+ if (typeof agentVal === "object" && agentVal !== null && !Array.isArray(agentVal)) {
10909
+ agents.unshift(agentVal);
10910
+ }
10911
+ if (agents.length === 0) {
10912
+ for (const [key, val] of Object.entries(config2)) {
10913
+ if (typeof val === "object" && val !== null && !Array.isArray(val)) {
10914
+ const v = val;
10915
+ if (v.model || v.systemPrompt || v.provider || v.apiKey) {
10916
+ agents.push({ _configKey: key, ...v });
10917
+ }
10918
+ }
10919
+ }
10920
+ }
10921
+ return agents;
10922
+ }
10923
+ function getModelProviders(config2) {
10924
+ let providersSection = config2.providers;
10925
+ if (!providersSection || typeof providersSection !== "object") {
10926
+ const model = config2.model;
10927
+ if (model?.providers)
10928
+ providersSection = model.providers;
10929
+ }
10728
10930
  const providers = MODEL_PROVIDERS.map((p) => {
10729
- const providersSection2 = config2.providers;
10730
- const providerConfig = providersSection2?.[p.configKey] ?? config2[p.configKey];
10731
- const apiKey = providerConfig?.apiKey || "";
10732
- const modelId = providerConfig?.model || providerConfig?.defaultModel || "";
10733
- const baseUrl = providerConfig?.baseUrl || providerConfig?.baseURL || "";
10931
+ const pc = providersSection?.[p.configKey] ?? config2[p.configKey];
10932
+ const apiKey = pc?.apiKey || "";
10933
+ const modelId = pc?.model || pc?.defaultModel || "";
10934
+ const baseUrl = pc?.baseUrl || pc?.baseURL || "";
10734
10935
  return {
10735
10936
  ...p,
10736
10937
  configured: Boolean(apiKey || p.id === "ollama" && baseUrl),
10737
- apiKey: apiKey ? apiKey.substring(0, 8) + "..." + apiKey.slice(-4) : "",
10938
+ apiKey: apiKey ? apiKey.substring(0, 6) + "\u2022\u2022\u2022\u2022\u2022\u2022" + apiKey.slice(-4) : "",
10738
10939
  apiKeyHasValue: Boolean(apiKey),
10739
10940
  modelId,
10740
10941
  baseUrl
10741
10942
  };
10742
10943
  });
10743
10944
  const currentModels = [];
10744
- const providersSection = config2.providers;
10745
10945
  for (const p of MODEL_PROVIDERS) {
10746
10946
  const pc = providersSection?.[p.configKey] ?? config2[p.configKey];
10747
10947
  if (pc?.apiKey) {
@@ -10750,25 +10950,38 @@ function getModelConfig(config2) {
10750
10950
  providerId: p.id,
10751
10951
  modelId: pc.model || pc.defaultModel || p.modelHint,
10752
10952
  hasApiKey: true,
10753
- baseUrl: pc.baseUrl || pc.baseURL || ""
10953
+ baseUrl: pc.baseUrl || pc.baseURL || p.baseUrlHint
10754
10954
  });
10755
10955
  }
10756
10956
  }
10757
10957
  return { providers, currentModels };
10758
10958
  }
10759
- function parseSubPath(reqPath, prefix) {
10760
- const fullPrefix = "/plugins" + prefix;
10761
- if (reqPath.startsWith(fullPrefix)) {
10762
- return reqPath.slice(fullPrefix.length) || "/";
10763
- }
10764
- return "/";
10959
+ function parseApiPath(reqUrl) {
10960
+ if (!reqUrl)
10961
+ return "/";
10962
+ const urlPath = reqUrl.split("?")[0];
10963
+ const gatewayPrefix = "/plugins/aicq-chat";
10964
+ if (urlPath.startsWith(gatewayPrefix)) {
10965
+ return urlPath.slice(gatewayPrefix.length) || "/";
10966
+ }
10967
+ return urlPath;
10765
10968
  }
10766
10969
  function json(res, data, status = 200) {
10767
10970
  if (!res.headersSent) {
10768
- res.writeHead(status, { "Content-Type": "application/json; charset=utf-8" });
10971
+ res.writeHead(status, { "Content-Type": "application/json; charset=utf-8", "Access-Control-Allow-Origin": "*" });
10769
10972
  }
10770
10973
  res.end(JSON.stringify(data));
10771
10974
  }
10975
+ function corsHeaders(res) {
10976
+ if (!res.headersSent) {
10977
+ res.writeHead(200, {
10978
+ "Content-Type": "application/json; charset=utf-8",
10979
+ "Access-Control-Allow-Origin": "*",
10980
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
10981
+ "Access-Control-Allow-Headers": "Content-Type"
10982
+ });
10983
+ }
10984
+ }
10772
10985
  async function readBody(req) {
10773
10986
  return new Promise((resolve3) => {
10774
10987
  const chunks = [];
@@ -10787,20 +11000,24 @@ async function readBody(req) {
10787
11000
  function createManagementHandler(ctx) {
10788
11001
  const { store, identityService, serverClient, serverUrl, aicqAgentId, logger, html } = ctx;
10789
11002
  return async (req, res) => {
10790
- const subPath = parseSubPath(req.url || "/", "/aicq-chat");
10791
- const rawPath = req.url || "/";
10792
- if (subPath === "/" || subPath === "/ui" || subPath === "" || rawPath === "/") {
11003
+ const urlPath = parseApiPath(req.url || "/");
11004
+ const method = (req.method || "GET").toUpperCase();
11005
+ if (method === "OPTIONS") {
11006
+ corsHeaders(res);
11007
+ res.end();
11008
+ return;
11009
+ }
11010
+ if (urlPath === "/" || urlPath === "/ui" || urlPath === "/index.html") {
10793
11011
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
10794
11012
  res.end(html);
10795
11013
  return;
10796
11014
  }
10797
- if (!subPath.startsWith("/api/")) {
11015
+ if (!urlPath.startsWith("/api/")) {
10798
11016
  res.writeHead(404, { "Content-Type": "application/json" });
10799
11017
  res.end(JSON.stringify({ error: "Not found" }));
10800
11018
  return;
10801
11019
  }
10802
- const apiPath = subPath.slice(4);
10803
- const method = (req.method || "GET").toUpperCase();
11020
+ const apiPath = urlPath.slice(4);
10804
11021
  try {
10805
11022
  if (apiPath === "/status" && method === "GET") {
10806
11023
  return json(res, {
@@ -10822,62 +11039,70 @@ function createManagementHandler(ctx) {
10822
11039
  sessionCount: store.sessions.size
10823
11040
  });
10824
11041
  }
11042
+ if (apiPath === "/config" && method === "GET") {
11043
+ const result = readConfig();
11044
+ if (!result)
11045
+ return json(res, { error: "No config file found. Create openclaw.json or stableclaw.json." });
11046
+ const stats = fs5.statSync(result.configPath);
11047
+ return json(res, {
11048
+ configPath: result.configPath,
11049
+ configSize: stats.size,
11050
+ configModified: stats.mtime.toISOString()
11051
+ });
11052
+ }
10825
11053
  if (apiPath === "/agents" && method === "GET") {
10826
- const localFriends = Array.from(store.friends.values()).map((f) => ({
10827
- id: f.id,
10828
- name: f.aiName || f.id.substring(0, 8),
10829
- friendType: f.friendType,
10830
- publicKeyFingerprint: f.publicKeyFingerprint,
10831
- permissions: f.permissions || [],
10832
- lastMessageAt: f.lastMessageAt?.toISOString() || null,
10833
- sessionCount: store.sessions.has(f.id) ? store.sessions.get(f.id)?.messageCount || 0 : 0
10834
- }));
10835
- let serverFriends = [];
10836
- try {
10837
- const resp = await fetch(serverUrl + "/api/v1/friends?nodeId=" + aicqAgentId);
10838
- if (resp.ok) {
10839
- const data = await resp.json();
10840
- serverFriends = data.friends || [];
10841
- }
10842
- } catch {
10843
- }
10844
- const localIds = new Set(localFriends.map((f) => f.id));
10845
- for (const sf of serverFriends) {
10846
- const sfId = sf.nodeId;
10847
- if (sfId && !localIds.has(sfId)) {
10848
- localFriends.push({
10849
- id: sfId,
10850
- name: sf.aiName || sfId.substring(0, 8),
10851
- friendType: sf.friendType || void 0,
10852
- publicKeyFingerprint: sf.publicKeyFingerprint || "",
10853
- permissions: sf.permissions || [],
10854
- lastMessageAt: sf.lastMessageAt || null,
10855
- sessionCount: 0
10856
- });
10857
- }
11054
+ const result = readConfig();
11055
+ if (!result) {
11056
+ return json(res, {
11057
+ agents: [],
11058
+ configSource: "none",
11059
+ currentAgentId: aicqAgentId,
11060
+ fingerprint: identityService.getPublicKeyFingerprint(),
11061
+ connected: serverClient.isConnected()
11062
+ });
10858
11063
  }
11064
+ const agents = extractAgentsFromConfig(result.config);
11065
+ globalThis.__aicq_agents = agents;
10859
11066
  return json(res, {
10860
- agents: localFriends,
11067
+ agents,
11068
+ configSource: path5.basename(result.configPath),
11069
+ configPath: result.configPath,
10861
11070
  currentAgentId: aicqAgentId,
10862
11071
  fingerprint: identityService.getPublicKeyFingerprint(),
10863
11072
  connected: serverClient.isConnected()
10864
11073
  });
10865
11074
  }
10866
11075
  if (apiPath.startsWith("/agents/") && method === "DELETE") {
10867
- const friendId = decodeURIComponent(apiPath.slice("/agents/".length));
10868
- if (!friendId)
10869
- return json(res, { success: false, message: "Missing agent ID" }, 400);
10870
- try {
10871
- await fetch(serverUrl + "/api/v1/friends/" + friendId, {
10872
- method: "DELETE",
10873
- headers: { "Content-Type": "application/json" },
10874
- body: JSON.stringify({ nodeId: aicqAgentId })
10875
- });
10876
- } catch {
11076
+ const idxStr = decodeURIComponent(apiPath.slice("/agents/".length));
11077
+ const idx = parseInt(idxStr, 10);
11078
+ if (isNaN(idx) || idx < 0)
11079
+ return json(res, { success: false, message: "Invalid agent index" }, 400);
11080
+ const result = readConfig();
11081
+ if (!result)
11082
+ return json(res, { success: false, message: "No config file found" }, 400);
11083
+ const config2 = result.config;
11084
+ let agentsArr;
11085
+ if (Array.isArray(config2.agents)) {
11086
+ agentsArr = config2.agents;
11087
+ } else if (typeof config2.agent === "object" && config2.agent !== null && idx === 0) {
11088
+ delete config2.agent;
11089
+ const written2 = writeConfig(config2);
11090
+ if (written2) {
11091
+ logger.info("[API] Agent deleted from config");
11092
+ return json(res, { success: true, message: "Agent removed" });
11093
+ }
11094
+ return json(res, { success: false, message: "Failed to write config" }, 500);
10877
11095
  }
10878
- store.removeFriend(friendId);
10879
- logger.info("[API] Agent/friend deleted: " + friendId);
10880
- return json(res, { success: true, message: "Agent removed" });
11096
+ if (!agentsArr || idx >= agentsArr.length) {
11097
+ return json(res, { success: false, message: "Agent index out of range" }, 400);
11098
+ }
11099
+ agentsArr.splice(idx, 1);
11100
+ const written = writeConfig(config2);
11101
+ if (written) {
11102
+ logger.info("[API] Agent deleted at index " + idx);
11103
+ return json(res, { success: true, message: "Agent removed" });
11104
+ }
11105
+ return json(res, { success: false, message: "Failed to write config" }, 500);
10881
11106
  }
10882
11107
  if (apiPath === "/friends" && method === "GET") {
10883
11108
  try {
@@ -10911,26 +11136,36 @@ function createManagementHandler(ctx) {
10911
11136
  const isTempNumber = /^\d{6}$/.test(target);
10912
11137
  let friendId = target;
10913
11138
  if (isTempNumber) {
10914
- const resolveResp = await fetch(serverUrl + "/api/v1/temp-number/" + target);
10915
- if (!resolveResp.ok)
10916
- return json(res, { success: false, message: "Temp number not found or expired" });
10917
- const resolveData = await resolveResp.json();
10918
- friendId = resolveData.nodeId;
10919
- }
10920
- const hsResp = await fetch(serverUrl + "/api/v1/handshake/initiate", {
10921
- method: "POST",
10922
- headers: { "Content-Type": "application/json" },
10923
- body: JSON.stringify({ requesterId: aicqAgentId, targetTempNumber: target })
10924
- });
10925
- if (!hsResp.ok)
10926
- return json(res, { success: false, message: "Handshake failed: " + await hsResp.text() });
10927
- const hsData = await hsResp.json();
10928
- logger.info("[API] Friend request sent to " + friendId);
10929
- return json(res, { success: true, message: "Friend request sent to " + friendId, sessionId: hsData.sessionId, targetNodeId: friendId });
11139
+ try {
11140
+ const resolveResp = await fetch(serverUrl + "/api/v1/temp-number/" + target);
11141
+ if (!resolveResp.ok)
11142
+ return json(res, { success: false, message: "Temp number not found or expired" });
11143
+ const resolveData = await resolveResp.json();
11144
+ friendId = resolveData.nodeId;
11145
+ } catch (err) {
11146
+ const msg = err instanceof Error ? err.message : String(err);
11147
+ return json(res, { success: false, message: "Failed to resolve temp number: " + msg }, 502);
11148
+ }
11149
+ }
11150
+ try {
11151
+ const hsResp = await fetch(serverUrl + "/api/v1/handshake/initiate", {
11152
+ method: "POST",
11153
+ headers: { "Content-Type": "application/json" },
11154
+ body: JSON.stringify({ requesterId: aicqAgentId, targetTempNumber: target })
11155
+ });
11156
+ if (!hsResp.ok)
11157
+ return json(res, { success: false, message: "Handshake failed: " + await hsResp.text() });
11158
+ const hsData = await hsResp.json();
11159
+ logger.info("[API] Friend request sent to " + friendId);
11160
+ return json(res, { success: true, message: "Friend request sent to " + friendId, sessionId: hsData.sessionId, targetNodeId: friendId });
11161
+ } catch (err) {
11162
+ const msg = err instanceof Error ? err.message : String(err);
11163
+ return json(res, { success: false, message: "Handshake request failed: " + msg }, 502);
11164
+ }
10930
11165
  }
10931
- if (apiPath.startsWith("/friends/") && !apiPath.includes("/permissions") && !apiPath.includes("/requests") && method === "DELETE") {
11166
+ if (apiPath.startsWith("/friends/") && method === "DELETE") {
10932
11167
  if (apiPath.includes("/requests/")) {
10933
- const parts = apiPath.split("/");
11168
+ } else if (apiPath.includes("/permissions")) {
10934
11169
  } else {
10935
11170
  const friendId = decodeURIComponent(apiPath.slice("/friends/".length));
10936
11171
  if (!friendId)
@@ -10968,7 +11203,7 @@ function createManagementHandler(ctx) {
10968
11203
  body: JSON.stringify({ nodeId: aicqAgentId, permissions })
10969
11204
  });
10970
11205
  if (!resp.ok)
10971
- return json(res, { success: false, message: "Failed to update: " + await resp.text() });
11206
+ return json(res, { success: false, message: "Failed: " + await resp.text() });
10972
11207
  const localFriend = store.getFriend(friendId);
10973
11208
  if (localFriend) {
10974
11209
  localFriend.permissions = permissions;
@@ -10998,29 +11233,39 @@ function createManagementHandler(ctx) {
10998
11233
  if (!requestId)
10999
11234
  return json(res, { success: false, message: "Missing request ID" }, 400);
11000
11235
  const body = await readBody(req);
11001
- const resp = await fetch(serverUrl + "/api/v1/friends/requests/" + requestId + "/accept", {
11002
- method: "POST",
11003
- headers: { "Content-Type": "application/json" },
11004
- body: JSON.stringify({ permissions: body.permissions || ["chat"] })
11005
- });
11006
- if (!resp.ok)
11007
- return json(res, { success: false, message: "Failed: " + await resp.text() });
11008
- logger.info("[API] Friend request accepted: " + requestId);
11009
- return json(res, { success: true, message: "Friend request accepted" });
11236
+ try {
11237
+ const resp = await fetch(serverUrl + "/api/v1/friends/requests/" + requestId + "/accept", {
11238
+ method: "POST",
11239
+ headers: { "Content-Type": "application/json" },
11240
+ body: JSON.stringify({ permissions: body.permissions || ["chat"] })
11241
+ });
11242
+ if (!resp.ok)
11243
+ return json(res, { success: false, message: "Failed: " + await resp.text() });
11244
+ logger.info("[API] Friend request accepted: " + requestId);
11245
+ return json(res, { success: true, message: "Friend request accepted" });
11246
+ } catch (err) {
11247
+ const msg = err instanceof Error ? err.message : String(err);
11248
+ return json(res, { success: false, message: msg }, 500);
11249
+ }
11010
11250
  }
11011
11251
  if (apiPath.match(/^\/friends\/requests\/[^/]+\/reject$/) && method === "POST") {
11012
11252
  const requestId = decodeURIComponent(apiPath.split("/")[3]);
11013
11253
  if (!requestId)
11014
11254
  return json(res, { success: false, message: "Missing request ID" }, 400);
11015
- const resp = await fetch(serverUrl + "/api/v1/friends/requests/" + requestId + "/reject", {
11016
- method: "POST",
11017
- headers: { "Content-Type": "application/json" },
11018
- body: JSON.stringify({})
11019
- });
11020
- if (!resp.ok)
11021
- return json(res, { success: false, message: "Failed: " + await resp.text() });
11022
- logger.info("[API] Friend request rejected: " + requestId);
11023
- return json(res, { success: true, message: "Friend request rejected" });
11255
+ try {
11256
+ const resp = await fetch(serverUrl + "/api/v1/friends/requests/" + requestId + "/reject", {
11257
+ method: "POST",
11258
+ headers: { "Content-Type": "application/json" },
11259
+ body: JSON.stringify({})
11260
+ });
11261
+ if (!resp.ok)
11262
+ return json(res, { success: false, message: "Failed: " + await resp.text() });
11263
+ logger.info("[API] Friend request rejected: " + requestId);
11264
+ return json(res, { success: true, message: "Friend request rejected" });
11265
+ } catch (err) {
11266
+ const msg = err instanceof Error ? err.message : String(err);
11267
+ return json(res, { success: false, message: msg }, 500);
11268
+ }
11024
11269
  }
11025
11270
  if (apiPath === "/sessions" && method === "GET") {
11026
11271
  const sessions = Array.from(store.sessions.values()).map((s) => ({
@@ -11031,9 +11276,10 @@ function createManagementHandler(ctx) {
11031
11276
  return json(res, { sessions });
11032
11277
  }
11033
11278
  if (apiPath === "/models" && method === "GET") {
11034
- const config2 = readOpenClawConfig();
11035
- const modelData = config2 ? getModelConfig(config2) : { providers: MODEL_PROVIDERS, currentModels: [] };
11036
- return json(res, modelData);
11279
+ const result = readConfig();
11280
+ if (!result)
11281
+ return json(res, { providers: MODEL_PROVIDERS, currentModels: [], error: "No config file found" });
11282
+ return json(res, getModelProviders(result.config));
11037
11283
  }
11038
11284
  if (apiPath.match(/^\/models\/[^/]+$/) && method === "PUT") {
11039
11285
  const providerId = decodeURIComponent(apiPath.slice("/models/".length));
@@ -11044,15 +11290,15 @@ function createManagementHandler(ctx) {
11044
11290
  const provider = MODEL_PROVIDERS.find((p) => p.id === providerId);
11045
11291
  if (!provider)
11046
11292
  return json(res, { success: false, message: "Unknown provider: " + providerId }, 400);
11047
- const config2 = readOpenClawConfig();
11048
- if (!config2) {
11049
- return json(res, { success: false, message: "Could not find openclaw.json config file. Make sure OpenClaw is properly configured." }, 400);
11050
- }
11051
- if (!config2.providers) {
11293
+ const result = readConfig();
11294
+ if (!result)
11295
+ return json(res, { success: false, message: "No config file found. Create openclaw.json or stableclaw.json." }, 400);
11296
+ const config2 = result.config;
11297
+ if (!config2.providers || typeof config2.providers !== "object") {
11052
11298
  config2.providers = {};
11053
11299
  }
11054
11300
  const providers = config2.providers;
11055
- if (!providers[provider.configKey]) {
11301
+ if (!providers[provider.configKey] || typeof providers[provider.configKey] !== "object") {
11056
11302
  providers[provider.configKey] = {};
11057
11303
  }
11058
11304
  const provConfig = providers[provider.configKey];
@@ -11062,10 +11308,9 @@ function createManagementHandler(ctx) {
11062
11308
  provConfig.model = modelId;
11063
11309
  if (baseUrl)
11064
11310
  provConfig.baseUrl = baseUrl;
11065
- const written = writeOpenClawConfig(config2);
11066
- if (!written) {
11311
+ const written = writeConfig(config2);
11312
+ if (!written)
11067
11313
  return json(res, { success: false, message: "Failed to write config file" }, 500);
11068
- }
11069
11314
  logger.info("[API] Model config saved for provider: " + providerId);
11070
11315
  return json(res, { success: true, message: "Model configuration saved for " + provider.name });
11071
11316
  }
@@ -11631,7 +11876,7 @@ var plugin = definePluginEntry({
11631
11876
  logger,
11632
11877
  html: managementHtml
11633
11878
  });
11634
- const mgmtPort = parseInt(process.env.AICQ_MGMT_PORT || "8099", 10);
11879
+ const mgmtPort = parseInt(process.env.AICQ_MGMT_PORT || "461099", 10);
11635
11880
  const mgmtServer = http.createServer((req, res) => {
11636
11881
  managementHandler(req, res).catch((err) => {
11637
11882
  logger.error("[HTTP] Management server error: " + (err instanceof Error ? err.message : err));