@ozaiya/openclaw-channel 0.10.7 → 0.10.9
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.d.ts +74 -0
- package/dist/src/channel.js +450 -82
- package/dist/src/channel.js.map +1 -1
- package/dist/src/configSchema.d.ts +74 -0
- package/dist/src/configSchema.js +74 -0
- package/dist/src/configSchema.js.map +1 -1
- package/dist/src/desktopContainer.js +0 -4
- package/dist/src/desktopContainer.js.map +1 -1
- package/dist/src/doubao.d.ts +29 -0
- package/dist/src/doubao.js +76 -0
- package/dist/src/doubao.js.map +1 -0
- package/dist/src/gateway.js +1 -7
- package/dist/src/gateway.js.map +1 -1
- package/dist/src/index.d.ts +5 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/socialMedia.d.ts +35 -0
- package/dist/src/socialMedia.js +242 -0
- package/dist/src/socialMedia.js.map +1 -0
- package/dist/src/types.d.ts +50 -0
- package/dist/src/xueqiu.d.ts +43 -0
- package/dist/src/xueqiu.js +500 -0
- package/dist/src/xueqiu.js.map +1 -0
- package/dist/src/yuanbao.d.ts +21 -0
- package/dist/src/yuanbao.js +154 -0
- package/dist/src/yuanbao.js.map +1 -0
- package/package.json +1 -1
package/dist/src/channel.js
CHANGED
|
@@ -22,6 +22,10 @@ import { maybeTranscribeInboundAudio, prependVoiceTranscriptToAgentInput, resolv
|
|
|
22
22
|
import { startGatewayMode } from "./gateway.js";
|
|
23
23
|
import { VoiceCallSession } from "./voiceCall.js";
|
|
24
24
|
import { PhoneCallSession } from "./phoneCall.js";
|
|
25
|
+
import { summarizeWithYuanbao } from "./yuanbao.js";
|
|
26
|
+
import { summarizeWithDoubao } from "./doubao.js";
|
|
27
|
+
import { fetchXueqiuPost, searchXueqiuPosts } from "./xueqiu.js";
|
|
28
|
+
import { fetchSocialMediaPost, searchSocialMedia, extractSocialMediaContent } from "./socialMedia.js";
|
|
25
29
|
const DEFAULT_API_BASE_URL = "https://api.ozai.dev";
|
|
26
30
|
const DEFAULT_WEBHOOK_PATH = "/ozaiya/webhook";
|
|
27
31
|
const DEFAULT_ACCOUNT_ID = "default";
|
|
@@ -710,6 +714,47 @@ async function stageInboundAttachmentsForAgent(params) {
|
|
|
710
714
|
}
|
|
711
715
|
return staged;
|
|
712
716
|
}
|
|
717
|
+
/**
|
|
718
|
+
* Build the full set of channel agent tools for a given bot account.
|
|
719
|
+
* Used by both the plugin's agentTools factory (for OpenClaw tool registration)
|
|
720
|
+
* and the text-based tool call fallback in deliver().
|
|
721
|
+
*/
|
|
722
|
+
function buildChannelTools(account, cfg) {
|
|
723
|
+
const resolveForGroup = (groupId) => resolveAccountForGroup(cfg, groupId) ?? account;
|
|
724
|
+
const tools = [
|
|
725
|
+
createScheduleMessageTool(account),
|
|
726
|
+
createSendDirectMessageTool(account),
|
|
727
|
+
createSendRichMessageTool(account, resolveForGroup),
|
|
728
|
+
createCreateGroupTool(account),
|
|
729
|
+
createInviteMemberTool(account),
|
|
730
|
+
createReactTool(account),
|
|
731
|
+
createEditMessageTool(account),
|
|
732
|
+
createDeleteMessageTool(account),
|
|
733
|
+
createPinMessageTool(account),
|
|
734
|
+
createSearchUsersTool(account),
|
|
735
|
+
createListGroupsTool(account),
|
|
736
|
+
createMakePhoneCallTool(account, cfg),
|
|
737
|
+
createHangUpCallTool(account),
|
|
738
|
+
createStartInAppCallTool(account, cfg),
|
|
739
|
+
];
|
|
740
|
+
const imageTool = createGenerateImageTool(account, cfg, resolveForGroup);
|
|
741
|
+
if (imageTool)
|
|
742
|
+
tools.push(imageTool);
|
|
743
|
+
tools.push(createDesktopTool());
|
|
744
|
+
const xueqiuTool = createFetchXueqiuTool(cfg);
|
|
745
|
+
if (xueqiuTool)
|
|
746
|
+
tools.push(xueqiuTool);
|
|
747
|
+
const xueqiuSearchTool = createSearchXueqiuTool(cfg);
|
|
748
|
+
if (xueqiuSearchTool)
|
|
749
|
+
tools.push(xueqiuSearchTool);
|
|
750
|
+
const scrapeTool = createScrapeSocialMediaTool(cfg);
|
|
751
|
+
if (scrapeTool)
|
|
752
|
+
tools.push(scrapeTool);
|
|
753
|
+
const summarizeTool = createSummarizeUrlTool(cfg);
|
|
754
|
+
if (summarizeTool)
|
|
755
|
+
tools.push(summarizeTool);
|
|
756
|
+
return tools;
|
|
757
|
+
}
|
|
713
758
|
export const ozaiyaPlugin = {
|
|
714
759
|
id: "ozaiya",
|
|
715
760
|
meta: {
|
|
@@ -947,28 +992,7 @@ export const ozaiyaPlugin = {
|
|
|
947
992
|
const account = (gatewayAccount?.botToken?.trim() ? gatewayAccount : null) ?? resolveAccount(cfg);
|
|
948
993
|
if (!account.botToken?.trim())
|
|
949
994
|
return [];
|
|
950
|
-
|
|
951
|
-
const tools = [
|
|
952
|
-
createScheduleMessageTool(account),
|
|
953
|
-
createSendDirectMessageTool(account),
|
|
954
|
-
createSendRichMessageTool(account, resolveForGroup),
|
|
955
|
-
createCreateGroupTool(account),
|
|
956
|
-
createInviteMemberTool(account),
|
|
957
|
-
createReactTool(account),
|
|
958
|
-
createEditMessageTool(account),
|
|
959
|
-
createDeleteMessageTool(account),
|
|
960
|
-
createPinMessageTool(account),
|
|
961
|
-
createSearchUsersTool(account),
|
|
962
|
-
createListGroupsTool(account),
|
|
963
|
-
createMakePhoneCallTool(account, cfg),
|
|
964
|
-
createHangUpCallTool(account),
|
|
965
|
-
createStartInAppCallTool(account, cfg),
|
|
966
|
-
];
|
|
967
|
-
const imageTool = createGenerateImageTool(account, cfg, resolveForGroup);
|
|
968
|
-
if (imageTool)
|
|
969
|
-
tools.push(imageTool);
|
|
970
|
-
tools.push(createDesktopTool());
|
|
971
|
-
return tools;
|
|
995
|
+
return buildChannelTools(account, cfg);
|
|
972
996
|
}),
|
|
973
997
|
gateway: {
|
|
974
998
|
startAccount: async (ctx) => {
|
|
@@ -1154,6 +1178,15 @@ export const ozaiyaPlugin = {
|
|
|
1154
1178
|
if (payload.event === "session.reset") {
|
|
1155
1179
|
await handleSessionReset(payload, ctx);
|
|
1156
1180
|
}
|
|
1181
|
+
else if (payload.event === "call.started") {
|
|
1182
|
+
await handleCallStarted(payload, ctx);
|
|
1183
|
+
}
|
|
1184
|
+
else if (payload.event === "call.ended") {
|
|
1185
|
+
await handleCallEnded(payload, ctx);
|
|
1186
|
+
}
|
|
1187
|
+
else if (payload.event === "voice.llm_request") {
|
|
1188
|
+
return handleVoiceLlmRequest(payload, ctx);
|
|
1189
|
+
}
|
|
1157
1190
|
},
|
|
1158
1191
|
});
|
|
1159
1192
|
let unregisterHttp = () => { };
|
|
@@ -2101,6 +2134,189 @@ function createHangUpCallTool(account) {
|
|
|
2101
2134
|
},
|
|
2102
2135
|
};
|
|
2103
2136
|
}
|
|
2137
|
+
function createFetchXueqiuTool(cfg) {
|
|
2138
|
+
const ozaiyaCfg = (cfg?.channels?.ozaiya ?? {});
|
|
2139
|
+
const xueqiuCfg = ozaiyaCfg.xueqiu;
|
|
2140
|
+
const socialMediaCfg = ozaiyaCfg.socialMedia;
|
|
2141
|
+
if (!xueqiuCfg && !socialMediaCfg)
|
|
2142
|
+
return null;
|
|
2143
|
+
return {
|
|
2144
|
+
label: "Fetch Xueqiu Post",
|
|
2145
|
+
name: "fetch_xueqiu",
|
|
2146
|
+
ownerOnly: false,
|
|
2147
|
+
description: "Fetch a Xueqiu (雪球) post's content and top comments given its URL. " +
|
|
2148
|
+
"URL format: https://xueqiu.com/{userId}/{postId}. " +
|
|
2149
|
+
"Returns the post title, author, engagement stats, full text, and top comments.",
|
|
2150
|
+
parameters: {
|
|
2151
|
+
type: "object",
|
|
2152
|
+
properties: {
|
|
2153
|
+
url: {
|
|
2154
|
+
type: "string",
|
|
2155
|
+
description: "The Xueqiu post URL, e.g. https://xueqiu.com/1234567890/123456789",
|
|
2156
|
+
},
|
|
2157
|
+
},
|
|
2158
|
+
required: ["url"],
|
|
2159
|
+
},
|
|
2160
|
+
execute: async (_toolCallId, rawArgs) => {
|
|
2161
|
+
try {
|
|
2162
|
+
const args = rawArgs;
|
|
2163
|
+
// Try media-fetch-api first
|
|
2164
|
+
if (socialMediaCfg) {
|
|
2165
|
+
const result = await fetchSocialMediaPost(args.url, socialMediaCfg);
|
|
2166
|
+
if (result) {
|
|
2167
|
+
return { content: [{ type: "text", text: result }] };
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
// Fallback to TS implementation
|
|
2171
|
+
if (xueqiuCfg) {
|
|
2172
|
+
const result = await fetchXueqiuPost(args.url, xueqiuCfg);
|
|
2173
|
+
if (result) {
|
|
2174
|
+
return { content: [{ type: "text", text: result }] };
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
return { content: [{ type: "text", text: "Failed to fetch Xueqiu post. The URL may be invalid or the post may not exist." }] };
|
|
2178
|
+
}
|
|
2179
|
+
catch (err) {
|
|
2180
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2181
|
+
return { content: [{ type: "text", text: `Error fetching Xueqiu post: ${msg}` }] };
|
|
2182
|
+
}
|
|
2183
|
+
},
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
function createSearchXueqiuTool(cfg) {
|
|
2187
|
+
const ozaiyaCfg = (cfg?.channels?.ozaiya ?? {});
|
|
2188
|
+
const xueqiuCfg = ozaiyaCfg.xueqiu;
|
|
2189
|
+
const socialMediaCfg = ozaiyaCfg.socialMedia;
|
|
2190
|
+
if (!xueqiuCfg && !socialMediaCfg)
|
|
2191
|
+
return null;
|
|
2192
|
+
return {
|
|
2193
|
+
label: "Search Xueqiu",
|
|
2194
|
+
name: "search_xueqiu",
|
|
2195
|
+
ownerOnly: false,
|
|
2196
|
+
description: "Search Xueqiu (雪球) for posts matching a keyword or stock name. " +
|
|
2197
|
+
"Use this when the user asks about stocks, investments, or financial topics on Xueqiu. " +
|
|
2198
|
+
"Returns a list of matching posts with authors, dates, excerpts, and URLs. " +
|
|
2199
|
+
"You can then use fetch_xueqiu with a specific URL to get the full post content and comments.",
|
|
2200
|
+
parameters: {
|
|
2201
|
+
type: "object",
|
|
2202
|
+
properties: {
|
|
2203
|
+
query: {
|
|
2204
|
+
type: "string",
|
|
2205
|
+
description: "Search keyword — a stock name (e.g. '华特气体'), stock code (e.g. 'SZ002549'), or topic",
|
|
2206
|
+
},
|
|
2207
|
+
count: {
|
|
2208
|
+
type: "number",
|
|
2209
|
+
description: "Number of results to return (default: 5, max: 10)",
|
|
2210
|
+
},
|
|
2211
|
+
},
|
|
2212
|
+
required: ["query"],
|
|
2213
|
+
},
|
|
2214
|
+
execute: async (_toolCallId, rawArgs) => {
|
|
2215
|
+
try {
|
|
2216
|
+
const args = rawArgs;
|
|
2217
|
+
const count = Math.min(Math.max(args.count ?? 5, 1), 10);
|
|
2218
|
+
// Try media-fetch-api first
|
|
2219
|
+
if (socialMediaCfg) {
|
|
2220
|
+
const result = await searchSocialMedia(args.query, "xueqiu", socialMediaCfg, undefined, count);
|
|
2221
|
+
if (result) {
|
|
2222
|
+
return { content: [{ type: "text", text: result }] };
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
// Fallback to TS implementation
|
|
2226
|
+
if (xueqiuCfg) {
|
|
2227
|
+
const result = await searchXueqiuPosts(args.query, xueqiuCfg, undefined, count);
|
|
2228
|
+
if (result) {
|
|
2229
|
+
return { content: [{ type: "text", text: result }] };
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
return { content: [{ type: "text", text: `Failed to search Xueqiu for "${args.query}". The service may be temporarily unavailable.` }] };
|
|
2233
|
+
}
|
|
2234
|
+
catch (err) {
|
|
2235
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2236
|
+
return { content: [{ type: "text", text: `Error searching Xueqiu: ${msg}` }] };
|
|
2237
|
+
}
|
|
2238
|
+
},
|
|
2239
|
+
};
|
|
2240
|
+
}
|
|
2241
|
+
function createScrapeSocialMediaTool(cfg) {
|
|
2242
|
+
const ozaiyaCfg = (cfg?.channels?.ozaiya ?? {});
|
|
2243
|
+
const socialMediaCfg = ozaiyaCfg.socialMedia;
|
|
2244
|
+
if (!socialMediaCfg)
|
|
2245
|
+
return null;
|
|
2246
|
+
return {
|
|
2247
|
+
label: "Scrape Social Media Post",
|
|
2248
|
+
name: "scrape_social_media",
|
|
2249
|
+
ownerOnly: false,
|
|
2250
|
+
description: "Scrape a social media post via browser automation — returns structured data including title, author, stats, full content text, and top comments. " +
|
|
2251
|
+
"Supports Xiaohongshu (小红书), Douyin (抖音), Bilibili (B站), Weibo (微博), Toutiao (头条), and Xueqiu (雪球) URLs.",
|
|
2252
|
+
parameters: {
|
|
2253
|
+
type: "object",
|
|
2254
|
+
properties: {
|
|
2255
|
+
url: {
|
|
2256
|
+
type: "string",
|
|
2257
|
+
description: "The social media post URL, e.g. https://www.xiaohongshu.com/explore/abc123, " +
|
|
2258
|
+
"https://www.bilibili.com/video/BVxxx, https://m.weibo.cn/detail/xxx, " +
|
|
2259
|
+
"https://www.douyin.com/video/xxx",
|
|
2260
|
+
},
|
|
2261
|
+
},
|
|
2262
|
+
required: ["url"],
|
|
2263
|
+
},
|
|
2264
|
+
execute: async (_toolCallId, rawArgs) => {
|
|
2265
|
+
try {
|
|
2266
|
+
const args = rawArgs;
|
|
2267
|
+
const result = await fetchSocialMediaPost(args.url, socialMediaCfg);
|
|
2268
|
+
if (!result) {
|
|
2269
|
+
return { content: [{ type: "text", text: "Failed to fetch social media post. The URL may be invalid or the service is unavailable." }] };
|
|
2270
|
+
}
|
|
2271
|
+
return { content: [{ type: "text", text: result }] };
|
|
2272
|
+
}
|
|
2273
|
+
catch (err) {
|
|
2274
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2275
|
+
return { content: [{ type: "text", text: `Error fetching social media post: ${msg}` }] };
|
|
2276
|
+
}
|
|
2277
|
+
},
|
|
2278
|
+
};
|
|
2279
|
+
}
|
|
2280
|
+
function createSummarizeUrlTool(cfg) {
|
|
2281
|
+
const ozaiyaCfg = (cfg?.channels?.ozaiya ?? {});
|
|
2282
|
+
const socialMediaCfg = ozaiyaCfg.socialMedia;
|
|
2283
|
+
if (!socialMediaCfg)
|
|
2284
|
+
return null;
|
|
2285
|
+
return {
|
|
2286
|
+
label: "Summarize URL Content",
|
|
2287
|
+
name: "summarize_url",
|
|
2288
|
+
ownerOnly: false,
|
|
2289
|
+
description: "Summarize a URL's content using an LLM backend (not a scraper). " +
|
|
2290
|
+
"Works with any URL but especially useful for WeChat articles (mp.weixin.qq.com), " +
|
|
2291
|
+
"Toutiao (toutiao.com), Douyin (douyin.com), and other Chinese social media. " +
|
|
2292
|
+
"Use this when scrape_social_media fails or when you need an LLM-generated summary instead of raw structured data.",
|
|
2293
|
+
parameters: {
|
|
2294
|
+
type: "object",
|
|
2295
|
+
properties: {
|
|
2296
|
+
url: {
|
|
2297
|
+
type: "string",
|
|
2298
|
+
description: "The URL to summarize, e.g. https://mp.weixin.qq.com/s/xxx, " +
|
|
2299
|
+
"https://www.toutiao.com/article/xxx, https://www.douyin.com/video/xxx",
|
|
2300
|
+
},
|
|
2301
|
+
},
|
|
2302
|
+
required: ["url"],
|
|
2303
|
+
},
|
|
2304
|
+
execute: async (_toolCallId, rawArgs) => {
|
|
2305
|
+
try {
|
|
2306
|
+
const args = rawArgs;
|
|
2307
|
+
const result = await extractSocialMediaContent(args.url, socialMediaCfg);
|
|
2308
|
+
if (!result) {
|
|
2309
|
+
return { content: [{ type: "text", text: "Failed to extract content. The URL may be invalid, the backend may be unavailable, or the backend is not configured for this URL type." }] };
|
|
2310
|
+
}
|
|
2311
|
+
return { content: [{ type: "text", text: result }] };
|
|
2312
|
+
}
|
|
2313
|
+
catch (err) {
|
|
2314
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2315
|
+
return { content: [{ type: "text", text: `Error extracting content: ${msg}` }] };
|
|
2316
|
+
}
|
|
2317
|
+
},
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2104
2320
|
/**
|
|
2105
2321
|
* Handle phone transcription in auto mode: dispatch to agent, speak the reply.
|
|
2106
2322
|
*/
|
|
@@ -2245,6 +2461,153 @@ ctx) {
|
|
|
2245
2461
|
}
|
|
2246
2462
|
}
|
|
2247
2463
|
}
|
|
2464
|
+
// Detect WeChat article URLs and enrich with Yuanbao summary
|
|
2465
|
+
const wechatUrlMatch = messageText?.match(/https?:\/\/mp\.weixin\.qq\.com\/s\/[A-Za-z0-9_-]+/);
|
|
2466
|
+
if (wechatUrlMatch) {
|
|
2467
|
+
const ozaiyaCfg = (ctx.cfg?.channels?.ozaiya ?? {});
|
|
2468
|
+
const yuanbaoCfg = ozaiyaCfg.yuanbao;
|
|
2469
|
+
if (yuanbaoCfg) {
|
|
2470
|
+
ctx.log?.info?.(`ozaiya: detected WeChat article URL, calling Yuanbao for summary`);
|
|
2471
|
+
try {
|
|
2472
|
+
const summary = await summarizeWithYuanbao(wechatUrlMatch[0], yuanbaoCfg, ctx.log);
|
|
2473
|
+
if (summary) {
|
|
2474
|
+
content.text = `${content.text}\n\n[WeChat Article Summary via Yuanbao]\n${summary}`;
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
catch (err) {
|
|
2478
|
+
ctx.log?.warn?.(`ozaiya: Yuanbao summarization failed: ${err}`);
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
}
|
|
2482
|
+
// Detect Toutiao URLs — try media-fetch-api first, fallback to Doubao
|
|
2483
|
+
const toutiaoUrlMatch = messageText?.match(/https?:\/\/(?:www\.)?toutiao\.com\/(?:article|video)\/\d+/);
|
|
2484
|
+
if (toutiaoUrlMatch) {
|
|
2485
|
+
const ozaiyaCfg = (ctx.cfg?.channels?.ozaiya ?? {});
|
|
2486
|
+
const socialMediaCfg = ozaiyaCfg.socialMedia;
|
|
2487
|
+
let fetched = false;
|
|
2488
|
+
if (socialMediaCfg) {
|
|
2489
|
+
ctx.log?.info?.(`ozaiya: detected Toutiao URL, fetching via media-fetch-api`);
|
|
2490
|
+
try {
|
|
2491
|
+
const postContent = await fetchSocialMediaPost(toutiaoUrlMatch[0], socialMediaCfg, ctx.log);
|
|
2492
|
+
if (postContent) {
|
|
2493
|
+
content.text = `${content.text}\n\n${postContent}`;
|
|
2494
|
+
fetched = true;
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
catch (err) {
|
|
2498
|
+
ctx.log?.warn?.(`ozaiya: media-fetch-api Toutiao fetch failed: ${err}`);
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
if (!fetched) {
|
|
2502
|
+
const doubaoCfg = ozaiyaCfg.doubao;
|
|
2503
|
+
if (doubaoCfg) {
|
|
2504
|
+
ctx.log?.info?.(`ozaiya: Toutiao media-fetch-api failed, falling back to Doubao`);
|
|
2505
|
+
try {
|
|
2506
|
+
const summary = await summarizeWithDoubao(toutiaoUrlMatch[0], doubaoCfg, ctx.log);
|
|
2507
|
+
if (summary) {
|
|
2508
|
+
content.text = `${content.text}\n\n[Content Summary via Doubao]\n${summary}`;
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
catch (err) {
|
|
2512
|
+
ctx.log?.warn?.(`ozaiya: Doubao summarization failed: ${err}`);
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
// Detect Douyin URLs — try media-fetch-api first, fallback to Doubao
|
|
2518
|
+
const douyinUrlMatch = messageText?.match(/https?:\/\/(?:v\.douyin\.com\/[a-zA-Z0-9_\-]+\/?|(?:www\.)?douyin\.com\/(?:video|note)\/\d+)/);
|
|
2519
|
+
if (douyinUrlMatch) {
|
|
2520
|
+
const ozaiyaCfg = (ctx.cfg?.channels?.ozaiya ?? {});
|
|
2521
|
+
const socialMediaCfg = ozaiyaCfg.socialMedia;
|
|
2522
|
+
let fetched = false;
|
|
2523
|
+
if (socialMediaCfg) {
|
|
2524
|
+
ctx.log?.info?.(`ozaiya: detected Douyin URL, fetching via media-fetch-api`);
|
|
2525
|
+
try {
|
|
2526
|
+
const postContent = await fetchSocialMediaPost(douyinUrlMatch[0], socialMediaCfg, ctx.log);
|
|
2527
|
+
if (postContent) {
|
|
2528
|
+
content.text = `${content.text}\n\n[抖音]\n${postContent}`;
|
|
2529
|
+
fetched = true;
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
catch (err) {
|
|
2533
|
+
ctx.log?.warn?.(`ozaiya: media-fetch-api Douyin fetch failed: ${err}`);
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
if (!fetched) {
|
|
2537
|
+
const doubaoCfg = ozaiyaCfg.doubao;
|
|
2538
|
+
if (doubaoCfg) {
|
|
2539
|
+
ctx.log?.info?.(`ozaiya: Douyin media-fetch-api failed, falling back to Doubao`);
|
|
2540
|
+
try {
|
|
2541
|
+
const summary = await summarizeWithDoubao(douyinUrlMatch[0], doubaoCfg, ctx.log);
|
|
2542
|
+
if (summary) {
|
|
2543
|
+
content.text = `${content.text}\n\n[Content Summary via Doubao]\n${summary}`;
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
catch (err) {
|
|
2547
|
+
ctx.log?.warn?.(`ozaiya: Doubao summarization failed: ${err}`);
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
// Detect social media URLs (XHS, Bilibili, Weibo) and enrich via media-fetch-api
|
|
2553
|
+
const socialMediaUrlMatch = messageText?.match(/https?:\/\/(?:(?:www\.)?(?:xiaohongshu\.com\/(?:explore|discovery\/item)\/[a-zA-Z0-9]+|xhslink\.com\/[a-zA-Z0-9\/]+)|(?:www\.)?bilibili\.com\/video\/(?:BV[a-zA-Z0-9]+|av\d+)|b23\.tv\/[a-zA-Z0-9]+|(?:www\.)?weibo\.com\/\d+\/[a-zA-Z0-9]+|m\.weibo\.cn\/(?:detail|status)\/\d+)/);
|
|
2554
|
+
if (socialMediaUrlMatch) {
|
|
2555
|
+
const ozaiyaCfg = (ctx.cfg?.channels?.ozaiya ?? {});
|
|
2556
|
+
const socialMediaCfg = ozaiyaCfg.socialMedia;
|
|
2557
|
+
if (socialMediaCfg) {
|
|
2558
|
+
const matchedUrl = socialMediaUrlMatch[0];
|
|
2559
|
+
const platformHint = matchedUrl.includes("bilibili.com") || matchedUrl.includes("b23.tv")
|
|
2560
|
+
? "B站"
|
|
2561
|
+
: matchedUrl.includes("weibo.c")
|
|
2562
|
+
? "微博"
|
|
2563
|
+
: "小红书";
|
|
2564
|
+
ctx.log?.info?.(`ozaiya: detected ${platformHint} URL, fetching content via media-fetch-api`);
|
|
2565
|
+
try {
|
|
2566
|
+
const postContent = await fetchSocialMediaPost(matchedUrl, socialMediaCfg, ctx.log);
|
|
2567
|
+
if (postContent) {
|
|
2568
|
+
content.text = `${content.text}\n\n[${platformHint}]\n${postContent}`;
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
catch (err) {
|
|
2572
|
+
ctx.log?.warn?.(`ozaiya: social media fetch failed: ${err}`);
|
|
2573
|
+
}
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
// Detect Xueqiu post URLs — try media-fetch-api first, fallback to TS CDP implementation
|
|
2577
|
+
const xueqiuUrlMatch = messageText?.match(/https?:\/\/xueqiu\.com\/\d+\/\d+/);
|
|
2578
|
+
if (xueqiuUrlMatch) {
|
|
2579
|
+
const ozaiyaCfg = (ctx.cfg?.channels?.ozaiya ?? {});
|
|
2580
|
+
const socialMediaCfg = ozaiyaCfg.socialMedia;
|
|
2581
|
+
let fetched = false;
|
|
2582
|
+
if (socialMediaCfg) {
|
|
2583
|
+
ctx.log?.info?.(`ozaiya: detected Xueqiu URL, fetching via media-fetch-api`);
|
|
2584
|
+
try {
|
|
2585
|
+
const postContent = await fetchSocialMediaPost(xueqiuUrlMatch[0], socialMediaCfg, ctx.log);
|
|
2586
|
+
if (postContent) {
|
|
2587
|
+
content.text = `${content.text}\n\n[雪球]\n${postContent}`;
|
|
2588
|
+
fetched = true;
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
catch (err) {
|
|
2592
|
+
ctx.log?.warn?.(`ozaiya: media-fetch-api Xueqiu fetch failed: ${err}`);
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
if (!fetched) {
|
|
2596
|
+
const xueqiuCfg = ozaiyaCfg.xueqiu;
|
|
2597
|
+
if (xueqiuCfg) {
|
|
2598
|
+
ctx.log?.info?.(`ozaiya: Xueqiu media-fetch-api unavailable, falling back to TS implementation`);
|
|
2599
|
+
try {
|
|
2600
|
+
const postContent = await fetchXueqiuPost(xueqiuUrlMatch[0], xueqiuCfg, ctx.log);
|
|
2601
|
+
if (postContent) {
|
|
2602
|
+
content.text = `${content.text}\n\n[雪球]\n${postContent}`;
|
|
2603
|
+
}
|
|
2604
|
+
}
|
|
2605
|
+
catch (err) {
|
|
2606
|
+
ctx.log?.warn?.(`ozaiya: Xueqiu TS fetch failed: ${err}`);
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2248
2611
|
const inboundAttachments = normalizeAttachments(content.files);
|
|
2249
2612
|
const attachmentSummary = buildAttachmentSummary(inboundAttachments);
|
|
2250
2613
|
const linkPreviewSummary = buildLinkPreviewSummary(content.linkPreviews);
|
|
@@ -2425,6 +2788,13 @@ ctx) {
|
|
|
2425
2788
|
}).catch((err) => {
|
|
2426
2789
|
ctx.log?.warn?.(`ozaiya: failed recording session: ${String(err)}`);
|
|
2427
2790
|
});
|
|
2791
|
+
// Build channel tools map for text-based tool call fallback.
|
|
2792
|
+
// When a model outputs tool calls as plain text instead of structured API tool_calls,
|
|
2793
|
+
// we match against registered tool names and execute via their .execute() method.
|
|
2794
|
+
// Uses the current bot's account directly (not the factory which re-resolves in gateway mode).
|
|
2795
|
+
const channelTools = buildChannelTools(account, ctx.cfg);
|
|
2796
|
+
const channelToolsByName = new Map(channelTools.map((t) => [t.name, t]));
|
|
2797
|
+
ctx.log?.info?.(`ozaiya: text fallback tools loaded: ${channelToolsByName.size} tools [${[...channelToolsByName.keys()].join(", ")}]`);
|
|
2428
2798
|
// Dispatch to agent with buffered block dispatcher
|
|
2429
2799
|
await ch.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
2430
2800
|
ctx: msgCtx,
|
|
@@ -2435,15 +2805,59 @@ ctx) {
|
|
|
2435
2805
|
ctx.log?.info?.(`ozaiya: deliver called, text length=${replyText?.length ?? 0}, empty=${!replyText?.trim()}, voiceReply=${voiceReply}, voiceReplyVoice=${voiceReplyVoice ?? 'none'}`);
|
|
2436
2806
|
if (!replyText?.trim())
|
|
2437
2807
|
return;
|
|
2438
|
-
//
|
|
2439
|
-
//
|
|
2440
|
-
//
|
|
2808
|
+
// Generic fallback: intercept tool calls that models output as text
|
|
2809
|
+
// instead of structured API tool_calls. Supports two formats:
|
|
2810
|
+
// 1. JSON function syntax: tool_name({"arg":"value"}) or tool_name({arg: "value"})
|
|
2811
|
+
// 2. XML: <function_calls><invoke name="tool_name"><parameter name="arg">value</parameter></invoke></function_calls>
|
|
2812
|
+
let textToolsExecuted = false;
|
|
2813
|
+
// --- Format 1: JSON function syntax tool_name({"key":"val"}) ---
|
|
2814
|
+
if (channelToolsByName.size > 0) {
|
|
2815
|
+
const toolNames = [...channelToolsByName.keys()].map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
2816
|
+
const jsonFnRegex = new RegExp(`\\b(${toolNames.join("|")})\\s*\\(\\s*(\\{[\\s\\S]*?\\})\\s*\\)`, "g");
|
|
2817
|
+
let jsonMatch;
|
|
2818
|
+
while ((jsonMatch = jsonFnRegex.exec(replyText)) !== null) {
|
|
2819
|
+
const toolName = jsonMatch[1];
|
|
2820
|
+
const tool = channelToolsByName.get(toolName);
|
|
2821
|
+
if (!tool)
|
|
2822
|
+
continue;
|
|
2823
|
+
// Always strip the tool call text from the message — users should never
|
|
2824
|
+
// see raw tool_name({...}) syntax even if execution fails.
|
|
2825
|
+
textToolsExecuted = true;
|
|
2826
|
+
try {
|
|
2827
|
+
// Try strict JSON first, then lenient (unquoted keys, trailing commas)
|
|
2828
|
+
let args;
|
|
2829
|
+
try {
|
|
2830
|
+
args = JSON.parse(jsonMatch[2]);
|
|
2831
|
+
}
|
|
2832
|
+
catch {
|
|
2833
|
+
// Handle JS-style object literals: unquoted keys, trailing commas
|
|
2834
|
+
const lenient = jsonMatch[2]
|
|
2835
|
+
.replace(/([{,]\s*)([a-zA-Z_]\w*)\s*:/g, '$1"$2":')
|
|
2836
|
+
.replace(/,\s*}/g, "}");
|
|
2837
|
+
args = JSON.parse(lenient);
|
|
2838
|
+
}
|
|
2839
|
+
ctx.log?.info?.(`ozaiya: text fallback — executing ${toolName}(${JSON.stringify(args)})`);
|
|
2840
|
+
await tool.execute(`text-fallback-${Date.now()}`, args);
|
|
2841
|
+
}
|
|
2842
|
+
catch (err) {
|
|
2843
|
+
ctx.log?.warn?.(`ozaiya: text fallback — ${toolName} failed: ${String(err)}`);
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
if (textToolsExecuted) {
|
|
2847
|
+
replyText = replyText.replace(jsonFnRegex, "").trim();
|
|
2848
|
+
if (!replyText)
|
|
2849
|
+
return;
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
// --- Format 2: XML <function_calls> ---
|
|
2441
2853
|
if (replyText.includes("<function_calls>") && replyText.includes("<invoke")) {
|
|
2442
2854
|
const invokeRegex = /<invoke\s+name="([^"]+)">([\s\S]*?)<\/invoke>/g;
|
|
2443
2855
|
let match;
|
|
2444
|
-
let executedTools = false;
|
|
2445
2856
|
while ((match = invokeRegex.exec(replyText)) !== null) {
|
|
2446
2857
|
const toolName = match[1];
|
|
2858
|
+
const tool = channelToolsByName.get(toolName);
|
|
2859
|
+
if (!tool)
|
|
2860
|
+
continue;
|
|
2447
2861
|
const paramsXml = match[2];
|
|
2448
2862
|
const paramRegex = /<parameter\s+name="([^"]+)">([^<]*)<\/parameter>/g;
|
|
2449
2863
|
const args = {};
|
|
@@ -2451,71 +2865,25 @@ ctx) {
|
|
|
2451
2865
|
while ((pm = paramRegex.exec(paramsXml)) !== null) {
|
|
2452
2866
|
args[pm[1]] = pm[2];
|
|
2453
2867
|
}
|
|
2454
|
-
ctx.log?.info?.(`ozaiya:
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
if (callResult && callResult.voiceEngine === "standard" && !callResult.joined) {
|
|
2462
|
-
if (activeVoiceCalls.has(callResult.callId)) {
|
|
2463
|
-
ctx.log?.info?.(`ozaiya: already in call ${callResult.callId}, skipping`);
|
|
2464
|
-
}
|
|
2465
|
-
else {
|
|
2466
|
-
const cfg = ctx.cfg;
|
|
2467
|
-
const ozaiyaCfg = (cfg?.channels?.ozaiya ?? {});
|
|
2468
|
-
const voiceCallCfg = ozaiyaCfg.voiceCall ?? {};
|
|
2469
|
-
if (voiceCallCfg.enabled !== false) {
|
|
2470
|
-
const effectiveVoiceCallCfg = { ...voiceCallCfg };
|
|
2471
|
-
if (account.voiceConfig?.provider === "deepgram" && account.voiceConfig.apiKey) {
|
|
2472
|
-
effectiveVoiceCallCfg.deepgramApiKey = account.voiceConfig.apiKey;
|
|
2473
|
-
}
|
|
2474
|
-
if (!effectiveVoiceCallCfg.volcengineTts && ozaiyaCfg.volcengineTts) {
|
|
2475
|
-
effectiveVoiceCallCfg.volcengineTts = ozaiyaCfg.volcengineTts;
|
|
2476
|
-
}
|
|
2477
|
-
const session = new VoiceCallSession({
|
|
2478
|
-
callId: callResult.callId,
|
|
2479
|
-
groupId: args.groupId,
|
|
2480
|
-
livekitToken: callResult.token,
|
|
2481
|
-
livekitUrl: callResult.url,
|
|
2482
|
-
voiceCallConfig: effectiveVoiceCallCfg,
|
|
2483
|
-
onTranscript: (text) => {
|
|
2484
|
-
void handleVoiceTranscript(text, session, route, account, { cfg, log: ctx.log });
|
|
2485
|
-
},
|
|
2486
|
-
log: ctx.log,
|
|
2487
|
-
});
|
|
2488
|
-
activeVoiceCalls.set(callResult.callId, session);
|
|
2489
|
-
try {
|
|
2490
|
-
await session.connect();
|
|
2491
|
-
ctx.log?.info?.(`ozaiya: bot joined LiveKit room for call ${callResult.callId}`);
|
|
2492
|
-
}
|
|
2493
|
-
catch (connErr) {
|
|
2494
|
-
activeVoiceCalls.delete(callResult.callId);
|
|
2495
|
-
await session.disconnect();
|
|
2496
|
-
await leaveCall(account.apiBaseUrl, account.botToken, callResult.callId).catch(() => { });
|
|
2497
|
-
ctx.log?.warn?.(`ozaiya: failed to join LiveKit: ${String(connErr)}`);
|
|
2498
|
-
}
|
|
2499
|
-
}
|
|
2500
|
-
}
|
|
2501
|
-
}
|
|
2502
|
-
}
|
|
2503
|
-
catch (err) {
|
|
2504
|
-
ctx.log?.warn?.(`ozaiya: XML tool call failed: ${String(err)}`);
|
|
2505
|
-
}
|
|
2868
|
+
ctx.log?.info?.(`ozaiya: text fallback (XML) — executing ${toolName}(${JSON.stringify(args)})`);
|
|
2869
|
+
try {
|
|
2870
|
+
await tool.execute(`text-fallback-xml-${Date.now()}`, args);
|
|
2871
|
+
textToolsExecuted = true;
|
|
2872
|
+
}
|
|
2873
|
+
catch (err) {
|
|
2874
|
+
ctx.log?.warn?.(`ozaiya: text fallback (XML) — ${toolName} failed: ${String(err)}`);
|
|
2506
2875
|
}
|
|
2507
2876
|
}
|
|
2508
|
-
// Strip XML function_calls from reply text, keep any remaining text
|
|
2509
2877
|
replyText = replyText
|
|
2510
2878
|
.replace(/<function_calls>[\s\S]*?<\/function_calls>/g, "")
|
|
2511
2879
|
.replace(/<function_results>[\s\S]*?<\/function_results>/g, "")
|
|
2512
2880
|
.replace(/\[\[reply_to_current\]\]/g, "")
|
|
2513
2881
|
.replace(/NO_REPLY/g, "")
|
|
2514
2882
|
.trim();
|
|
2515
|
-
if (!replyText &&
|
|
2516
|
-
return;
|
|
2883
|
+
if (!replyText && textToolsExecuted)
|
|
2884
|
+
return;
|
|
2517
2885
|
if (!replyText)
|
|
2518
|
-
return;
|
|
2886
|
+
return;
|
|
2519
2887
|
}
|
|
2520
2888
|
// Voice reply: synthesize TTS audio and send as voice message
|
|
2521
2889
|
if (voiceReply) {
|