@syengup/friday-channel-next 0.1.30 → 0.1.37
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/README.md +8 -4
- package/dist/index.js +1 -1
- package/dist/src/agent/abort-run.d.ts +12 -1
- package/dist/src/agent/abort-run.js +24 -9
- package/dist/src/agent/dispatch-bridge.d.ts +1 -1
- package/dist/src/agent/media-bridge.d.ts +8 -1
- package/dist/src/agent/media-bridge.js +23 -2
- package/dist/src/agent/node-pairing-bridge.d.ts +11 -8
- package/dist/src/agent/node-pairing-bridge.js +6 -2
- package/dist/src/agent/subagent-registry.js +0 -3
- package/dist/src/agent-forward-runtime.d.ts +15 -0
- package/dist/src/agent-forward-runtime.js +2 -0
- package/dist/src/agent-id.d.ts +8 -0
- package/dist/src/agent-id.js +21 -0
- package/dist/src/channel-actions.js +48 -15
- package/dist/src/channel.js +22 -3
- package/dist/src/collect-message-media-paths.js +10 -1
- package/dist/src/friday-session.js +34 -10
- package/dist/src/history/normalize-message.js +22 -8
- package/dist/src/http/handlers/agent-config.d.ts +27 -0
- package/dist/src/http/handlers/agent-config.js +188 -0
- package/dist/src/http/handlers/agent-files.d.ts +21 -0
- package/dist/src/http/handlers/agent-files.js +137 -0
- package/dist/src/http/handlers/agent-tools-catalog.d.ts +10 -0
- package/dist/src/http/handlers/agent-tools-catalog.js +33 -0
- package/dist/src/http/handlers/agents-list.js +1 -19
- package/dist/src/http/handlers/cancel.js +14 -6
- package/dist/src/http/handlers/device-approve.js +3 -1
- package/dist/src/http/handlers/files-download.js +6 -8
- package/dist/src/http/handlers/files.d.ts +16 -0
- package/dist/src/http/handlers/files.js +81 -13
- package/dist/src/http/handlers/health.js +18 -4
- package/dist/src/http/handlers/history-messages.js +1 -1
- package/dist/src/http/handlers/history-sessions.js +5 -3
- package/dist/src/http/handlers/messages.js +33 -14
- package/dist/src/http/handlers/models-list.d.ts +5 -0
- package/dist/src/http/handlers/models-list.js +9 -1
- package/dist/src/http/handlers/nodes-approve.js +1 -6
- package/dist/src/http/handlers/plugin-info.js +1 -1
- package/dist/src/http/handlers/sessions-settings.js +15 -10
- package/dist/src/http/server.js +27 -2
- package/dist/src/link-preview/og-parse.js +3 -1
- package/dist/src/link-preview/ssrf-guard.js +6 -2
- package/dist/src/media-fetch.js +4 -1
- package/dist/src/plugin-install-info.js +4 -1
- package/dist/src/session/session-manager.js +9 -3
- package/dist/src/session-usage-store.js +3 -1
- package/dist/src/skills-discovery.d.ts +59 -0
- package/dist/src/skills-discovery.js +252 -0
- package/dist/src/sse/offline-queue.js +4 -1
- package/dist/src/thinking-levels.d.ts +21 -0
- package/dist/src/thinking-levels.js +48 -0
- package/dist/src/tool-catalog.d.ts +53 -0
- package/dist/src/tool-catalog.js +191 -0
- package/dist/src/upgrade-runtime.d.ts +1 -1
- package/dist/src/version.js +4 -2
- package/index.ts +43 -35
- package/install.js +131 -43
- package/package.json +10 -1
- package/src/agent/abort-run.ts +23 -8
- package/src/agent/dispatch-bridge.ts +2 -1
- package/src/agent/media-bridge.test.ts +71 -0
- package/src/agent/media-bridge.ts +30 -1
- package/src/agent/node-pairing-bridge.ts +29 -15
- package/src/agent/run-usage-accumulator.ts +4 -2
- package/src/agent/subagent-registry.ts +0 -4
- package/src/agent-forward-runtime.ts +11 -0
- package/src/agent-id.ts +24 -0
- package/src/agent-run-context-bridge.ts +3 -1
- package/src/channel-actions.test.ts +57 -4
- package/src/channel-actions.ts +41 -15
- package/src/channel.lifecycle.test.ts +41 -0
- package/src/channel.outbound.test.ts +18 -4
- package/src/channel.ts +140 -120
- package/src/collect-message-media-paths.ts +15 -6
- package/src/config.ts +1 -4
- package/src/e2e/agents-list.e2e.test.ts +9 -2
- package/src/e2e/attachments-inbound.e2e.test.ts +5 -1
- package/src/e2e/attachments-outbound.e2e.test.ts +7 -2
- package/src/e2e/auto-approve.integration.test.ts +13 -7
- package/src/e2e/cancel-reconnect-errors.e2e.test.ts +18 -3
- package/src/e2e/connect-and-connected.e2e.test.ts +5 -1
- package/src/e2e/offline-replay.e2e.test.ts +17 -3
- package/src/e2e/send-text.e2e.test.ts +11 -2
- package/src/e2e/slash-commands.e2e.test.ts +5 -1
- package/src/e2e/status-cors-auth.e2e.test.ts +11 -2
- package/src/e2e/subagent-smoke.e2e.test.ts +68 -28
- package/src/e2e/subagent.e2e.test.ts +136 -53
- package/src/e2e/tool-lifecycle.e2e.test.ts +5 -1
- package/src/friday-session.forward-agent.test.ts +44 -12
- package/src/friday-session.ts +44 -20
- package/src/history/normalize-message.test.ts +35 -8
- package/src/history/normalize-message.ts +24 -12
- package/src/history/read-transcript.ts +1 -4
- package/src/http/handlers/agent-config.test.ts +212 -0
- package/src/http/handlers/agent-config.ts +232 -0
- package/src/http/handlers/agent-files.test.ts +136 -0
- package/src/http/handlers/agent-files.ts +149 -0
- package/src/http/handlers/agent-tools-catalog.ts +42 -0
- package/src/http/handlers/agents-list.test.ts +1 -5
- package/src/http/handlers/agents-list.ts +1 -22
- package/src/http/handlers/cancel.test.ts +23 -4
- package/src/http/handlers/cancel.ts +14 -6
- package/src/http/handlers/device-approve.test.ts +12 -3
- package/src/http/handlers/device-approve.ts +33 -21
- package/src/http/handlers/files-download.ts +17 -13
- package/src/http/handlers/files.test.ts +120 -0
- package/src/http/handlers/files.ts +115 -17
- package/src/http/handlers/health.test.ts +43 -11
- package/src/http/handlers/health.ts +22 -6
- package/src/http/handlers/history-messages.test.ts +51 -9
- package/src/http/handlers/history-messages.ts +4 -1
- package/src/http/handlers/history-sessions.test.ts +46 -9
- package/src/http/handlers/history-sessions.ts +5 -3
- package/src/http/handlers/history-set-title.test.ts +14 -5
- package/src/http/handlers/link-preview.test.ts +57 -16
- package/src/http/handlers/link-preview.ts +4 -1
- package/src/http/handlers/messages.test.ts +12 -8
- package/src/http/handlers/messages.ts +64 -21
- package/src/http/handlers/models-list.test.ts +114 -0
- package/src/http/handlers/models-list.ts +26 -8
- package/src/http/handlers/nodes-approve.test.ts +15 -4
- package/src/http/handlers/nodes-approve.ts +38 -40
- package/src/http/handlers/plugin-info.ts +5 -6
- package/src/http/handlers/plugin-upgrade.ts +4 -1
- package/src/http/handlers/sessions-settings.ts +16 -11
- package/src/http/handlers/sse.ts +3 -1
- package/src/http/server.ts +33 -6
- package/src/link-preview/og-parse.test.ts +6 -2
- package/src/link-preview/og-parse.ts +10 -3
- package/src/link-preview/preview-service.ts +4 -1
- package/src/link-preview/ssrf-guard.test.ts +78 -16
- package/src/link-preview/ssrf-guard.ts +7 -2
- package/src/media-fetch.test.ts +8 -3
- package/src/media-fetch.ts +5 -3
- package/src/openclaw.d.ts +41 -10
- package/src/plugin-install-info.ts +20 -9
- package/src/run-metadata.ts +2 -1
- package/src/session/session-manager.ts +19 -11
- package/src/session-usage-snapshot.ts +3 -1
- package/src/session-usage-store.ts +3 -1
- package/src/skills-discovery.test.ts +152 -0
- package/src/skills-discovery.ts +264 -0
- package/src/sse/emitter.test.ts +1 -1
- package/src/sse/emitter.ts +9 -3
- package/src/sse/offline-queue.ts +17 -8
- package/src/test-support/app-simulator.ts +17 -3
- package/src/test-support/mock-dispatch.ts +17 -4
- package/src/thinking-levels.test.ts +143 -0
- package/src/thinking-levels.ts +70 -0
- package/src/tool-catalog.ts +261 -0
- package/src/upgrade-runtime.ts +4 -2
- package/src/version.ts +6 -2
- package/tsconfig.json +1 -1
package/index.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
|
|
2
1
|
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/core";
|
|
3
2
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry";
|
|
4
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
PluginHookBeforeToolCallEvent,
|
|
5
|
+
PluginHookAfterToolCallEvent,
|
|
6
|
+
PluginHookToolContext,
|
|
7
|
+
} from "openclaw/plugin-sdk/plugins/types";
|
|
5
8
|
import { fridayNextChannelPlugin } from "./src/channel.js";
|
|
6
9
|
import { setFridayNextRuntime } from "./src/runtime.js";
|
|
7
10
|
import { resolveFridayNextConfig } from "./src/config.js";
|
|
@@ -44,7 +47,7 @@ function deviceIdFromToolContext(ctx: PluginHookToolContext): string | null {
|
|
|
44
47
|
const sk =
|
|
45
48
|
typeof ctx.sessionKey === "string" && ctx.sessionKey.trim()
|
|
46
49
|
? ctx.sessionKey.trim()
|
|
47
|
-
: (ctx.runId ? getOpenClawAgentRunContext(ctx.runId)?.sessionKey?.trim() : undefined) ?? "";
|
|
50
|
+
: ((ctx.runId ? getOpenClawAgentRunContext(ctx.runId)?.sessionKey?.trim() : undefined) ?? "");
|
|
48
51
|
if (sk) {
|
|
49
52
|
const d = resolveFridayDeviceIdForSessionKey(sk);
|
|
50
53
|
if (d) return d;
|
|
@@ -83,7 +86,7 @@ export default defineChannelPluginEntry({
|
|
|
83
86
|
id: "friday-next",
|
|
84
87
|
name: "Friday Next",
|
|
85
88
|
description: "Friday Next Apple 应用通道",
|
|
86
|
-
plugin: fridayNextChannelPlugin
|
|
89
|
+
plugin: fridayNextChannelPlugin,
|
|
87
90
|
setRuntime: setFridayNextRuntime,
|
|
88
91
|
registerFull: (api: OpenClawPluginApi) => {
|
|
89
92
|
setFridayAgentForwardRuntime(api);
|
|
@@ -93,7 +96,9 @@ export default defineChannelPluginEntry({
|
|
|
93
96
|
lastApiRoutesRegistered = new WeakRef(api);
|
|
94
97
|
registerFridayNextHttpRoutes(api);
|
|
95
98
|
} else {
|
|
96
|
-
const cfg = resolveFridayNextConfig(
|
|
99
|
+
const cfg = resolveFridayNextConfig(
|
|
100
|
+
getHostOpenClawConfigSnapshot(getFridayNextRuntime().config),
|
|
101
|
+
);
|
|
97
102
|
sseEmitter.setBacklogLimit(cfg.sseBacklogPerDevice);
|
|
98
103
|
}
|
|
99
104
|
|
|
@@ -147,36 +152,39 @@ export default defineChannelPluginEntry({
|
|
|
147
152
|
};
|
|
148
153
|
});
|
|
149
154
|
|
|
150
|
-
api.on(
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
155
|
+
api.on(
|
|
156
|
+
"before_tool_call",
|
|
157
|
+
(event: PluginHookBeforeToolCallEvent, ctx: PluginHookToolContext) => {
|
|
158
|
+
if (!shouldForwardToolEventToFriday(ctx)) return;
|
|
159
|
+
const deviceId = deviceIdFromToolContext(ctx);
|
|
160
|
+
const runId = ctx.runId ?? "(unknown)";
|
|
161
|
+
|
|
162
|
+
const logLine = (detail: string) => {
|
|
163
|
+
hookLogger.debug(
|
|
164
|
+
`[TOOL_CALL] toolName=${event.toolName} runId=${runId} deviceId=${deviceId ?? "(unknown)"} detail=${detail}`,
|
|
165
|
+
);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
if (!deviceId) {
|
|
169
|
+
logLine("SKIP_no_deviceId");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
logLine("START");
|
|
174
|
+
sseEmitter.broadcastToolEvent(deviceId.toUpperCase(), runId, {
|
|
175
|
+
type: "tool-hook",
|
|
176
|
+
data: {
|
|
177
|
+
when: "before",
|
|
178
|
+
runId,
|
|
179
|
+
deviceId: deviceId.toUpperCase(),
|
|
180
|
+
sessionKey: ctx.sessionKey,
|
|
181
|
+
toolName: event.toolName,
|
|
182
|
+
params: event.params,
|
|
183
|
+
ts: Date.now(),
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
},
|
|
187
|
+
);
|
|
180
188
|
|
|
181
189
|
api.on("after_tool_call", (event: PluginHookAfterToolCallEvent, ctx: PluginHookToolContext) => {
|
|
182
190
|
if (!shouldForwardToolEventToFriday(ctx)) return;
|
package/install.js
CHANGED
|
@@ -14,11 +14,7 @@ function realHome() {
|
|
|
14
14
|
const h = execSync(`sh -c 'echo ~${sudoUser}'`, { encoding: "utf8" }).trim();
|
|
15
15
|
if (h && !h.startsWith("~") && existsSync(h)) return h;
|
|
16
16
|
} catch {}
|
|
17
|
-
for (const g of [
|
|
18
|
-
`/home/${sudoUser}`,
|
|
19
|
-
`/Users/${sudoUser}`,
|
|
20
|
-
`C:\\Users\\${sudoUser}`,
|
|
21
|
-
]) {
|
|
17
|
+
for (const g of [`/home/${sudoUser}`, `/Users/${sudoUser}`, `C:\\Users\\${sudoUser}`]) {
|
|
22
18
|
if (existsSync(g)) return g;
|
|
23
19
|
}
|
|
24
20
|
return current;
|
|
@@ -30,13 +26,23 @@ const OPENCLAW_CONFIG = join(USER_HOME, ".openclaw", "openclaw.json");
|
|
|
30
26
|
const G = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
31
27
|
const Y = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
32
28
|
const R = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
33
|
-
function log(msg) {
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
function log(msg) {
|
|
30
|
+
console.log(` ${msg}`);
|
|
31
|
+
}
|
|
32
|
+
function warn(msg) {
|
|
33
|
+
console.log(` ${Y("!")} ${msg}`);
|
|
34
|
+
}
|
|
35
|
+
function err(msg) {
|
|
36
|
+
console.error(` ${R("X")} ${msg}`);
|
|
37
|
+
}
|
|
36
38
|
|
|
37
39
|
function has(cmd) {
|
|
38
|
-
try {
|
|
39
|
-
|
|
40
|
+
try {
|
|
41
|
+
execSync(`${cmd} --version`, { stdio: "ignore" });
|
|
42
|
+
return true;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
let openclawCmd = "openclaw";
|
|
@@ -79,7 +85,10 @@ if (!hasOpenclaw()) {
|
|
|
79
85
|
let tooOld = false;
|
|
80
86
|
for (let i = 0; i < 3; i++) {
|
|
81
87
|
if (cur[i] > MIN_OPENCLAW[i]) break;
|
|
82
|
-
if (cur[i] < MIN_OPENCLAW[i]) {
|
|
88
|
+
if (cur[i] < MIN_OPENCLAW[i]) {
|
|
89
|
+
tooOld = true;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
83
92
|
}
|
|
84
93
|
if (tooOld) {
|
|
85
94
|
err(`OpenClaw version ${m[0]} is too old.`);
|
|
@@ -100,7 +109,7 @@ log("Installing Friday Next channel plugin...");
|
|
|
100
109
|
try {
|
|
101
110
|
const out = execSync(
|
|
102
111
|
`${openclawCmd} plugins install @syengup/friday-channel-next@latest --force`,
|
|
103
|
-
{ encoding: "utf8", stdio: "pipe", timeout: 120000 }
|
|
112
|
+
{ encoding: "utf8", stdio: "pipe", timeout: 120000 },
|
|
104
113
|
);
|
|
105
114
|
if (out.trim()) console.log(out.trim());
|
|
106
115
|
log("Plugin registered with install record — auto-upgrade enabled.");
|
|
@@ -108,8 +117,12 @@ try {
|
|
|
108
117
|
// Remove old manual install to avoid "duplicate plugin id" warning.
|
|
109
118
|
const legacyDir = join(USER_HOME, ".openclaw", "extensions", "friday-channel-next");
|
|
110
119
|
if (existsSync(legacyDir)) {
|
|
111
|
-
try {
|
|
112
|
-
|
|
120
|
+
try {
|
|
121
|
+
rmSync(legacyDir, { recursive: true, force: true });
|
|
122
|
+
log("Removed legacy manual install.");
|
|
123
|
+
} catch {
|
|
124
|
+
/* non-critical */
|
|
125
|
+
}
|
|
113
126
|
}
|
|
114
127
|
} catch (e) {
|
|
115
128
|
const msg = (e.stderr || e.stdout || e.message || "").toString();
|
|
@@ -150,7 +163,10 @@ function setConfig(path, value) {
|
|
|
150
163
|
}
|
|
151
164
|
|
|
152
165
|
function ensureArrayContains(arr, item) {
|
|
153
|
-
if (!arr.includes(item)) {
|
|
166
|
+
if (!arr.includes(item)) {
|
|
167
|
+
arr.push(item);
|
|
168
|
+
configChanged = true;
|
|
169
|
+
}
|
|
154
170
|
}
|
|
155
171
|
|
|
156
172
|
// Plugins
|
|
@@ -161,30 +177,58 @@ ensureArrayContains(config.plugins.allow, "canvas");
|
|
|
161
177
|
|
|
162
178
|
if (!config.plugins.entries) config.plugins.entries = {};
|
|
163
179
|
for (const id of ["friday-next", "canvas"]) {
|
|
164
|
-
if (!config.plugins.entries[id]) {
|
|
165
|
-
|
|
180
|
+
if (!config.plugins.entries[id]) {
|
|
181
|
+
config.plugins.entries[id] = { enabled: true };
|
|
182
|
+
configChanged = true;
|
|
183
|
+
} else if (!config.plugins.entries[id].enabled) {
|
|
184
|
+
config.plugins.entries[id].enabled = true;
|
|
185
|
+
configChanged = true;
|
|
186
|
+
}
|
|
166
187
|
}
|
|
167
188
|
|
|
168
189
|
// llm_output hook requires allowConversationAccess for non-bundled plugins.
|
|
169
|
-
if (!config.plugins.entries["friday-next"].hooks) {
|
|
170
|
-
|
|
190
|
+
if (!config.plugins.entries["friday-next"].hooks) {
|
|
191
|
+
config.plugins.entries["friday-next"].hooks = {};
|
|
192
|
+
configChanged = true;
|
|
193
|
+
}
|
|
194
|
+
if (!config.plugins.entries["friday-next"].hooks.allowConversationAccess) {
|
|
195
|
+
config.plugins.entries["friday-next"].hooks.allowConversationAccess = true;
|
|
196
|
+
configChanged = true;
|
|
197
|
+
}
|
|
171
198
|
|
|
172
199
|
// Channel
|
|
173
200
|
if (!config.channels) config.channels = {};
|
|
174
|
-
if (!config.channels["friday-next"]) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
201
|
+
if (!config.channels["friday-next"]) {
|
|
202
|
+
config.channels["friday-next"] = { enabled: true, transport: "http+sse" };
|
|
203
|
+
configChanged = true;
|
|
204
|
+
} else {
|
|
205
|
+
if (!config.channels["friday-next"].enabled) {
|
|
206
|
+
config.channels["friday-next"].enabled = true;
|
|
207
|
+
configChanged = true;
|
|
208
|
+
}
|
|
209
|
+
if (!config.channels["friday-next"].transport) {
|
|
210
|
+
config.channels["friday-next"].transport = "http+sse";
|
|
211
|
+
configChanged = true;
|
|
212
|
+
}
|
|
178
213
|
}
|
|
179
214
|
|
|
180
215
|
// Gateway bind + nodes
|
|
181
216
|
if (!config.gateway) config.gateway = {};
|
|
182
|
-
if (config.gateway.bind !== "lan") {
|
|
217
|
+
if (config.gateway.bind !== "lan") {
|
|
218
|
+
config.gateway.bind = "lan";
|
|
219
|
+
configChanged = true;
|
|
220
|
+
}
|
|
183
221
|
if (!config.gateway.nodes) config.gateway.nodes = {};
|
|
184
222
|
if (!Array.isArray(config.gateway.nodes.allowCommands)) config.gateway.nodes.allowCommands = [];
|
|
185
223
|
for (const cmd of [
|
|
186
|
-
"canvas.navigate",
|
|
187
|
-
"canvas.
|
|
224
|
+
"canvas.navigate",
|
|
225
|
+
"canvas.present",
|
|
226
|
+
"canvas.hide",
|
|
227
|
+
"canvas.eval",
|
|
228
|
+
"canvas.snapshot",
|
|
229
|
+
"canvas.a2ui.push",
|
|
230
|
+
"canvas.a2ui.reset",
|
|
231
|
+
"canvas.a2ui.pushJSONL",
|
|
188
232
|
]) {
|
|
189
233
|
ensureArrayContains(config.gateway.nodes.allowCommands, cmd);
|
|
190
234
|
}
|
|
@@ -193,7 +237,11 @@ for (const cmd of [
|
|
|
193
237
|
if (!config.agents) config.agents = {};
|
|
194
238
|
if (!Array.isArray(config.agents.list)) config.agents.list = [];
|
|
195
239
|
let mainAgent = config.agents.list.find((a) => a.id === "main");
|
|
196
|
-
if (!mainAgent) {
|
|
240
|
+
if (!mainAgent) {
|
|
241
|
+
mainAgent = { id: "main" };
|
|
242
|
+
config.agents.list.push(mainAgent);
|
|
243
|
+
configChanged = true;
|
|
244
|
+
}
|
|
197
245
|
if (!mainAgent.tools) mainAgent.tools = {};
|
|
198
246
|
if (!Array.isArray(mainAgent.tools.alsoAllow)) mainAgent.tools.alsoAllow = [];
|
|
199
247
|
for (const tool of ["canvas", "nodes"]) {
|
|
@@ -202,7 +250,10 @@ for (const tool of ["canvas", "nodes"]) {
|
|
|
202
250
|
if (Array.isArray(mainAgent.tools.deny)) {
|
|
203
251
|
for (const tool of ["canvas", "nodes"]) {
|
|
204
252
|
const idx = mainAgent.tools.deny.indexOf(tool);
|
|
205
|
-
if (idx !== -1) {
|
|
253
|
+
if (idx !== -1) {
|
|
254
|
+
mainAgent.tools.deny.splice(idx, 1);
|
|
255
|
+
configChanged = true;
|
|
256
|
+
}
|
|
206
257
|
}
|
|
207
258
|
}
|
|
208
259
|
|
|
@@ -224,7 +275,11 @@ log("Restarting OpenClaw gateway... (this can take 20-30s)");
|
|
|
224
275
|
try {
|
|
225
276
|
// A full gateway restart commonly takes 20s+ on a fresh boot; give it plenty of room
|
|
226
277
|
// so we don't kill it mid-restart and report a false failure.
|
|
227
|
-
const out = execSync(`${openclawCmd} gateway restart`, {
|
|
278
|
+
const out = execSync(`${openclawCmd} gateway restart`, {
|
|
279
|
+
encoding: "utf8",
|
|
280
|
+
stdio: "pipe",
|
|
281
|
+
timeout: 90000,
|
|
282
|
+
});
|
|
228
283
|
if (out.trim()) console.log(out.trim());
|
|
229
284
|
} catch (e) {
|
|
230
285
|
if (e.stdout?.trim()) console.log(e.stdout.trim());
|
|
@@ -250,15 +305,18 @@ function getLanIp() {
|
|
|
250
305
|
return "127.0.0.1";
|
|
251
306
|
}
|
|
252
307
|
|
|
253
|
-
try {
|
|
308
|
+
try {
|
|
309
|
+
config = JSON.parse(readFileSync(OPENCLAW_CONFIG, "utf8"));
|
|
310
|
+
} catch {
|
|
311
|
+
config = {};
|
|
312
|
+
}
|
|
254
313
|
|
|
255
314
|
const gatewayPort = config.gateway?.port || 18789;
|
|
256
315
|
const gatewayToken = config.gateway?.auth?.token || "(not set)";
|
|
257
316
|
const bindMode = config.gateway?.bind || "localhost";
|
|
258
317
|
|
|
259
|
-
const gatewayUrl =
|
|
260
|
-
? `http://${getLanIp()}:${gatewayPort}`
|
|
261
|
-
: `http://127.0.0.1:${gatewayPort}`;
|
|
318
|
+
const gatewayUrl =
|
|
319
|
+
bindMode === "lan" ? `http://${getLanIp()}:${gatewayPort}` : `http://127.0.0.1:${gatewayPort}`;
|
|
262
320
|
|
|
263
321
|
// Always verify against loopback: the gateway binds 0.0.0.0 so it's reachable here,
|
|
264
322
|
// and this avoids false negatives from LAN/NAT routing of the advertised IP.
|
|
@@ -272,19 +330,38 @@ async function verifyGateway(url, token, retries = 30) {
|
|
|
272
330
|
try {
|
|
273
331
|
const res = await new Promise((resolve, reject) => {
|
|
274
332
|
const req = http.request(
|
|
275
|
-
{
|
|
276
|
-
|
|
277
|
-
|
|
333
|
+
{
|
|
334
|
+
hostname,
|
|
335
|
+
port,
|
|
336
|
+
path: "/friday-next/status",
|
|
337
|
+
method: "GET",
|
|
338
|
+
headers: { authorization: `Bearer ${token}` },
|
|
339
|
+
timeout: 5000,
|
|
340
|
+
},
|
|
341
|
+
(res) => {
|
|
342
|
+
let body = "";
|
|
343
|
+
res.on("data", (c) => (body += c));
|
|
344
|
+
res.on("end", () => resolve({ status: res.statusCode, body }));
|
|
345
|
+
},
|
|
278
346
|
);
|
|
279
347
|
req.on("error", reject);
|
|
280
|
-
req.on("timeout", () => {
|
|
348
|
+
req.on("timeout", () => {
|
|
349
|
+
req.destroy();
|
|
350
|
+
reject(new Error("timeout"));
|
|
351
|
+
});
|
|
281
352
|
req.end();
|
|
282
353
|
});
|
|
283
354
|
if (res.status === 200) {
|
|
284
355
|
try {
|
|
285
356
|
const data = JSON.parse(res.body);
|
|
286
357
|
if (data.ok) {
|
|
287
|
-
log(
|
|
358
|
+
log(
|
|
359
|
+
"Gateway verified OK (friday-next " +
|
|
360
|
+
data.version +
|
|
361
|
+
", " +
|
|
362
|
+
data.connections +
|
|
363
|
+
" connections).",
|
|
364
|
+
);
|
|
288
365
|
return true;
|
|
289
366
|
}
|
|
290
367
|
warn("Plugin responded but ok=false — " + JSON.stringify(data));
|
|
@@ -294,8 +371,14 @@ async function verifyGateway(url, token, retries = 30) {
|
|
|
294
371
|
continue;
|
|
295
372
|
}
|
|
296
373
|
}
|
|
297
|
-
if (res.status === 401) {
|
|
298
|
-
|
|
374
|
+
if (res.status === 401) {
|
|
375
|
+
warn("Auth token mismatch — check gateway.auth.token.");
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
if (res.status === 404) {
|
|
379
|
+
warn("Route not found — plugin may not be loaded.");
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
299
382
|
if (i < retries) warn(`Gateway responded ${res.status}, retrying (${i}/${retries})...`);
|
|
300
383
|
} catch {
|
|
301
384
|
if (i < retries) warn(`Gateway not reachable, retrying (${i}/${retries})...`);
|
|
@@ -404,14 +487,19 @@ async function detectPublicIp() {
|
|
|
404
487
|
const ipStr = await new Promise((resolve, reject) => {
|
|
405
488
|
const req = http.get(url, { timeout: 3000 }, (res) => {
|
|
406
489
|
let body = "";
|
|
407
|
-
res.on("data", (c) => body += c);
|
|
490
|
+
res.on("data", (c) => (body += c));
|
|
408
491
|
res.on("end", () => resolve(body.trim()));
|
|
409
492
|
});
|
|
410
493
|
req.on("error", reject);
|
|
411
|
-
req.on("timeout", () => {
|
|
494
|
+
req.on("timeout", () => {
|
|
495
|
+
req.destroy();
|
|
496
|
+
reject(new Error("timeout"));
|
|
497
|
+
});
|
|
412
498
|
});
|
|
413
499
|
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ipStr)) return ipStr;
|
|
414
|
-
} catch {
|
|
500
|
+
} catch {
|
|
501
|
+
/* try next */
|
|
502
|
+
}
|
|
415
503
|
}
|
|
416
504
|
return null;
|
|
417
505
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syengup/friday-channel-next",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.37",
|
|
4
4
|
"description": "OpenClaw Friday Next Apple channel plugin",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "tsc -p tsconfig.json",
|
|
17
|
+
"lint": "eslint .",
|
|
18
|
+
"lint:fix": "eslint . --fix",
|
|
19
|
+
"format": "prettier --write .",
|
|
20
|
+
"format:check": "prettier --check .",
|
|
17
21
|
"prepublishOnly": "pnpm build && rm -rf dist/attachments",
|
|
18
22
|
"test": "npm run test:unit && npm run test:e2e",
|
|
19
23
|
"test:unit": "vitest run",
|
|
@@ -58,12 +62,17 @@
|
|
|
58
62
|
"qrcode-terminal": "^0.12.0"
|
|
59
63
|
},
|
|
60
64
|
"devDependencies": {
|
|
65
|
+
"@eslint/js": "^10.0.1",
|
|
61
66
|
"@types/node": "^25.6.0",
|
|
62
67
|
"chalk": "^5.6.2",
|
|
68
|
+
"eslint": "^10.5.0",
|
|
69
|
+
"eslint-config-prettier": "^10.1.8",
|
|
63
70
|
"jiti": "^2.6.1",
|
|
64
71
|
"json5": "^2.2.3",
|
|
72
|
+
"prettier": "^3.8.4",
|
|
65
73
|
"tslog": "^4.10.2",
|
|
66
74
|
"typescript": "^6.0.3",
|
|
75
|
+
"typescript-eslint": "^8.61.1",
|
|
67
76
|
"vitest": "^4.1.5",
|
|
68
77
|
"zod": "^4.3.6"
|
|
69
78
|
}
|
package/src/agent/abort-run.ts
CHANGED
|
@@ -1,10 +1,25 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
export type AbortRunResult = { aborted: boolean; drained: boolean };
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Abort the active run for a channel `sessionKey`.
|
|
5
|
+
*
|
|
6
|
+
* A session has at most one active run at a time, and the SDK keys active runs by
|
|
7
|
+
* their internal `sessionId` (not the channel runId). So resolve sessionKey → sessionId
|
|
8
|
+
* first, then abort-and-drain so the caller learns whether the run actually settled.
|
|
9
|
+
*/
|
|
10
|
+
export async function abortRunForSessionKey(sessionKey: string): Promise<AbortRunResult> {
|
|
11
|
+
if (process.env.VITEST === "true") return { aborted: false, drained: false };
|
|
12
|
+
const key = sessionKey.trim();
|
|
13
|
+
if (!key) return { aborted: false, drained: false };
|
|
14
|
+
try {
|
|
15
|
+
const { resolveActiveEmbeddedRunSessionId, abortAndDrainAgentHarnessRun } =
|
|
16
|
+
await import("openclaw/plugin-sdk/agent-harness");
|
|
17
|
+
const sessionId = resolveActiveEmbeddedRunSessionId(key);
|
|
18
|
+
if (!sessionId) return { aborted: false, drained: false };
|
|
19
|
+
const result = await abortAndDrainAgentHarnessRun({ sessionId, sessionKey: key });
|
|
20
|
+
return { aborted: result.aborted, drained: result.drained };
|
|
21
|
+
} catch {
|
|
22
|
+
// optional at runtime
|
|
23
|
+
return { aborted: false, drained: false };
|
|
9
24
|
}
|
|
10
25
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
const saveMediaBuffer = vi.fn();
|
|
4
|
+
vi.mock("openclaw/plugin-sdk/media-store", () => ({
|
|
5
|
+
saveMediaBuffer: (...args: unknown[]) => saveMediaBuffer(...args),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
const IMAGE_MAX = 6 * 1024 * 1024;
|
|
9
|
+
const DOCUMENT_MAX = 100 * 1024 * 1024;
|
|
10
|
+
vi.mock("openclaw/plugin-sdk/media-runtime", () => ({
|
|
11
|
+
mediaKindFromMime: (mime?: string) => (mime?.startsWith("image/") ? "image" : "document"),
|
|
12
|
+
maxBytesForKind: (kind: string) => (kind === "image" ? IMAGE_MAX : DOCUMENT_MAX),
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
import { saveInboundMediaBuffer } from "./media-bridge.js";
|
|
16
|
+
|
|
17
|
+
describe("saveInboundMediaBuffer", () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
saveMediaBuffer.mockReset();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("forwards the original filename so core preserves name+extension", async () => {
|
|
23
|
+
// Without the filename, core's media-store saves inbound media as a bare uuid
|
|
24
|
+
// (no extension) and the agent sees `[media attached: file://.../inbound/<uuid>]`
|
|
25
|
+
// with zero file-format signal. Passing originalFilename (5th arg) restores it.
|
|
26
|
+
saveMediaBuffer.mockResolvedValue({
|
|
27
|
+
id: "report---uuid.pdf",
|
|
28
|
+
path: "/m/inbound/report---uuid.pdf",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const out = await saveInboundMediaBuffer(Buffer.from("x"), "application/pdf", "report.pdf");
|
|
32
|
+
|
|
33
|
+
expect(saveMediaBuffer).toHaveBeenCalledWith(
|
|
34
|
+
expect.any(Buffer),
|
|
35
|
+
"application/pdf",
|
|
36
|
+
"inbound",
|
|
37
|
+
DOCUMENT_MAX,
|
|
38
|
+
"report.pdf",
|
|
39
|
+
);
|
|
40
|
+
expect(out.path).toContain(".pdf");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("uses openclaw's per-kind byte cap instead of the 5MB save default", async () => {
|
|
44
|
+
saveMediaBuffer.mockResolvedValue({ id: "uuid.png", path: "/m/inbound/uuid.png" });
|
|
45
|
+
|
|
46
|
+
await saveInboundMediaBuffer(Buffer.from("x"), "image/png", "photo.png");
|
|
47
|
+
|
|
48
|
+
expect(saveMediaBuffer).toHaveBeenCalledWith(
|
|
49
|
+
expect.any(Buffer),
|
|
50
|
+
"image/png",
|
|
51
|
+
"inbound",
|
|
52
|
+
IMAGE_MAX,
|
|
53
|
+
"photo.png",
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("works without a filename (still applies the per-kind cap)", async () => {
|
|
58
|
+
saveMediaBuffer.mockResolvedValue({ id: "uuid", path: "/m/inbound/uuid" });
|
|
59
|
+
|
|
60
|
+
const out = await saveInboundMediaBuffer(Buffer.from("x"), "image/png");
|
|
61
|
+
|
|
62
|
+
expect(saveMediaBuffer).toHaveBeenCalledWith(
|
|
63
|
+
expect.any(Buffer),
|
|
64
|
+
"image/png",
|
|
65
|
+
"inbound",
|
|
66
|
+
IMAGE_MAX,
|
|
67
|
+
undefined,
|
|
68
|
+
);
|
|
69
|
+
expect(out.id).toBe("uuid");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -3,13 +3,42 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import crypto from "node:crypto";
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* openclaw's own per-kind byte cap for a mime type (image 6MB / audio·video 16MB /
|
|
8
|
+
* document 100MB). Unknown mimes fall back to the most permissive ("document") cap.
|
|
9
|
+
* Returns undefined if the media runtime isn't importable (tests / stripped runtime),
|
|
10
|
+
* letting `saveMediaBuffer` apply its built-in default.
|
|
11
|
+
*/
|
|
12
|
+
export async function resolveMediaMaxBytes(mimeType: string): Promise<number | undefined> {
|
|
13
|
+
try {
|
|
14
|
+
const { maxBytesForKind, mediaKindFromMime } =
|
|
15
|
+
await import("openclaw/plugin-sdk/media-runtime");
|
|
16
|
+
return maxBytesForKind(mediaKindFromMime(mimeType) ?? "document");
|
|
17
|
+
} catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
6
22
|
export async function saveInboundMediaBuffer(
|
|
7
23
|
buffer: Buffer,
|
|
8
24
|
mimeType: string,
|
|
25
|
+
originalFilename?: string,
|
|
9
26
|
): Promise<{ id: string; path: string }> {
|
|
10
27
|
try {
|
|
11
28
|
const sdk = await import("openclaw/plugin-sdk/media-store");
|
|
12
|
-
|
|
29
|
+
// Accept whatever openclaw itself supports for this media kind instead of
|
|
30
|
+
// saveMediaBuffer's conservative 5MB default.
|
|
31
|
+
const maxBytes = await resolveMediaMaxBytes(mimeType);
|
|
32
|
+
// Pass the original filename (5th arg) so core's media-store preserves the
|
|
33
|
+
// name+extension instead of saving a bare uuid. Otherwise the agent receives
|
|
34
|
+
// `[media attached: file://.../inbound/<uuid>]` with no file-format signal.
|
|
35
|
+
const saved = await sdk.saveMediaBuffer(
|
|
36
|
+
buffer,
|
|
37
|
+
mimeType,
|
|
38
|
+
"inbound",
|
|
39
|
+
maxBytes,
|
|
40
|
+
originalFilename,
|
|
41
|
+
);
|
|
13
42
|
if (saved?.id && saved?.path) return { id: saved.id, path: saved.path };
|
|
14
43
|
} catch {
|
|
15
44
|
// fallback for tests or stripped runtime
|