openclaw-plugin-yuanbao 2.14.0 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/src/access/http/request.d.ts +6 -0
- package/dist/src/access/http/request.js +1 -1
- package/dist/src/access/ws/biz-codec.js +4 -0
- package/dist/src/access/ws/client.js +1 -0
- package/dist/src/access/ws/proto/biz.json +5 -0
- package/dist/src/business/actions/handler.js +9 -2
- package/dist/src/business/messaging/context.d.ts +4 -0
- package/dist/src/business/messaging/handlers/custom/forward-records-proto.d.ts +8 -0
- package/dist/src/business/messaging/handlers/custom/forward-records-proto.js +103 -0
- package/dist/src/business/messaging/handlers/custom/forward-records.d.ts +63 -0
- package/dist/src/business/messaging/handlers/custom/forward-records.js +169 -0
- package/dist/src/business/messaging/handlers/custom/index.js +28 -3
- package/dist/src/business/messaging/handlers/types.d.ts +2 -0
- package/dist/src/business/outbound/queue.d.ts +1 -0
- package/dist/src/business/outbound/queue.js +1 -1
- package/dist/src/business/pipeline/middlewares/dispatch-reply.js +6 -1
- package/dist/src/business/pipeline/middlewares/extract-content.js +10 -3
- package/dist/src/business/tools/remind.js +4 -4
- package/dist/src/business/trace/context.d.ts +9 -0
- package/dist/src/business/trace/context.js +7 -0
- package/dist/src/infra/reply-classify.d.ts +19 -0
- package/dist/src/infra/reply-classify.js +27 -0
- package/dist/src/infra/transport.js +11 -12
- package/dist/src/types.d.ts +2 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +9 -4
- package/dist/src/business/messaging/chat-history.test.d.ts +0 -4
- package/dist/src/business/messaging/chat-history.test.js +0 -48
- package/dist/src/business/messaging/extract.test.d.ts +0 -4
- package/dist/src/business/messaging/extract.test.js +0 -71
- package/dist/src/business/messaging/handlers/custom.test.d.ts +0 -6
- package/dist/src/business/messaging/handlers/custom.test.js +0 -82
- package/dist/src/business/messaging/handlers/face.test.d.ts +0 -6
- package/dist/src/business/messaging/handlers/face.test.js +0 -59
- package/dist/src/business/messaging/handlers/image.test.d.ts +0 -6
- package/dist/src/business/messaging/handlers/image.test.js +0 -88
- package/dist/src/business/messaging/handlers/index.test.d.ts +0 -6
- package/dist/src/business/messaging/handlers/index.test.js +0 -72
- package/dist/src/business/messaging/handlers/text.test.d.ts +0 -6
- package/dist/src/business/messaging/handlers/text.test.js +0 -39
- package/dist/src/business/messaging/quote.test.d.ts +0 -4
- package/dist/src/business/messaging/quote.test.js +0 -118
- package/dist/src/business/messaging/targets.test.d.ts +0 -6
- package/dist/src/business/messaging/targets.test.js +0 -46
- package/dist/src/business/pipeline/engine.test.d.ts +0 -4
- package/dist/src/business/pipeline/engine.test.js +0 -194
- package/dist/src/business/pipeline/middlewares/build-context.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/build-context.test.js +0 -132
- package/dist/src/business/pipeline/middlewares/dispatch-reply.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/dispatch-reply.test.js +0 -212
- package/dist/src/business/pipeline/middlewares/download-media.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/download-media.test.js +0 -318
- package/dist/src/business/pipeline/middlewares/extract-content.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/extract-content.test.js +0 -92
- package/dist/src/business/pipeline/middlewares/guard-command.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/guard-command.test.js +0 -129
- package/dist/src/business/pipeline/middlewares/guard-group-command.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/guard-group-command.test.js +0 -99
- package/dist/src/business/pipeline/middlewares/guard-send-access.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/guard-send-access.test.js +0 -77
- package/dist/src/business/pipeline/middlewares/guard-special-command.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/guard-special-command.test.js +0 -149
- package/dist/src/business/pipeline/middlewares/prepare-sender.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/prepare-sender.test.js +0 -131
- package/dist/src/business/pipeline/middlewares/record-member.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/record-member.test.js +0 -64
- package/dist/src/business/pipeline/middlewares/resolve-mention.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/resolve-mention.test.js +0 -130
- package/dist/src/business/pipeline/middlewares/resolve-quote.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/resolve-quote.test.js +0 -149
- package/dist/src/business/pipeline/middlewares/resolve-route.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/resolve-route.test.js +0 -87
- package/dist/src/business/pipeline/middlewares/rewrite-body.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/rewrite-body.test.js +0 -107
- package/dist/src/business/pipeline/middlewares/skip-placeholder.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/skip-placeholder.test.js +0 -97
- package/dist/src/business/pipeline/middlewares/skip-self.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/skip-self.test.js +0 -34
- package/dist/src/business/pipeline/test-helpers/mock-ctx.d.ts +0 -25
- package/dist/src/business/pipeline/test-helpers/mock-ctx.js +0 -116
- package/dist/src/business/utils/media.test.d.ts +0 -5
- package/dist/src/business/utils/media.test.js +0 -218
- package/dist/src/business/utils/utils.test.d.ts +0 -6
- package/dist/src/business/utils/utils.test.js +0 -43
- package/dist/src/infra/cache/ttl-db.test.d.ts +0 -4
- package/dist/src/infra/cache/ttl-db.test.js +0 -55
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ through direct messages and group chats.
|
|
|
18
18
|
|
|
19
19
|
## Quick start
|
|
20
20
|
|
|
21
|
-
> **Requires OpenClaw 2026.
|
|
21
|
+
> **Requires OpenClaw 2026.5.7 or above.** Run `openclaw --version` to check. Upgrade with `openclaw update`.
|
|
22
22
|
|
|
23
23
|
### 1. Add the Yuanbao channel with your credentials
|
|
24
24
|
|
|
@@ -44,6 +44,12 @@ export declare function getTokenStatus(accountId: string): {
|
|
|
44
44
|
status: "valid" | "expired" | "refreshing" | "none";
|
|
45
45
|
expiresAt: number | null;
|
|
46
46
|
};
|
|
47
|
+
export declare function computeSignature(params: {
|
|
48
|
+
nonce: string;
|
|
49
|
+
timestamp: string;
|
|
50
|
+
appKey: string;
|
|
51
|
+
appSecret: string;
|
|
52
|
+
}): string;
|
|
47
53
|
export declare function verifySignature(expected: string, actual: string): boolean;
|
|
48
54
|
export declare function getSignToken(account: ResolvedYuanbaoAccount, log?: Log): Promise<SignTokenData>;
|
|
49
55
|
export declare function forceRefreshSignToken(account: ResolvedYuanbaoAccount, log?: Log): Promise<SignTokenData>;
|
|
@@ -46,7 +46,7 @@ export function getTokenStatus(accountId) {
|
|
|
46
46
|
expiresAt: cached.expiresAt,
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
|
-
function computeSignature(params) {
|
|
49
|
+
export function computeSignature(params) {
|
|
50
50
|
const plain = params.nonce + params.timestamp + params.appKey + params.appSecret;
|
|
51
51
|
return createHmac("sha256", params.appSecret).update(plain).digest("hex");
|
|
52
52
|
}
|
|
@@ -74,6 +74,7 @@ export function toProtoMsgBody(elements) {
|
|
|
74
74
|
url: c.url,
|
|
75
75
|
fileSize: c.file_size,
|
|
76
76
|
fileName: c.file_name,
|
|
77
|
+
extMap: c.ext_map,
|
|
77
78
|
},
|
|
78
79
|
};
|
|
79
80
|
});
|
|
@@ -122,6 +123,9 @@ export function fromProtoMsgBody(elements) {
|
|
|
122
123
|
if (mc?.fileName) {
|
|
123
124
|
content.file_name = mc.fileName;
|
|
124
125
|
}
|
|
126
|
+
if (mc?.extMap && Object.keys(mc.extMap).length > 0) {
|
|
127
|
+
content.ext_map = mc.extMap;
|
|
128
|
+
}
|
|
125
129
|
return {
|
|
126
130
|
msg_type: el.msgType || "",
|
|
127
131
|
msg_content: content,
|
|
@@ -15,6 +15,7 @@ import { resolveYuanbaoAccount } from "../../accounts.js";
|
|
|
15
15
|
import { createLog } from "../../logger.js";
|
|
16
16
|
import { getYuanbaoRuntime } from "../../runtime.js";
|
|
17
17
|
import { createMessageSender } from "../outbound/create-sender.js";
|
|
18
|
+
import { getActiveTraceContext } from "../trace/context.js";
|
|
18
19
|
import { resolveActionTarget } from "./resolve-target.js";
|
|
19
20
|
import { searchSticker } from "./sticker/send.js";
|
|
20
21
|
/**
|
|
@@ -135,8 +136,14 @@ export async function handleAction(input) {
|
|
|
135
136
|
}
|
|
136
137
|
log.error(`${item.type} send failed: ${result.error}`);
|
|
137
138
|
}
|
|
138
|
-
else
|
|
139
|
-
|
|
139
|
+
else {
|
|
140
|
+
// Mark outbound delivered on the active agent-run trace context so
|
|
141
|
+
// dispatch-reply won't mistake an action-only reply (e.g. a sticker
|
|
142
|
+
// with no text) for an empty reply and send the fallback text.
|
|
143
|
+
getActiveTraceContext()?.markActionDelivered();
|
|
144
|
+
if (result.messageId) {
|
|
145
|
+
lastMessageId = result.messageId;
|
|
146
|
+
}
|
|
140
147
|
}
|
|
141
148
|
}
|
|
142
149
|
return { channel: "yuanbao", ok: true, messageId: lastMessageId };
|
|
@@ -7,6 +7,10 @@ import type { ResolvedYuanbaoAccount } from "../../types.js";
|
|
|
7
7
|
/** Message processing context */
|
|
8
8
|
export type MessageHandlerContext = {
|
|
9
9
|
groupCode?: string;
|
|
10
|
+
/** User ID/account that sent the message; used for ext_map key matching. */
|
|
11
|
+
fromAccount?: string;
|
|
12
|
+
/** Display name of the message sender; used by forwarded-record parsing. */
|
|
13
|
+
senderNickname?: string;
|
|
10
14
|
account: ResolvedYuanbaoAccount;
|
|
11
15
|
config: OpenClawConfig;
|
|
12
16
|
core: PluginRuntime;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protobuf decoder for WeChat forwarded chat-record payloads.
|
|
3
|
+
*
|
|
4
|
+
* The server stores `ForwardMsgData` in `MsgContent.ext_map` as a base64
|
|
5
|
+
* string whose decoded bytes are protobuf wire data.
|
|
6
|
+
*/
|
|
7
|
+
import type { ForwardMsgData } from "./forward-records.js";
|
|
8
|
+
export declare function decodeForwardMsgDataBase64(value: string): ForwardMsgData | undefined;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protobuf decoder for WeChat forwarded chat-record payloads.
|
|
3
|
+
*
|
|
4
|
+
* The server stores `ForwardMsgData` in `MsgContent.ext_map` as a base64
|
|
5
|
+
* string whose decoded bytes are protobuf wire data.
|
|
6
|
+
*/
|
|
7
|
+
import protobuf from "protobufjs";
|
|
8
|
+
const FORWARD_PROTO_DESCRIPTOR = {
|
|
9
|
+
nested: {
|
|
10
|
+
ForwardMsgData: {
|
|
11
|
+
fields: {
|
|
12
|
+
sub_type: { type: "int32", id: 1 },
|
|
13
|
+
msg_begin_time: { type: "int64", id: 2 },
|
|
14
|
+
msg_end_time: { type: "int64", id: 3 },
|
|
15
|
+
nick_name: { type: "string", id: 4 },
|
|
16
|
+
msg: { rule: "repeated", type: "Msg", id: 5 },
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
Msg: {
|
|
20
|
+
fields: {
|
|
21
|
+
sender: { type: "string", id: 1 },
|
|
22
|
+
time: { type: "int64", id: 2 },
|
|
23
|
+
plainText: { type: "string", id: 3 },
|
|
24
|
+
msgContent: { rule: "repeated", type: "MsgContent", id: 4 },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
MsgContent: {
|
|
28
|
+
fields: {
|
|
29
|
+
type: { type: "int32", id: 1 },
|
|
30
|
+
text: { type: "string", id: 2 },
|
|
31
|
+
multimedia: { rule: "repeated", type: "Multimedia", id: 3 },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
Multimedia: {
|
|
35
|
+
fields: {
|
|
36
|
+
type: { type: "string", id: 1 },
|
|
37
|
+
url: { type: "string", id: 2 },
|
|
38
|
+
origin_url: { type: "string", id: 3 },
|
|
39
|
+
file_name: { type: "string", id: 4 },
|
|
40
|
+
size: { type: "int64", id: 5 },
|
|
41
|
+
width: { type: "int32", id: 6 },
|
|
42
|
+
height: { type: "int32", id: 7 },
|
|
43
|
+
style: { type: "string", id: 8 },
|
|
44
|
+
pendants: { rule: "repeated", type: "string", id: 9 },
|
|
45
|
+
created_time: { type: "int64", id: 10 },
|
|
46
|
+
index_url: { type: "string", id: 11 },
|
|
47
|
+
cover_url: { type: "string", id: 12 },
|
|
48
|
+
duration: { type: "int64", id: 13 },
|
|
49
|
+
guide_id: { type: "int32", id: 14 },
|
|
50
|
+
media_id: { type: "string", id: 15 },
|
|
51
|
+
extract: { type: "bool", id: 16 },
|
|
52
|
+
title: { type: "string", id: 17 },
|
|
53
|
+
content: { type: "string", id: 18 },
|
|
54
|
+
session_id: { type: "string", id: 19 },
|
|
55
|
+
question_id: { type: "string", id: 20 },
|
|
56
|
+
goods_trace_id: { type: "string", id: 21 },
|
|
57
|
+
cache_key: { type: "string", id: 22 },
|
|
58
|
+
chat_record_id: { type: "string", id: 23 },
|
|
59
|
+
doc_type: { type: "string", id: 24 },
|
|
60
|
+
image_base64: { type: "string", id: 25 },
|
|
61
|
+
parse_file_type: { type: "string", id: 26 },
|
|
62
|
+
parse_file_url: { type: "string", id: 27 },
|
|
63
|
+
sensitive: { type: "bool", id: 28 },
|
|
64
|
+
extra: { type: "string", id: 29 },
|
|
65
|
+
icon_url: { type: "string", id: 30 },
|
|
66
|
+
source: { type: "string", id: 31 },
|
|
67
|
+
app_id: { type: "string", id: 32 },
|
|
68
|
+
link_url: { type: "string", id: 33 },
|
|
69
|
+
msg_count: { type: "int32", id: 34 },
|
|
70
|
+
msg_time: { type: "int64", id: 35 },
|
|
71
|
+
forward_msg_id: { type: "string", id: 36 },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
let forwardMsgDataType;
|
|
77
|
+
function getForwardMsgDataType() {
|
|
78
|
+
if (!forwardMsgDataType) {
|
|
79
|
+
forwardMsgDataType = protobuf.Root.fromJSON(FORWARD_PROTO_DESCRIPTOR).lookupType("ForwardMsgData");
|
|
80
|
+
}
|
|
81
|
+
return forwardMsgDataType;
|
|
82
|
+
}
|
|
83
|
+
export function decodeForwardMsgDataBase64(value) {
|
|
84
|
+
try {
|
|
85
|
+
const bytes = Buffer.from(value, "base64");
|
|
86
|
+
if (bytes.length === 0) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
const type = getForwardMsgDataType();
|
|
90
|
+
const decoded = type.decode(bytes);
|
|
91
|
+
const object = type.toObject(decoded, {
|
|
92
|
+
longs: String,
|
|
93
|
+
enums: Number,
|
|
94
|
+
bytes: String,
|
|
95
|
+
arrays: true,
|
|
96
|
+
objects: true,
|
|
97
|
+
});
|
|
98
|
+
return object && typeof object === "object" ? object : undefined;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeChat forwarded chat-record (elem_type 1009) parsing.
|
|
3
|
+
*
|
|
4
|
+
* A forwarded chat record carries a truncated summary in `msg_content.data.text`
|
|
5
|
+
* and the full structured detail in `msg_content.ext_map` (protobuf field 999).
|
|
6
|
+
* The ext_map value is a base64-encoded protobuf `ForwardMsgData`.
|
|
7
|
+
*
|
|
8
|
+
* This module turns that detail into readable chat-record lines and appends
|
|
9
|
+
* contained images/files to the shared `medias` list so the existing download
|
|
10
|
+
* pipeline fetches them — matching how imageHandler / fileHandler already behave.
|
|
11
|
+
*/
|
|
12
|
+
import type { ExtractTextFromMsgBodyResult } from "../types.js";
|
|
13
|
+
/** Multimedia entry inside a forwarded message content. */
|
|
14
|
+
export interface ForwardMultimedia {
|
|
15
|
+
type?: string;
|
|
16
|
+
url?: string;
|
|
17
|
+
origin_url?: string;
|
|
18
|
+
parse_file_url?: string;
|
|
19
|
+
link_url?: string;
|
|
20
|
+
file_name?: string;
|
|
21
|
+
media_id?: string;
|
|
22
|
+
doc_type?: string;
|
|
23
|
+
title?: string;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
/** One content fragment of a forwarded message. */
|
|
27
|
+
export interface ForwardMsgContent {
|
|
28
|
+
type?: number;
|
|
29
|
+
text?: string;
|
|
30
|
+
multimedia?: ForwardMultimedia[];
|
|
31
|
+
}
|
|
32
|
+
/** One message inside a forwarded chat record. */
|
|
33
|
+
export interface ForwardMsg {
|
|
34
|
+
sender?: string;
|
|
35
|
+
time?: number | string;
|
|
36
|
+
plainText?: string;
|
|
37
|
+
msgContent?: ForwardMsgContent[];
|
|
38
|
+
}
|
|
39
|
+
/** Parsed `ForwardMsgData` (ext_map value). */
|
|
40
|
+
export interface ForwardMsgData {
|
|
41
|
+
sub_type?: number;
|
|
42
|
+
msg_begin_time?: number | string;
|
|
43
|
+
msg_end_time?: number | string;
|
|
44
|
+
nick_name?: string;
|
|
45
|
+
msg?: ForwardMsg[];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Extract the `ForwardMsgData` from a `msg_content.ext_map`.
|
|
49
|
+
*
|
|
50
|
+
* Matching strategy (per spec): prefer a `wexin_forward_msg_*` key that ends
|
|
51
|
+
* with `_{userId}`; otherwise fall back to the first entry that parses into a
|
|
52
|
+
* WeChat chat record (`sub_type === 1`). Returns undefined when none matches.
|
|
53
|
+
*/
|
|
54
|
+
export declare function parseForwardMsgData(extMap: Record<string, unknown> | undefined, userId?: string): ForwardMsgData | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* Build a structured text block from a forwarded chat record and append any
|
|
57
|
+
* contained images/files to `resData.medias` (and link URLs to
|
|
58
|
+
* `resData.linkUrls`) for the downstream download / link-understanding steps.
|
|
59
|
+
*
|
|
60
|
+
* @param senderNickname display name of the user who forwarded the record.
|
|
61
|
+
* @returns the structured text, or undefined when the record has no messages.
|
|
62
|
+
*/
|
|
63
|
+
export declare function buildForwardRecordsText(data: ForwardMsgData, resData: ExtractTextFromMsgBodyResult, senderNickname?: string): string | undefined;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeChat forwarded chat-record (elem_type 1009) parsing.
|
|
3
|
+
*
|
|
4
|
+
* A forwarded chat record carries a truncated summary in `msg_content.data.text`
|
|
5
|
+
* and the full structured detail in `msg_content.ext_map` (protobuf field 999).
|
|
6
|
+
* The ext_map value is a base64-encoded protobuf `ForwardMsgData`.
|
|
7
|
+
*
|
|
8
|
+
* This module turns that detail into readable chat-record lines and appends
|
|
9
|
+
* contained images/files to the shared `medias` list so the existing download
|
|
10
|
+
* pipeline fetches them — matching how imageHandler / fileHandler already behave.
|
|
11
|
+
*/
|
|
12
|
+
import { sanitizeMediaFilename } from "../../../utils/media.js";
|
|
13
|
+
import { decodeForwardMsgDataBase64 } from "./forward-records-proto.js";
|
|
14
|
+
/** EnumMsgContentType inside a forwarded record. */
|
|
15
|
+
var ForwardContentType;
|
|
16
|
+
(function (ForwardContentType) {
|
|
17
|
+
ForwardContentType[ForwardContentType["Text"] = 1] = "Text";
|
|
18
|
+
ForwardContentType[ForwardContentType["Multimedia"] = 2] = "Multimedia";
|
|
19
|
+
ForwardContentType[ForwardContentType["ForwardMsg"] = 3] = "ForwardMsg";
|
|
20
|
+
})(ForwardContentType || (ForwardContentType = {}));
|
|
21
|
+
/** ext_map key prefix for WeChat forwarded chat records. */
|
|
22
|
+
const FORWARD_KEY_PREFIX = "wexin_forward_msg_";
|
|
23
|
+
/** Cap the number of records folded into a prompt to keep it bounded. */
|
|
24
|
+
const MAX_RECORDS = 100;
|
|
25
|
+
const HEADER_RECORDS = "以下为用户的聊天记录";
|
|
26
|
+
/**
|
|
27
|
+
* Extract the `ForwardMsgData` from a `msg_content.ext_map`.
|
|
28
|
+
*
|
|
29
|
+
* Matching strategy (per spec): prefer a `wexin_forward_msg_*` key that ends
|
|
30
|
+
* with `_{userId}`; otherwise fall back to the first entry that parses into a
|
|
31
|
+
* WeChat chat record (`sub_type === 1`). Returns undefined when none matches.
|
|
32
|
+
*/
|
|
33
|
+
export function parseForwardMsgData(extMap, userId) {
|
|
34
|
+
if (!extMap || typeof extMap !== "object") {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
const entries = Object.entries(extMap).filter(([key]) => key.startsWith(FORWARD_KEY_PREFIX));
|
|
38
|
+
if (entries.length === 0) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
// Prefer a key whose suffix matches the current user; otherwise keep order.
|
|
42
|
+
const ordered = userId
|
|
43
|
+
? [...entries].sort(([a], [b]) => {
|
|
44
|
+
const am = a.endsWith(`_${userId}`) ? 0 : 1;
|
|
45
|
+
const bm = b.endsWith(`_${userId}`) ? 0 : 1;
|
|
46
|
+
return am - bm;
|
|
47
|
+
})
|
|
48
|
+
: entries;
|
|
49
|
+
for (const [, value] of ordered) {
|
|
50
|
+
const data = coerceForwardData(value);
|
|
51
|
+
if (data && Number(data.sub_type) === 1) {
|
|
52
|
+
return data;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
function coerceForwardData(value) {
|
|
58
|
+
if (typeof value === "string") {
|
|
59
|
+
return decodeForwardMsgDataBase64(value);
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Build a structured text block from a forwarded chat record and append any
|
|
65
|
+
* contained images/files to `resData.medias` (and link URLs to
|
|
66
|
+
* `resData.linkUrls`) for the downstream download / link-understanding steps.
|
|
67
|
+
*
|
|
68
|
+
* @param senderNickname display name of the user who forwarded the record.
|
|
69
|
+
* @returns the structured text, or undefined when the record has no messages.
|
|
70
|
+
*/
|
|
71
|
+
export function buildForwardRecordsText(data, resData, senderNickname) {
|
|
72
|
+
const msgList = Array.isArray(data.msg) ? data.msg.slice(0, MAX_RECORDS) : [];
|
|
73
|
+
if (msgList.length === 0) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const lines = [];
|
|
77
|
+
if (senderNickname) {
|
|
78
|
+
lines.push(`当前用户的昵称为${senderNickname}`);
|
|
79
|
+
}
|
|
80
|
+
lines.push(HEADER_RECORDS);
|
|
81
|
+
for (const msg of msgList) {
|
|
82
|
+
const sender = msg.sender ?? "";
|
|
83
|
+
const parts = buildMessageParts(msg, resData);
|
|
84
|
+
const body = parts.length > 0 ? parts.join(" ") : (msg.plainText ?? "");
|
|
85
|
+
lines.push(`${sender}:${body}`);
|
|
86
|
+
}
|
|
87
|
+
return lines.join("\n");
|
|
88
|
+
}
|
|
89
|
+
function buildMessageParts(msg, resData) {
|
|
90
|
+
const contents = Array.isArray(msg.msgContent) ? msg.msgContent : [];
|
|
91
|
+
if (contents.length === 0) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
const parts = [];
|
|
95
|
+
for (const content of contents) {
|
|
96
|
+
switch (content.type) {
|
|
97
|
+
case ForwardContentType.Text:
|
|
98
|
+
if (content.text) {
|
|
99
|
+
parts.push(content.text);
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
case ForwardContentType.Multimedia:
|
|
103
|
+
for (const media of content.multimedia ?? []) {
|
|
104
|
+
const part = appendMedia(media, resData);
|
|
105
|
+
if (part) {
|
|
106
|
+
parts.push(part);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
case ForwardContentType.ForwardMsg:
|
|
111
|
+
parts.push("[嵌套聊天记录]");
|
|
112
|
+
break;
|
|
113
|
+
default:
|
|
114
|
+
if (msg.plainText) {
|
|
115
|
+
parts.push(msg.plainText);
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return parts;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Resolve a single multimedia item to a text placeholder and, for downloadable
|
|
124
|
+
* media (image/file/code/video-as-file), push it to `resData.medias`. Link
|
|
125
|
+
* shares are recorded in `resData.linkUrls` for link understanding.
|
|
126
|
+
*/
|
|
127
|
+
function appendMedia(media, resData) {
|
|
128
|
+
const mediaType = (media.type || media.doc_type || "").toLowerCase();
|
|
129
|
+
const url = media.url || media.origin_url || media.parse_file_url || media.link_url || "";
|
|
130
|
+
switch (mediaType) {
|
|
131
|
+
case "image": {
|
|
132
|
+
if (!url) {
|
|
133
|
+
return `[image:${media.file_name || "image"}]`;
|
|
134
|
+
}
|
|
135
|
+
const count = resData.medias.filter(m => m.mediaType === "image").length + 1;
|
|
136
|
+
const mediaName = sanitizeMediaFilename(media.file_name || media.media_id, `image${count}`);
|
|
137
|
+
resData.medias.push({ mediaType: "image", url, mediaName });
|
|
138
|
+
return `[image:${mediaName}]`;
|
|
139
|
+
}
|
|
140
|
+
case "file":
|
|
141
|
+
case "code":
|
|
142
|
+
case "document": {
|
|
143
|
+
if (!url) {
|
|
144
|
+
return `[file:${media.file_name || "file"}]`;
|
|
145
|
+
}
|
|
146
|
+
const count = resData.medias.filter(m => m.mediaType === "file").length + 1;
|
|
147
|
+
const mediaName = sanitizeMediaFilename(media.file_name || media.media_id, `file${count}`);
|
|
148
|
+
resData.medias.push({ mediaType: "file", url, mediaName });
|
|
149
|
+
return `[file:${mediaName}]`;
|
|
150
|
+
}
|
|
151
|
+
case "url": {
|
|
152
|
+
if (url) {
|
|
153
|
+
resData.linkUrls.push(url);
|
|
154
|
+
}
|
|
155
|
+
return `[link] ${[media.title || media.file_name, url].filter(Boolean).join(" ")}`.trimEnd();
|
|
156
|
+
}
|
|
157
|
+
case "video": {
|
|
158
|
+
if (!url) {
|
|
159
|
+
return `[video] ${media.file_name || "video"}`;
|
|
160
|
+
}
|
|
161
|
+
const count = resData.medias.filter(m => m.mediaType === "file").length + 1;
|
|
162
|
+
const mediaName = sanitizeMediaFilename(media.file_name || media.media_id, `video${count}`);
|
|
163
|
+
resData.medias.push({ mediaType: "file", url, mediaName });
|
|
164
|
+
return `[video:${mediaName}]`;
|
|
165
|
+
}
|
|
166
|
+
default:
|
|
167
|
+
return `[${mediaType || "media"}] ${url || media.file_name || ""}`.trimEnd();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* supports constructing custom message body.
|
|
6
6
|
*/
|
|
7
7
|
import { createLog } from "../../../../logger.js";
|
|
8
|
+
import { buildForwardRecordsText, parseForwardMsgData } from "./forward-records.js";
|
|
8
9
|
import { extractLinkCard, extractLinkCardUrls } from "./link-card.js";
|
|
9
10
|
/**
|
|
10
11
|
* Build @user TIMCustomElem message body element.
|
|
@@ -71,15 +72,39 @@ export const customHandler = {
|
|
|
71
72
|
}
|
|
72
73
|
return extractLinkCard(customContent);
|
|
73
74
|
}
|
|
75
|
+
case 1009: {
|
|
76
|
+
// WeChat forwarded chat record: detail lives in msg_content.ext_map.
|
|
77
|
+
const extMap = elem.msg_content?.ext_map;
|
|
78
|
+
const extEntries = extMap ? Object.entries(extMap) : [];
|
|
79
|
+
const forwardData = parseForwardMsgData(extMap, ctx.fromAccount);
|
|
80
|
+
const summary = typeof customContent?.text === "string" ? customContent.text : undefined;
|
|
81
|
+
if (forwardData) {
|
|
82
|
+
const text = buildForwardRecordsText(forwardData, resData, ctx.senderNickname);
|
|
83
|
+
if (text) {
|
|
84
|
+
log.info("forwarded chat record parsed", {
|
|
85
|
+
recordCount: forwardData.msg?.length ?? 0,
|
|
86
|
+
mediaCount: resData.medias.length,
|
|
87
|
+
linkCount: resData.linkUrls.length,
|
|
88
|
+
});
|
|
89
|
+
return text;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Fallback: use the truncated summary so the AI still gets context.
|
|
93
|
+
log.warn("forwarded chat record parse failed", {
|
|
94
|
+
hasExtMap: Boolean(extMap),
|
|
95
|
+
extMapKeyCount: extEntries.length,
|
|
96
|
+
extMapValueTypes: extEntries.map(([, value]) => typeof value),
|
|
97
|
+
hasSummary: Boolean(summary),
|
|
98
|
+
});
|
|
99
|
+
return summary || "用户转发了一条聊天记录,但插件未收到详细内容";
|
|
100
|
+
}
|
|
74
101
|
default:
|
|
75
102
|
return FALLBACK_TEXT;
|
|
76
103
|
}
|
|
77
104
|
}
|
|
78
105
|
catch {
|
|
79
106
|
// JSON parse failed, fall back to placeholder
|
|
80
|
-
log.debug("TIMCustomElem data JSON parse failed"
|
|
81
|
-
data: elem.msg_content?.data,
|
|
82
|
-
});
|
|
107
|
+
log.debug("TIMCustomElem data JSON parse failed");
|
|
83
108
|
}
|
|
84
109
|
}
|
|
85
110
|
return FALLBACK_TEXT;
|
|
@@ -19,6 +19,7 @@ export interface QueueSessionOptions {
|
|
|
19
19
|
maxChars?: number;
|
|
20
20
|
chunkText?: (text: string, maxChars: number) => string[];
|
|
21
21
|
}
|
|
22
|
+
export declare function defaultChunkText(text: string, max: number): string[];
|
|
22
23
|
export declare function createQueueSession(opts: QueueSessionOptions): QueueSession;
|
|
23
24
|
declare function createMergeTextSession(opts: QueueSessionOptions): QueueSession;
|
|
24
25
|
export { createMergeTextSession as createMergeTextSessionForTest };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Outbound queue (MessageSender-based). */
|
|
2
2
|
import { createLog } from "../../logger.js";
|
|
3
3
|
import { mdFence, mdBlock, mdAtomic, mdTable } from "../utils/markdown.js";
|
|
4
|
-
function defaultChunkText(text, max) {
|
|
4
|
+
export function defaultChunkText(text, max) {
|
|
5
5
|
if (text.length <= max) {
|
|
6
6
|
return [text];
|
|
7
7
|
}
|
|
@@ -202,7 +202,12 @@ export const dispatchReply = {
|
|
|
202
202
|
}
|
|
203
203
|
// ⭐ Flush outbound queue
|
|
204
204
|
const flushed = await queueSession.flush();
|
|
205
|
-
|
|
205
|
+
// The model may reply purely through a message action (e.g. sticker/react)
|
|
206
|
+
// which is delivered via handleAction and bypasses queueSession entirely.
|
|
207
|
+
// Such deliveries mark the agent-run trace context, so treat them as real
|
|
208
|
+
// outbound content and skip the fallback reply.
|
|
209
|
+
const deliveredViaAction = ctx.traceContext?.hasActionDelivered() ?? false;
|
|
210
|
+
if (!flushed && !hasSentContent && !deliveredViaAction && !ctx.abortSignal?.aborted) {
|
|
206
211
|
const { fallbackReply } = account;
|
|
207
212
|
if (fallbackReply) {
|
|
208
213
|
ctx.log.warn("[dispatch-reply] AI returned no reply content, using fallback reply");
|
|
@@ -15,15 +15,22 @@ export const extractContent = {
|
|
|
15
15
|
// Direct message opened from group chat panel; carry group_code
|
|
16
16
|
ctx.groupCode = raw.private_from_group_code;
|
|
17
17
|
}
|
|
18
|
-
// Build minimal ctx compatible with extractTextFromMsgBody's MessageHandlerContext
|
|
19
|
-
//
|
|
18
|
+
// Build minimal ctx compatible with extractTextFromMsgBody's MessageHandlerContext.
|
|
19
|
+
// Pass the real logger through so element-level diagnostics are not swallowed.
|
|
20
20
|
const minCtx = {
|
|
21
21
|
account: ctx.account,
|
|
22
22
|
config: ctx.config,
|
|
23
23
|
core: ctx.core,
|
|
24
|
-
log: {
|
|
24
|
+
log: {
|
|
25
|
+
info: ctx.log.info.bind(ctx.log),
|
|
26
|
+
warn: ctx.log.warn.bind(ctx.log),
|
|
27
|
+
error: ctx.log.error.bind(ctx.log),
|
|
28
|
+
verbose: ctx.log.debug.bind(ctx.log),
|
|
29
|
+
},
|
|
25
30
|
wsClient: ctx.wsClient,
|
|
26
31
|
groupCode: ctx.groupCode,
|
|
32
|
+
fromAccount: ctx.fromAccount,
|
|
33
|
+
senderNickname: ctx.senderNickname,
|
|
27
34
|
};
|
|
28
35
|
const { rawBody, isAtBot, medias, mentions, linkUrls } = extractTextFromMsgBody(minCtx, raw.msg_body);
|
|
29
36
|
ctx.rawBody = rawBody;
|
|
@@ -222,10 +222,10 @@ function resolveCronBase(params, intent) {
|
|
|
222
222
|
// ============================================================================
|
|
223
223
|
/** Builds a Gateway job config for a one-time job. */
|
|
224
224
|
function buildOnceJob(params, time, to, accountId, intent) {
|
|
225
|
-
const { name,
|
|
225
|
+
const { name, atStr: at, message } = resolveOnceBase(params, time, intent);
|
|
226
226
|
return {
|
|
227
227
|
name,
|
|
228
|
-
schedule: { kind: 'at',
|
|
228
|
+
schedule: { kind: 'at', at },
|
|
229
229
|
sessionTarget: 'isolated',
|
|
230
230
|
wakeMode: 'now',
|
|
231
231
|
deleteAfterRun: true,
|
|
@@ -411,7 +411,7 @@ async function executeGateway(gatewayTool, p, resolvedTo, accountId) {
|
|
|
411
411
|
if (isCronExpression(p.time)) {
|
|
412
412
|
const job = buildCronJob({ ...p, content: p.content.trim() }, resolvedTo, accountId, intent);
|
|
413
413
|
try {
|
|
414
|
-
const cronResult = await gatewayTool('cron.add', { timeoutMs: DEFAULT_GATEWAY_TIMEOUT_MS },
|
|
414
|
+
const cronResult = await gatewayTool('cron.add', { timeoutMs: DEFAULT_GATEWAY_TIMEOUT_MS }, job);
|
|
415
415
|
const typeLabel = intent === 'task' ? '循环任务' : '周期提醒';
|
|
416
416
|
return json({
|
|
417
417
|
status: 'ok',
|
|
@@ -430,7 +430,7 @@ async function executeGateway(gatewayTool, p, resolvedTo, accountId) {
|
|
|
430
430
|
return json(timeResult);
|
|
431
431
|
const job = buildOnceJob({ ...p, content: p.content.trim() }, timeResult.timeSpec, resolvedTo, accountId, intent);
|
|
432
432
|
try {
|
|
433
|
-
const cronResult = await gatewayTool('cron.add', { timeoutMs: DEFAULT_GATEWAY_TIMEOUT_MS },
|
|
433
|
+
const cronResult = await gatewayTool('cron.add', { timeoutMs: DEFAULT_GATEWAY_TIMEOUT_MS }, job);
|
|
434
434
|
return json({
|
|
435
435
|
status: 'ok',
|
|
436
436
|
action: 'add',
|
|
@@ -4,6 +4,15 @@ export type YuanbaoTraceContext = {
|
|
|
4
4
|
seqId?: string;
|
|
5
5
|
/** Auto-incremented based on inbound seqId */
|
|
6
6
|
nextMsgSeq: () => number | undefined;
|
|
7
|
+
/**
|
|
8
|
+
* Mark that an outbound message was successfully delivered via a message
|
|
9
|
+
* action (e.g. sticker/react/send) during this agent run. Used by
|
|
10
|
+
* dispatch-reply to avoid sending the fallback reply when the model already
|
|
11
|
+
* replied through an action rather than the deliver callback.
|
|
12
|
+
*/
|
|
13
|
+
markActionDelivered: () => void;
|
|
14
|
+
/** Whether any action-driven outbound succeeded within this agent run. */
|
|
15
|
+
hasActionDelivered: () => boolean;
|
|
7
16
|
};
|
|
8
17
|
/**
|
|
9
18
|
* Generate a random trace ID (32-char hex string).
|
|
@@ -59,6 +59,11 @@ export function resolveTraceContext(params) {
|
|
|
59
59
|
seqCounter++;
|
|
60
60
|
return baseSeq + seqCounter;
|
|
61
61
|
};
|
|
62
|
+
let actionDelivered = false;
|
|
63
|
+
const markActionDelivered = () => {
|
|
64
|
+
actionDelivered = true;
|
|
65
|
+
};
|
|
66
|
+
const hasActionDelivered = () => actionDelivered;
|
|
62
67
|
const log = createLog("trace");
|
|
63
68
|
log.debug("[msg-trace] resolve context", {
|
|
64
69
|
traceId,
|
|
@@ -69,6 +74,8 @@ export function resolveTraceContext(params) {
|
|
|
69
74
|
traceId,
|
|
70
75
|
traceparent: buildTraceparent(traceId),
|
|
71
76
|
nextMsgSeq,
|
|
77
|
+
markActionDelivered,
|
|
78
|
+
hasActionDelivered,
|
|
72
79
|
...(seqId ? { seqId } : {}),
|
|
73
80
|
};
|
|
74
81
|
}
|