@cremini/skillpack 1.0.9 → 1.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 (40) hide show
  1. package/README.md +4 -4
  2. package/dist/cli.js +4 -0
  3. package/package.json +4 -3
  4. package/runtime/README.md +11 -1
  5. package/runtime/server/dist/adapters/markdown.js +74 -0
  6. package/runtime/server/dist/adapters/markdown.js.map +1 -0
  7. package/runtime/server/dist/adapters/slack.js +369 -0
  8. package/runtime/server/dist/adapters/slack.js.map +1 -0
  9. package/runtime/server/dist/adapters/telegram.js +199 -0
  10. package/runtime/server/dist/adapters/telegram.js.map +1 -0
  11. package/runtime/server/dist/adapters/types.js +2 -0
  12. package/runtime/server/dist/adapters/types.js.map +1 -0
  13. package/runtime/server/dist/adapters/web.js +201 -0
  14. package/runtime/server/dist/adapters/web.js.map +1 -0
  15. package/runtime/server/dist/agent.js +223 -0
  16. package/runtime/server/dist/agent.js.map +1 -0
  17. package/runtime/server/dist/config.js +73 -0
  18. package/runtime/server/dist/config.js.map +1 -0
  19. package/runtime/server/dist/index.js +146 -0
  20. package/runtime/server/dist/index.js.map +1 -0
  21. package/runtime/server/dist/lifecycle.js +85 -0
  22. package/runtime/server/dist/lifecycle.js.map +1 -0
  23. package/runtime/server/dist/memory.js +195 -0
  24. package/runtime/server/dist/memory.js.map +1 -0
  25. package/runtime/server/package-lock.json +4028 -244
  26. package/runtime/server/package.json +13 -3
  27. package/runtime/start.bat +40 -4
  28. package/runtime/start.sh +30 -2
  29. package/runtime/web/index.html +145 -18
  30. package/runtime/web/js/api-key-dialog.js +153 -0
  31. package/runtime/web/js/api.js +25 -0
  32. package/runtime/web/js/chat-apps-dialog.js +192 -0
  33. package/runtime/web/{app.js → js/chat.js} +112 -193
  34. package/runtime/web/js/config.js +16 -0
  35. package/runtime/web/js/main.js +56 -0
  36. package/runtime/web/js/settings.js +205 -0
  37. package/runtime/web/styles.css +301 -10
  38. package/runtime/server/chat-proxy.js +0 -229
  39. package/runtime/server/index.js +0 -63
  40. package/runtime/server/routes.js +0 -104
@@ -3,11 +3,21 @@
3
3
  "version": "1.0.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
- "start": "node index.js"
6
+ "build": "tsc",
7
+ "start": "node dist/index.js"
7
8
  },
8
9
  "dependencies": {
9
- "express": "^5.1.0",
10
10
  "@mariozechner/pi-coding-agent": "^0.57.1",
11
+ "@slack/bolt": "^4.6.0",
12
+ "express": "^5.1.0",
13
+ "node-telegram-bot-api": "^0.66.0",
11
14
  "ws": "^8.19.0"
15
+ },
16
+ "devDependencies": {
17
+ "@types/express": "^5.0.0",
18
+ "@types/node": "^22.0.0",
19
+ "@types/node-telegram-bot-api": "^0.64.0",
20
+ "@types/ws": "^8.18.0",
21
+ "typescript": "^5.9.3"
12
22
  }
13
- }
23
+ }
package/runtime/start.bat CHANGED
@@ -1,15 +1,51 @@
1
1
  @echo off
2
2
  cd /d "%~dp0"
3
3
 
4
+ rem Read the pack name
5
+ set "PACK_NAME=Skills Pack"
6
+ where node >nul 2>nul
7
+ if %errorlevel% equ 0 (
8
+ if exist "skillpack.json" (
9
+ for /f "delims=" %%i in ('node -e "console.log(JSON.parse(require('fs').readFileSync('skillpack.json','utf-8')).name)" 2^>nul') do set "PACK_NAME=%%i"
10
+ )
11
+ )
12
+
4
13
  echo.
5
- echo Starting Skills Pack...
14
+ echo Starting %PACK_NAME%...
6
15
  echo.
7
16
 
17
+ rem Install dependencies
8
18
  if not exist "server\node_modules" (
9
19
  echo Installing dependencies...
10
- cd server && npm ci --omit=dev && cd ..
20
+ cd server && npm install --omit=dev && cd ..
11
21
  echo.
12
22
  )
13
23
 
14
- rem Start the server (port detection and browser launch are handled by server\index.js)
15
- cd server && node index.js
24
+ rem First-run flag
25
+ set "FIRST_RUN=1"
26
+
27
+ :loop
28
+ set "SKILLPACK_FIRST_RUN=%FIRST_RUN%"
29
+ set "PACK_ROOT=%~dp0"
30
+ set "NODE_ENV=production"
31
+ node server\dist\index.js
32
+ set "EXIT_CODE=%errorlevel%"
33
+
34
+ set "FIRST_RUN=0"
35
+
36
+ rem Only restart on exit code 75 (/restart command)
37
+ if %EXIT_CODE% equ 75 (
38
+ echo.
39
+ echo Restarting...
40
+ timeout /t 1 /nobreak >nul
41
+ goto loop
42
+ )
43
+
44
+ rem All other exit codes → stop
45
+ if %EXIT_CODE% equ 64 (
46
+ echo.
47
+ echo Shutdown complete.
48
+ ) else if %EXIT_CODE% neq 0 (
49
+ echo.
50
+ echo Process exited with code %EXIT_CODE%.
51
+ )
package/runtime/start.sh CHANGED
@@ -18,5 +18,33 @@ if [ ! -d "server/node_modules" ]; then
18
18
  echo ""
19
19
  fi
20
20
 
21
- # Start the server
22
- cd server && node index.js
21
+ # First-run flag (controls browser auto-open on first launch only)
22
+ FIRST_RUN=1
23
+
24
+ while true; do
25
+ SKILLPACK_FIRST_RUN="$FIRST_RUN" \
26
+ PACK_ROOT="$(pwd)" \
27
+ NODE_ENV="production" \
28
+ node server/dist/index.js
29
+ EXIT_CODE=$?
30
+
31
+ FIRST_RUN=0
32
+
33
+ # Only restart on exit code 75 (/restart command)
34
+ if [ "$EXIT_CODE" -eq 75 ]; then
35
+ echo ""
36
+ echo " Restarting..."
37
+ sleep 1
38
+ continue
39
+ fi
40
+
41
+ # All other exit codes (0, 64, crash, Ctrl+C, kill, etc.) → stop
42
+ if [ "$EXIT_CODE" -eq 64 ]; then
43
+ echo ""
44
+ echo " Shutdown complete."
45
+ elif [ "$EXIT_CODE" -ne 0 ]; then
46
+ echo ""
47
+ echo " Process exited with code $EXIT_CODE."
48
+ fi
49
+ break
50
+ done
@@ -19,26 +19,31 @@
19
19
  <p id="pack-desc">Loading...</p>
20
20
  </div>
21
21
 
22
- <div class="sidebar-section">
22
+ <div class="sidebar-actions-group">
23
+ <button id="open-chatapps-btn" class="action-btn chatapps-btn">
24
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="action-btn-icon">
25
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
26
+ </svg>
27
+ <span class="action-btn-label">Connect Chat Apps</span>
28
+ </button>
29
+ <button id="open-apikey-btn" class="action-btn apikey-btn">
30
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="action-btn-icon">
31
+ <path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"></path>
32
+ </svg>
33
+ <span class="action-btn-label">Provide Model API Key</span>
34
+ </button>
35
+ </div>
36
+
37
+ <div class="sidebar-skills-section">
23
38
  <h3>Skills</h3>
24
39
  <ul id="skills-list"></ul>
25
40
  </div>
26
41
 
27
- <div class="sidebar-section">
28
- <h3>API KEYS</h3>
29
- <div class="provider-select-wrapper">
30
- <select id="provider-select">
31
- <option value="openai">OpenAI</option>
32
- <option value="anthropic">Anthropic</option>
33
- </select>
34
- </div>
35
- <div class="api-key-form">
36
- <input type="password" id="api-key-input" placeholder="sk-..." />
37
- </div>
38
- <div class="api-key-footer">
39
- <p id="key-status" class="status-text"></p>
40
- <button id="save-key-btn">Save</button>
41
- </div>
42
+ <div class="sidebar-settings-section">
43
+ <button id="open-settings-btn" class="settings-trigger-btn">
44
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="btn-icon"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>
45
+ Settings
46
+ </button>
42
47
  </div>
43
48
  </aside>
44
49
 
@@ -65,8 +70,130 @@
65
70
  </div>
66
71
  </main>
67
72
  </div>
73
+
74
+ <!-- Settings Dialog -->
75
+ <dialog id="settings-dialog" class="settings-modal">
76
+ <div class="settings-modal-header">
77
+ <h2>Settings</h2>
78
+ <button id="close-settings-btn" class="close-btn" aria-label="Close">
79
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
80
+ </button>
81
+ </div>
82
+ <div class="settings-modal-body">
83
+ <div class="settings-sections">
84
+ <!-- General Section -->
85
+ <div class="settings-section">
86
+ <h3 class="section-title">General</h3>
87
+ <div class="form-group">
88
+ <label>Provider</label>
89
+ <div class="provider-select-wrapper">
90
+ <select id="provider-select">
91
+ <option value="openai">OpenAI</option>
92
+ <option value="anthropic">Anthropic</option>
93
+ </select>
94
+ </div>
95
+ </div>
96
+ <div class="form-group">
97
+ <label>API Key</label>
98
+ <input type="password" id="api-key-input" placeholder="sk-..." class="form-input" />
99
+ <p id="key-status" class="status-text" style="margin-top: 8px; min-height: 18px;"></p>
100
+ </div>
101
+ </div>
102
+
103
+ <!-- IM Bots Section -->
104
+ <div class="settings-section">
105
+ <h3 class="section-title">IM Bots</h3>
106
+ <div class="form-group">
107
+ <label>Telegram Bot Token</label>
108
+ <input type="password" id="telegram-token-input" placeholder="123456:ABC-DEF..." class="form-input" />
109
+ </div>
110
+ <div class="form-group">
111
+ <label>Slack Bot Token</label>
112
+ <input type="password" id="slack-bot-token-input" placeholder="xoxb-..." class="form-input" />
113
+ </div>
114
+ <div class="form-group">
115
+ <label>Slack App Token</label>
116
+ <input type="password" id="slack-app-token-input" placeholder="xapp-..." class="form-input" />
117
+ </div>
118
+ </div>
119
+ </div>
120
+ </div>
121
+ <div class="settings-modal-footer">
122
+ <button id="restart-service-btn" class="secondary-btn" hidden>Restart Service</button>
123
+ <button id="save-settings-btn" class="primary-btn">Save Settings</button>
124
+ </div>
125
+ </dialog>
126
+
127
+ <!-- API Key Dialog -->
128
+ <dialog id="apikey-dialog" class="settings-modal compact-modal">
129
+ <div class="settings-modal-header">
130
+ <h2>Model API Key</h2>
131
+ <button id="close-apikey-btn" class="close-btn" aria-label="Close">
132
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
133
+ </button>
134
+ </div>
135
+ <div class="settings-modal-body">
136
+ <div class="settings-sections">
137
+ <div class="form-group">
138
+ <label>Provider</label>
139
+ <div class="provider-select-wrapper">
140
+ <select id="apikey-provider-select">
141
+ <option value="openai">OpenAI</option>
142
+ <option value="anthropic">Anthropic</option>
143
+ </select>
144
+ </div>
145
+ </div>
146
+ <div class="form-group">
147
+ <label>API Key</label>
148
+ <input type="password" id="apikey-input" placeholder="sk-..." class="form-input" />
149
+ <p id="apikey-status" class="status-text" style="margin-top: 8px; min-height: 18px;"></p>
150
+ </div>
151
+ </div>
152
+ </div>
153
+ <div class="settings-modal-footer">
154
+ <button id="save-apikey-btn" class="primary-btn">Save API Key</button>
155
+ </div>
156
+ </dialog>
157
+
158
+ <!-- Chat Apps Dialog -->
159
+ <dialog id="chatapps-dialog" class="settings-modal">
160
+ <div class="settings-modal-header">
161
+ <h2>Connect Chat Apps</h2>
162
+ <button id="close-chatapps-btn" class="close-btn" aria-label="Close">
163
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
164
+ </button>
165
+ </div>
166
+ <div class="settings-modal-body">
167
+ <div class="settings-sections">
168
+ <div class="settings-section">
169
+ <h3 class="section-title">Telegram</h3>
170
+ <div class="form-group">
171
+ <label>Bot Token</label>
172
+ <input type="password" id="chatapps-telegram-token" placeholder="123456:ABC-DEF..." class="form-input" />
173
+ </div>
174
+ </div>
175
+ <div class="settings-section">
176
+ <h3 class="section-title">Slack</h3>
177
+ <div class="form-group">
178
+ <label>Bot Token</label>
179
+ <input type="password" id="chatapps-slack-bot-token" placeholder="xoxb-..." class="form-input" />
180
+ </div>
181
+ <div class="form-group">
182
+ <label>App Token</label>
183
+ <input type="password" id="chatapps-slack-app-token" placeholder="xapp-..." class="form-input" />
184
+ </div>
185
+ </div>
186
+ </div>
187
+ </div>
188
+ <div class="settings-modal-footer">
189
+ <p id="chatapps-status" class="status-text"></p>
190
+ <button id="restart-chatapps-btn" class="secondary-btn" hidden>Restart Service</button>
191
+ <button id="save-chatapps-btn" class="primary-btn">Save & Connect</button>
192
+ </div>
193
+ </dialog>
194
+
68
195
  <script src="marked.min.js"></script>
69
- <script src="app.js"></script>
196
+ <script type="module" src="js/main.js"></script>
70
197
  </body>
71
198
 
72
- </html>
199
+ </html>
@@ -0,0 +1,153 @@
1
+ /**
2
+ * API Key Dialog Module
3
+ *
4
+ * 负责 Model API Key 的配置管理。
5
+ * 独立的 Dialog,从原 SettingDialog 的 API Key 部分拆分出来。
6
+ */
7
+ import { state } from "./config.js";
8
+ import { saveConfigData } from "./api.js";
9
+
10
+ // --- DOM Elements ---
11
+ let dialog;
12
+ let openBtn;
13
+ let closeBtn;
14
+ let saveBtn;
15
+ let providerSelect;
16
+ let apiKeyInput;
17
+ let statusEl;
18
+
19
+ // --- Public API ---
20
+
21
+ export function initApiKeyDialog() {
22
+ dialog = document.getElementById("apikey-dialog");
23
+ openBtn = document.getElementById("open-apikey-btn");
24
+ closeBtn = document.getElementById("close-apikey-btn");
25
+ saveBtn = document.getElementById("save-apikey-btn");
26
+ providerSelect = document.getElementById("apikey-provider-select");
27
+ apiKeyInput = document.getElementById("apikey-input");
28
+ statusEl = document.getElementById("apikey-status");
29
+
30
+ if (!dialog) return;
31
+
32
+ if (openBtn) {
33
+ openBtn.addEventListener("click", open);
34
+ }
35
+ if (closeBtn) {
36
+ closeBtn.addEventListener("click", close);
37
+ }
38
+ if (saveBtn) {
39
+ saveBtn.addEventListener("click", handleSave);
40
+ }
41
+ if (providerSelect) {
42
+ providerSelect.addEventListener("change", updatePlaceholder);
43
+ }
44
+ }
45
+
46
+ /**
47
+ * 根据当前连接状态更新按钮外观
48
+ */
49
+ export function updateApiKeyButton() {
50
+ if (!openBtn) return;
51
+ const config = state.config;
52
+ if (config && config.hasApiKey) {
53
+ openBtn.classList.add("connected");
54
+ openBtn.querySelector(".action-btn-label").textContent = "API Key Configured";
55
+ } else {
56
+ openBtn.classList.remove("connected");
57
+ openBtn.querySelector(".action-btn-label").textContent = "Provide Model API Key";
58
+ }
59
+ }
60
+
61
+ // --- Internal Helpers ---
62
+
63
+ function open() {
64
+ populateForm();
65
+ dialog.showModal();
66
+ }
67
+
68
+ function close() {
69
+ dialog.close();
70
+ setStatus("", "");
71
+ }
72
+
73
+ function populateForm() {
74
+ const config = state.config;
75
+ if (!config) return;
76
+
77
+ // Provider
78
+ if (config.provider && providerSelect) {
79
+ providerSelect.value = config.provider;
80
+ }
81
+ updatePlaceholder();
82
+
83
+ setStatus("", "");
84
+
85
+ if (config.hasApiKey && config.apiKey) {
86
+ apiKeyInput.value = config.apiKey;
87
+ } else if (config.hasApiKey) {
88
+ apiKeyInput.value = "***************************************************";
89
+ } else {
90
+ apiKeyInput.value = "";
91
+ }
92
+ }
93
+
94
+ function updatePlaceholder() {
95
+ if (!providerSelect || !apiKeyInput) return;
96
+ const p = providerSelect.value;
97
+ if (p === "openai") apiKeyInput.placeholder = "sk-proj-...";
98
+ else if (p === "anthropic") apiKeyInput.placeholder = "sk-ant-api03-...";
99
+ else apiKeyInput.placeholder = "sk-...";
100
+ }
101
+
102
+ async function handleSave() {
103
+ const key = apiKeyInput.value.trim();
104
+ const provider = providerSelect.value;
105
+
106
+ if (!key) {
107
+ setStatus("Please enter an API key", "error");
108
+ return;
109
+ }
110
+
111
+ const updates = { provider };
112
+ if (key !== "***************************************************" && key !== state.config.apiKey) {
113
+ updates.key = key;
114
+ }
115
+
116
+ try {
117
+ saveBtn.disabled = true;
118
+ const res = await saveConfigData(updates);
119
+
120
+ state.config.provider = res.provider;
121
+ if (updates.key) {
122
+ state.config.hasApiKey = true;
123
+ state.config.apiKey = updates.key;
124
+ }
125
+
126
+ if (state.config.hasApiKey && state.config.apiKey) {
127
+ apiKeyInput.value = state.config.apiKey;
128
+ } else if (state.config.hasApiKey) {
129
+ apiKeyInput.value = "***************************************************";
130
+ } else {
131
+ apiKeyInput.value = "";
132
+ }
133
+
134
+ state.config.runtimeControl = res.runtimeControl;
135
+ state.restartRequired = !!res.requiresRestart;
136
+
137
+ setStatus("API key saved successfully", "success");
138
+ updateApiKeyButton();
139
+
140
+ // 延迟关闭让用户看到成功消息
141
+ setTimeout(() => close(), 1200);
142
+ } catch (err) {
143
+ setStatus("Save failed: " + err.message, "error");
144
+ } finally {
145
+ saveBtn.disabled = false;
146
+ }
147
+ }
148
+
149
+ function setStatus(message, status) {
150
+ if (!statusEl) return;
151
+ statusEl.textContent = message;
152
+ statusEl.className = status ? `status-text ${status}` : "status-text";
153
+ }
@@ -0,0 +1,25 @@
1
+ import { state } from "./config.js";
2
+
3
+ export async function saveConfigData(updates) {
4
+ const res = await fetch(state.API_BASE + "/api/config/update", {
5
+ method: "POST",
6
+ headers: { "Content-Type": "application/json" },
7
+ body: JSON.stringify(updates),
8
+ });
9
+ if (!res.ok) {
10
+ throw new Error("Save Config Failed");
11
+ }
12
+ return await res.json();
13
+ }
14
+
15
+ export async function restartRuntime() {
16
+ const res = await fetch(state.API_BASE + "/api/runtime/restart", {
17
+ method: "POST",
18
+ headers: { "Content-Type": "application/json" },
19
+ });
20
+ const payload = await res.json();
21
+ if (!res.ok) {
22
+ throw new Error(payload.message || "Restart Failed");
23
+ }
24
+ return payload;
25
+ }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Chat Apps (IM Bots) Dialog Module
3
+ *
4
+ * 负责 IM Bots(Telegram / Slack)Token 的配置管理。
5
+ * 独立的 Dialog,从原 SettingDialog 的 IM Bots 部分拆分出来。
6
+ */
7
+ import { state } from "./config.js";
8
+ import { saveConfigData, restartRuntime } from "./api.js";
9
+
10
+ // --- DOM Elements ---
11
+ let dialog;
12
+ let openBtn;
13
+ let closeBtn;
14
+ let saveBtn;
15
+ let restartBtn;
16
+ let telegramTokenInput;
17
+ let slackBotTokenInput;
18
+ let slackAppTokenInput;
19
+ let statusEl;
20
+
21
+ // --- Public API ---
22
+
23
+ export function initChatAppsDialog() {
24
+ dialog = document.getElementById("chatapps-dialog");
25
+ openBtn = document.getElementById("open-chatapps-btn");
26
+ closeBtn = document.getElementById("close-chatapps-btn");
27
+ saveBtn = document.getElementById("save-chatapps-btn");
28
+ restartBtn = document.getElementById("restart-chatapps-btn");
29
+ telegramTokenInput = document.getElementById("chatapps-telegram-token");
30
+ slackBotTokenInput = document.getElementById("chatapps-slack-bot-token");
31
+ slackAppTokenInput = document.getElementById("chatapps-slack-app-token");
32
+ statusEl = document.getElementById("chatapps-status");
33
+
34
+ if (!dialog) return;
35
+
36
+ if (openBtn) {
37
+ openBtn.addEventListener("click", open);
38
+ }
39
+ if (closeBtn) {
40
+ closeBtn.addEventListener("click", close);
41
+ }
42
+ if (saveBtn) {
43
+ saveBtn.addEventListener("click", handleSave);
44
+ }
45
+ if (restartBtn) {
46
+ restartBtn.addEventListener("click", handleRestart);
47
+ }
48
+ }
49
+
50
+ /**
51
+ * 根据当前连接状态更新按钮外观
52
+ */
53
+ export function updateChatAppsButton() {
54
+ if (!openBtn) return;
55
+ const config = state.config;
56
+ const adapters = config?.adapters || {};
57
+ const hasAnyToken =
58
+ (adapters.telegram && adapters.telegram.token) ||
59
+ (adapters.slack && (adapters.slack.botToken || adapters.slack.appToken));
60
+
61
+ if (hasAnyToken) {
62
+ openBtn.classList.add("connected");
63
+ openBtn.querySelector(".action-btn-label").textContent = "Connected to Chat Apps";
64
+ } else {
65
+ openBtn.classList.remove("connected");
66
+ openBtn.querySelector(".action-btn-label").textContent = "Connect Chat Apps";
67
+ }
68
+ }
69
+
70
+ // --- Internal Helpers ---
71
+
72
+ function open() {
73
+ populateForm();
74
+ dialog.showModal();
75
+ }
76
+
77
+ function close() {
78
+ dialog.close();
79
+ setStatus("", "");
80
+ }
81
+
82
+ function populateForm() {
83
+ const config = state.config;
84
+ if (!config) return;
85
+
86
+ const adapters = config.adapters || {};
87
+
88
+ if (adapters.telegram && adapters.telegram.token) {
89
+ telegramTokenInput.value = adapters.telegram.token;
90
+ } else {
91
+ telegramTokenInput.value = "";
92
+ }
93
+
94
+ if (adapters.slack) {
95
+ slackBotTokenInput.value = adapters.slack.botToken || "";
96
+ slackAppTokenInput.value = adapters.slack.appToken || "";
97
+ } else {
98
+ slackBotTokenInput.value = "";
99
+ slackAppTokenInput.value = "";
100
+ }
101
+
102
+ // Restart required status
103
+ if (state.restartRequired) {
104
+ const canRestart = config.runtimeControl?.canManagedRestart;
105
+ setStatus(
106
+ canRestart
107
+ ? "Settings changed. Restart service to apply."
108
+ : "Settings changed. Restart the service manually to apply.",
109
+ "warning",
110
+ );
111
+ updateRestartButton(canRestart);
112
+ } else {
113
+ setStatus("", "");
114
+ updateRestartButton(false);
115
+ }
116
+ }
117
+
118
+ async function handleSave() {
119
+ const telegramToken = telegramTokenInput.value.trim();
120
+ const slackBotToken = slackBotTokenInput.value.trim();
121
+ const slackAppToken = slackAppTokenInput.value.trim();
122
+
123
+ const adapters = {};
124
+ if (telegramToken) adapters.telegram = { token: telegramToken };
125
+ if (slackBotToken || slackAppToken) {
126
+ adapters.slack = {
127
+ botToken: slackBotToken || undefined,
128
+ appToken: slackAppToken || undefined,
129
+ };
130
+ }
131
+
132
+ const updates = { adapters };
133
+
134
+ try {
135
+ saveBtn.disabled = true;
136
+ const res = await saveConfigData(updates);
137
+
138
+ state.config.adapters = res.adapters;
139
+ state.config.runtimeControl = res.runtimeControl;
140
+ state.restartRequired = !!res.requiresRestart;
141
+
142
+ if (res.requiresRestart) {
143
+ setStatus(
144
+ res.runtimeControl?.canManagedRestart
145
+ ? "Settings saved. Restart service to apply changes."
146
+ : "Settings saved. Restart the service manually to apply changes.",
147
+ "warning",
148
+ );
149
+ updateRestartButton(!!res.runtimeControl?.canManagedRestart);
150
+ } else {
151
+ setStatus("Settings saved", "success");
152
+ updateRestartButton(false);
153
+ }
154
+
155
+ updateChatAppsButton();
156
+ } catch (err) {
157
+ setStatus("Save failed: " + err.message, "error");
158
+ } finally {
159
+ saveBtn.disabled = false;
160
+ }
161
+ }
162
+
163
+ async function handleRestart() {
164
+ if (!restartBtn) return;
165
+
166
+ restartBtn.disabled = true;
167
+ if (saveBtn) saveBtn.disabled = true;
168
+ setStatus("Restarting service...", "warning");
169
+
170
+ try {
171
+ await restartRuntime();
172
+ setTimeout(() => {
173
+ window.location.reload();
174
+ }, 6000);
175
+ } catch (err) {
176
+ if (saveBtn) saveBtn.disabled = false;
177
+ restartBtn.disabled = false;
178
+ setStatus("Restart failed: " + err.message, "error");
179
+ }
180
+ }
181
+
182
+ function updateRestartButton(show) {
183
+ if (!restartBtn) return;
184
+ restartBtn.hidden = !show;
185
+ restartBtn.disabled = false;
186
+ }
187
+
188
+ function setStatus(message, status) {
189
+ if (!statusEl) return;
190
+ statusEl.textContent = message;
191
+ statusEl.className = status ? `status-text ${status}` : "status-text";
192
+ }