@newbase-clawchat/openclaw-clawchat 2026.4.29 → 2026.4.30
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 +33 -10
- package/dist/index.js +27 -0
- package/dist/src/api-client.js +156 -0
- package/dist/src/api-types.js +17 -0
- package/dist/src/buffered-stream.js +177 -0
- package/dist/src/channel.js +191 -0
- package/dist/src/client.js +176 -0
- package/dist/src/commands.js +35 -0
- package/dist/src/config.js +214 -0
- package/dist/src/inbound.js +133 -0
- package/dist/src/login.runtime.js +130 -0
- package/dist/src/media-runtime.js +85 -0
- package/dist/src/message-mapper.js +82 -0
- package/dist/src/outbound.js +181 -0
- package/dist/src/protocol.js +38 -0
- package/dist/src/reply-dispatcher.js +440 -0
- package/dist/src/runtime.js +288 -0
- package/dist/src/streaming.js +65 -0
- package/dist/src/tools-schema.js +38 -0
- package/dist/src/tools.js +287 -0
- package/openclaw.plugin.json +12 -0
- package/package.json +25 -5
- package/skills/clawchat-activate/SKILL.md +17 -8
- package/src/buffered-stream.test.ts +10 -0
- package/src/buffered-stream.ts +6 -6
- package/src/channel.outbound.test.ts +3 -3
- package/src/channel.ts +11 -3
- package/src/client.test.ts +8 -1
- package/src/client.ts +11 -10
- package/src/commands.test.ts +6 -0
- package/src/commands.ts +5 -1
- package/src/config.test.ts +3 -0
- package/src/config.ts +7 -0
- package/src/inbound.test.ts +4 -1
- package/src/inbound.ts +11 -10
- package/src/login.runtime.test.ts +36 -0
- package/src/login.runtime.ts +54 -26
- package/src/manifest.test.ts +98 -22
- package/src/outbound.test.ts +6 -5
- package/src/outbound.ts +8 -7
- package/src/reply-dispatcher.test.ts +418 -3
- package/src/reply-dispatcher.ts +137 -12
- package/src/runtime.ts +1 -0
- package/src/streaming.test.ts +12 -9
- package/src/streaming.ts +6 -6
- package/src/tools.test.ts +81 -18
- package/src/tools.ts +63 -72
package/src/login.runtime.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { createInterface, type Interface as ReadlineInterface } from "node:readline/promises";
|
|
2
|
-
import { writeConfigFile } from "openclaw/plugin-sdk/config-runtime";
|
|
3
2
|
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
|
|
4
3
|
import { createOpenclawClawlingApiClient } from "./api-client.ts";
|
|
5
|
-
import { ClawlingApiError } from "./api-types.ts";
|
|
4
|
+
import { ClawlingApiError, type AgentConnectResult } from "./api-types.ts";
|
|
6
5
|
import {
|
|
7
6
|
CHANNEL_ID,
|
|
8
7
|
mergeOpenclawClawchatToolAllow,
|
|
@@ -20,6 +19,14 @@ export const AGENTS_CONNECT_PLATFORM = "openclaw" as const;
|
|
|
20
19
|
*/
|
|
21
20
|
export const AGENTS_CONNECT_TYPE = "clawbot" as const;
|
|
22
21
|
|
|
22
|
+
export type OpenclawClawchatMutateConfigFile = <T = void>(params: {
|
|
23
|
+
afterWrite: { mode: "auto" } | { mode: "none" | "restart"; reason: string };
|
|
24
|
+
mutate: (
|
|
25
|
+
draft: OpenClawConfig,
|
|
26
|
+
context: { snapshot: unknown; previousHash: string | null },
|
|
27
|
+
) => Promise<T | void> | T | void;
|
|
28
|
+
}) => Promise<unknown>;
|
|
29
|
+
|
|
23
30
|
export interface LoginParams {
|
|
24
31
|
cfg: OpenClawConfig;
|
|
25
32
|
accountId?: string | null;
|
|
@@ -31,7 +38,9 @@ export interface LoginParams {
|
|
|
31
38
|
readInviteCode?: () => Promise<string>;
|
|
32
39
|
/** Override for the HTTP client — used by tests. */
|
|
33
40
|
apiClientFactory?: typeof createOpenclawClawlingApiClient;
|
|
34
|
-
/**
|
|
41
|
+
/** Official runtime config mutator. Production callers must provide this. */
|
|
42
|
+
mutateConfigFile?: OpenclawClawchatMutateConfigFile;
|
|
43
|
+
/** Test-only config persistence override. */
|
|
35
44
|
persistConfig?: (cfg: OpenClawConfig) => Promise<void> | void;
|
|
36
45
|
}
|
|
37
46
|
|
|
@@ -60,6 +69,46 @@ async function promptInviteCodeFromStdin(runtime: {
|
|
|
60
69
|
}
|
|
61
70
|
}
|
|
62
71
|
|
|
72
|
+
function buildLoginConfig(cfg: OpenClawConfig, result: AgentConnectResult): OpenClawConfig {
|
|
73
|
+
const channels = (cfg.channels ?? {}) as Record<string, unknown>;
|
|
74
|
+
const existing = (channels[CHANNEL_ID] ?? {}) as Record<string, unknown>;
|
|
75
|
+
const nextSection: Record<string, unknown> = {
|
|
76
|
+
...existing,
|
|
77
|
+
enabled: true,
|
|
78
|
+
token: result.access_token,
|
|
79
|
+
userId: result.agent.user_id,
|
|
80
|
+
};
|
|
81
|
+
if (result.refresh_token) {
|
|
82
|
+
nextSection.refreshToken = result.refresh_token;
|
|
83
|
+
}
|
|
84
|
+
return mergeOpenclawClawchatToolAllow({
|
|
85
|
+
...cfg,
|
|
86
|
+
channels: { ...channels, [CHANNEL_ID]: nextSection },
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function persistLoginConfig(
|
|
91
|
+
params: LoginParams,
|
|
92
|
+
result: AgentConnectResult,
|
|
93
|
+
): Promise<void> {
|
|
94
|
+
if (params.mutateConfigFile) {
|
|
95
|
+
await params.mutateConfigFile({
|
|
96
|
+
afterWrite: { mode: "auto" },
|
|
97
|
+
mutate(draft) {
|
|
98
|
+
Object.assign(draft, buildLoginConfig(draft, result));
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (params.persistConfig) {
|
|
105
|
+
await params.persistConfig(buildLoginConfig(params.cfg, result));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
throw new Error("openclaw-clawchat: mutateConfigFile is required to persist login credentials");
|
|
110
|
+
}
|
|
111
|
+
|
|
63
112
|
/**
|
|
64
113
|
* Run the `openclaw channels login --channel openclaw-clawchat` flow:
|
|
65
114
|
* 1. Read the existing channel section; require `baseUrl` to be set so we
|
|
@@ -67,7 +116,7 @@ async function promptInviteCodeFromStdin(runtime: {
|
|
|
67
116
|
* 2. Prompt the user for an invite code on stdin.
|
|
68
117
|
* 3. POST it to `${baseUrl}/v1/agents/connect`.
|
|
69
118
|
* 4. Write the returned `websocket_url` / `token` / `user_id` back into
|
|
70
|
-
* the config so subsequent
|
|
119
|
+
* the config so subsequent Gateway runs pick them up.
|
|
71
120
|
*
|
|
72
121
|
* Errors surface with clear messages (missing baseUrl, empty invite,
|
|
73
122
|
* server-side rejection) so the caller can relay them to the operator.
|
|
@@ -115,34 +164,13 @@ export async function runOpenclawClawlingLogin(params: LoginParams): Promise<voi
|
|
|
115
164
|
);
|
|
116
165
|
}
|
|
117
166
|
|
|
118
|
-
// Merge credentials into cfg.channels.openclaw-clawchat and persist
|
|
119
|
-
// immediately so a subsequent `openclaw gateway run` picks them up
|
|
120
|
-
// without any manual edit. `baseUrl` / `websocketUrl` stay untouched —
|
|
121
|
-
// the built-in defaults (or operator overrides) remain authoritative
|
|
122
|
-
// because `/v1/agents/connect` doesn't return them.
|
|
123
|
-
const channels = (cfg.channels ?? {}) as Record<string, unknown>;
|
|
124
|
-
const existing = (channels[CHANNEL_ID] ?? {}) as Record<string, unknown>;
|
|
125
|
-
const nextSection: Record<string, unknown> = {
|
|
126
|
-
...existing,
|
|
127
|
-
enabled: true,
|
|
128
|
-
token: result.access_token,
|
|
129
|
-
userId: result.agent.user_id,
|
|
130
|
-
};
|
|
131
|
-
if (result.refresh_token) {
|
|
132
|
-
nextSection.refreshToken = result.refresh_token;
|
|
133
|
-
}
|
|
134
|
-
const nextCfg: OpenClawConfig = mergeOpenclawClawchatToolAllow({
|
|
135
|
-
...cfg,
|
|
136
|
-
channels: { ...channels, [CHANNEL_ID]: nextSection },
|
|
137
|
-
});
|
|
138
|
-
|
|
139
167
|
const tokenPreview = redactToken(result.access_token);
|
|
140
168
|
runtime.log(
|
|
141
169
|
`Updating config: channels.${CHANNEL_ID}.token=${tokenPreview} userId=${result.agent.user_id}${
|
|
142
170
|
result.refresh_token ? " refreshToken=***" : ""
|
|
143
171
|
} …`,
|
|
144
172
|
);
|
|
145
|
-
await (params
|
|
173
|
+
await persistLoginConfig(params, result);
|
|
146
174
|
runtime.log(`Config file updated.`);
|
|
147
175
|
|
|
148
176
|
runtime.log(
|
package/src/manifest.test.ts
CHANGED
|
@@ -6,11 +6,24 @@ import packageJson from "../package.json" with { type: "json" };
|
|
|
6
6
|
interface PackageJsonWithOpenclaw {
|
|
7
7
|
name: string;
|
|
8
8
|
files: string[];
|
|
9
|
+
scripts: Record<string, string>;
|
|
10
|
+
devDependencies: Record<string, string>;
|
|
11
|
+
peerDependencies: Record<string, string>;
|
|
9
12
|
openclaw: {
|
|
10
13
|
extensions: string[];
|
|
14
|
+
runtimeExtensions?: string[];
|
|
11
15
|
setupEntry?: string;
|
|
12
|
-
channel?: {
|
|
13
|
-
|
|
16
|
+
channel?: {
|
|
17
|
+
id: string;
|
|
18
|
+
label: string;
|
|
19
|
+
selectionLabel?: string;
|
|
20
|
+
docsPath?: string;
|
|
21
|
+
docsLabel?: string;
|
|
22
|
+
blurb: string;
|
|
23
|
+
order?: number;
|
|
24
|
+
aliases?: string[];
|
|
25
|
+
};
|
|
26
|
+
install: { npmSpec: string; minHostVersion: string };
|
|
14
27
|
};
|
|
15
28
|
}
|
|
16
29
|
|
|
@@ -31,23 +44,52 @@ describe("openclaw-clawchat manifest", () => {
|
|
|
31
44
|
expect(pkg.openclaw.install.npmSpec).toBe("@newbase-clawchat/openclaw-clawchat");
|
|
32
45
|
});
|
|
33
46
|
|
|
47
|
+
it("requires an OpenClaw host with runtime config mutation support", () => {
|
|
48
|
+
const pkg = packageJson as PackageJsonWithOpenclaw;
|
|
49
|
+
expect(pkg.peerDependencies.openclaw).toBe("^2026.4.29");
|
|
50
|
+
expect(pkg.devDependencies.openclaw).toBe("2026.4.29");
|
|
51
|
+
expect(pkg.openclaw.install.minHostVersion).toBe(">=2026.4.29");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("publishes compiled runtime entrypoints for npm plugin installs", () => {
|
|
55
|
+
const pkg = packageJson as PackageJsonWithOpenclaw;
|
|
56
|
+
expect(pkg.openclaw.extensions).toEqual(["./index.ts"]);
|
|
57
|
+
expect(pkg.openclaw.runtimeExtensions).toEqual(["./dist/index.js"]);
|
|
58
|
+
expect(pkg.files).toContain("dist");
|
|
59
|
+
expect(pkg.scripts.build).toBe("tsc -p tsconfig.build.json");
|
|
60
|
+
expect(pkg.scripts.prepack).toBe("npm run build");
|
|
61
|
+
expect(fs.existsSync(new URL("../tsconfig.build.json", import.meta.url))).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("publishes channel catalog metadata for OpenClaw CLI discovery", () => {
|
|
65
|
+
const pkg = packageJson as PackageJsonWithOpenclaw;
|
|
66
|
+
expect(pkg.openclaw.channel).toEqual({
|
|
67
|
+
id: "openclaw-clawchat",
|
|
68
|
+
label: "Clawling Chat",
|
|
69
|
+
selectionLabel: "Clawling Chat",
|
|
70
|
+
docsPath: "/channels/openclaw-clawchat",
|
|
71
|
+
docsLabel: "openclaw-clawchat",
|
|
72
|
+
blurb: "Clawling Protocol v2 over WebSocket (chat-sdk).",
|
|
73
|
+
order: 110,
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
34
77
|
it("declares supported channel/command activation hints for plugin loading", () => {
|
|
35
78
|
expect(pluginManifest.activation).toEqual({
|
|
79
|
+
onStartup: true,
|
|
36
80
|
onChannels: ["openclaw-clawchat"],
|
|
37
81
|
onCommands: ["clawchat-login"],
|
|
38
82
|
});
|
|
39
|
-
expect(pluginManifest.activation).not.toHaveProperty("onStartup");
|
|
40
83
|
expect(pluginManifest.commandAliases).toEqual([
|
|
41
84
|
{ name: "clawchat-login", kind: "runtime-slash" },
|
|
42
85
|
]);
|
|
43
86
|
});
|
|
44
87
|
|
|
45
|
-
it("does not publish setup migration or setup-runtime
|
|
88
|
+
it("does not publish setup migration or setup-runtime entry metadata", () => {
|
|
46
89
|
const pkg = packageJson as PackageJsonWithOpenclaw;
|
|
47
90
|
expect(pkg.files).not.toContain("setup-api.ts");
|
|
48
91
|
expect(pkg.files).not.toContain("setup-entry.ts");
|
|
49
92
|
expect(pkg.openclaw.setupEntry).toBeUndefined();
|
|
50
|
-
expect(pkg.openclaw.channel).toBeUndefined();
|
|
51
93
|
expect(fs.existsSync(new URL("../setup-api.ts", import.meta.url))).toBe(false);
|
|
52
94
|
expect(fs.existsSync(new URL("../setup-entry.ts", import.meta.url))).toBe(false);
|
|
53
95
|
});
|
|
@@ -73,16 +115,50 @@ describe("openclaw-clawchat manifest", () => {
|
|
|
73
115
|
expect(skill).not.toMatch(/clawchat_activate/);
|
|
74
116
|
});
|
|
75
117
|
|
|
76
|
-
it("
|
|
118
|
+
it("declares ownership of registered ClawChat agent tools", () => {
|
|
119
|
+
expect(pluginManifest.contracts?.tools).toEqual([
|
|
120
|
+
"clawchat_activate",
|
|
121
|
+
"clawchat_get_account_profile",
|
|
122
|
+
"clawchat_get_user_profile",
|
|
123
|
+
"clawchat_list_account_friends",
|
|
124
|
+
"clawchat_update_account_profile",
|
|
125
|
+
"clawchat_upload_avatar_image",
|
|
126
|
+
"clawchat_upload_media_file",
|
|
127
|
+
]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("keeps the optional OpenClaw source checkout local-only", () => {
|
|
131
|
+
expect(fs.existsSync(new URL("../.gitmodules", import.meta.url))).toBe(false);
|
|
132
|
+
|
|
133
|
+
const gitignore = fs.readFileSync(new URL("../.gitignore", import.meta.url), "utf8");
|
|
134
|
+
expect(gitignore).toMatch(/^tmp\/openclaw\/$/m);
|
|
135
|
+
|
|
136
|
+
const pkg = packageJson as PackageJsonWithOpenclaw;
|
|
137
|
+
expect(pkg.scripts["dev:openclaw-source"]).toBe(
|
|
138
|
+
"test -d tmp/openclaw || git clone --depth=1 https://github.com/openclaw/openclaw.git tmp/openclaw",
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const readme = fs.readFileSync(new URL("../README.md", import.meta.url), "utf8");
|
|
142
|
+
expect(readme).toMatch(/npm run dev:openclaw-source/);
|
|
143
|
+
expect(readme).toMatch(
|
|
144
|
+
/git clone --depth=1 https:\/\/github\.com\/openclaw\/openclaw\.git tmp\/openclaw/,
|
|
145
|
+
);
|
|
146
|
+
expect(readme).toMatch(/local-only/i);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("keeps default Vitest discovery scoped to plugin sources", () => {
|
|
150
|
+
const configUrl = new URL("../vitest.config.ts", import.meta.url);
|
|
151
|
+
expect(fs.existsSync(configUrl)).toBe(true);
|
|
152
|
+
const config = fs.readFileSync(configUrl, "utf8");
|
|
153
|
+
expect(config).toMatch(/include:\s*\["src\/\*\*\/\*\.test\.ts"\]/);
|
|
154
|
+
expect(config).toMatch(/"tmp\/\*\*"/);
|
|
155
|
+
expect(config).toMatch(/"\.e2e\/\*\*"/);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("keeps the activation skill on clawchat_activate with channel-login fallback", () => {
|
|
77
159
|
const skill = fs.readFileSync(new URL("../skills/clawchat-activate/SKILL.md", import.meta.url), "utf8");
|
|
78
160
|
expect(skill).toMatch(/name:\s*clawchat-activate/);
|
|
79
|
-
expect(skill).
|
|
80
|
-
expect(skill).not.toMatch(/command-dispatch:\s*tool/);
|
|
81
|
-
expect(skill).not.toMatch(/command-tool:/);
|
|
82
|
-
expect(skill).not.toMatch(/command-dispatch:/);
|
|
83
|
-
expect(skill).not.toMatch(/command-command:/);
|
|
84
|
-
expect(skill).not.toMatch(/command-arg-mode:/);
|
|
85
|
-
expect(skill).not.toMatch(/user-invocable:/);
|
|
161
|
+
expect(skill).toMatch(/clawchat_activate/);
|
|
86
162
|
expect(skill).not.toMatch(/`clawchat\s+A1B2C3`/i);
|
|
87
163
|
expect(skill).not.toMatch(/`clawchat\s*<code>`/i);
|
|
88
164
|
expect(skill).not.toMatch(/\/clawchat_activate A1B2C3/);
|
|
@@ -92,33 +168,33 @@ describe("openclaw-clawchat manifest", () => {
|
|
|
92
168
|
expect(skill).toMatch(/do not append/i);
|
|
93
169
|
expect(skill).toMatch(/prompt[^\n]+invite code[^\n]+provide/i);
|
|
94
170
|
expect(skill).toMatch(/channel login/i);
|
|
171
|
+
expect(skill).toMatch(/openclaw channels status --probe/);
|
|
95
172
|
expect(skill).toMatch(/openclaw gateway restart/);
|
|
96
173
|
expect(skill).not.toMatch(/ask the user to send/i);
|
|
97
174
|
expect(skill).not.toMatch(/give the exact/i);
|
|
98
|
-
expect(skill).toMatch(/
|
|
99
|
-
expect(skill).toMatch(/execute[^\n]+openclaw gateway restart/i);
|
|
175
|
+
expect(skill).toMatch(/restart[^\n]+only when/i);
|
|
100
176
|
});
|
|
101
177
|
|
|
102
|
-
it("documents
|
|
178
|
+
it("documents clawchat_activate as the natural-language activation path with CLI fallback", () => {
|
|
103
179
|
const readme = fs.readFileSync(new URL("../README.md", import.meta.url), "utf8");
|
|
104
180
|
const docs = fs.readFileSync(new URL("../docs/openclaw-clawchat.md", import.meta.url), "utf8");
|
|
181
|
+
expect(readme).toMatch(/clawchat_activate/i);
|
|
182
|
+
expect(docs).toMatch(/clawchat_activate/i);
|
|
105
183
|
expect(readme).toMatch(/openclaw channels login --channel openclaw-clawchat/i);
|
|
106
184
|
expect(docs).toMatch(/openclaw channels login --channel openclaw-clawchat/i);
|
|
185
|
+
expect(readme).toMatch(/openclaw channels status --probe/i);
|
|
186
|
+
expect(docs).toMatch(/openclaw channels status --probe/i);
|
|
107
187
|
expect(readme).toMatch(/openclaw gateway restart/i);
|
|
108
188
|
expect(docs).toMatch(/openclaw gateway restart/i);
|
|
109
189
|
expect(readme).not.toMatch(/activation skill[^.]+\/clawchat-login/i);
|
|
110
190
|
expect(docs).not.toMatch(/natural-language activation requests[^.]+\/clawchat-login/i);
|
|
111
191
|
expect(readme).not.toMatch(/\/clawchat-activate\s+A1B2C3/i);
|
|
112
192
|
expect(docs).not.toMatch(/\/clawchat-activate\s+A1B2C3/i);
|
|
113
|
-
expect(readme).not.toMatch(/clawchat_activate/);
|
|
114
|
-
expect(docs).not.toMatch(/clawchat_activate/);
|
|
115
193
|
expect(readme).not.toMatch(/\/clawchat_activate\s+A1B2C3/i);
|
|
116
194
|
expect(docs).not.toMatch(/\/clawchat_activate\s+A1B2C3/i);
|
|
117
195
|
expect(readme).not.toMatch(/direct users to/i);
|
|
118
196
|
expect(docs).not.toMatch(/direct the\s+user/i);
|
|
119
|
-
expect(
|
|
120
|
-
expect(docs).
|
|
121
|
-
expect(readme).toMatch(/activation skill[^.]+execute/i);
|
|
122
|
-
expect(docs).toMatch(/natural-language activation requests[^.]+execute/i);
|
|
197
|
+
expect(readme).toMatch(/activation skill calls/i);
|
|
198
|
+
expect(docs).toMatch(/Natural-language activation requests should call `clawchat_activate`/i);
|
|
123
199
|
});
|
|
124
200
|
});
|
package/src/outbound.test.ts
CHANGED
|
@@ -65,9 +65,9 @@ describe("openclaw-clawchat outbound", () => {
|
|
|
65
65
|
});
|
|
66
66
|
expect((client.sendMessage as ReturnType<typeof vi.fn>).mock.calls[0][0]).toMatchObject({
|
|
67
67
|
chat_id: "user-1",
|
|
68
|
-
chat_type: "direct",
|
|
69
68
|
body: { fragments: [{ kind: "text", text: "hello" }] },
|
|
70
69
|
});
|
|
70
|
+
expect((client.sendMessage as ReturnType<typeof vi.fn>).mock.calls[0][0]).not.toHaveProperty("chat_type");
|
|
71
71
|
expect(client.replyMessage).not.toHaveBeenCalled();
|
|
72
72
|
expect(result?.messageId).toBe("server-m1");
|
|
73
73
|
expect(result?.acceptedAt).toBe(1234);
|
|
@@ -78,26 +78,27 @@ describe("openclaw-clawchat outbound", () => {
|
|
|
78
78
|
await sendOpenclawClawlingText({
|
|
79
79
|
client,
|
|
80
80
|
account: baseAccount(),
|
|
81
|
-
to: { chatId: "
|
|
81
|
+
to: { chatId: "chat-1", chatType: "direct" },
|
|
82
82
|
text: "reply",
|
|
83
83
|
replyCtx: {
|
|
84
84
|
replyToMessageId: "m-orig",
|
|
85
|
+
replyPreviewChatId: "chat-1",
|
|
85
86
|
replyPreviewSenderId: "user-2",
|
|
86
87
|
replyPreviewNickName: "Sender",
|
|
87
88
|
replyPreviewText: "original",
|
|
88
89
|
},
|
|
89
90
|
});
|
|
90
91
|
expect((client.replyMessage as ReturnType<typeof vi.fn>).mock.calls[0][0]).toMatchObject({
|
|
91
|
-
chat_id: "
|
|
92
|
-
chat_type: "direct",
|
|
92
|
+
chat_id: "chat-1",
|
|
93
93
|
replyTo: {
|
|
94
94
|
msgId: "m-orig",
|
|
95
|
-
senderId: "
|
|
95
|
+
senderId: "chat-1",
|
|
96
96
|
nickName: "Sender",
|
|
97
97
|
fragments: [{ kind: "text", text: "original" }],
|
|
98
98
|
},
|
|
99
99
|
body: { fragments: [{ kind: "text", text: "reply" }] },
|
|
100
100
|
});
|
|
101
|
+
expect((client.replyMessage as ReturnType<typeof vi.fn>).mock.calls[0][0]).not.toHaveProperty("chat_type");
|
|
101
102
|
expect(client.sendMessage).not.toHaveBeenCalled();
|
|
102
103
|
});
|
|
103
104
|
|
package/src/outbound.ts
CHANGED
|
@@ -22,6 +22,7 @@ export interface OutboundTarget {
|
|
|
22
22
|
|
|
23
23
|
export interface OutboundReplyCtx {
|
|
24
24
|
replyToMessageId: string;
|
|
25
|
+
replyPreviewChatId?: string;
|
|
25
26
|
replyPreviewSenderId: string;
|
|
26
27
|
replyPreviewNickName: string;
|
|
27
28
|
replyPreviewText: string;
|
|
@@ -38,6 +39,7 @@ export interface SendParams {
|
|
|
38
39
|
to: OutboundTarget;
|
|
39
40
|
text: string;
|
|
40
41
|
replyCtx?: OutboundReplyCtx;
|
|
42
|
+
richFragments?: Fragment[];
|
|
41
43
|
mediaFragments?: ClawlingMediaFragment[];
|
|
42
44
|
mentions?: string[];
|
|
43
45
|
log?: LogSink;
|
|
@@ -92,8 +94,9 @@ export function parseOpenclawRecipient(to: string): { chatId: string; chatType:
|
|
|
92
94
|
|
|
93
95
|
export async function sendOpenclawClawlingText(params: SendParams): Promise<SendResult | null> {
|
|
94
96
|
const text = (params.text ?? "").trim();
|
|
97
|
+
const richFragments = params.richFragments ?? [];
|
|
95
98
|
const mediaFragments = params.mediaFragments ?? [];
|
|
96
|
-
if (!text && mediaFragments.length === 0) {
|
|
99
|
+
if (!text && richFragments.length === 0 && mediaFragments.length === 0) {
|
|
97
100
|
params.log?.info?.(
|
|
98
101
|
`[${params.account.accountId}] openclaw-clawchat outbound suppressed: empty text and no media`,
|
|
99
102
|
);
|
|
@@ -106,7 +109,7 @@ export async function sendOpenclawClawlingText(params: SendParams): Promise<Send
|
|
|
106
109
|
// with one of the SDK's narrow Fragment members (ImageFragment / FileFragment /
|
|
107
110
|
// AudioFragment / VideoFragment) based on its runtime `kind`. The wide local
|
|
108
111
|
// shape lets us build a single uniform array without a per-kind switch.
|
|
109
|
-
const fragments = [...textFragments, ...mediaFragments] as Fragment[];
|
|
112
|
+
const fragments = [...textFragments, ...richFragments, ...mediaFragments] as Fragment[];
|
|
110
113
|
|
|
111
114
|
const useReply = params.replyCtx && mediaFragments.length === 0;
|
|
112
115
|
if (params.replyCtx && mediaFragments.length > 0) {
|
|
@@ -121,26 +124,24 @@ export async function sendOpenclawClawlingText(params: SendParams): Promise<Send
|
|
|
121
124
|
mode = "reply";
|
|
122
125
|
ack = await params.client.replyMessage({
|
|
123
126
|
chat_id: params.to.chatId,
|
|
124
|
-
chat_type: params.to.chatType,
|
|
125
127
|
mode: "normal",
|
|
126
128
|
replyTo: {
|
|
127
129
|
msgId: params.replyCtx.replyToMessageId,
|
|
128
|
-
senderId: params.replyCtx.replyPreviewSenderId,
|
|
130
|
+
senderId: params.replyCtx.replyPreviewChatId ?? params.replyCtx.replyPreviewSenderId,
|
|
129
131
|
nickName: params.replyCtx.replyPreviewNickName,
|
|
130
132
|
fragments: [{ kind: "text", text: params.replyCtx.replyPreviewText }],
|
|
131
133
|
},
|
|
132
134
|
body: { fragments },
|
|
133
135
|
context: { mentions },
|
|
134
|
-
});
|
|
136
|
+
} as Parameters<ClawlingChatClient["replyMessage"]>[0]);
|
|
135
137
|
} else {
|
|
136
138
|
mode = "send";
|
|
137
139
|
ack = await params.client.sendMessage({
|
|
138
140
|
chat_id: params.to.chatId,
|
|
139
|
-
chat_type: params.to.chatType,
|
|
140
141
|
mode: "normal",
|
|
141
142
|
body: { fragments },
|
|
142
143
|
context: { mentions, reply: null },
|
|
143
|
-
});
|
|
144
|
+
} as Parameters<ClawlingChatClient["sendMessage"]>[0]);
|
|
144
145
|
}
|
|
145
146
|
params.log?.info?.(
|
|
146
147
|
`[${params.account.accountId}] openclaw-clawchat outbound mode=${mode} msg=${ack.payload.message_id} text_len=${text.length} media=${mediaFragments.length} trace=${ack.trace_id}`,
|