@syengup/friday-channel-next 0.1.20 → 0.1.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +4 -4
- package/dist/src/agent-forward-runtime.d.ts +2 -0
- package/dist/src/agent-forward-runtime.js +2 -0
- package/dist/src/history/normalize-message.js +13 -4
- package/dist/src/http/handlers/agents-list.d.ts +8 -0
- package/dist/src/http/handlers/agents-list.js +60 -2
- package/index.ts +7 -6
- package/package.json +1 -1
- package/src/agent-forward-runtime.ts +4 -0
- package/src/history/normalize-message.test.ts +30 -0
- package/src/history/normalize-message.ts +12 -3
- package/src/http/handlers/agents-list.test.ts +52 -1
- package/src/http/handlers/agents-list.ts +64 -2
package/dist/index.js
CHANGED
|
@@ -10,6 +10,8 @@ import { forwardAgentEventRaw, getLastRegisteredFridayDeviceId, resolveFridayDev
|
|
|
10
10
|
import { setFridayAgentForwardRuntime } from "./src/agent-forward-runtime.js";
|
|
11
11
|
import { getOpenClawAgentRunContext } from "./src/agent-run-context-bridge.js";
|
|
12
12
|
import { accumulateRunUsage } from "./src/agent/run-usage-accumulator.js";
|
|
13
|
+
import { createFridayNextLogger } from "./src/logging.js";
|
|
14
|
+
const hookLogger = createFridayNextLogger("hook");
|
|
13
15
|
export { fridayNextChannelPlugin } from "./src/channel.js";
|
|
14
16
|
export { setFridayNextRuntime } from "./src/runtime.js";
|
|
15
17
|
/** `api.on` returns void — register tool hooks at most once per process. */
|
|
@@ -136,8 +138,7 @@ export default defineChannelPluginEntry({
|
|
|
136
138
|
const deviceId = deviceIdFromToolContext(ctx);
|
|
137
139
|
const runId = ctx.runId ?? "(unknown)";
|
|
138
140
|
const logLine = (detail) => {
|
|
139
|
-
|
|
140
|
-
console.error(`[Friday-HOOK] [${ts}] [TOOL_CALL] toolName=${event.toolName} runId=${runId} deviceId=${deviceId ?? "(unknown)"} detail=${detail}`);
|
|
141
|
+
hookLogger.debug(`[TOOL_CALL] toolName=${event.toolName} runId=${runId} deviceId=${deviceId ?? "(unknown)"} detail=${detail}`);
|
|
141
142
|
};
|
|
142
143
|
if (!deviceId) {
|
|
143
144
|
logLine("SKIP_no_deviceId");
|
|
@@ -163,8 +164,7 @@ export default defineChannelPluginEntry({
|
|
|
163
164
|
const deviceId = deviceIdFromToolContext(ctx);
|
|
164
165
|
const runId = ctx.runId ?? "(unknown)";
|
|
165
166
|
const logLine = (detail) => {
|
|
166
|
-
|
|
167
|
-
console.error(`[Friday-HOOK] [${ts}] [TOOL_DONE] toolName=${event.toolName} runId=${runId} deviceId=${deviceId ?? "(unknown)"} detail=${detail}`);
|
|
167
|
+
hookLogger.debug(`[TOOL_DONE] toolName=${event.toolName} runId=${runId} deviceId=${deviceId ?? "(unknown)"} detail=${detail}`);
|
|
168
168
|
};
|
|
169
169
|
if (!deviceId) {
|
|
170
170
|
logLine("SKIP_no_deviceId");
|
|
@@ -14,6 +14,8 @@ export type FridayAgentForwardRuntime = {
|
|
|
14
14
|
sessionKey: string;
|
|
15
15
|
update: (entry: Record<string, unknown>) => Record<string, unknown> | null | Promise<Record<string, unknown> | null>;
|
|
16
16
|
}) => Promise<Record<string, unknown> | null>;
|
|
17
|
+
/** Resolves an agent's workspace dir — used to read IDENTITY.md for the name fallback. */
|
|
18
|
+
resolveAgentWorkspaceDir?: (cfg: unknown, agentId: string) => string;
|
|
17
19
|
getConfig: () => unknown;
|
|
18
20
|
};
|
|
19
21
|
/** Called from `registerFull` so terminal lifecycle forwards can read `sessions.json` after persist. */
|
|
@@ -6,6 +6,8 @@ export function setFridayAgentForwardRuntime(api) {
|
|
|
6
6
|
loadSessionStore: api.runtime.agent.session.loadSessionStore,
|
|
7
7
|
updateSessionStoreEntry: api.runtime.agent.session
|
|
8
8
|
.updateSessionStoreEntry,
|
|
9
|
+
resolveAgentWorkspaceDir: api.runtime.agent
|
|
10
|
+
.resolveAgentWorkspaceDir,
|
|
9
11
|
getConfig: () => api.runtime.config.current(),
|
|
10
12
|
};
|
|
11
13
|
}
|
|
@@ -180,16 +180,25 @@ export function normalizeHistoryMessage(raw, index) {
|
|
|
180
180
|
}
|
|
181
181
|
if (role === "toolResult") {
|
|
182
182
|
const split = splitMediaLines(parsed.text);
|
|
183
|
+
const toolName = readString(record.toolName);
|
|
184
|
+
// Canvas snapshots come back as base64 image blocks on the `canvas` tool result so the *agent*
|
|
185
|
+
// can "see" the rendered page — they must never surface as chat attachments on history rebuild.
|
|
186
|
+
// The streaming deliver path already drops the temp-file form (see isCanvasSnapshotMediaPath in
|
|
187
|
+
// http/handlers/messages.ts); this is the transcript-rebuild counterpart. The canvas tool has no
|
|
188
|
+
// other image-returning action, so all images on a canvas result are snapshots.
|
|
189
|
+
const isCanvasResult = toolName === "canvas";
|
|
190
|
+
const images = isCanvasResult ? [] : parsed.images;
|
|
191
|
+
const mediaPaths = isCanvasResult ? [] : split.paths;
|
|
183
192
|
const toolResult = {
|
|
184
193
|
...(readString(record.toolCallId) ? { toolCallId: readString(record.toolCallId) } : {}),
|
|
185
|
-
...(
|
|
194
|
+
...(toolName ? { toolName } : {}),
|
|
186
195
|
...(record.isError === true ? { isError: true } : {}),
|
|
187
196
|
...(split.text ? { text: split.text } : {}),
|
|
188
|
-
...(
|
|
197
|
+
...(images.length ? { images } : {}),
|
|
189
198
|
};
|
|
190
199
|
message.toolResult = toolResult;
|
|
191
|
-
if (
|
|
192
|
-
message.mediaPaths =
|
|
200
|
+
if (mediaPaths.length)
|
|
201
|
+
message.mediaPaths = mediaPaths;
|
|
193
202
|
return message;
|
|
194
203
|
}
|
|
195
204
|
const split = splitMediaLines(parsed.text);
|
|
@@ -10,4 +10,12 @@ export interface FridayAgentEntry {
|
|
|
10
10
|
emoji?: string;
|
|
11
11
|
avatar?: string;
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* Extract the `Name` field from an agent's IDENTITY.md, mirroring OpenClaw's
|
|
15
|
+
* `parseIdentityMarkdown` (src/agents/identity-file.ts) for the name label only:
|
|
16
|
+
* drop the leading "- ", split on the first ":", strip markdown emphasis, and
|
|
17
|
+
* skip the unfilled template placeholder. Returns the raw value verbatim (e.g.
|
|
18
|
+
* "星期五 (Friday)") so it matches what ControlUI shows under "身份名称".
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseIdentityNameFromMarkdown(content: string): string | undefined;
|
|
13
21
|
export declare function handleAgentsList(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getFridayAgentForwardRuntime, } from "../../agent-forward-runtime.js";
|
|
2
4
|
import { extractBearerToken } from "../middleware/auth.js";
|
|
3
5
|
const DEFAULT_AGENT_ID = "main";
|
|
4
6
|
/** Agent ids already in path/shell-safe form skip the slug rewrite below. */
|
|
@@ -31,6 +33,60 @@ function resolvePrimaryModel(model) {
|
|
|
31
33
|
function readString(value) {
|
|
32
34
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
33
35
|
}
|
|
36
|
+
/** Unfilled IDENTITY.md template prompts that must not surface as a real name. */
|
|
37
|
+
const IDENTITY_NAME_PLACEHOLDERS = new Set(["pick something you like"]);
|
|
38
|
+
/**
|
|
39
|
+
* Extract the `Name` field from an agent's IDENTITY.md, mirroring OpenClaw's
|
|
40
|
+
* `parseIdentityMarkdown` (src/agents/identity-file.ts) for the name label only:
|
|
41
|
+
* drop the leading "- ", split on the first ":", strip markdown emphasis, and
|
|
42
|
+
* skip the unfilled template placeholder. Returns the raw value verbatim (e.g.
|
|
43
|
+
* "星期五 (Friday)") so it matches what ControlUI shows under "身份名称".
|
|
44
|
+
*/
|
|
45
|
+
export function parseIdentityNameFromMarkdown(content) {
|
|
46
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
47
|
+
const cleaned = rawLine.trim().replace(/^\s*-\s*/, "");
|
|
48
|
+
const colonIndex = cleaned.indexOf(":");
|
|
49
|
+
if (colonIndex === -1)
|
|
50
|
+
continue;
|
|
51
|
+
const label = cleaned.slice(0, colonIndex).replace(/[*_`]/g, "").trim().toLowerCase();
|
|
52
|
+
if (label !== "name")
|
|
53
|
+
continue;
|
|
54
|
+
const value = cleaned
|
|
55
|
+
.slice(colonIndex + 1)
|
|
56
|
+
.replace(/^[*_`\s]+|[*_`\s]+$/g, "")
|
|
57
|
+
.trim();
|
|
58
|
+
if (!value)
|
|
59
|
+
continue;
|
|
60
|
+
let normalized = value.replace(/[–—]/g, "-");
|
|
61
|
+
if (normalized.startsWith("(") && normalized.endsWith(")")) {
|
|
62
|
+
normalized = normalized.slice(1, -1).trim();
|
|
63
|
+
}
|
|
64
|
+
if (IDENTITY_NAME_PLACEHOLDERS.has(normalized.toLowerCase()))
|
|
65
|
+
continue;
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Name fallback for agents with no `name`/`identity.name` in config (e.g. the
|
|
72
|
+
* implicit `main`): resolve the agent's workspace and parse its IDENTITY.md, the
|
|
73
|
+
* same source ControlUI reads. Best-effort — any failure yields undefined.
|
|
74
|
+
*/
|
|
75
|
+
function readWorkspaceIdentityName(rt, cfg, agentId) {
|
|
76
|
+
const resolveWorkspace = rt.resolveAgentWorkspaceDir;
|
|
77
|
+
if (!resolveWorkspace)
|
|
78
|
+
return undefined;
|
|
79
|
+
try {
|
|
80
|
+
const workspace = resolveWorkspace(cfg, agentId);
|
|
81
|
+
if (!workspace)
|
|
82
|
+
return undefined;
|
|
83
|
+
const content = fs.readFileSync(path.join(workspace, "IDENTITY.md"), "utf-8");
|
|
84
|
+
return parseIdentityNameFromMarkdown(content);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
34
90
|
/**
|
|
35
91
|
* Reads the configured agents directly from the runtime config (same approach as
|
|
36
92
|
* models-list.ts). When no agents are configured OpenClaw runs an implicit "main"
|
|
@@ -64,7 +120,9 @@ function resolveConfiguredAgents() {
|
|
|
64
120
|
const identity = agent.identity;
|
|
65
121
|
entries.push({
|
|
66
122
|
id,
|
|
67
|
-
name: readString(agent.name) ??
|
|
123
|
+
name: readString(agent.name) ??
|
|
124
|
+
readString(identity?.name) ??
|
|
125
|
+
readWorkspaceIdentityName(rt, cfg, id),
|
|
68
126
|
description: readString(agent.description),
|
|
69
127
|
model: resolvePrimaryModel(agent.model),
|
|
70
128
|
thinkingDefault: readString(agent.thinkingDefault),
|
package/index.ts
CHANGED
|
@@ -17,6 +17,9 @@ import {
|
|
|
17
17
|
import { setFridayAgentForwardRuntime } from "./src/agent-forward-runtime.js";
|
|
18
18
|
import { getOpenClawAgentRunContext } from "./src/agent-run-context-bridge.js";
|
|
19
19
|
import { accumulateRunUsage } from "./src/agent/run-usage-accumulator.js";
|
|
20
|
+
import { createFridayNextLogger } from "./src/logging.js";
|
|
21
|
+
|
|
22
|
+
const hookLogger = createFridayNextLogger("hook");
|
|
20
23
|
|
|
21
24
|
export { fridayNextChannelPlugin } from "./src/channel.js";
|
|
22
25
|
export { setFridayNextRuntime } from "./src/runtime.js";
|
|
@@ -148,9 +151,8 @@ export default defineChannelPluginEntry({
|
|
|
148
151
|
const runId = ctx.runId ?? "(unknown)";
|
|
149
152
|
|
|
150
153
|
const logLine = (detail: string) => {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
`[Friday-HOOK] [${ts}] [TOOL_CALL] toolName=${event.toolName} runId=${runId} deviceId=${deviceId ?? "(unknown)"} detail=${detail}`,
|
|
154
|
+
hookLogger.debug(
|
|
155
|
+
`[TOOL_CALL] toolName=${event.toolName} runId=${runId} deviceId=${deviceId ?? "(unknown)"} detail=${detail}`,
|
|
154
156
|
);
|
|
155
157
|
};
|
|
156
158
|
|
|
@@ -180,9 +182,8 @@ export default defineChannelPluginEntry({
|
|
|
180
182
|
const runId = ctx.runId ?? "(unknown)";
|
|
181
183
|
|
|
182
184
|
const logLine = (detail: string) => {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
`[Friday-HOOK] [${ts}] [TOOL_DONE] toolName=${event.toolName} runId=${runId} deviceId=${deviceId ?? "(unknown)"} detail=${detail}`,
|
|
185
|
+
hookLogger.debug(
|
|
186
|
+
`[TOOL_DONE] toolName=${event.toolName} runId=${runId} deviceId=${deviceId ?? "(unknown)"} detail=${detail}`,
|
|
186
187
|
);
|
|
187
188
|
};
|
|
188
189
|
|
package/package.json
CHANGED
|
@@ -14,6 +14,8 @@ export type FridayAgentForwardRuntime = {
|
|
|
14
14
|
entry: Record<string, unknown>,
|
|
15
15
|
) => Record<string, unknown> | null | Promise<Record<string, unknown> | null>;
|
|
16
16
|
}) => Promise<Record<string, unknown> | null>;
|
|
17
|
+
/** Resolves an agent's workspace dir — used to read IDENTITY.md for the name fallback. */
|
|
18
|
+
resolveAgentWorkspaceDir?: (cfg: unknown, agentId: string) => string;
|
|
17
19
|
getConfig: () => unknown;
|
|
18
20
|
};
|
|
19
21
|
|
|
@@ -26,6 +28,8 @@ export function setFridayAgentForwardRuntime(api: OpenClawPluginApi): void {
|
|
|
26
28
|
loadSessionStore: api.runtime.agent.session.loadSessionStore,
|
|
27
29
|
updateSessionStoreEntry: (api.runtime.agent.session as Record<string, unknown>)
|
|
28
30
|
.updateSessionStoreEntry as FridayAgentForwardRuntime["updateSessionStoreEntry"],
|
|
31
|
+
resolveAgentWorkspaceDir: (api.runtime.agent as Record<string, unknown>)
|
|
32
|
+
.resolveAgentWorkspaceDir as FridayAgentForwardRuntime["resolveAgentWorkspaceDir"],
|
|
29
33
|
getConfig: () => api.runtime.config.current(),
|
|
30
34
|
};
|
|
31
35
|
}
|
|
@@ -108,6 +108,36 @@ describe("normalizeHistoryMessage", () => {
|
|
|
108
108
|
});
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
+
it("drops base64 image blocks from a canvas snapshot toolResult (agent-only, never a chat attachment)", () => {
|
|
112
|
+
const out = normalizeHistoryMessage(
|
|
113
|
+
{
|
|
114
|
+
role: "toolResult",
|
|
115
|
+
toolCallId: "tc-canvas",
|
|
116
|
+
toolName: "canvas",
|
|
117
|
+
content: [{ type: "image", mimeType: "image/jpeg", data: "BASE64SNAPSHOT" }],
|
|
118
|
+
...meta("entry-canvas", 5),
|
|
119
|
+
},
|
|
120
|
+
0,
|
|
121
|
+
);
|
|
122
|
+
expect(out?.role).toBe("toolResult");
|
|
123
|
+
expect(out?.toolResult).toEqual({ toolCallId: "tc-canvas", toolName: "canvas" });
|
|
124
|
+
expect(out?.toolResult?.images).toBeUndefined();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("keeps image blocks on non-canvas toolResults", () => {
|
|
128
|
+
const out = normalizeHistoryMessage(
|
|
129
|
+
{
|
|
130
|
+
role: "toolResult",
|
|
131
|
+
toolCallId: "tc-img",
|
|
132
|
+
toolName: "image_generation",
|
|
133
|
+
content: [{ type: "image", mimeType: "image/png", data: "REALIMG" }],
|
|
134
|
+
...meta("entry-img", 6),
|
|
135
|
+
},
|
|
136
|
+
0,
|
|
137
|
+
);
|
|
138
|
+
expect(out?.toolResult?.images).toEqual([{ mimeType: "image/png", data: "REALIMG" }]);
|
|
139
|
+
});
|
|
140
|
+
|
|
111
141
|
it("strips MEDIA: lines from text into mediaPaths", () => {
|
|
112
142
|
const out = normalizeHistoryMessage(
|
|
113
143
|
{
|
|
@@ -253,15 +253,24 @@ export function normalizeHistoryMessage(
|
|
|
253
253
|
|
|
254
254
|
if (role === "toolResult") {
|
|
255
255
|
const split = splitMediaLines(parsed.text);
|
|
256
|
+
const toolName = readString(record.toolName);
|
|
257
|
+
// Canvas snapshots come back as base64 image blocks on the `canvas` tool result so the *agent*
|
|
258
|
+
// can "see" the rendered page — they must never surface as chat attachments on history rebuild.
|
|
259
|
+
// The streaming deliver path already drops the temp-file form (see isCanvasSnapshotMediaPath in
|
|
260
|
+
// http/handlers/messages.ts); this is the transcript-rebuild counterpart. The canvas tool has no
|
|
261
|
+
// other image-returning action, so all images on a canvas result are snapshots.
|
|
262
|
+
const isCanvasResult = toolName === "canvas";
|
|
263
|
+
const images = isCanvasResult ? [] : parsed.images;
|
|
264
|
+
const mediaPaths = isCanvasResult ? [] : split.paths;
|
|
256
265
|
const toolResult: FridayHistoryToolResult = {
|
|
257
266
|
...(readString(record.toolCallId) ? { toolCallId: readString(record.toolCallId) } : {}),
|
|
258
|
-
...(
|
|
267
|
+
...(toolName ? { toolName } : {}),
|
|
259
268
|
...(record.isError === true ? { isError: true } : {}),
|
|
260
269
|
...(split.text ? { text: split.text } : {}),
|
|
261
|
-
...(
|
|
270
|
+
...(images.length ? { images } : {}),
|
|
262
271
|
};
|
|
263
272
|
message.toolResult = toolResult;
|
|
264
|
-
if (
|
|
273
|
+
if (mediaPaths.length) message.mediaPaths = mediaPaths;
|
|
265
274
|
return message;
|
|
266
275
|
}
|
|
267
276
|
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
|
-
import
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { handleAgentsList, parseIdentityNameFromMarkdown } from "./agents-list.js";
|
|
4
7
|
import { setMockRuntime } from "../../test-support/mock-runtime.js";
|
|
5
8
|
import {
|
|
6
9
|
setFridayAgentForwardRuntime,
|
|
@@ -108,6 +111,33 @@ describe("handleAgentsList", () => {
|
|
|
108
111
|
]);
|
|
109
112
|
});
|
|
110
113
|
|
|
114
|
+
it("falls back to the IDENTITY.md name when config has none", async () => {
|
|
115
|
+
const workspace = fs.mkdtempSync(path.join(os.tmpdir(), "friday-identity-"));
|
|
116
|
+
fs.writeFileSync(
|
|
117
|
+
path.join(workspace, "IDENTITY.md"),
|
|
118
|
+
"# IDENTITY.md\n\n- **Name:** 星期五 (Friday)\n- **Emoji:** 🌿\n",
|
|
119
|
+
);
|
|
120
|
+
try {
|
|
121
|
+
setFridayAgentForwardRuntime({
|
|
122
|
+
runtime: {
|
|
123
|
+
agent: {
|
|
124
|
+
session: { resolveStorePath: () => "", loadSessionStore: () => ({}) },
|
|
125
|
+
resolveAgentWorkspaceDir: () => workspace,
|
|
126
|
+
},
|
|
127
|
+
config: { current: () => ({ agents: { list: [{ id: "main" }] } }) },
|
|
128
|
+
},
|
|
129
|
+
} as any);
|
|
130
|
+
|
|
131
|
+
const res = new MockRes();
|
|
132
|
+
await handleAgentsList(makeReq(AUTH), res as any);
|
|
133
|
+
|
|
134
|
+
const body = JSON.parse(res.body);
|
|
135
|
+
expect(body.agents).toEqual([{ id: "main", name: "星期五 (Friday)", isDefault: true }]);
|
|
136
|
+
} finally {
|
|
137
|
+
fs.rmSync(workspace, { recursive: true, force: true });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
|
|
111
141
|
it("defaults to the first entry when none is marked default and dedups ids", async () => {
|
|
112
142
|
setConfig({
|
|
113
143
|
agents: {
|
|
@@ -127,3 +157,24 @@ describe("handleAgentsList", () => {
|
|
|
127
157
|
expect(body.agents[0].isDefault).toBe(true);
|
|
128
158
|
});
|
|
129
159
|
});
|
|
160
|
+
|
|
161
|
+
describe("parseIdentityNameFromMarkdown", () => {
|
|
162
|
+
it("extracts the Name value from the OpenClaw template format", () => {
|
|
163
|
+
const md = "# IDENTITY.md\n\n- **Name:** 星期五 (Friday)\n- **Emoji:** 🌿\n";
|
|
164
|
+
expect(parseIdentityNameFromMarkdown(md)).toBe("星期五 (Friday)");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("handles a plain unstyled `Name:` line", () => {
|
|
168
|
+
expect(parseIdentityNameFromMarkdown("Name: Jarvis")).toBe("Jarvis");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("returns undefined when there is no Name field", () => {
|
|
172
|
+
expect(parseIdentityNameFromMarkdown("- **Emoji:** 🌿\n- **Vibe:** calm")).toBeUndefined();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("skips the unfilled template placeholder", () => {
|
|
176
|
+
expect(
|
|
177
|
+
parseIdentityNameFromMarkdown("- **Name:** _(pick something you like)_"),
|
|
178
|
+
).toBeUndefined();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
-
import
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import {
|
|
5
|
+
getFridayAgentForwardRuntime,
|
|
6
|
+
type FridayAgentForwardRuntime,
|
|
7
|
+
} from "../../agent-forward-runtime.js";
|
|
3
8
|
import { extractBearerToken } from "../middleware/auth.js";
|
|
4
9
|
|
|
5
10
|
const DEFAULT_AGENT_ID = "main";
|
|
@@ -54,6 +59,60 @@ function readString(value: unknown): string | undefined {
|
|
|
54
59
|
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
55
60
|
}
|
|
56
61
|
|
|
62
|
+
/** Unfilled IDENTITY.md template prompts that must not surface as a real name. */
|
|
63
|
+
const IDENTITY_NAME_PLACEHOLDERS = new Set(["pick something you like"]);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Extract the `Name` field from an agent's IDENTITY.md, mirroring OpenClaw's
|
|
67
|
+
* `parseIdentityMarkdown` (src/agents/identity-file.ts) for the name label only:
|
|
68
|
+
* drop the leading "- ", split on the first ":", strip markdown emphasis, and
|
|
69
|
+
* skip the unfilled template placeholder. Returns the raw value verbatim (e.g.
|
|
70
|
+
* "星期五 (Friday)") so it matches what ControlUI shows under "身份名称".
|
|
71
|
+
*/
|
|
72
|
+
export function parseIdentityNameFromMarkdown(content: string): string | undefined {
|
|
73
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
74
|
+
const cleaned = rawLine.trim().replace(/^\s*-\s*/, "");
|
|
75
|
+
const colonIndex = cleaned.indexOf(":");
|
|
76
|
+
if (colonIndex === -1) continue;
|
|
77
|
+
const label = cleaned.slice(0, colonIndex).replace(/[*_`]/g, "").trim().toLowerCase();
|
|
78
|
+
if (label !== "name") continue;
|
|
79
|
+
const value = cleaned
|
|
80
|
+
.slice(colonIndex + 1)
|
|
81
|
+
.replace(/^[*_`\s]+|[*_`\s]+$/g, "")
|
|
82
|
+
.trim();
|
|
83
|
+
if (!value) continue;
|
|
84
|
+
let normalized = value.replace(/[–—]/g, "-");
|
|
85
|
+
if (normalized.startsWith("(") && normalized.endsWith(")")) {
|
|
86
|
+
normalized = normalized.slice(1, -1).trim();
|
|
87
|
+
}
|
|
88
|
+
if (IDENTITY_NAME_PLACEHOLDERS.has(normalized.toLowerCase())) continue;
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Name fallback for agents with no `name`/`identity.name` in config (e.g. the
|
|
96
|
+
* implicit `main`): resolve the agent's workspace and parse its IDENTITY.md, the
|
|
97
|
+
* same source ControlUI reads. Best-effort — any failure yields undefined.
|
|
98
|
+
*/
|
|
99
|
+
function readWorkspaceIdentityName(
|
|
100
|
+
rt: FridayAgentForwardRuntime,
|
|
101
|
+
cfg: unknown,
|
|
102
|
+
agentId: string,
|
|
103
|
+
): string | undefined {
|
|
104
|
+
const resolveWorkspace = rt.resolveAgentWorkspaceDir;
|
|
105
|
+
if (!resolveWorkspace) return undefined;
|
|
106
|
+
try {
|
|
107
|
+
const workspace = resolveWorkspace(cfg, agentId);
|
|
108
|
+
if (!workspace) return undefined;
|
|
109
|
+
const content = fs.readFileSync(path.join(workspace, "IDENTITY.md"), "utf-8");
|
|
110
|
+
return parseIdentityNameFromMarkdown(content);
|
|
111
|
+
} catch {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
57
116
|
/**
|
|
58
117
|
* Reads the configured agents directly from the runtime config (same approach as
|
|
59
118
|
* models-list.ts). When no agents are configured OpenClaw runs an implicit "main"
|
|
@@ -89,7 +148,10 @@ function resolveConfiguredAgents(): ResolvedAgents {
|
|
|
89
148
|
const identity = agent.identity as Record<string, unknown> | undefined;
|
|
90
149
|
entries.push({
|
|
91
150
|
id,
|
|
92
|
-
name:
|
|
151
|
+
name:
|
|
152
|
+
readString(agent.name) ??
|
|
153
|
+
readString(identity?.name) ??
|
|
154
|
+
readWorkspaceIdentityName(rt, cfg, id),
|
|
93
155
|
description: readString(agent.description),
|
|
94
156
|
model: resolvePrimaryModel(agent.model),
|
|
95
157
|
thinkingDefault: readString(agent.thinkingDefault),
|