@shadanai/openme 0.1.0

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.
Files changed (147) hide show
  1. package/bin/openme.js +2 -0
  2. package/dist/agents/agent.d.ts +10 -0
  3. package/dist/agents/agent.js +2 -0
  4. package/dist/agents/agent.js.map +1 -0
  5. package/dist/agents/happy.d.ts +15 -0
  6. package/dist/agents/happy.js +43 -0
  7. package/dist/agents/happy.js.map +1 -0
  8. package/dist/agents/openclaw.d.ts +16 -0
  9. package/dist/agents/openclaw.js +55 -0
  10. package/dist/agents/openclaw.js.map +1 -0
  11. package/dist/agents/proxy.d.ts +15 -0
  12. package/dist/agents/proxy.js +36 -0
  13. package/dist/agents/proxy.js.map +1 -0
  14. package/dist/cli/cmd-agent.d.ts +1 -0
  15. package/dist/cli/cmd-agent.js +73 -0
  16. package/dist/cli/cmd-agent.js.map +1 -0
  17. package/dist/cli/cmd-datasource.d.ts +1 -0
  18. package/dist/cli/cmd-datasource.js +78 -0
  19. package/dist/cli/cmd-datasource.js.map +1 -0
  20. package/dist/cli/cmd-init.d.ts +7 -0
  21. package/dist/cli/cmd-init.js +26 -0
  22. package/dist/cli/cmd-init.js.map +1 -0
  23. package/dist/cli/cmd-install.d.ts +5 -0
  24. package/dist/cli/cmd-install.js +22 -0
  25. package/dist/cli/cmd-install.js.map +1 -0
  26. package/dist/cli/cmd-start.d.ts +5 -0
  27. package/dist/cli/cmd-start.js +136 -0
  28. package/dist/cli/cmd-start.js.map +1 -0
  29. package/dist/cli/cmd-status.d.ts +1 -0
  30. package/dist/cli/cmd-status.js +26 -0
  31. package/dist/cli/cmd-status.js.map +1 -0
  32. package/dist/cli/cmd-stop.d.ts +1 -0
  33. package/dist/cli/cmd-stop.js +17 -0
  34. package/dist/cli/cmd-stop.js.map +1 -0
  35. package/dist/cli/cmd-workspace.d.ts +1 -0
  36. package/dist/cli/cmd-workspace.js +32 -0
  37. package/dist/cli/cmd-workspace.js.map +1 -0
  38. package/dist/cli/index.d.ts +1 -0
  39. package/dist/cli/index.js +46 -0
  40. package/dist/cli/index.js.map +1 -0
  41. package/dist/core/config-sync.d.ts +6 -0
  42. package/dist/core/config-sync.js +41 -0
  43. package/dist/core/config-sync.js.map +1 -0
  44. package/dist/core/config.d.ts +96 -0
  45. package/dist/core/config.js +91 -0
  46. package/dist/core/config.js.map +1 -0
  47. package/dist/core/node-id.d.ts +1 -0
  48. package/dist/core/node-id.js +5 -0
  49. package/dist/core/node-id.js.map +1 -0
  50. package/dist/core/register.d.ts +19 -0
  51. package/dist/core/register.js +31 -0
  52. package/dist/core/register.js.map +1 -0
  53. package/dist/data/connectors/baidupan.d.ts +20 -0
  54. package/dist/data/connectors/baidupan.js +69 -0
  55. package/dist/data/connectors/baidupan.js.map +1 -0
  56. package/dist/data/connectors/connector.d.ts +12 -0
  57. package/dist/data/connectors/connector.js +2 -0
  58. package/dist/data/connectors/connector.js.map +1 -0
  59. package/dist/data/connectors/dingtalk.d.ts +18 -0
  60. package/dist/data/connectors/dingtalk.js +81 -0
  61. package/dist/data/connectors/dingtalk.js.map +1 -0
  62. package/dist/data/connectors/email.d.ts +18 -0
  63. package/dist/data/connectors/email.js +191 -0
  64. package/dist/data/connectors/email.js.map +1 -0
  65. package/dist/data/connectors/feishu.d.ts +18 -0
  66. package/dist/data/connectors/feishu.js +78 -0
  67. package/dist/data/connectors/feishu.js.map +1 -0
  68. package/dist/data/connectors/github.d.ts +10 -0
  69. package/dist/data/connectors/github.js +36 -0
  70. package/dist/data/connectors/github.js.map +1 -0
  71. package/dist/data/connectors/index.d.ts +3 -0
  72. package/dist/data/connectors/index.js +23 -0
  73. package/dist/data/connectors/index.js.map +1 -0
  74. package/dist/data/connectors/local-fs.d.ts +20 -0
  75. package/dist/data/connectors/local-fs.js +57 -0
  76. package/dist/data/connectors/local-fs.js.map +1 -0
  77. package/dist/data/connectors/notion.d.ts +10 -0
  78. package/dist/data/connectors/notion.js +46 -0
  79. package/dist/data/connectors/notion.js.map +1 -0
  80. package/dist/data/connectors/wecom.d.ts +18 -0
  81. package/dist/data/connectors/wecom.js +74 -0
  82. package/dist/data/connectors/wecom.js.map +1 -0
  83. package/dist/data/keys/store.d.ts +16 -0
  84. package/dist/data/keys/store.js +106 -0
  85. package/dist/data/keys/store.js.map +1 -0
  86. package/dist/data/profile/builder.d.ts +10 -0
  87. package/dist/data/profile/builder.js +48 -0
  88. package/dist/data/profile/builder.js.map +1 -0
  89. package/dist/data/skills/generator.d.ts +3 -0
  90. package/dist/data/skills/generator.js +72 -0
  91. package/dist/data/skills/generator.js.map +1 -0
  92. package/dist/data/sync/scheduler.d.ts +12 -0
  93. package/dist/data/sync/scheduler.js +51 -0
  94. package/dist/data/sync/scheduler.js.map +1 -0
  95. package/dist/deps/detector.d.ts +8 -0
  96. package/dist/deps/detector.js +19 -0
  97. package/dist/deps/detector.js.map +1 -0
  98. package/dist/deps/installer.d.ts +1 -0
  99. package/dist/deps/installer.js +38 -0
  100. package/dist/deps/installer.js.map +1 -0
  101. package/dist/deps/platform.d.ts +4 -0
  102. package/dist/deps/platform.js +24 -0
  103. package/dist/deps/platform.js.map +1 -0
  104. package/dist/health/monitor.d.ts +2 -0
  105. package/dist/health/monitor.js +18 -0
  106. package/dist/health/monitor.js.map +1 -0
  107. package/dist/proxy/auth.d.ts +7 -0
  108. package/dist/proxy/auth.js +22 -0
  109. package/dist/proxy/auth.js.map +1 -0
  110. package/dist/proxy/routes-agent.d.ts +3 -0
  111. package/dist/proxy/routes-agent.js +51 -0
  112. package/dist/proxy/routes-agent.js.map +1 -0
  113. package/dist/proxy/routes-config.d.ts +2 -0
  114. package/dist/proxy/routes-config.js +51 -0
  115. package/dist/proxy/routes-config.js.map +1 -0
  116. package/dist/proxy/routes-datasource.d.ts +2 -0
  117. package/dist/proxy/routes-datasource.js +30 -0
  118. package/dist/proxy/routes-datasource.js.map +1 -0
  119. package/dist/proxy/routes-keys.d.ts +7 -0
  120. package/dist/proxy/routes-keys.js +44 -0
  121. package/dist/proxy/routes-keys.js.map +1 -0
  122. package/dist/proxy/routes-profile.d.ts +2 -0
  123. package/dist/proxy/routes-profile.js +29 -0
  124. package/dist/proxy/routes-profile.js.map +1 -0
  125. package/dist/proxy/routes-status.d.ts +2 -0
  126. package/dist/proxy/routes-status.js +9 -0
  127. package/dist/proxy/routes-status.js.map +1 -0
  128. package/dist/proxy/routes-ttyd.d.ts +3 -0
  129. package/dist/proxy/routes-ttyd.js +41 -0
  130. package/dist/proxy/routes-ttyd.js.map +1 -0
  131. package/dist/proxy/routes-workspace.d.ts +2 -0
  132. package/dist/proxy/routes-workspace.js +77 -0
  133. package/dist/proxy/routes-workspace.js.map +1 -0
  134. package/dist/proxy/server.d.ts +22 -0
  135. package/dist/proxy/server.js +130 -0
  136. package/dist/proxy/server.js.map +1 -0
  137. package/dist/proxy/ttyd-manager.d.ts +19 -0
  138. package/dist/proxy/ttyd-manager.js +68 -0
  139. package/dist/proxy/ttyd-manager.js.map +1 -0
  140. package/dist/proxy/utils.d.ts +3 -0
  141. package/dist/proxy/utils.js +13 -0
  142. package/dist/proxy/utils.js.map +1 -0
  143. package/dist/proxy/ws-proxy.d.ts +6 -0
  144. package/dist/proxy/ws-proxy.js +30 -0
  145. package/dist/proxy/ws-proxy.js.map +1 -0
  146. package/package.json +40 -0
  147. package/ui/index.html +631 -0
package/ui/index.html ADDED
@@ -0,0 +1,631 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>OpenMe</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+
10
+ /* ── Theme: CSS Variables ── */
11
+ :root, [data-theme="dark"] {
12
+ --bg-primary: #0d1117;
13
+ --bg-secondary: #161b22;
14
+ --bg-tertiary: #21262d;
15
+ --border: #30363d;
16
+ --text-primary: #c9d1d9;
17
+ --text-secondary: #8b949e;
18
+ --text-muted: #484f58;
19
+ --accent: #58a6ff;
20
+ --success: #3fb950;
21
+ --danger: #f85149;
22
+ --warning: #d29922;
23
+ --btn-primary-bg: #238636;
24
+ --btn-bg: #21262d;
25
+ --btn-hover: #30363d;
26
+ --input-bg: #161b22;
27
+ }
28
+ [data-theme="light"] {
29
+ --bg-primary: #ffffff;
30
+ --bg-secondary: #f6f8fa;
31
+ --bg-tertiary: #eaeef2;
32
+ --border: #d0d7de;
33
+ --text-primary: #1f2328;
34
+ --text-secondary: #656d76;
35
+ --text-muted: #8b949e;
36
+ --accent: #0969da;
37
+ --success: #1a7f37;
38
+ --danger: #cf222e;
39
+ --warning: #9a6700;
40
+ --btn-primary-bg: #1f883d;
41
+ --btn-bg: #f3f4f6;
42
+ --btn-hover: #eaeef2;
43
+ --input-bg: #f6f8fa;
44
+ }
45
+
46
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: var(--bg-primary); color: var(--text-primary); display: flex; height: 100vh; overflow: hidden; }
47
+ nav { width: 180px; min-width: 180px; background: var(--bg-secondary); border-right: 1px solid var(--border); display: flex; flex-direction: column; }
48
+ nav .logo { padding: 16px; font-size: 18px; font-weight: 700; color: var(--accent); border-bottom: 1px solid var(--border); }
49
+ nav a { display: block; padding: 10px 16px; color: var(--text-secondary); text-decoration: none; font-size: 14px; }
50
+ nav a:hover, nav a.active { color: var(--text-primary); background: var(--bg-tertiary); }
51
+ nav a.active { border-left: 3px solid var(--accent); }
52
+ nav .nav-bottom { margin-top: auto; padding: 8px; border-top: 1px solid var(--border); display: flex; gap: 6px; justify-content: center; }
53
+ nav .nav-bottom button { background: var(--btn-bg); border: 1px solid var(--border); color: var(--text-secondary); border-radius: 4px; padding: 4px 8px; cursor: pointer; font-size: 12px; }
54
+ nav .nav-bottom button:hover { background: var(--btn-hover); color: var(--text-primary); }
55
+ main { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
56
+ .tab-content { flex: 1; display: none; overflow: auto; }
57
+ .tab-content.active { display: flex; flex-direction: column; }
58
+ .tab-content iframe { flex: 1; border: none; width: 100%; height: 100%; }
59
+ .page { padding: 20px; }
60
+ h2 { margin-bottom: 16px; font-size: 18px; }
61
+ .cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; margin-bottom: 20px; }
62
+ .card { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 6px; padding: 14px; }
63
+ .card h3 { font-size: 12px; color: var(--text-secondary); margin-bottom: 6px; text-transform: uppercase; }
64
+ .card .val { font-size: 20px; font-weight: 600; }
65
+ .ok { color: var(--success); } .err { color: var(--danger); } .warn { color: var(--warning); }
66
+ table { width: 100%; border-collapse: collapse; margin-bottom: 16px; }
67
+ th, td { text-align: left; padding: 8px 12px; border-bottom: 1px solid var(--bg-tertiary); font-size: 13px; }
68
+ th { color: var(--text-secondary); font-weight: 500; }
69
+ .btn { display: inline-block; padding: 6px 14px; border-radius: 4px; border: 1px solid var(--border); background: var(--btn-bg); color: var(--text-primary); cursor: pointer; font-size: 13px; }
70
+ .btn:hover { background: var(--btn-hover); }
71
+ .btn-primary { background: var(--btn-primary-bg); border-color: var(--btn-primary-bg); color: #fff; }
72
+ .terminal-bar { display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: var(--bg-secondary); border-bottom: 1px solid var(--border); }
73
+ .terminal-bar .tab { padding: 4px 12px; border-radius: 4px; cursor: pointer; font-size: 13px; color: var(--text-secondary); }
74
+ .terminal-bar .tab.active { background: var(--bg-tertiary); color: var(--text-primary); }
75
+ .terminal-bar .tab .close { margin-left: 6px; opacity: 0.5; }
76
+ .terminal-bar .tab .close:hover { opacity: 1; }
77
+ .terminal-bar select { background: var(--btn-bg); color: var(--text-primary); border: 1px solid var(--border); padding: 4px 8px; border-radius: 4px; font-size: 13px; }
78
+ pre { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 6px; padding: 12px; overflow: auto; font-size: 13px; max-height: 400px; }
79
+ .empty { color: var(--text-muted); font-style: italic; padding: 20px; }
80
+ .connector-card { background: var(--bg-secondary); border: 1px solid var(--border); border-radius: 8px; padding: 16px; cursor: pointer; transition: all .15s; text-align: center; min-height: 100px; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 8px; }
81
+ .connector-card:hover { border-color: var(--accent); background: var(--bg-tertiary); }
82
+ .connector-card.active { border-color: var(--success); }
83
+ .connector-card .icon { font-size: 28px; }
84
+ .connector-card .name { font-size: 13px; font-weight: 600; }
85
+ .connector-card .status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
86
+ .connector-card .status-dot.on { background: var(--success); }
87
+ .connector-card .status-dot.off { background: var(--text-muted); }
88
+ .form-row { margin-bottom: 12px; }
89
+ .form-row label { display: block; font-size: 12px; color: var(--text-secondary); margin-bottom: 4px; }
90
+ .form-row input, .form-row select { width: 100%; padding: 6px 10px; background: var(--input-bg); color: var(--text-primary); border: 1px solid var(--border); border-radius: 4px; font-size: 13px; }
91
+ #config-editor { width: 100%; height: 60vh; background: var(--input-bg); color: var(--text-primary); border: 1px solid var(--border); border-radius: 6px; padding: 12px; font-family: monospace; font-size: 13px; resize: vertical; }
92
+ </style>
93
+ </head>
94
+ <body>
95
+ <nav>
96
+ <div class="logo">🤖 OpenMe</div>
97
+ <a href="#dashboard" class="active" data-tab="dashboard" data-i18n="nav.dashboard">📊 Dashboard</a>
98
+ <a href="#chat" data-tab="chat" data-i18n="nav.chat">💬 Chat</a>
99
+ <a href="#terminal" data-tab="terminal" data-i18n="nav.terminal">🖥 Terminal</a>
100
+ <a href="#datasources" data-tab="datasources" data-i18n="nav.datasources">🔌 Data Sources</a>
101
+ <a href="#settings" data-tab="settings" data-i18n="nav.settings">⚙ Settings</a>
102
+ <div class="nav-bottom">
103
+ <button onclick="toggleTheme()" id="theme-btn" title="Toggle theme">🌙</button>
104
+ <button onclick="toggleLang()" id="lang-btn" title="Toggle language">EN</button>
105
+ </div>
106
+ </nav>
107
+ <main>
108
+ <div id="dashboard" class="tab-content active">
109
+ <div class="page">
110
+ <h2 data-i18n="dashboard.title">Dashboard</h2>
111
+ <div class="cards" id="status-cards"></div>
112
+ <h2 data-i18n="dashboard.agents">Agents</h2>
113
+ <table id="agents-table"><thead><tr><th data-i18n="table.name">Name</th><th data-i18n="table.status">Status</th><th data-i18n="table.info">Info</th><th data-i18n="table.actions">Actions</th></tr></thead><tbody></tbody></table>
114
+ <h2 data-i18n="dashboard.datasources">Data Sources</h2>
115
+ <table id="ds-table"><thead><tr><th data-i18n="table.type">Type</th><th data-i18n="table.enabled">Enabled</th><th data-i18n="table.syncTo">Sync To</th><th data-i18n="table.interval">Interval</th><th data-i18n="table.actions">Actions</th></tr></thead><tbody></tbody></table>
116
+ </div>
117
+ </div>
118
+ <div id="chat" class="tab-content"><iframe id="openclaw-frame"></iframe></div>
119
+ <div id="terminal" class="tab-content">
120
+ <div class="terminal-bar">
121
+ <select id="term-preset">
122
+ <option value="bash">Bash</option>
123
+ <option value="claude">Claude Code</option>
124
+ <option value="kiro-cli chat">Kiro</option>
125
+ </select>
126
+ <button class="btn btn-primary" onclick="createSession()" data-i18n="terminal.new">+ New</button>
127
+ <div id="term-tabs" style="display:flex;gap:4px;margin-left:8px;"></div>
128
+ </div>
129
+ <div id="term-frame" style="flex:1;"></div>
130
+ </div>
131
+ <div id="datasources" class="tab-content">
132
+ <div class="page" id="ds-page">
133
+ <!-- List view -->
134
+ <div id="ds-list-view">
135
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
136
+ <h2 data-i18n="ds.title">Data Sources</h2>
137
+ </div>
138
+ <p style="color:var(--text-secondary);margin-bottom:16px;" data-i18n="ds.addHint">Click a connector to add or manage a data source.</p>
139
+ <h3 style="margin-bottom:12px;color:var(--text-secondary);font-size:13px;" data-i18n="ds.available">Available Connectors</h3>
140
+ <div class="cards" id="ds-connectors"></div>
141
+ <h3 style="margin:20px 0 12px;color:var(--text-secondary);font-size:13px;" data-i18n="ds.connected">Connected</h3>
142
+ <div class="cards" id="ds-active"></div>
143
+ </div>
144
+ <!-- Detail view -->
145
+ <div id="ds-detail-view" style="display:none;">
146
+ <div style="display:flex;align-items:center;gap:12px;margin-bottom:20px;">
147
+ <button class="btn" onclick="closeDsDetail()">← Back</button>
148
+ <h2 id="ds-detail-title"></h2>
149
+ <span id="ds-detail-status" style="font-size:13px;"></span>
150
+ </div>
151
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;">
152
+ <div>
153
+ <h3 style="margin-bottom:12px;" data-i18n="ds.config">Configuration</h3>
154
+ <div id="ds-detail-form"></div>
155
+ <div style="margin-top:12px;display:flex;gap:8px;">
156
+ <button class="btn btn-primary" onclick="saveDsConfig()" data-i18n="settings.save">Save</button>
157
+ <button class="btn" onclick="triggerDsSync()" data-i18n="ds.syncNow">Sync Now</button>
158
+ <button class="btn" style="border-color:var(--danger);color:var(--danger);" onclick="removeDsSource()" data-i18n="ds.remove">Remove</button>
159
+ </div>
160
+ </div>
161
+ <div>
162
+ <h3 style="margin-bottom:12px;" data-i18n="ds.data">Synced Data</h3>
163
+ <div id="ds-detail-files" style="max-height:50vh;overflow:auto;"></div>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ </div>
168
+ </div>
169
+ <div id="settings" class="tab-content">
170
+ <div class="page">
171
+ <h2 data-i18n="settings.title">Settings</h2>
172
+ <p style="margin-bottom:12px;color:var(--text-secondary);" data-i18n="settings.desc">Edit ~/.openme/config.json</p>
173
+ <textarea id="config-editor"></textarea>
174
+ <div style="margin-top:12px;">
175
+ <button class="btn btn-primary" onclick="saveConfig()" data-i18n="settings.save">Save</button>
176
+ <button class="btn" onclick="loadConfig()" data-i18n="settings.reload">Reload</button>
177
+ </div>
178
+ </div>
179
+ </div>
180
+ </main>
181
+
182
+ <script>
183
+ // ── i18n ──
184
+ const I18N = {
185
+ en: {
186
+ 'nav.dashboard': '📊 Dashboard', 'nav.chat': '💬 Chat', 'nav.terminal': '🖥 Terminal',
187
+ 'nav.datasources': '🔌 Data Sources', 'nav.settings': '⚙ Settings',
188
+ 'dashboard.title': 'Dashboard', 'dashboard.agents': 'Agents', 'dashboard.datasources': 'Data Sources',
189
+ 'table.name': 'Name', 'table.status': 'Status', 'table.info': 'Info', 'table.actions': 'Actions',
190
+ 'table.type': 'Type', 'table.enabled': 'Enabled', 'table.syncTo': 'Sync To', 'table.interval': 'Interval',
191
+ 'terminal.new': '+ New',
192
+ 'workspace.title': 'Workspace', 'workspace.viewProfile': 'View Profile', 'workspace.refreshProfile': 'Refresh Profile',
193
+ 'ds.title': 'Data Sources', 'ds.addHint': 'Click a connector to add or manage a data source.',
194
+ 'ds.available': 'Available Connectors', 'ds.connected': 'Connected',
195
+ 'ds.config': 'Configuration', 'ds.data': 'Synced Data', 'ds.syncNow': 'Sync Now', 'ds.remove': 'Remove',
196
+ 'ds.enabled': 'Enabled', 'ds.syncTo': 'Sync Directory', 'ds.syncInterval': 'Sync Interval',
197
+ 'ds.credentials': 'Credentials Path', 'ds.added': 'Data source added', 'ds.removed': 'Data source removed',
198
+ 'ds.saved': 'Configuration saved', 'ds.noData': 'No synced data yet', 'ds.backToList': 'Back to file list',
199
+ 'ds.credTitle': 'Credentials', 'ds.credConfigured': 'Credentials configured', 'ds.credNotSet': 'Credentials not set — fill in below',
200
+ 'settings.title': 'Settings', 'settings.desc': 'Edit ~/.openme/config.json', 'settings.save': 'Save', 'settings.reload': 'Reload',
201
+ 'status.running': 'Running', 'status.error': 'Error', 'status.offline': 'Offline',
202
+ 'status.uptime': 'Uptime', 'status.version': 'Version', 'status.status': 'Status',
203
+ 'agent.restart': 'Restart', 'agent.running': '✓ Running', 'agent.stopped': '✗ Stopped',
204
+ 'ds.yes': 'Yes', 'ds.no': 'No', 'ds.sync': 'Sync', 'ds.syncTriggered': 'Sync triggered',
205
+ 'ws.noFiles': 'Cannot load workspace files', 'ws.noProfile': 'No profile generated yet',
206
+ 'ws.refreshTriggered': 'Profile refresh triggered',
207
+ 'cfg.saved': 'Config saved', 'cfg.invalidJson': 'Invalid JSON',
208
+ 'term.noSessions': 'No terminal sessions. Click "+ New" to create one.',
209
+ },
210
+ zh: {
211
+ 'nav.dashboard': '📊 仪表盘', 'nav.chat': '💬 对话', 'nav.terminal': '🖥 终端',
212
+ 'nav.datasources': '🔌 数据源', 'nav.settings': '⚙ 设置',
213
+ 'dashboard.title': '仪表盘', 'dashboard.agents': '代理服务', 'dashboard.datasources': '数据源',
214
+ 'table.name': '名称', 'table.status': '状态', 'table.info': '信息', 'table.actions': '操作',
215
+ 'table.type': '类型', 'table.enabled': '已启用', 'table.syncTo': '同步目录', 'table.interval': '间隔',
216
+ 'terminal.new': '+ 新建',
217
+ 'workspace.title': '工作区', 'workspace.viewProfile': '查看画像', 'workspace.refreshProfile': '刷新画像',
218
+ 'ds.title': '数据源', 'ds.addHint': '点击连接器来添加或管理数据源。',
219
+ 'ds.available': '可用连接器', 'ds.connected': '已连接',
220
+ 'ds.config': '配置', 'ds.data': '已同步数据', 'ds.syncNow': '立即同步', 'ds.remove': '移除',
221
+ 'ds.enabled': '已启用', 'ds.syncTo': '同步目录', 'ds.syncInterval': '同步间隔',
222
+ 'ds.credentials': '凭证路径', 'ds.added': '数据源已添加', 'ds.removed': '数据源已移除',
223
+ 'ds.saved': '配置已保存', 'ds.noData': '暂无同步数据', 'ds.backToList': '返回文件列表',
224
+ 'ds.credTitle': '连接凭证', 'ds.credConfigured': '凭证已配置', 'ds.credNotSet': '凭证未设置 — 请在下方填写',
225
+ 'settings.title': '设置', 'settings.desc': '编辑 ~/.openme/config.json', 'settings.save': '保存', 'settings.reload': '重新加载',
226
+ 'status.running': '运行中', 'status.error': '异常', 'status.offline': '离线',
227
+ 'status.uptime': '运行时间', 'status.version': '版本', 'status.status': '状态',
228
+ 'agent.restart': '重启', 'agent.running': '✓ 运行中', 'agent.stopped': '✗ 已停止',
229
+ 'ds.yes': '是', 'ds.no': '否', 'ds.sync': '同步', 'ds.syncTriggered': '同步已触发',
230
+ 'ws.noFiles': '无法加载工作区文件', 'ws.noProfile': '尚未生成用户画像',
231
+ 'ws.refreshTriggered': '画像刷新已触发',
232
+ 'cfg.saved': '配置已保存', 'cfg.invalidJson': 'JSON 格式错误',
233
+ 'term.noSessions': '暂无终端会话,点击 "+ 新建" 创建。',
234
+ }
235
+ };
236
+
237
+ let currentLang = localStorage.getItem('openme-lang') || 'zh';
238
+ let currentTheme = localStorage.getItem('openme-theme') || 'dark';
239
+
240
+ function t(key) { return I18N[currentLang]?.[key] || I18N.en[key] || key; }
241
+
242
+ function applyI18n() {
243
+ document.querySelectorAll('[data-i18n]').forEach(el => {
244
+ el.textContent = t(el.dataset.i18n);
245
+ });
246
+ document.getElementById('lang-btn').textContent = currentLang === 'zh' ? 'EN' : '中';
247
+ }
248
+
249
+ // ── Sync with openclaw iframe ──
250
+ const OC_SETTINGS_KEY = 'openclaw.control.settings.v1';
251
+ const OC_LOCALE_KEY = 'openclaw.i18n.locale';
252
+
253
+ function syncThemeToOpenclaw() {
254
+ try {
255
+ const raw = localStorage.getItem(OC_SETTINGS_KEY);
256
+ const settings = raw ? JSON.parse(raw) : {};
257
+ settings.theme = currentTheme;
258
+ localStorage.setItem(OC_SETTINGS_KEY, JSON.stringify(settings));
259
+ } catch {}
260
+ }
261
+
262
+ function syncLangToOpenclaw() {
263
+ const localeMap = { zh: 'zh-CN', en: 'en' };
264
+ localStorage.setItem(OC_LOCALE_KEY, localeMap[currentLang] || 'en');
265
+ }
266
+
267
+ function reloadOpenclawIframe() {
268
+ const iframe = document.getElementById('openclaw-frame');
269
+ if (iframe) iframe.src = '/openclaw/' + (ocToken ? '?token=' + encodeURIComponent(ocToken) : '');
270
+ }
271
+
272
+ // Read initial state from openclaw settings if available
273
+ function readOpenclawSettings() {
274
+ try {
275
+ const raw = localStorage.getItem(OC_SETTINGS_KEY);
276
+ if (raw) {
277
+ const s = JSON.parse(raw);
278
+ if (s.theme === 'dark' || s.theme === 'light') currentTheme = s.theme;
279
+ }
280
+ const locale = localStorage.getItem(OC_LOCALE_KEY);
281
+ if (locale) currentLang = locale.startsWith('zh') ? 'zh' : 'en';
282
+ } catch {}
283
+ }
284
+ readOpenclawSettings();
285
+
286
+ function applyTheme() {
287
+ document.documentElement.setAttribute('data-theme', currentTheme);
288
+ document.getElementById('theme-btn').textContent = currentTheme === 'dark' ? '☀️' : '🌙';
289
+ }
290
+
291
+ function toggleTheme() {
292
+ currentTheme = currentTheme === 'dark' ? 'light' : 'dark';
293
+ localStorage.setItem('openme-theme', currentTheme);
294
+ applyTheme();
295
+ syncThemeToOpenclaw();
296
+ reloadOpenclawIframe();
297
+ }
298
+
299
+ function toggleLang() {
300
+ currentLang = currentLang === 'zh' ? 'en' : 'zh';
301
+ localStorage.setItem('openme-lang', currentLang);
302
+ applyI18n();
303
+ syncLangToOpenclaw();
304
+ reloadOpenclawIframe();
305
+ loadDashboard();
306
+ }
307
+
308
+ // ── API ──
309
+ const API = '/openme/api';
310
+ const TOKEN = window.__OPENME__?.token || '';
311
+ const authHeaders = () => ({ 'Content-Type': 'application/json', 'Authorization': `Bearer ${TOKEN}` });
312
+ const authFetch = (url, opts) => fetch(url, { ...opts, headers: { ...authHeaders(), ...opts?.headers } });
313
+
314
+ // ── Tab switching ──
315
+ document.querySelectorAll('nav a[data-tab]').forEach(a => {
316
+ a.addEventListener('click', e => {
317
+ e.preventDefault();
318
+ document.querySelectorAll('nav a[data-tab]').forEach(x => x.classList.remove('active'));
319
+ document.querySelectorAll('.tab-content').forEach(x => x.classList.remove('active'));
320
+ a.classList.add('active');
321
+ document.getElementById(a.dataset.tab).classList.add('active');
322
+ if (a.dataset.tab === 'settings') loadConfig();
323
+ if (a.dataset.tab === 'datasources') loadDatasources();
324
+ if (a.dataset.tab === 'terminal') loadSessions();
325
+ });
326
+ });
327
+
328
+ // ── Dashboard ──
329
+ async function loadDashboard() {
330
+ try {
331
+ const s = await (await authFetch(`${API}/status`)).json();
332
+ document.getElementById('status-cards').innerHTML = `
333
+ <div class="card"><h3>${t('status.status')}</h3><div class="val ${s.ok?'ok':'err'}">${s.ok?t('status.running'):t('status.error')}</div></div>
334
+ <div class="card"><h3>${t('status.uptime')}</h3><div class="val">${Math.floor(s.uptime)}s</div></div>
335
+ <div class="card"><h3>${t('status.version')}</h3><div class="val">${s.version||'?'}</div></div>`;
336
+ } catch { document.getElementById('status-cards').innerHTML = `<div class="card"><h3>${t('status.status')}</h3><div class="val err">${t('status.offline')}</div></div>`; }
337
+ try {
338
+ const agents = await (await authFetch(`${API}/agents`)).json();
339
+ if (Array.isArray(agents)) {
340
+ document.querySelector('#agents-table tbody').innerHTML = agents.map(a =>
341
+ `<tr><td>${a.name}</td><td class="${a.ok?'ok':'err'}">${a.ok?t('agent.running'):t('agent.stopped')}</td><td>${JSON.stringify(a.port||a.serverUrl||'')}</td><td><button class="btn" onclick="restartAgent('${a.name}')">${t('agent.restart')}</button></td></tr>`
342
+ ).join('');
343
+ }
344
+ } catch {}
345
+ try {
346
+ const ds = await (await authFetch(`${API}/datasources`)).json();
347
+ if (Array.isArray(ds)) {
348
+ document.querySelector('#ds-table tbody').innerHTML = ds.map(d =>
349
+ `<tr><td>${d.type}</td><td class="${d.enabled?'ok':'warn'}">${d.enabled?t('ds.yes'):t('ds.no')}</td><td>${d.syncTo}</td><td>${d.syncInterval||'-'}</td><td><button class="btn" onclick="syncDS('${d.type}')">${t('ds.sync')}</button></td></tr>`
350
+ ).join('');
351
+ }
352
+ } catch {}
353
+ }
354
+ async function restartAgent(name) { await authFetch(`${API}/agents/${name}/restart`, {method:'POST'}); loadDashboard(); }
355
+ async function syncDS(type) { await authFetch(`${API}/datasources/${type}/sync`, {method:'POST'}); alert(t('ds.syncTriggered')); }
356
+
357
+ // ── Terminal ──
358
+ let sessions = [], activeSession = null;
359
+ async function loadSessions() {
360
+ try {
361
+ const data = await (await authFetch(`${API}/ttyd/sessions`)).json();
362
+ sessions = Array.isArray(data) ? data : [];
363
+ } catch { sessions = []; }
364
+ renderTermTabs();
365
+ }
366
+ function renderTermTabs() {
367
+ document.getElementById('term-tabs').innerHTML = sessions.map(s =>
368
+ `<div class="tab ${activeSession===s.id?'active':''}" onclick="selectSession('${s.id}',${s.port})">${s.cmd}<span class="close" onclick="event.stopPropagation();deleteSession('${s.id}')">×</span></div>`
369
+ ).join('');
370
+ const frame = document.getElementById('term-frame');
371
+ for (const ch of [...frame.childNodes]) { if (ch.nodeName !== 'IFRAME') ch.remove(); }
372
+ for (const s of sessions) {
373
+ const fid = 'ttyd-' + s.id;
374
+ if (!document.getElementById(fid)) {
375
+ const iframe = document.createElement('iframe');
376
+ iframe.id = fid;
377
+ iframe.src = `http://${location.hostname}:${s.port}/`;
378
+ iframe.style.cssText = 'width:100%;height:100%;border:none;display:none;';
379
+ frame.appendChild(iframe);
380
+ }
381
+ }
382
+ for (const el of frame.querySelectorAll('iframe')) {
383
+ el.style.display = el.id === 'ttyd-' + activeSession ? 'block' : 'none';
384
+ }
385
+ }
386
+ function selectSession(id, port) { activeSession = id; renderTermTabs(); }
387
+ async function createSession() {
388
+ const cmd = document.getElementById('term-preset').value;
389
+ try {
390
+ const res = await authFetch(`${API}/ttyd/sessions`, {method:'POST', body: JSON.stringify({cmd})});
391
+ const s = await res.json();
392
+ if (!res.ok) { alert(s.error || 'Failed'); return; }
393
+ sessions.push(s);
394
+ activeSession = s.id;
395
+ setTimeout(() => renderTermTabs(), 1000);
396
+ } catch(e) { alert('Failed: ' + e.message); }
397
+ }
398
+ async function deleteSession(id) {
399
+ await authFetch(`${API}/ttyd/sessions/${id}`, {method:'DELETE'});
400
+ const el = document.getElementById('ttyd-' + id);
401
+ if (el) el.remove();
402
+ sessions = sessions.filter(s=>s.id!==id);
403
+ if (activeSession===id) { activeSession = sessions[0]?.id || null; }
404
+ renderTermTabs();
405
+ if (!activeSession) document.getElementById('term-frame').innerHTML = `<div class="empty">${t('term.noSessions')}</div>`;
406
+ }
407
+
408
+ // ── Data Sources ──
409
+ const CONNECTORS = [
410
+ { type: 'github', icon: '🐙', name: 'GitHub', defaultSyncTo: 'data/code', defaultInterval: '1h',
411
+ fields: [{ key: 'token', label: 'Personal Access Token', placeholder: 'ghp_xxxx' }] },
412
+ { type: 'notion', icon: '📝', name: 'Notion', defaultSyncTo: 'data/notes', defaultInterval: '30m',
413
+ fields: [{ key: 'token', label: 'Integration Token', placeholder: 'ntn_xxxx' }] },
414
+ { type: 'email', icon: '📧', name: 'Email (IMAP)', defaultSyncTo: 'data/email', defaultInterval: '15m',
415
+ fields: [
416
+ { key: 'host', label: 'IMAP Server', placeholder: 'imap.gmail.com' },
417
+ { key: 'port', label: 'Port', placeholder: '993' },
418
+ { key: 'user', label: 'Email Address', placeholder: 'you@gmail.com' },
419
+ { key: 'pass', label: 'Password / App Password', placeholder: '', type: 'password' },
420
+ { key: 'folders', label: 'Folders (comma separated)', placeholder: 'INBOX' },
421
+ { key: 'maxMessages', label: 'Max Messages per Folder', placeholder: '50' },
422
+ ] },
423
+ { type: 'feishu', icon: '🐦', name: 'Feishu', defaultSyncTo: 'data/feishu', defaultInterval: '30m',
424
+ fields: [
425
+ { key: 'appId', label: 'App ID', placeholder: 'cli_xxxx' },
426
+ { key: 'appSecret', label: 'App Secret', placeholder: '', type: 'password' },
427
+ ] },
428
+ { type: 'dingtalk', icon: '💬', name: 'DingTalk', defaultSyncTo: 'data/dingtalk', defaultInterval: '30m',
429
+ fields: [
430
+ { key: 'appKey', label: 'App Key', placeholder: 'dingxxxx' },
431
+ { key: 'appSecret', label: 'App Secret', placeholder: '', type: 'password' },
432
+ ] },
433
+ { type: 'wecom', icon: '💼', name: 'WeCom', defaultSyncTo: 'data/wecom', defaultInterval: '30m',
434
+ fields: [
435
+ { key: 'corpId', label: 'Corp ID', placeholder: 'ww_xxxx' },
436
+ { key: 'corpSecret', label: 'Corp Secret', placeholder: '', type: 'password' },
437
+ ] },
438
+ { type: 'baidupan', icon: '☁️', name: 'Baidu Pan', defaultSyncTo: 'data/baidupan', defaultInterval: '1h',
439
+ fields: [
440
+ { key: 'accessToken', label: 'Access Token (OAuth2)', placeholder: '' },
441
+ { key: 'remotePath', label: 'Remote Path', placeholder: '/' },
442
+ ] },
443
+ { type: 'local-fs', icon: '💾', name: 'Local FS', defaultSyncTo: 'data/local', defaultInterval: '5m',
444
+ fields: [
445
+ { key: 'path', label: 'Local Directory Path', placeholder: '~/Documents' },
446
+ { key: 'mode', label: 'Mode', placeholder: 'symlink or copy' },
447
+ ] },
448
+ ];
449
+ let activeDsList = [];
450
+ let currentDsType = null;
451
+
452
+ async function loadDatasources() {
453
+ try {
454
+ const ds = await (await authFetch(`${API}/datasources`)).json();
455
+ activeDsList = Array.isArray(ds) ? ds : [];
456
+ } catch { activeDsList = []; }
457
+ renderDsList();
458
+ }
459
+
460
+ function renderDsList() {
461
+ const activeTypes = new Set(activeDsList.map(d => d.type));
462
+ // Available (not yet connected)
463
+ document.getElementById('ds-connectors').innerHTML = CONNECTORS
464
+ .filter(c => !activeTypes.has(c.type))
465
+ .map(c => `<div class="connector-card" onclick="addDsSource('${c.type}')"><div class="icon">${c.icon}</div><div class="name">${c.name}</div></div>`)
466
+ .join('') || `<div class="empty">—</div>`;
467
+ // Connected
468
+ document.getElementById('ds-active').innerHTML = activeDsList
469
+ .map(d => {
470
+ const c = CONNECTORS.find(x => x.type === d.type) || { icon: '📦', name: d.type };
471
+ return `<div class="connector-card active" onclick="openDsDetail('${d.type}')"><div class="icon">${c.icon}</div><div class="name">${c.name}</div><span class="status-dot ${d.enabled?'on':'off'}"></span></div>`;
472
+ }).join('') || `<div class="empty">${t('ds.noData')}</div>`;
473
+ }
474
+
475
+ async function addDsSource(type) {
476
+ const c = CONNECTORS.find(x => x.type === type);
477
+ const body = JSON.stringify({ dataSources: { [type]: { enabled: true, credentials: `~/.openme/credentials/${type}.json`, syncInterval: c?.defaultInterval || '1h', syncTo: c?.defaultSyncTo || `data/${type}` } } });
478
+ await authFetch(`${API}/config`, { method: 'PATCH', body });
479
+ alert(t('ds.added'));
480
+ await loadDatasources();
481
+ openDsDetail(type);
482
+ }
483
+
484
+ async function openDsDetail(type) {
485
+ currentDsType = type;
486
+ const c = CONNECTORS.find(x => x.type === type) || { icon: '📦', name: type, fields: [] };
487
+ document.getElementById('ds-list-view').style.display = 'none';
488
+ document.getElementById('ds-detail-view').style.display = 'block';
489
+ document.getElementById('ds-detail-title').textContent = `${c.icon} ${c.name}`;
490
+
491
+ // Load current config for this source
492
+ let ds = {};
493
+ let creds = {};
494
+ try {
495
+ const cfg = await (await authFetch(`${API}/config`)).json();
496
+ ds = cfg.dataSources?.[type] || {};
497
+ } catch {}
498
+
499
+ // Load saved credentials
500
+ let hasKey = false;
501
+ try {
502
+ const res = await authFetch(`${API}/keys/${type}`);
503
+ if (res.ok) { creds = await res.json(); hasKey = true; }
504
+ } catch {}
505
+
506
+ const enabled = ds.enabled !== false;
507
+ document.getElementById('ds-detail-status').innerHTML = `<span class="status-dot ${enabled?'on':'off'}"></span> ${enabled ? t('ds.enabled') : 'Disabled'}`;
508
+
509
+ // Build form: sync config + credential fields with saved values
510
+ const credFields = (c.fields || []).map(f => {
511
+ const saved = creds[f.key] || '';
512
+ return `<div class="form-row"><label>${f.label}</label><input id="ds-cred-${f.key}" type="${f.type||'text'}" placeholder="${f.placeholder||''}" value="${String(saved).replace(/"/g,'&quot;')}"></div>`;
513
+ }).join('');
514
+
515
+ document.getElementById('ds-detail-form').innerHTML = `
516
+ <div class="form-row"><label>${t('ds.enabled')}</label><select id="ds-f-enabled"><option value="true" ${enabled?'selected':''}>Yes</option><option value="false" ${!enabled?'selected':''}>No</option></select></div>
517
+ <div class="form-row"><label>${t('ds.syncTo')}</label><input id="ds-f-syncTo" value="${ds.syncTo || ''}"></div>
518
+ <div class="form-row"><label>${t('ds.syncInterval')}</label><input id="ds-f-interval" value="${ds.syncInterval || '1h'}" placeholder="15m / 1h / 24h"></div>
519
+ <hr style="border:none;border-top:1px solid var(--border);margin:16px 0;">
520
+ <h4 style="margin-bottom:12px;font-size:14px;">${t('ds.credTitle')}</h4>
521
+ ${hasKey ? `<p style="color:var(--success);font-size:13px;margin-bottom:8px;">✓ ${t('ds.credConfigured')}</p>` : `<p style="color:var(--warning);font-size:13px;margin-bottom:8px;">⚠ ${t('ds.credNotSet')}</p>`}
522
+ ${credFields}
523
+ `;
524
+
525
+ // Load synced files
526
+ try {
527
+ const syncTo = ds.syncTo || `data/${type}`;
528
+ const files = await (await authFetch(`${API}/workspace/files/${syncTo}?recursive=true`)).json();
529
+ if (Array.isArray(files)) {
530
+ const fileItems = files.filter(f => f.type === 'file');
531
+ document.getElementById('ds-detail-files').innerHTML = fileItems.length
532
+ ? fileItems.map(f => `<div style="padding:3px 0;font-size:13px;cursor:pointer;" onclick="viewFile('${f.path.replace(/'/g,"\\'")}')">📄 ${f.path.replace(/^[^/]+\//, '')}</div>`).join('')
533
+ : `<div class="empty">${t('ds.noData')}</div>`;
534
+ } else if (files.content) {
535
+ document.getElementById('ds-detail-files').innerHTML = `<pre>${files.content}</pre>`;
536
+ } else {
537
+ document.getElementById('ds-detail-files').innerHTML = `<div class="empty">${t('ds.noData')}</div>`;
538
+ }
539
+ } catch { document.getElementById('ds-detail-files').innerHTML = `<div class="empty">${t('ds.noData')}</div>`; }
540
+ }
541
+
542
+ async function viewFile(path) {
543
+ try {
544
+ const f = await (await authFetch(`${API}/workspace/files/${path}`)).json();
545
+ if (f.content) document.getElementById('ds-detail-files').innerHTML = `<div style="margin-bottom:8px;"><a href="#" onclick="openDsDetail(currentDsType);return false;" style="color:var(--accent);font-size:13px;">← ${t('ds.backToList')}</a></div><pre style="white-space:pre-wrap;font-size:12px;">${f.content.replace(/</g,'&lt;')}</pre>`;
546
+ } catch {}
547
+ }
548
+
549
+ function closeDsDetail() {
550
+ currentDsType = null;
551
+ document.getElementById('ds-list-view').style.display = 'block';
552
+ document.getElementById('ds-detail-view').style.display = 'none';
553
+ loadDatasources();
554
+ }
555
+
556
+ async function saveDsConfig() {
557
+ if (!currentDsType) return;
558
+ const c = CONNECTORS.find(x => x.type === currentDsType);
559
+
560
+ // Save sync config
561
+ const body = JSON.stringify({ dataSources: { [currentDsType]: {
562
+ enabled: document.getElementById('ds-f-enabled').value === 'true',
563
+ syncTo: document.getElementById('ds-f-syncTo').value,
564
+ syncInterval: document.getElementById('ds-f-interval').value,
565
+ credentials: `~/.openme/credentials/${currentDsType}.json`,
566
+ }}});
567
+ await authFetch(`${API}/config`, { method: 'PATCH', body });
568
+
569
+ // Save credentials — merge with existing so empty fields don't wipe saved values
570
+ if (c?.fields) {
571
+ let existing = {};
572
+ try { const r = await authFetch(`${API}/keys/${currentDsType}`); if (r.ok) existing = await r.json(); } catch {}
573
+ const creds = { ...existing };
574
+ let changed = false;
575
+ for (const f of c.fields) {
576
+ const val = document.getElementById(`ds-cred-${f.key}`)?.value;
577
+ if (val) { creds[f.key] = val; changed = true; }
578
+ }
579
+ if (changed) {
580
+ await authFetch(`${API}/keys/${currentDsType}`, { method: 'PUT', body: JSON.stringify(creds) });
581
+ }
582
+ }
583
+ alert(t('ds.saved'));
584
+ }
585
+
586
+ async function triggerDsSync() {
587
+ if (!currentDsType) return;
588
+ await authFetch(`${API}/datasources/${currentDsType}/sync`, { method: 'POST' });
589
+ alert(t('ds.syncTriggered'));
590
+ openDsDetail(currentDsType);
591
+ }
592
+
593
+ async function removeDsSource() {
594
+ if (!currentDsType) return;
595
+ // Set to null in config to remove via merge patch
596
+ const cfg = await (await authFetch(`${API}/config`)).json();
597
+ delete cfg.dataSources[currentDsType];
598
+ await authFetch(`${API}/config`, { method: 'PATCH', body: JSON.stringify({ dataSources: cfg.dataSources }) });
599
+ alert(t('ds.removed'));
600
+ closeDsDetail();
601
+ }
602
+
603
+ // ── Settings ──
604
+ async function loadConfig() {
605
+ try {
606
+ const c = await (await authFetch(`${API}/config`)).json();
607
+ document.getElementById('config-editor').value = JSON.stringify(c, null, 2);
608
+ } catch { document.getElementById('config-editor').value = '// Failed to load config'; }
609
+ }
610
+ async function saveConfig() {
611
+ try {
612
+ const body = document.getElementById('config-editor').value;
613
+ JSON.parse(body);
614
+ await authFetch(`${API}/config`, {method:'PATCH', body});
615
+ alert(t('cfg.saved'));
616
+ } catch(e) { alert(t('cfg.invalidJson') + ': ' + e.message); }
617
+ }
618
+
619
+ // ── Init ──
620
+ const ocToken = window.__OPENME__?.ocToken || '';
621
+ const ocFrame = document.getElementById('openclaw-frame');
622
+ if (ocFrame) ocFrame.src = '/openclaw/' + (ocToken ? '?token=' + encodeURIComponent(ocToken) : '');
623
+ applyTheme();
624
+ applyI18n();
625
+ syncThemeToOpenclaw();
626
+ syncLangToOpenclaw();
627
+ loadDashboard();
628
+ setInterval(loadDashboard, 15000);
629
+ </script>
630
+ </body>
631
+ </html>