aicq-openclaw-plugin 1.0.2 → 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
@@ -7769,6 +7769,7 @@ var require_dist = __commonJS({
7769
7769
  // dist/index.js
7770
7770
  var dotenv = __toESM(require_main(), 1);
7771
7771
  import * as path6 from "path";
7772
+ import * as http from "http";
7772
7773
  import { definePluginEntry } from "openclaw/plugin-sdk/core";
7773
7774
 
7774
7775
  // dist/config.js
@@ -9816,670 +9817,859 @@ var CSS = `
9816
9817
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
9817
9818
  :root {
9818
9819
  --bg: #0f1117; --bg2: #1a1d27; --bg3: #242836; --bg4: #2e3347;
9819
- --text: #e4e6ef; --text2: #9499b3; --text3: #5c6080;
9820
- --accent: #e04040; --accent2: #ff5a5a; --ok: #34d399; --warn: #fbbf24;
9821
- --danger: #ef4444; --info: #60a5fa; --border: #2e3347; --radius: 8px;
9822
- --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);
9823
9827
  }
9824
- 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; }
9825
9829
  a { color: var(--info); text-decoration: none; }
9826
- button { font: inherit; cursor: pointer; border: none; border-radius: var(--radius); padding: 6px 14px; font-size: 13px; transition: background .15s, opacity .15s; }
9827
- button:disabled { opacity: .45; cursor: default; }
9828
- .btn { background: var(--bg3); color: var(--text); }
9829
- .btn:hover:not(:disabled) { background: var(--bg4); }
9830
- .btn-primary { background: var(--accent); color: #fff; }
9831
- .btn-primary:hover:not(:disabled) { background: var(--accent2); }
9832
- .btn-danger { background: #7f1d1d; color: #fca5a5; }
9833
- .btn-danger:hover:not(:disabled) { background: #991b1b; }
9834
- .btn-ok { background: #065f46; color: #6ee7b7; }
9835
- .btn-ok:hover:not(:disabled) { background: #064e3b; }
9836
- .btn-sm { padding: 3px 10px; font-size: 12px; }
9837
- .btn-ghost { background: transparent; color: var(--text2); }
9838
- .btn-ghost:hover:not(:disabled) { background: var(--bg3); color: var(--text); }
9839
-
9840
- input, select, textarea {
9841
- font: inherit; background: var(--bg); color: var(--text); border: 1px solid var(--border);
9842
- border-radius: var(--radius); padding: 7px 12px; width: 100%; outline: none;
9843
- 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;
9844
9881
  }
9845
- input:focus, select:focus, textarea:focus { border-color: var(--accent); }
9846
- input::placeholder { color: var(--text3); }
9847
- select { cursor: pointer; appearance: auto; }
9848
9882
 
9849
- .topbar {
9850
- 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;
9851
9895
  background: var(--bg2); border-bottom: 1px solid var(--border);
9852
- position: sticky; top: 0; z-index: 10;
9853
9896
  }
9854
- .topbar h1 { font-size: 16px; font-weight: 600; display: flex; align-items: center; gap: 8px; }
9855
- .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; }
9856
- .topbar .status { margin-left: auto; display: flex; align-items: center; gap: 8px; font-size: 12px; color: var(--text2); }
9857
- .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%; }
9858
9906
  .dot-ok { background: var(--ok); box-shadow: 0 0 6px var(--ok); }
9859
9907
  .dot-err { background: var(--danger); box-shadow: 0 0 6px var(--danger); }
9860
-
9861
- .tabs {
9862
- display: flex; gap: 0; background: var(--bg2); border-bottom: 1px solid var(--border);
9863
- padding: 0 24px;
9864
- }
9865
- .tab-btn {
9866
- padding: 10px 20px; font-size: 13px; font-weight: 500; color: var(--text2);
9867
- border-radius: 0; border-bottom: 2px solid transparent;
9868
- background: transparent; transition: all .15s;
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;
9869
9921
  }
9870
- .tab-btn:hover { color: var(--text); background: var(--bg); }
9871
- .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; }
9872
9937
 
9873
- .content { padding: 24px; max-width: 1200px; }
9874
- .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; }
9875
9946
 
9876
9947
  .card {
9877
- 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);
9878
9949
  padding: 20px; margin-bottom: 16px;
9879
9950
  }
9880
- .card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
9881
- .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; }
9882
9954
 
9883
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); }
9884
9962
 
9885
9963
  table { width: 100%; border-collapse: collapse; font-size: 13px; }
9886
- 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; }
9887
- 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); }
9888
9971
  tbody tr:hover { background: var(--bg3); }
9889
- .mono { font-family: 'SF Mono', 'Fira Code', monospace; font-size: 12px; color: var(--text2); word-break: break-all; }
9890
- .badge { display: inline-block; padding: 2px 8px; border-radius: 10px; font-size: 11px; font-weight: 500; }
9891
- .badge-ok { background: #065f46; color: #6ee7b7; }
9892
- .badge-warn { background: #78350f; color: #fde68a; }
9893
- .badge-info { background: #1e3a5f; color: #93c5fd; }
9894
- .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); }
9895
9979
  .badge-ghost { background: var(--bg3); color: var(--text2); }
9980
+ .badge-accent { background: var(--accent-bg); color: var(--accent2); }
9896
9981
 
9897
- .empty { text-align: center; padding: 48px 20px; color: var(--text3); }
9898
- .empty .icon { font-size: 40px; margin-bottom: 12px; opacity: .4; }
9899
- .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); }
9900
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 */
9901
10007
  .modal-overlay {
9902
- 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;
9903
10009
  align-items: center; justify-content: center; z-index: 100;
10010
+ animation: fadeIn .15s ease-out;
9904
10011
  }
9905
10012
  .modal-overlay.hidden { display: none; }
9906
10013
  .modal {
9907
- background: var(--bg2); border: 1px solid var(--border); border-radius: 12px;
9908
- 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;
9909
10017
  }
9910
- .modal h3 { font-size: 16px; margin-bottom: 16px; }
9911
- .form-group { margin-bottom: 14px; }
9912
- .form-group label { display: block; font-size: 12px; font-weight: 500; color: var(--text2); margin-bottom: 4px; }
9913
- .form-group .hint { font-size: 11px; color: var(--text3); margin-top: 3px; }
9914
- .form-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 18px; }
9915
-
9916
- .perm-checks { display: flex; gap: 12px; }
9917
- .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; }
9918
10035
  .perm-checks input[type=checkbox] { width: 16px; height: 16px; accent-color: var(--accent); }
9919
10036
 
9920
- .stats-row { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 12px; margin-bottom: 20px; }
9921
- .stat-card { background: var(--bg3); border-radius: var(--radius); padding: 14px 18px; }
9922
- .stat-card .label { font-size: 11px; color: var(--text3); text-transform: uppercase; letter-spacing: .5px; }
9923
- .stat-card .value { font-size: 22px; font-weight: 700; margin-top: 2px; }
9924
-
9925
- .provider-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
9926
- .provider-card {
9927
- background: var(--bg3); border: 1px solid var(--border); border-radius: var(--radius);
9928
- padding: 16px; cursor: pointer; transition: border-color .15s;
9929
- }
9930
- .provider-card:hover { border-color: var(--accent); }
9931
- .provider-card .name { font-weight: 600; margin-bottom: 4px; }
9932
- .provider-card .desc { font-size: 12px; color: var(--text3); }
9933
- .provider-card .status-dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; margin-right: 6px; }
9934
-
9935
- .section-desc { font-size: 13px; color: var(--text2); margin-bottom: 16px; }
9936
-
9937
- .tag { display: inline-flex; align-items: center; gap: 4px; background: var(--bg3); padding: 2px 8px; border-radius: 4px; font-size: 11px; color: var(--text2); }
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; }
9938
10042
 
9939
- .loading { display: flex; align-items: center; justify-content: center; padding: 40px; color: var(--text3); }
9940
- .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; }
9941
10046
  @keyframes spin { to { transform: rotate(360deg); } }
9942
10047
 
10048
+ /* Toast */
10049
+ .toast-container { position: fixed; bottom: 20px; right: 20px; z-index: 200; display: flex; flex-direction: column; gap: 8px; }
9943
10050
  .toast {
9944
- position: fixed; bottom: 20px; right: 20px; padding: 12px 20px; border-radius: var(--radius);
9945
- color: #fff; font-size: 13px; z-index: 200; animation: slideIn .2s ease-out;
9946
- 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;
9947
10054
  }
9948
10055
  .toast.hidden { display: none; }
9949
- .toast-ok { background: #065f46; }
9950
- .toast-err { background: #991b1b; }
9951
- .toast-info { background: #1e3a5f; }
9952
- @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; } }
9953
10061
 
10062
+ /* Actions cell */
9954
10063
  .actions-cell { display: flex; gap: 4px; }
9955
- .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; }
10071
+
10072
+ /* Section desc */
10073
+ .section-desc { font-size: 13px; color: var(--text2); margin-bottom: 20px; line-height: 1.6; }
9956
10074
 
10075
+ /* Responsive */
9957
10076
  @media (max-width: 768px) {
9958
- .content { padding: 12px; }
9959
- .topbar, .tabs { padding-left: 12px; padding-right: 12px; }
9960
- table { font-size: 12px; }
9961
- .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); }
9962
10081
  .provider-grid { grid-template-columns: 1fr; }
10082
+ .form-row { grid-template-columns: 1fr; }
9963
10083
  }
9964
10084
  `;
9965
10085
  var JS = `
9966
- const API = '/plugins/aicq-chat/api';
9967
-
9968
- let currentTab = 'agents';
9969
- let agentsData = null, friendsData = null, requestsData = null, sessionsData = null, identityData = null, modelsConfig = null, statusData = null;
9970
-
9971
- // \u2500\u2500 Utility \u2500\u2500
9972
- async function api(path, opts = {}) {
9973
- const res = await fetch(API + path, { headers: { 'Content-Type': 'application/json', ...opts.headers }, ...opts });
9974
- return res.json();
9975
- }
9976
-
9977
- function $(sel) { return document.querySelector(sel); }
9978
- function $$(sel) { return document.querySelectorAll(sel); }
9979
-
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
9980
10100
  function toast(msg, type = 'info') {
9981
- const t = document.getElementById('toast');
9982
- 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' };
9983
10104
  t.className = 'toast toast-' + type;
9984
- t.classList.remove('hidden');
9985
- clearTimeout(t._t);
9986
- 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;
9987
10115
  }
9988
10116
 
9989
- function showModal(id) { $(id).classList.remove('hidden'); }
9990
- function hideModal(id) { $(id).classList.add('hidden'); }
9991
-
9992
- function escHtml(s) {
9993
- if (!s) return '';
9994
- const d = document.createElement('div');
9995
- d.textContent = s;
9996
- 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 }; }
9997
10125
  }
9998
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; }
9999
10129
  function timeAgo(iso) {
10000
- if (!iso) return '-';
10130
+ if (!iso) return '\u2014';
10001
10131
  const diff = Date.now() - new Date(iso).getTime();
10002
- const mins = Math.floor(diff / 60000);
10003
- if (mins < 1) return 'just now';
10004
- if (mins < 60) return mins + 'm ago';
10005
- const hrs = Math.floor(mins / 60);
10006
- if (hrs < 24) return hrs + 'h ago';
10007
- 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();
10008
10136
  }
10009
-
10010
- function copyText(text) {
10011
- 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');
10012
10153
  }
10013
10154
 
10014
- // \u2500\u2500 Tab switching \u2500\u2500
10015
- function switchTab(tab) {
10016
- currentTab = tab;
10017
- $$('.tab-btn').forEach(b => b.classList.toggle('active', b.dataset.tab === tab));
10018
- $$('.content').forEach(c => c.classList.toggle('hidden', c.id !== 'tab-' + tab));
10019
- if (tab === 'agents') loadAgents();
10020
- else if (tab === 'aicq') loadAICQ();
10021
- 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'); }
10022
10159
  }
10023
10160
 
10024
- // \u2500\u2500 Status \u2500\u2500
10025
- async function loadStatus() {
10026
- try {
10027
- statusData = await api('/status');
10028
- const dot = $('#status-dot');
10029
- const txt = $('#status-text');
10030
- if (statusData.connected) {
10031
- dot.className = 'dot dot-ok';
10032
- txt.textContent = 'Connected';
10033
- } else {
10034
- dot.className = 'dot dot-err';
10035
- txt.textContent = 'Disconnected';
10036
- }
10037
- } catch (e) {
10038
- $('#status-dot').className = 'dot dot-err';
10039
- $('#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;
10040
10168
  }
10041
10169
  }
10042
10170
 
10043
- // \u2500\u2500 TAB 1: Agent Management \u2500\u2500
10044
- async function loadAgents() {
10045
- setLoading('agents', true);
10046
- try {
10047
- const data = await api('/agents');
10048
- agentsData = data;
10049
- renderAgents(data);
10050
- } catch (e) {
10051
- toast('Failed to load agents: ' + e.message, 'err');
10052
- }
10053
- 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;
10054
10237
  }
10055
10238
 
10056
- 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() {
10057
10243
  const el = $('#agents-content');
10058
- const agents = data.agents || [];
10059
- const statsHtml = \`
10060
- <div class="stats-row">
10061
- <div class="stat-card"><div class="label">Total Agents</div><div class="value">\${agents.length}</div></div>
10062
- <div class="stat-card"><div class="label">Current Agent</div><div class="value" style="font-size:16px">\${escHtml(data.currentAgentId || '-')}</div></div>
10063
- <div class="stat-card"><div class="label">Fingerprint</div><div class="value mono" style="font-size:11px">\${escHtml(data.fingerprint || '-')}</div></div>
10064
- <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>
10065
- </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; }
10066
10247
 
10067
- if (agents.length === 0) {
10068
- el.innerHTML = statsHtml + '<div class="empty"><div class="icon">\u{1F916}</div><p>No agents configured</p></div>';
10069
- return;
10070
- }
10248
+ const agents = data.agents || [];
10249
+ const configSource = data.configSource || 'unknown';
10071
10250
 
10072
10251
  let rows = '';
10073
- agents.forEach(a => {
10074
- const isCurrent = a.id === data.currentAgentId;
10075
- rows += \`<tr>
10076
- <td><span class="mono">\${escHtml(a.id)}</span> \${isCurrent ? '<span class="badge badge-ok">current</span>' : ''}</td>
10077
- <td>\${escHtml(a.name || a.aiName || '-')}</td>
10078
- <td><span class="tag">\${escHtml(a.friendType || '-')}</span></td>
10079
- <td>\${a.sessionCount != null ? '<span class="badge badge-info">' + a.sessionCount + ' sessions</span>' : '-'}</td>
10080
- <td class="mono truncate">\${escHtml(a.publicKeyFingerprint || '-')}</td>
10081
- <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>
10082
10263
  <td>
10083
10264
  <div class="actions-cell">
10084
- <button class="btn btn-sm btn-ghost" onclick="copyText('\${escHtml(a.id)}')" title="Copy ID">\u{1F4CB}</button>
10085
- \${!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>
10086
10267
  </div>
10087
10268
  </td>
10088
- </tr>\`;
10269
+ </tr>\\\`;
10089
10270
  });
10090
10271
 
10091
- el.innerHTML = statsHtml + \`
10092
- <div class="card">
10093
- <div class="card-header">
10094
- <div class="card-title">Agent List</div>
10095
- <div style="display:flex;gap:8px">
10096
- <button class="btn btn-sm" onclick="loadAgents()">\u{1F504} Refresh</button>
10097
- </div>
10098
- </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">
10099
10287
  <div style="overflow-x:auto">
10100
10288
  <table>
10101
- <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>
10102
- <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>
10103
10291
  </table>
10104
10292
  </div>
10105
- </div>\`;
10293
+ </div>
10294
+ \\\`);
10106
10295
  }
10107
10296
 
10108
- async function deleteAgent(id) {
10109
- if (!confirm('Delete agent ' + id + '? This cannot be undone.')) return;
10110
- try {
10111
- const r = await api('/agents/' + encodeURIComponent(id), { method: 'DELETE' });
10112
- if (r.success) { toast('Agent deleted', 'ok'); loadAgents(); }
10113
- else { toast(r.message || 'Delete failed', 'err'); }
10114
- } 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
+ });
10115
10302
  }
10116
10303
 
10117
- // \u2500\u2500 TAB 2: AICQ Management \u2500\u2500
10118
- let aicqSubTab = 'friends';
10119
-
10120
- async function loadAICQ() {
10121
- setLoading('aicq', true);
10122
- try {
10123
- const [friends, requests, sessions, identity] = await Promise.all([
10124
- api('/friends'),
10125
- api('/friends/requests'),
10126
- api('/sessions'),
10127
- api('/identity')
10128
- ]);
10129
- friendsData = friends;
10130
- requestsData = requests;
10131
- sessionsData = sessions;
10132
- identityData = identity;
10133
- renderAICQSubTabs();
10134
- switchAICQSubTab(aicqSubTab);
10135
- } catch (e) {
10136
- toast('Failed to load AICQ data: ' + e.message, 'err');
10137
- }
10138
- 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');
10139
10318
  }
10140
10319
 
10141
- function renderAICQSubTabs() {
10142
- const friendCount = (friendsData?.friends || []).length;
10143
- const reqCount = (requestsData?.requests || []).length;
10144
- const sessCount = (sessionsData?.sessions || []).length;
10145
- $('#aicq-subtabs').innerHTML = \`
10146
- <button class="tab-btn \${aicqSubTab==='friends'?'active':''}" onclick="switchAICQSubTab('friends')">Friends (\${friendCount})</button>
10147
- <button class="tab-btn \${aicqSubTab==='requests'?'active':''}" onclick="switchAICQSubTab('requests')">Requests (\${reqCount})</button>
10148
- <button class="tab-btn \${aicqSubTab==='sessions'?'active':''}" onclick="switchAICQSubTab('sessions')">Sessions (\${sessCount})</button>
10149
- \`;
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'); }
10150
10325
  }
10151
10326
 
10152
- function switchAICQSubTab(tab) {
10153
- aicqSubTab = tab;
10154
- renderAICQSubTabs();
10155
- $$('#aicq-content > div').forEach(d => d.classList.add('hidden'));
10156
- if (tab === 'friends') renderFriends();
10157
- else if (tab === 'requests') renderRequests();
10158
- 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 || []);
10159
10355
  }
10356
+ window.friendsSubTab = 'friends';
10160
10357
 
10161
- function renderFriends() {
10162
- const el = $('#aicq-friends');
10163
- el.classList.remove('hidden');
10164
- const friends = friendsData?.friends || [];
10165
-
10166
- if (friends.length === 0) {
10167
- 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>';
10168
- return;
10169
- }
10170
-
10358
+ function renderFriendsList(friends) {
10359
+ const el = $('#friends-content');
10171
10360
  let rows = '';
10172
10361
  friends.forEach(f => {
10173
- const perms = (f.permissions || []).map(p => '<span class="badge badge-' + (p === 'exec' ? 'warn' : 'ok') + '">' + p + '</span>').join(' ');
10174
- rows += \`<tr>
10175
- <td class="mono">\${escHtml(f.id)}</td>
10176
- <td>\${escHtml(f.aiName || '-')}</td>
10177
- <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>
10178
10366
  <td>\${perms || '<span class="badge badge-ghost">none</span>'}</td>
10179
- <td class="mono" style="font-size:11px">\${escHtml(f.publicKeyFingerprint || '-')}</td>
10180
- <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>
10181
10369
  <td>
10182
10370
  <div class="actions-cell">
10183
- <button class="btn btn-sm btn-ghost" onclick="editPermissions('\${escHtml(f.id)}', \${JSON.stringify(f.permissions || [])})">\u2699\uFE0F</button>
10184
- <button class="btn btn-sm btn-danger" onclick="removeFriend('\${escHtml(f.id)}')">\u{1F5D1}\uFE0F</button>
10185
- <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>
10186
10373
  </div>
10187
10374
  </td>
10188
- </tr>\`;
10375
+ </tr>\\\`;
10189
10376
  });
10190
10377
 
10191
- el.innerHTML = \`
10192
- <div class="card">
10193
- <div class="card-header">
10194
- <div class="card-title">Friends (\${friends.length})</div>
10195
- <div style="display:flex;gap:8px">
10196
- <button class="btn btn-sm btn-primary" onclick="showAddFriend()">\u2795 Add Friend</button>
10197
- <button class="btn btn-sm" onclick="loadAICQ()">\u{1F504} Refresh</button>
10198
- </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>
10199
10385
  </div>
10200
- <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">
10201
10392
  <table>
10202
- <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>
10203
- <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>
10204
10395
  </table>
10205
10396
  </div>
10206
- </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
+ \\\`);
10207
10400
  }
10208
10401
 
10209
- function renderRequests() {
10210
- const el = $('#aicq-requests');
10211
- el.classList.remove('hidden');
10212
- const reqs = requestsData?.requests || [];
10213
-
10214
- if (reqs.length === 0) {
10215
- el.innerHTML = '<div class="empty"><div class="icon">\u{1F4E8}</div><p>No pending friend requests</p></div>';
10216
- return;
10217
- }
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
+ }
10218
10410
 
10411
+ function renderRequestsList(requests) {
10412
+ const el = $('#friends-content');
10219
10413
  let rows = '';
10220
- reqs.forEach(r => {
10221
- const statusBadge = r.status === 'pending' ? '<span class="badge badge-warn">pending</span>' :
10222
- r.status === 'accepted' ? '<span class="badge badge-ok">accepted</span>' :
10223
- '<span class="badge badge-ghost">' + escHtml(r.status) + '</span>';
10224
- rows += \`<tr>
10225
- <td class="mono">\${escHtml(r.id)}</td>
10226
- <td class="mono">\${escHtml(r.fromId || r.requesterId || '-')}</td>
10227
- <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>
10228
10420
  <td>\${timeAgo(r.createdAt)}</td>
10229
- <td>\${escHtml(r.message || '-')}</td>
10230
10421
  <td>
10231
- <div class="actions-cell">
10232
- \${r.status === 'pending' ? \`
10233
- <button class="btn btn-sm btn-ok" onclick="acceptRequest('\${escHtml(r.id)}')">\u2713 Accept</button>
10234
- <button class="btn btn-sm btn-danger" onclick="rejectRequest('\${escHtml(r.id)}')">\u2717 Reject</button>
10235
- \` : '-'}
10236
- </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'}
10237
10423
  </td>
10238
- </tr>\`;
10424
+ </tr>\\\`;
10239
10425
  });
10240
-
10241
- el.innerHTML = \`
10242
- <div class="card">
10243
- <div class="card-header">
10244
- <div class="card-title">Friend Requests (\${reqs.length})</div>
10245
- <button class="btn btn-sm" onclick="loadAICQ()">\u{1F504} Refresh</button>
10246
- </div>
10247
- <div style="overflow-x:auto">
10248
- <table>
10249
- <thead><tr><th>Request ID</th><th>From</th><th>Status</th><th>Time</th><th>Message</th><th>Actions</th></tr></thead>
10250
- <tbody>\${rows}</tbody>
10251
- </table>
10252
- </div>
10253
- </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
+ \\\`);
10254
10436
  }
10255
10437
 
10256
- function renderSessions() {
10257
- const el = $('#aicq-sessions');
10258
- el.classList.remove('hidden');
10259
- const sessions = sessionsData?.sessions || [];
10260
-
10261
- if (sessions.length === 0) {
10262
- el.innerHTML = '<div class="empty"><div class="icon">\u{1F517}</div><p>No active encrypted sessions</p></div>';
10263
- return;
10264
- }
10265
-
10438
+ function renderSessionsList(sessions) {
10439
+ const el = $('#friends-content');
10266
10440
  let rows = '';
10267
10441
  sessions.forEach(s => {
10268
- rows += \`<tr>
10269
- <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>
10270
10444
  <td>\${timeAgo(s.createdAt)}</td>
10271
- <td><span class="badge badge-info">\${s.messageCount} msgs</span></td>
10272
- </tr>\`;
10445
+ <td><span class="badge badge-info">\${s.messageCount} messages</span></td>
10446
+ </tr>\\\`;
10273
10447
  });
10274
-
10275
- el.innerHTML = \`
10276
- <div class="card">
10277
- <div class="card-header">
10278
- <div class="card-title">Encrypted Sessions (\${sessions.length})</div>
10279
- <button class="btn btn-sm" onclick="loadAICQ()">\u{1F504} Refresh</button>
10280
- </div>
10281
- <div style="overflow-x:auto">
10282
- <table>
10283
- <thead><tr><th>Peer ID</th><th>Established</th><th>Messages</th></tr></thead>
10284
- <tbody>\${rows}</tbody>
10285
- </table>
10286
- </div>
10287
- </div>\`;
10288
- }
10289
-
10290
- async function showAddFriend() {
10291
- showModal('modal-add-friend');
10292
- $('#add-friend-target').value = '';
10293
- $('#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
+ \\\`);
10294
10458
  }
10295
10459
 
10460
+ function showAddFriendModal() { $('#add-friend-target').value = ''; showModal('modal-add-friend'); setTimeout(() => $('#add-friend-target')?.focus(), 100); }
10296
10461
  async function addFriend() {
10297
10462
  const target = $('#add-friend-target').value.trim();
10298
- 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; }
10299
10464
  hideModal('modal-add-friend');
10300
10465
  toast('Sending friend request...', 'info');
10301
- try {
10302
- const r = await api('/friends', { method: 'POST', body: JSON.stringify({ target }) });
10303
- if (r.success) { toast(r.message || 'Friend request sent!', 'ok'); loadAICQ(); }
10304
- else { toast(r.message || 'Failed to add friend', 'err'); }
10305
- } 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'); }
10306
10469
  }
10307
-
10308
10470
  async function removeFriend(id) {
10309
- if (!confirm('Remove friend ' + id + '? This will delete the encrypted session.')) return;
10310
- try {
10311
- const r = await api('/friends/' + encodeURIComponent(id), { method: 'DELETE' });
10312
- if (r.success) { toast('Friend removed', 'ok'); loadAICQ(); }
10313
- else { toast(r.message || 'Failed to remove', 'err'); }
10314
- } 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'); }
10315
10475
  }
10316
10476
 
10317
- let editingFriendId = null;
10318
- function editPermissions(id, perms) {
10319
- editingFriendId = id;
10320
- const chatChecked = perms.includes('chat') ? 'checked' : '';
10321
- const execChecked = perms.includes('exec') ? 'checked' : '';
10322
- $('#perm-chat').checked = chatChecked;
10323
- $('#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');
10324
10482
  showModal('modal-permissions');
10325
10483
  }
10326
-
10327
- async function savePermissions() {
10484
+ async function saveFriendPerms() {
10328
10485
  const perms = [];
10329
10486
  if ($('#perm-chat').checked) perms.push('chat');
10330
10487
  if ($('#perm-exec').checked) perms.push('exec');
10331
- try {
10332
- const r = await api('/friends/' + encodeURIComponent(editingFriendId) + '/permissions', {
10333
- method: 'PUT', body: JSON.stringify({ permissions: perms })
10334
- });
10335
- if (r.success) { toast('Permissions updated', 'ok'); hideModal('modal-permissions'); loadAICQ(); }
10336
- else { toast(r.message || 'Failed to update', 'err'); }
10337
- } 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'); }
10338
10491
  }
10339
-
10340
- async function acceptRequest(id) {
10341
- try {
10342
- const r = await api('/friends/requests/' + encodeURIComponent(id) + '/accept', { method: 'POST', body: JSON.stringify({ permissions: ['chat'] }) });
10343
- if (r.success) { toast('Request accepted', 'ok'); loadAICQ(); }
10344
- else { toast(r.message || 'Failed', 'err'); }
10345
- } 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'); }
10346
10495
  }
10347
-
10348
- async function rejectRequest(id) {
10349
- try {
10350
- const r = await api('/friends/requests/' + encodeURIComponent(id) + '/reject', { method: 'POST', body: JSON.stringify({}) });
10351
- if (r.success) { toast('Request rejected', 'ok'); loadAICQ(); }
10352
- else { toast(r.message || 'Failed', 'err'); }
10353
- } 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'); }
10354
10499
  }
10355
10500
 
10356
- // \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
+
10357
10506
  async function loadModels() {
10358
- setLoading('models', true);
10359
- try {
10360
- modelsConfig = await api('/models');
10361
- renderModels(modelsConfig);
10362
- } catch (e) {
10363
- toast('Failed to load models: ' + e.message, 'err');
10364
- }
10365
- 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);
10366
10513
  }
10367
10514
 
10368
10515
  function renderModels(data) {
10369
10516
  const el = $('#models-content');
10370
10517
  const providers = data.providers || [];
10371
- const configured = providers.filter(p => p.configured);
10518
+ const configured = providers.filter(p => p.configured).length;
10372
10519
 
10373
- let provCards = '';
10520
+ let cards = '';
10374
10521
  providers.forEach(p => {
10375
- const statusColor = p.configured ? 'var(--ok)' : 'var(--text3)';
10376
- const statusText = p.configured ? 'Configured' : 'Not configured';
10377
- provCards += \`
10378
- <div class="provider-card" onclick="showModelConfig('\${escHtml(p.id)}')">
10379
- <div class="name"><span class="status-dot" style="background:\${statusColor}"></span>\${escHtml(p.name)}</div>
10380
- <div class="desc">\${escHtml(p.description || '')}</div>
10381
- <div style="margin-top:8px;font-size:11px;color:var(--text3)">\${statusText}</div>
10382
- </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>\\\`;
10383
10540
  });
10384
10541
 
10385
- let currentModelsHtml = '';
10386
- if (data.currentModels && data.currentModels.length > 0) {
10542
+ let activeModelsSection = '';
10543
+ if (data.currentModels && data.currentModels.length) {
10387
10544
  let rows = '';
10388
10545
  data.currentModels.forEach(m => {
10389
- rows += \`<tr>
10390
- <td>\${escHtml(m.provider || '-')}</td>
10391
- <td class="mono">\${escHtml(m.modelId || '-')}</td>
10392
- <td>\${m.hasApiKey ? '<span class="badge badge-ok">\u25CF</span>' : '<span class="badge badge-danger">\u2717</span>'}</td>
10393
- <td class="mono">\${escHtml(m.baseUrl || '-')}</td>
10394
- <td><button class="btn btn-sm btn-ghost" onclick="showModelConfig('\${escHtml(m.providerId || m.provider || '')}')">Edit</button></td>
10395
- </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>\\\`;
10396
10553
  });
10397
- currentModelsHtml = \`
10398
- <div class="card">
10399
- <div class="card-header">
10400
- <div class="card-title">Configured Models</div>
10401
- </div>
10402
- <div style="overflow-x:auto">
10403
- <table>
10404
- <thead><tr><th>Provider</th><th>Model ID</th><th>API Key</th><th>Base URL</th><th>Actions</th></tr></thead>
10405
- <tbody>\${rows}</tbody>
10406
- </table>
10407
- </div>
10408
- </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>\\\`;
10409
10562
  }
10410
10563
 
10411
- el.innerHTML = \`
10412
- <p class="section-desc">Quickly configure LLM providers. Click a provider card to set up your API key and model.</p>
10413
- <div class="provider-grid">\${provCards}</div>
10414
- \${currentModelsHtml}
10415
- \`;
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
+ \\\`);
10416
10577
  }
10417
10578
 
10418
- let editingProviderId = null;
10419
- function showModelConfig(providerId) {
10420
- editingProviderId = providerId;
10421
- const provider = (modelsConfig?.providers || []).find(p => p.id === providerId);
10422
- if (!provider) { toast('Provider not found', 'err'); return; }
10423
-
10424
- $('#model-provider-name').textContent = provider.name;
10425
- $('#model-api-key').value = provider.apiKey || '';
10426
- $('#model-api-key').placeholder = provider.apiKeyHint || 'Enter your API key';
10427
- $('#model-model-id').value = provider.modelId || '';
10428
- $('#model-model-id').placeholder = provider.modelHint || 'e.g. gpt-4o';
10429
- $('#model-base-url').value = provider.baseUrl || '';
10430
- $('#model-base-url').placeholder = provider.baseUrlHint || 'Default: provider endpoint';
10431
-
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';
10432
10593
  showModal('modal-model-config');
10433
10594
  }
10434
-
10435
10595
  async function saveModelConfig() {
10436
10596
  const apiKey = $('#model-api-key').value.trim();
10437
10597
  const modelId = $('#model-model-id').value.trim();
10438
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')]);
10439
10614
 
10440
- if (!apiKey && !modelId) {
10441
- 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>');
10442
10617
  return;
10443
10618
  }
10444
10619
 
10445
- hideModal('modal-model-config');
10446
- toast('Saving model config...', 'info');
10620
+ html(el, \\\`
10621
+ <p class="section-desc">AICQ plugin runtime configuration and system information.</p>
10447
10622
 
10448
- try {
10449
- const r = await api('/models/' + encodeURIComponent(editingProviderId), {
10450
- method: 'PUT',
10451
- body: JSON.stringify({ apiKey, modelId, baseUrl })
10452
- });
10453
- if (r.success) { toast('Model config saved!', 'ok'); loadModels(); }
10454
- else { toast(r.message || 'Failed to save', 'err'); }
10455
- } catch (e) { toast('Error: ' + e.message, 'err'); }
10456
- }
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>
10457
10628
 
10458
- // \u2500\u2500 Loading helpers \u2500\u2500
10459
- function setLoading(tab, loading) {
10460
- const el = $('#tab-' + tab);
10461
- if (!el) return;
10462
- const existing = el.querySelector('.loading-bar');
10463
- if (loading && !existing) {
10464
- const bar = document.createElement('div');
10465
- bar.className = 'loading-bar';
10466
- bar.innerHTML = '<div class="spinner"></div> Loading...';
10467
- bar.style.cssText = 'padding:12px;text-align:center;color:var(--text3);font-size:13px;display:flex;align-items:center;justify-content:center;gap:8px';
10468
- el.prepend(bar);
10469
- } else if (!loading && existing) {
10470
- existing.remove();
10471
- }
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
+ \\\`);
10472
10648
  }
10473
10649
 
10474
- // \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
10475
10653
  document.addEventListener('DOMContentLoaded', () => {
10476
- $$('.tab-btn[data-tab]').forEach(btn => {
10477
- btn.addEventListener('click', () => switchTab(btn.dataset.tab));
10478
- });
10479
- loadStatus();
10480
- 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
+
10481
10660
  // Auto-refresh status every 30s
10482
- 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);
10483
10673
  });
10484
10674
  `;
10485
10675
  var HTML = `<!DOCTYPE html>
@@ -10487,62 +10677,86 @@ var HTML = `<!DOCTYPE html>
10487
10677
  <head>
10488
10678
  <meta charset="UTF-8">
10489
10679
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
10490
- <title>AICQ Management</title>
10680
+ <title>AICQ Management Console</title>
10491
10681
  <style>${CSS}</style>
10492
10682
  </head>
10493
10683
  <body>
10494
- <div class="topbar">
10495
- <h1><span class="logo">AQ</span> AICQ Management</h1>
10496
- <div class="status">
10497
- <span id="status-dot" class="dot dot-err"></span>
10498
- <span id="status-text">Connecting...</span>
10499
- </div>
10500
- </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">
10501
10726
 
10502
- <div class="tabs">
10503
- <button class="tab-btn active" data-tab="agents" onclick="switchTab('agents')">\u{1F916} Agent Management</button>
10504
- <button class="tab-btn" data-tab="aicq" onclick="switchTab('aicq')">\u{1F4AC} AICQ Management</button>
10505
- <button class="tab-btn" data-tab="models" onclick="switchTab('models')">\u{1F9E0} Model Management</button>
10506
- </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>
10507
10729
 
10508
- <!-- TAB 1: Agent Management -->
10509
- <div class="content" id="tab-agents">
10510
- <p class="section-desc">View and manage AICQ agent identities. Each agent has its own Ed25519 key pair and encrypted session state.</p>
10511
- <div id="agents-content">
10512
- <div class="loading"><div class="spinner"></div> Loading agents...</div>
10513
- </div>
10514
- </div>
10730
+ <!-- Agents -->
10731
+ <div class="page" id="page-agents"><div id="agents-content"></div></div>
10515
10732
 
10516
- <!-- TAB 2: AICQ Management -->
10517
- <div class="content hidden" id="tab-aicq">
10518
- <p class="section-desc">Manage encrypted friend connections, pending requests, and active sessions on the AICQ network.</p>
10519
- <div class="tabs" id="aicq-subtabs" style="padding:0;margin-bottom:16px;border:none"></div>
10520
- <div id="aicq-content">
10521
- <div id="aicq-friends" class="hidden"></div>
10522
- <div id="aicq-requests" class="hidden"></div>
10523
- <div id="aicq-sessions" class="hidden"></div>
10524
- </div>
10525
- </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>
10526
10738
 
10527
- <!-- TAB 3: Model Management -->
10528
- <div class="content hidden" id="tab-models">
10529
- <p class="section-desc">Quickly configure LLM providers for your agents. Select a provider and enter your API key to get started.</p>
10530
- <div id="models-content">
10531
- <div class="loading"><div class="spinner"></div> Loading model config...</div>
10532
- </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>
10533
10747
  </div>
10534
10748
 
10535
10749
  <!-- Modal: Add Friend -->
10536
10750
  <div class="modal-overlay hidden" id="modal-add-friend" onclick="if(event.target===this)hideModal('modal-add-friend')">
10537
10751
  <div class="modal">
10538
- <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>
10539
10753
  <div class="form-group">
10540
- <label>Temp Number or Friend ID</label>
10541
- <input id="add-friend-target" type="text" placeholder="Enter 6-digit temp number or node ID" onkeydown="if(event.key==='Enter')addFriend()">
10542
- <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>
10543
10757
  </div>
10544
10758
  <div class="form-actions">
10545
- <button class="btn" onclick="hideModal('modal-add-friend')">Cancel</button>
10759
+ <button class="btn btn-default" onclick="hideModal('modal-add-friend')">Cancel</button>
10546
10760
  <button class="btn btn-primary" onclick="addFriend()">Send Request</button>
10547
10761
  </div>
10548
10762
  </div>
@@ -10551,49 +10765,59 @@ var HTML = `<!DOCTYPE html>
10551
10765
  <!-- Modal: Edit Permissions -->
10552
10766
  <div class="modal-overlay hidden" id="modal-permissions" onclick="if(event.target===this)hideModal('modal-permissions')">
10553
10767
  <div class="modal">
10554
- <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>
10555
10769
  <div class="form-group">
10556
- <label>Permissions for this friend</label>
10557
- <div class="perm-checks" style="margin-top:8px">
10558
- <label><input type="checkbox" id="perm-chat" checked> Chat <span style="color:var(--text3);font-size:11px">(send/receive messages)</span></label>
10559
- <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>
10560
10774
  </div>
10561
10775
  </div>
10562
10776
  <div class="form-actions">
10563
- <button class="btn" onclick="hideModal('modal-permissions')">Cancel</button>
10564
- <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>
10565
10779
  </div>
10566
10780
  </div>
10567
10781
  </div>
10568
10782
 
10569
10783
  <!-- Modal: Model Config -->
10570
10784
  <div class="modal-overlay hidden" id="modal-model-config" onclick="if(event.target===this)hideModal('modal-model-config')">
10571
- <div class="modal" style="max-width:520px">
10572
- <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>
10573
10788
  <div class="form-group">
10574
- <label>API Key</label>
10575
- <input id="model-api-key" type="password" placeholder="sk-...">
10576
- <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>
10577
10792
  </div>
10578
10793
  <div class="form-group">
10579
- <label>Model ID</label>
10580
- <input id="model-model-id" type="text" placeholder="gpt-4o">
10581
- <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>
10582
10797
  </div>
10583
10798
  <div class="form-group">
10584
- <label>Base URL <span style="color:var(--text3);font-size:11px">(optional)</span></label>
10585
- <input id="model-base-url" type="text" placeholder="https://api.openai.com/v1">
10586
- <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>
10587
10802
  </div>
10588
10803
  <div class="form-actions">
10589
- <button class="btn" onclick="hideModal('modal-model-config')">Cancel</button>
10590
- <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>
10591
10806
  </div>
10592
10807
  </div>
10593
10808
  </div>
10594
10809
 
10595
- <!-- Toast -->
10596
- <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>
10597
10821
 
10598
10822
  <script>${JS}</script>
10599
10823
  </body>
@@ -10607,113 +10831,60 @@ import * as fs5 from "fs";
10607
10831
  import * as path5 from "path";
10608
10832
  import * as os from "os";
10609
10833
  var MODEL_PROVIDERS = [
10610
- {
10611
- id: "openai",
10612
- name: "OpenAI",
10613
- description: "GPT-4o, GPT-4, GPT-3.5 and more",
10614
- apiKeyHint: "sk-...",
10615
- modelHint: "gpt-4o",
10616
- baseUrlHint: "https://api.openai.com/v1",
10617
- configKey: "openai"
10618
- },
10619
- {
10620
- id: "anthropic",
10621
- name: "Anthropic",
10622
- description: "Claude 4, Claude 3.5 Sonnet, Haiku",
10623
- apiKeyHint: "sk-ant-...",
10624
- modelHint: "claude-sonnet-4-20250514",
10625
- baseUrlHint: "https://api.anthropic.com",
10626
- configKey: "anthropic"
10627
- },
10628
- {
10629
- id: "google",
10630
- name: "Google AI",
10631
- description: "Gemini 2.5 Pro, Gemini 2.5 Flash",
10632
- apiKeyHint: "AI...",
10633
- modelHint: "gemini-2.5-pro",
10634
- baseUrlHint: "",
10635
- configKey: "google"
10636
- },
10637
- {
10638
- id: "groq",
10639
- name: "Groq",
10640
- description: "Llama 3, Mixtral \u2014 ultra fast inference",
10641
- apiKeyHint: "gsk_...",
10642
- modelHint: "llama-3.3-70b-versatile",
10643
- baseUrlHint: "https://api.groq.com/openai/v1",
10644
- configKey: "groq"
10645
- },
10646
- {
10647
- id: "deepseek",
10648
- name: "DeepSeek",
10649
- description: "DeepSeek V3, DeepSeek R1",
10650
- apiKeyHint: "sk-...",
10651
- modelHint: "deepseek-chat",
10652
- baseUrlHint: "https://api.deepseek.com/v1",
10653
- configKey: "deepseek"
10654
- },
10655
- {
10656
- id: "ollama",
10657
- name: "Ollama (Local)",
10658
- description: "Run models locally on your machine",
10659
- apiKeyHint: "(no key needed)",
10660
- modelHint: "llama3",
10661
- baseUrlHint: "http://localhost:11434/v1",
10662
- configKey: "ollama"
10663
- },
10664
- {
10665
- id: "openrouter",
10666
- name: "OpenRouter",
10667
- description: "Unified API for 200+ models",
10668
- apiKeyHint: "sk-or-...",
10669
- modelHint: "openai/gpt-4o",
10670
- baseUrlHint: "https://openrouter.ai/api/v1",
10671
- configKey: "openrouter"
10672
- },
10673
- {
10674
- id: "mistral",
10675
- name: "Mistral AI",
10676
- description: "Mistral Large, Medium, Small",
10677
- apiKeyHint: "(your key)",
10678
- modelHint: "mistral-large-latest",
10679
- baseUrlHint: "https://api.mistral.ai/v1",
10680
- configKey: "mistral"
10681
- }
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" }
10682
10844
  ];
10683
- function findOpenClawConfig() {
10684
- const candidates = [
10845
+ function findConfigPath() {
10846
+ const openclawPaths = [
10685
10847
  path5.join(process.cwd(), "openclaw.json"),
10686
- path5.join(process.cwd(), "stableclaw.json"),
10687
10848
  path5.join(os.homedir(), ".config", "openclaw", "openclaw.json"),
10688
- path5.join(os.homedir(), ".config", "stableclaw", "stableclaw.json"),
10689
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"),
10690
10862
  path5.join(os.homedir(), ".stableclaw", "stableclaw.json"),
10691
- path5.join(os.homedir(), "openclaw.json"),
10692
10863
  path5.join(os.homedir(), "stableclaw.json")
10693
10864
  ];
10694
- for (const p of candidates) {
10865
+ for (const p of stableclawPaths) {
10695
10866
  try {
10696
- if (fs5.existsSync(p)) {
10867
+ if (fs5.existsSync(p))
10697
10868
  return p;
10698
- }
10699
10869
  } catch {
10700
10870
  }
10701
10871
  }
10702
10872
  return null;
10703
10873
  }
10704
- function readOpenClawConfig() {
10705
- const configPath = findOpenClawConfig();
10874
+ function readConfig() {
10875
+ const configPath = findConfigPath();
10706
10876
  if (!configPath)
10707
10877
  return null;
10708
10878
  try {
10709
10879
  const raw = fs5.readFileSync(configPath, "utf-8");
10710
- return JSON.parse(raw);
10880
+ const config2 = JSON.parse(raw);
10881
+ return { config: config2, configPath };
10711
10882
  } catch {
10712
10883
  return null;
10713
10884
  }
10714
10885
  }
10715
- function writeOpenClawConfig(config2) {
10716
- const configPath = findOpenClawConfig();
10886
+ function writeConfig(config2) {
10887
+ const configPath = findConfigPath();
10717
10888
  if (!configPath)
10718
10889
  return false;
10719
10890
  try {
@@ -10723,24 +10894,54 @@ function writeOpenClawConfig(config2) {
10723
10894
  return false;
10724
10895
  }
10725
10896
  }
10726
- 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
+ }
10727
10930
  const providers = MODEL_PROVIDERS.map((p) => {
10728
- const providersSection2 = config2.providers;
10729
- const providerConfig = providersSection2?.[p.configKey] ?? config2[p.configKey];
10730
- const apiKey = providerConfig?.apiKey || "";
10731
- const modelId = providerConfig?.model || providerConfig?.defaultModel || "";
10732
- 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 || "";
10733
10935
  return {
10734
10936
  ...p,
10735
10937
  configured: Boolean(apiKey || p.id === "ollama" && baseUrl),
10736
- 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) : "",
10737
10939
  apiKeyHasValue: Boolean(apiKey),
10738
10940
  modelId,
10739
10941
  baseUrl
10740
10942
  };
10741
10943
  });
10742
10944
  const currentModels = [];
10743
- const providersSection = config2.providers;
10744
10945
  for (const p of MODEL_PROVIDERS) {
10745
10946
  const pc = providersSection?.[p.configKey] ?? config2[p.configKey];
10746
10947
  if (pc?.apiKey) {
@@ -10749,25 +10950,38 @@ function getModelConfig(config2) {
10749
10950
  providerId: p.id,
10750
10951
  modelId: pc.model || pc.defaultModel || p.modelHint,
10751
10952
  hasApiKey: true,
10752
- baseUrl: pc.baseUrl || pc.baseURL || ""
10953
+ baseUrl: pc.baseUrl || pc.baseURL || p.baseUrlHint
10753
10954
  });
10754
10955
  }
10755
10956
  }
10756
10957
  return { providers, currentModels };
10757
10958
  }
10758
- function parseSubPath(reqPath, prefix) {
10759
- const fullPrefix = "/plugins" + prefix;
10760
- if (reqPath.startsWith(fullPrefix)) {
10761
- return reqPath.slice(fullPrefix.length) || "/";
10762
- }
10763
- 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;
10764
10968
  }
10765
10969
  function json(res, data, status = 200) {
10766
10970
  if (!res.headersSent) {
10767
- 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": "*" });
10768
10972
  }
10769
10973
  res.end(JSON.stringify(data));
10770
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
+ }
10771
10985
  async function readBody(req) {
10772
10986
  return new Promise((resolve3) => {
10773
10987
  const chunks = [];
@@ -10786,19 +11000,24 @@ async function readBody(req) {
10786
11000
  function createManagementHandler(ctx) {
10787
11001
  const { store, identityService, serverClient, serverUrl, aicqAgentId, logger, html } = ctx;
10788
11002
  return async (req, res) => {
10789
- const subPath = parseSubPath(req.url || "/", "/aicq-chat");
10790
- if (subPath === "/" || subPath === "/ui" || subPath === "") {
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") {
10791
11011
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
10792
11012
  res.end(html);
10793
11013
  return;
10794
11014
  }
10795
- if (!subPath.startsWith("/api/")) {
11015
+ if (!urlPath.startsWith("/api/")) {
10796
11016
  res.writeHead(404, { "Content-Type": "application/json" });
10797
11017
  res.end(JSON.stringify({ error: "Not found" }));
10798
11018
  return;
10799
11019
  }
10800
- const apiPath = subPath.slice(4);
10801
- const method = (req.method || "GET").toUpperCase();
11020
+ const apiPath = urlPath.slice(4);
10802
11021
  try {
10803
11022
  if (apiPath === "/status" && method === "GET") {
10804
11023
  return json(res, {
@@ -10820,62 +11039,70 @@ function createManagementHandler(ctx) {
10820
11039
  sessionCount: store.sessions.size
10821
11040
  });
10822
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
+ }
10823
11053
  if (apiPath === "/agents" && method === "GET") {
10824
- const localFriends = Array.from(store.friends.values()).map((f) => ({
10825
- id: f.id,
10826
- name: f.aiName || f.id.substring(0, 8),
10827
- friendType: f.friendType,
10828
- publicKeyFingerprint: f.publicKeyFingerprint,
10829
- permissions: f.permissions || [],
10830
- lastMessageAt: f.lastMessageAt?.toISOString() || null,
10831
- sessionCount: store.sessions.has(f.id) ? store.sessions.get(f.id)?.messageCount || 0 : 0
10832
- }));
10833
- let serverFriends = [];
10834
- try {
10835
- const resp = await fetch(serverUrl + "/api/v1/friends?nodeId=" + aicqAgentId);
10836
- if (resp.ok) {
10837
- const data = await resp.json();
10838
- serverFriends = data.friends || [];
10839
- }
10840
- } catch {
10841
- }
10842
- const localIds = new Set(localFriends.map((f) => f.id));
10843
- for (const sf of serverFriends) {
10844
- const sfId = sf.nodeId;
10845
- if (sfId && !localIds.has(sfId)) {
10846
- localFriends.push({
10847
- id: sfId,
10848
- name: sf.aiName || sfId.substring(0, 8),
10849
- friendType: sf.friendType || void 0,
10850
- publicKeyFingerprint: sf.publicKeyFingerprint || "",
10851
- permissions: sf.permissions || [],
10852
- lastMessageAt: sf.lastMessageAt || null,
10853
- sessionCount: 0
10854
- });
10855
- }
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
+ });
10856
11063
  }
11064
+ const agents = extractAgentsFromConfig(result.config);
11065
+ globalThis.__aicq_agents = agents;
10857
11066
  return json(res, {
10858
- agents: localFriends,
11067
+ agents,
11068
+ configSource: path5.basename(result.configPath),
11069
+ configPath: result.configPath,
10859
11070
  currentAgentId: aicqAgentId,
10860
11071
  fingerprint: identityService.getPublicKeyFingerprint(),
10861
11072
  connected: serverClient.isConnected()
10862
11073
  });
10863
11074
  }
10864
11075
  if (apiPath.startsWith("/agents/") && method === "DELETE") {
10865
- const friendId = decodeURIComponent(apiPath.slice("/agents/".length));
10866
- if (!friendId)
10867
- return json(res, { success: false, message: "Missing agent ID" }, 400);
10868
- try {
10869
- await fetch(serverUrl + "/api/v1/friends/" + friendId, {
10870
- method: "DELETE",
10871
- headers: { "Content-Type": "application/json" },
10872
- body: JSON.stringify({ nodeId: aicqAgentId })
10873
- });
10874
- } 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);
11095
+ }
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" });
10875
11104
  }
10876
- store.removeFriend(friendId);
10877
- logger.info("[API] Agent/friend deleted: " + friendId);
10878
- return json(res, { success: true, message: "Agent removed" });
11105
+ return json(res, { success: false, message: "Failed to write config" }, 500);
10879
11106
  }
10880
11107
  if (apiPath === "/friends" && method === "GET") {
10881
11108
  try {
@@ -10909,26 +11136,36 @@ function createManagementHandler(ctx) {
10909
11136
  const isTempNumber = /^\d{6}$/.test(target);
10910
11137
  let friendId = target;
10911
11138
  if (isTempNumber) {
10912
- const resolveResp = await fetch(serverUrl + "/api/v1/temp-number/" + target);
10913
- if (!resolveResp.ok)
10914
- return json(res, { success: false, message: "Temp number not found or expired" });
10915
- const resolveData = await resolveResp.json();
10916
- friendId = resolveData.nodeId;
10917
- }
10918
- const hsResp = await fetch(serverUrl + "/api/v1/handshake/initiate", {
10919
- method: "POST",
10920
- headers: { "Content-Type": "application/json" },
10921
- body: JSON.stringify({ requesterId: aicqAgentId, targetTempNumber: target })
10922
- });
10923
- if (!hsResp.ok)
10924
- return json(res, { success: false, message: "Handshake failed: " + await hsResp.text() });
10925
- const hsData = await hsResp.json();
10926
- logger.info("[API] Friend request sent to " + friendId);
10927
- 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
+ }
10928
11165
  }
10929
- if (apiPath.startsWith("/friends/") && !apiPath.includes("/permissions") && !apiPath.includes("/requests") && method === "DELETE") {
11166
+ if (apiPath.startsWith("/friends/") && method === "DELETE") {
10930
11167
  if (apiPath.includes("/requests/")) {
10931
- const parts = apiPath.split("/");
11168
+ } else if (apiPath.includes("/permissions")) {
10932
11169
  } else {
10933
11170
  const friendId = decodeURIComponent(apiPath.slice("/friends/".length));
10934
11171
  if (!friendId)
@@ -10966,7 +11203,7 @@ function createManagementHandler(ctx) {
10966
11203
  body: JSON.stringify({ nodeId: aicqAgentId, permissions })
10967
11204
  });
10968
11205
  if (!resp.ok)
10969
- return json(res, { success: false, message: "Failed to update: " + await resp.text() });
11206
+ return json(res, { success: false, message: "Failed: " + await resp.text() });
10970
11207
  const localFriend = store.getFriend(friendId);
10971
11208
  if (localFriend) {
10972
11209
  localFriend.permissions = permissions;
@@ -10996,29 +11233,39 @@ function createManagementHandler(ctx) {
10996
11233
  if (!requestId)
10997
11234
  return json(res, { success: false, message: "Missing request ID" }, 400);
10998
11235
  const body = await readBody(req);
10999
- const resp = await fetch(serverUrl + "/api/v1/friends/requests/" + requestId + "/accept", {
11000
- method: "POST",
11001
- headers: { "Content-Type": "application/json" },
11002
- body: JSON.stringify({ permissions: body.permissions || ["chat"] })
11003
- });
11004
- if (!resp.ok)
11005
- return json(res, { success: false, message: "Failed: " + await resp.text() });
11006
- logger.info("[API] Friend request accepted: " + requestId);
11007
- 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
+ }
11008
11250
  }
11009
11251
  if (apiPath.match(/^\/friends\/requests\/[^/]+\/reject$/) && method === "POST") {
11010
11252
  const requestId = decodeURIComponent(apiPath.split("/")[3]);
11011
11253
  if (!requestId)
11012
11254
  return json(res, { success: false, message: "Missing request ID" }, 400);
11013
- const resp = await fetch(serverUrl + "/api/v1/friends/requests/" + requestId + "/reject", {
11014
- method: "POST",
11015
- headers: { "Content-Type": "application/json" },
11016
- body: JSON.stringify({})
11017
- });
11018
- if (!resp.ok)
11019
- return json(res, { success: false, message: "Failed: " + await resp.text() });
11020
- logger.info("[API] Friend request rejected: " + requestId);
11021
- 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
+ }
11022
11269
  }
11023
11270
  if (apiPath === "/sessions" && method === "GET") {
11024
11271
  const sessions = Array.from(store.sessions.values()).map((s) => ({
@@ -11029,9 +11276,10 @@ function createManagementHandler(ctx) {
11029
11276
  return json(res, { sessions });
11030
11277
  }
11031
11278
  if (apiPath === "/models" && method === "GET") {
11032
- const config2 = readOpenClawConfig();
11033
- const modelData = config2 ? getModelConfig(config2) : { providers: MODEL_PROVIDERS, currentModels: [] };
11034
- 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));
11035
11283
  }
11036
11284
  if (apiPath.match(/^\/models\/[^/]+$/) && method === "PUT") {
11037
11285
  const providerId = decodeURIComponent(apiPath.slice("/models/".length));
@@ -11042,15 +11290,15 @@ function createManagementHandler(ctx) {
11042
11290
  const provider = MODEL_PROVIDERS.find((p) => p.id === providerId);
11043
11291
  if (!provider)
11044
11292
  return json(res, { success: false, message: "Unknown provider: " + providerId }, 400);
11045
- const config2 = readOpenClawConfig();
11046
- if (!config2) {
11047
- return json(res, { success: false, message: "Could not find openclaw.json config file. Make sure OpenClaw is properly configured." }, 400);
11048
- }
11049
- 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") {
11050
11298
  config2.providers = {};
11051
11299
  }
11052
11300
  const providers = config2.providers;
11053
- if (!providers[provider.configKey]) {
11301
+ if (!providers[provider.configKey] || typeof providers[provider.configKey] !== "object") {
11054
11302
  providers[provider.configKey] = {};
11055
11303
  }
11056
11304
  const provConfig = providers[provider.configKey];
@@ -11060,10 +11308,9 @@ function createManagementHandler(ctx) {
11060
11308
  provConfig.model = modelId;
11061
11309
  if (baseUrl)
11062
11310
  provConfig.baseUrl = baseUrl;
11063
- const written = writeOpenClawConfig(config2);
11064
- if (!written) {
11311
+ const written = writeConfig(config2);
11312
+ if (!written)
11065
11313
  return json(res, { success: false, message: "Failed to write config file" }, 500);
11066
- }
11067
11314
  logger.info("[API] Model config saved for provider: " + providerId);
11068
11315
  return json(res, { success: true, message: "Model configuration saved for " + provider.name });
11069
11316
  }
@@ -11619,28 +11866,52 @@ var plugin = definePluginEntry({
11619
11866
  });
11620
11867
  logger.info("[Init] Registered 13 gateway methods for management UI");
11621
11868
  }
11622
- if (api.registerHttpRoute) {
11623
- const managementHtml = getManagementHTML();
11624
- const managementHandler = createManagementHandler({
11625
- store,
11626
- identityService,
11627
- serverClient,
11628
- serverUrl,
11629
- aicqAgentId,
11630
- logger,
11631
- html: managementHtml
11869
+ const managementHtml = getManagementHTML();
11870
+ const managementHandler = createManagementHandler({
11871
+ store,
11872
+ identityService,
11873
+ serverClient,
11874
+ serverUrl,
11875
+ aicqAgentId,
11876
+ logger,
11877
+ html: managementHtml
11878
+ });
11879
+ const mgmtPort = parseInt(process.env.AICQ_MGMT_PORT || "461099", 10);
11880
+ const mgmtServer = http.createServer((req, res) => {
11881
+ managementHandler(req, res).catch((err) => {
11882
+ logger.error("[HTTP] Management server error: " + (err instanceof Error ? err.message : err));
11883
+ if (!res.headersSent) {
11884
+ res.writeHead(500, { "Content-Type": "text/plain" });
11885
+ }
11886
+ res.end("Internal Server Error");
11632
11887
  });
11888
+ });
11889
+ mgmtServer.listen(mgmtPort, "127.0.0.1", () => {
11890
+ logger.info("[Init] Management UI HTTP server running at http://127.0.0.1:" + mgmtPort + "/");
11891
+ });
11892
+ mgmtServer.on("error", (err) => {
11893
+ if (err.code === "EADDRINUSE") {
11894
+ logger.warn("[Init] Management UI port " + mgmtPort + " already in use, trying " + (mgmtPort + 1));
11895
+ mgmtServer.close();
11896
+ mgmtServer.listen(mgmtPort + 1, "127.0.0.1", () => {
11897
+ logger.info("[Init] Management UI HTTP server running at http://127.0.0.1:" + (mgmtPort + 1) + "/");
11898
+ });
11899
+ } else {
11900
+ logger.error("[Init] Management UI HTTP server error: " + err.message);
11901
+ }
11902
+ });
11903
+ if (api.registerHttpRoute) {
11633
11904
  api.registerHttpRoute({
11634
11905
  path: "/aicq-chat",
11635
11906
  auth: "gateway",
11636
11907
  match: "prefix",
11637
11908
  handler: managementHandler
11638
11909
  });
11639
- logger.info("[Init] Management UI registered at /plugins/aicq-chat/");
11910
+ logger.info("[Init] Management UI also registered via gateway at /plugins/aicq-chat/");
11640
11911
  }
11641
11912
  logger.info("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
11642
11913
  logger.info(" AICQ Plugin activated successfully!");
11643
- logger.info(" Management UI: /plugins/aicq-chat/");
11914
+ logger.info(" Management UI: http://127.0.0.1:" + mgmtPort + "/");
11644
11915
  logger.info("\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550");
11645
11916
  }
11646
11917
  });