@unlinearity/cli-wechat-bridge 1.1.6 → 1.2.1

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.
Files changed (88) hide show
  1. package/LICENSE.txt +661 -661
  2. package/README.md +468 -475
  3. package/bin/_run-entry.mjs +29 -6
  4. package/bin/wechat-check-update.mjs +6 -1
  5. package/bin/wechat-claude.mjs +6 -1
  6. package/bin/wechat-codex.mjs +6 -1
  7. package/bin/wechat-daemon.mjs +6 -1
  8. package/bin/wechat-opencode.mjs +6 -1
  9. package/bin/wechat-pi.mjs +6 -1
  10. package/bin/wechat-setup.mjs +6 -1
  11. package/bin/wecom-check-update.mjs +10 -0
  12. package/bin/wecom-claude.mjs +10 -0
  13. package/bin/wecom-codex.mjs +10 -0
  14. package/bin/wecom-daemon.mjs +10 -0
  15. package/bin/wecom-opencode.mjs +10 -0
  16. package/bin/wecom-pi.mjs +10 -0
  17. package/bin/wecom-setup.mjs +10 -0
  18. package/dist/bridge/adapter-control.js +58 -0
  19. package/dist/bridge/bridge-adapters.claude.js +161 -4
  20. package/dist/bridge/bridge-adapters.codex.js +206 -14
  21. package/dist/bridge/bridge-adapters.core.js +25 -1
  22. package/dist/bridge/bridge-adapters.opencode.js +298 -20
  23. package/dist/bridge/bridge-adapters.pi.js +45 -0
  24. package/dist/bridge/bridge-adapters.shared.js +6 -0
  25. package/dist/bridge/bridge-controller.js +6 -0
  26. package/dist/bridge/bridge-final-reply.js +6 -0
  27. package/dist/bridge/bridge-process-reaper.js +41 -2
  28. package/dist/bridge/bridge-state.js +16 -3
  29. package/dist/bridge/bridge-utils.js +6 -31
  30. package/dist/bridge/claude-hook.js +6 -0
  31. package/dist/bridge/claude-native-controls.js +145 -0
  32. package/dist/bridge/codex-visible-thread-proxy.js +140 -0
  33. package/dist/bridge/native-terminal-control.js +271 -0
  34. package/dist/bridge/opencode-v2-compat.js +450 -0
  35. package/dist/bridge/wechat-bridge.js +380 -171
  36. package/dist/bridge/wechat-forwarding.js +6 -0
  37. package/dist/bridge/wechat-outbound-queue.js +6 -0
  38. package/dist/channels/wechat/bridge-final-reply.js +13 -22
  39. package/dist/channels/wechat/index.js +6 -0
  40. package/dist/channels/wechat/wechat-driver.js +191 -0
  41. package/dist/channels/wechat/wechat-forwarding.js +18 -0
  42. package/dist/channels/wechat/wechat-outbound-queue.js +49 -3
  43. package/dist/channels/wecom/bridge-final-reply.js +32 -0
  44. package/dist/channels/wecom/setup-cli.js +12 -0
  45. package/dist/channels/wecom/setup.js +182 -0
  46. package/dist/channels/wecom/wecom-channel-port.js +45 -0
  47. package/dist/channels/wecom/wecom-config.js +72 -0
  48. package/dist/channels/wecom/wecom-driver.js +106 -0
  49. package/dist/channels/wecom/wecom-media.js +147 -0
  50. package/dist/channels/wecom/wecom-message.js +149 -0
  51. package/dist/channels/wecom/wecom-transport.js +314 -0
  52. package/dist/commands/check-update.js +6 -0
  53. package/dist/companion/codex-remote-client.js +30 -3
  54. package/dist/companion/codex-visible-client-link.js +107 -0
  55. package/dist/companion/local-companion-link.js +6 -0
  56. package/dist/companion/local-companion-start.js +62 -11
  57. package/dist/companion/local-companion.js +25 -1
  58. package/dist/companion/opencode-tui-bridge-plugin.js +6 -0
  59. package/dist/companion/opencode-v2-tui-bridge-plugin.js +93 -0
  60. package/dist/companion/pi-tui-bridge-extension.js +62 -3
  61. package/dist/core/bridge-control.js +10 -0
  62. package/dist/core/bridge-defer.js +25 -0
  63. package/dist/core/channel-driver.js +1 -0
  64. package/dist/core/channel-types.js +3 -1
  65. package/dist/core/conversation-routing.js +58 -0
  66. package/dist/core/direct-run.js +23 -0
  67. package/dist/core/inbound-message-claims.js +67 -0
  68. package/dist/core/outbound-target.js +32 -0
  69. package/dist/core/text-utils.js +6 -0
  70. package/dist/core/turn-coordinator.js +85 -0
  71. package/dist/daemon/daemon-link.js +9 -0
  72. package/dist/daemon/emoji-bindings.js +6 -0
  73. package/dist/daemon/wechat-daemon.js +675 -122
  74. package/dist/i18n/index.js +6 -0
  75. package/dist/i18n/messages-en.js +9 -1
  76. package/dist/i18n/messages-zh.js +9 -1
  77. package/dist/runtime/create-runtime-host.js +6 -0
  78. package/dist/runtime/legacy-adapter-runtime.js +15 -0
  79. package/dist/runtime/runtime-types.js +3 -0
  80. package/dist/utils/atomic-file.js +6 -0
  81. package/dist/utils/doctor.js +36 -10
  82. package/dist/utils/version-checker.js +17 -11
  83. package/dist/wechat/channel-config.js +6 -0
  84. package/dist/wechat/setup.js +119 -13
  85. package/dist/wechat/wechat-channel.js +6 -0
  86. package/dist/wechat/wechat-transport.js +169 -58
  87. package/package.json +16 -4
  88. package/scripts/ensure-node-pty-permissions.mjs +73 -73
@@ -0,0 +1,72 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
3
+ // Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
4
+ // This file is part of CLI-WeChat-Bridge. Modifications and derivative works
5
+ // must be released under AGPL-3.0-or-later with full source code; see
6
+ // LICENSE.txt. Network services built on it must offer source to users.
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { CHANNEL_DATA_DIR, ensureChannelDataDir, } from "../../wechat/channel-config.js";
10
+ export const WECOM_DATA_DIR = path.join(CHANNEL_DATA_DIR, "wecom");
11
+ export const WECOM_CREDENTIALS_FILE = path.join(WECOM_DATA_DIR, "account.json");
12
+ export const WECOM_INBOUND_ATTACHMENTS_DIR = path.join(WECOM_DATA_DIR, "inbound-attachments");
13
+ export const WECOM_INBOUND_MESSAGE_CLAIMS_DIR = path.join(WECOM_DATA_DIR, "inbound-message-claims");
14
+ export function ensureWecomDataDir() {
15
+ ensureChannelDataDir();
16
+ fs.mkdirSync(WECOM_DATA_DIR, { recursive: true });
17
+ }
18
+ export function loadStoredWecomAccount(filePath = WECOM_CREDENTIALS_FILE) {
19
+ try {
20
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
21
+ if (parsed.version !== 1 ||
22
+ typeof parsed.botId !== "string" ||
23
+ !parsed.botId.trim() ||
24
+ typeof parsed.secret !== "string" ||
25
+ !parsed.secret.trim() ||
26
+ typeof parsed.operatorUserId !== "string" ||
27
+ !parsed.operatorUserId.trim() ||
28
+ typeof parsed.pairedAt !== "string") {
29
+ return null;
30
+ }
31
+ return {
32
+ version: 1,
33
+ botId: parsed.botId.trim(),
34
+ secret: parsed.secret.trim(),
35
+ operatorUserId: parsed.operatorUserId.trim(),
36
+ pairedAt: parsed.pairedAt,
37
+ };
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ export function resolveWecomAccount(env = process.env, stored = loadStoredWecomAccount()) {
44
+ const botId = env.WECOM_BOT_ID?.trim() || stored?.botId || "";
45
+ const secret = env.WECOM_BOT_SECRET?.trim() || stored?.secret || "";
46
+ const operatorUserId = env.WECOM_OPERATOR_USER_ID?.trim() || stored?.operatorUserId || "";
47
+ if (!botId || !secret || !operatorUserId) {
48
+ return null;
49
+ }
50
+ return {
51
+ version: 1,
52
+ botId,
53
+ secret,
54
+ operatorUserId,
55
+ pairedAt: stored?.pairedAt || new Date().toISOString(),
56
+ };
57
+ }
58
+ export function saveWecomAccount(account, filePath = WECOM_CREDENTIALS_FILE) {
59
+ if (filePath === WECOM_CREDENTIALS_FILE) {
60
+ ensureWecomDataDir();
61
+ }
62
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
63
+ const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
64
+ fs.writeFileSync(tempPath, JSON.stringify(account, null, 2), "utf-8");
65
+ try {
66
+ fs.chmodSync(tempPath, 0o600);
67
+ }
68
+ catch {
69
+ // Best effort on Windows.
70
+ }
71
+ fs.renameSync(tempPath, filePath);
72
+ }
@@ -0,0 +1,106 @@
1
+ import { buildWecomInboundPrompt, formatWecomVisibleText, } from "./wecom-message.js";
2
+ import { truncatePreview } from "../../core/text-utils.js";
3
+ export function toWecomOutputKind(context) {
4
+ const known = new Set([
5
+ "status",
6
+ "notice",
7
+ "thinking",
8
+ "final_reply",
9
+ "approval_required",
10
+ "user_input_required",
11
+ "mirrored_input",
12
+ "task_failed",
13
+ "fatal_error",
14
+ ]);
15
+ return known.has(context)
16
+ ? context
17
+ : context === "mirrored_user_input"
18
+ ? "mirrored_input"
19
+ : "notice";
20
+ }
21
+ /** ChannelDriver over the official WeCom smart-bot WebSocket transport. */
22
+ export class WecomChannelDriver {
23
+ id = "wecom";
24
+ displayName = "WeCom";
25
+ operatorDescription = "the paired WeCom operator";
26
+ capabilities = {
27
+ streamingReplies: true,
28
+ outboundAttachments: true,
29
+ multiConversation: true,
30
+ pushInbound: true,
31
+ };
32
+ transport;
33
+ accountId;
34
+ operatorId;
35
+ logError;
36
+ constructor(options) {
37
+ this.transport = options.transport;
38
+ this.accountId = options.accountId;
39
+ this.operatorId = options.operatorId;
40
+ this.logError = options.logError;
41
+ }
42
+ defaultConversation() {
43
+ return this.directConversation(this.operatorId);
44
+ }
45
+ directConversation(senderId) {
46
+ return {
47
+ channelId: "wecom",
48
+ accountId: this.accountId,
49
+ conversationId: senderId,
50
+ recipientId: senderId,
51
+ metadata: { chatType: "direct" },
52
+ };
53
+ }
54
+ start(handlers) {
55
+ this.transport.setHandlers({
56
+ onMessage: async (message) => {
57
+ await handlers.onInboundMessage(message);
58
+ },
59
+ onUnauthorized: handlers.onUnauthorizedSender
60
+ ? async (senderId, chatType) => {
61
+ await handlers.onUnauthorizedSender(senderId, chatType);
62
+ }
63
+ : undefined,
64
+ onFatal: handlers.onChannelFatal
65
+ ? async (error) => {
66
+ await handlers.onChannelFatal(error);
67
+ }
68
+ : undefined,
69
+ onConnected: handlers.onChannelConnected
70
+ ? async () => {
71
+ await handlers.onChannelConnected();
72
+ }
73
+ : undefined,
74
+ });
75
+ this.transport.start();
76
+ }
77
+ waitUntilReady() {
78
+ return this.transport.waitUntilConnected();
79
+ }
80
+ async sendText(params) {
81
+ const { target, text, context, log } = params;
82
+ const startedAtMs = Date.now();
83
+ log(`wecom_send_started: context=${context} recipient=${target.recipientId} chars=${Array.from(text).length}`);
84
+ try {
85
+ await this.transport.sendText(target, formatWecomVisibleText(text), toWecomOutputKind(context));
86
+ log(`wecom_send_completed: context=${context} recipient=${target.recipientId} elapsed_ms=${Date.now() - startedAtMs}`);
87
+ return { status: "sent" };
88
+ }
89
+ catch (error) {
90
+ const message = error instanceof Error ? error.message : String(error);
91
+ log(`wecom_send_failed: context=${context} recipient=${target.recipientId} error=${truncatePreview(message, 400)}`);
92
+ this.logError(`Failed to send WeCom ${context}: ${message}`);
93
+ return {
94
+ status: "target_stale",
95
+ error,
96
+ target,
97
+ };
98
+ }
99
+ }
100
+ buildInboundPrompt(text, attachments) {
101
+ return buildWecomInboundPrompt(text, attachments);
102
+ }
103
+ formatVisibleText(text) {
104
+ return formatWecomVisibleText(text);
105
+ }
106
+ }
@@ -0,0 +1,147 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
3
+ // Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
4
+ // This file is part of CLI-WeChat-Bridge. Modifications and derivative works
5
+ // must be released under AGPL-3.0-or-later with full source code; see
6
+ // LICENSE.txt. Network services built on it must offer source to users.
7
+ import fs from "node:fs";
8
+ import path from "node:path";
9
+ import { decryptFile } from "@wecom/aibot-node-sdk";
10
+ import { WECOM_INBOUND_ATTACHMENTS_DIR } from "./wecom-config.js";
11
+ const DEFAULT_INBOUND_LIMIT_MB = {
12
+ image: 20,
13
+ file: 50,
14
+ video: 50,
15
+ };
16
+ const INBOUND_LIMIT_ENV_KEYS = {
17
+ image: "WECOM_MAX_INBOUND_IMAGE_MB",
18
+ file: "WECOM_MAX_INBOUND_FILE_MB",
19
+ video: "WECOM_MAX_INBOUND_VIDEO_MB",
20
+ };
21
+ function resolvePositiveMb(value, fallback) {
22
+ const parsed = Number(value);
23
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
24
+ }
25
+ export function getWecomInboundLimitBytes(kind, env = process.env) {
26
+ return Math.floor(resolvePositiveMb(env[INBOUND_LIMIT_ENV_KEYS[kind]], DEFAULT_INBOUND_LIMIT_MB[kind]) * 1024 * 1024);
27
+ }
28
+ function sanitizeFileName(value, fallback) {
29
+ const name = value.trim().replace(/\\/g, "/").split("/").pop() || fallback;
30
+ const cleaned = Array.from(name)
31
+ .map((character) => (character.charCodeAt(0) < 32 ? "_" : character))
32
+ .join("")
33
+ .replace(/[<>:"/\\|?*]+/g, "_")
34
+ .replace(/^\.+/, "")
35
+ .slice(0, 180);
36
+ return cleaned || fallback;
37
+ }
38
+ function parseContentDispositionFileName(value) {
39
+ if (!value) {
40
+ return undefined;
41
+ }
42
+ const utf8 = value.match(/filename\*=UTF-8''([^;]+)/i)?.[1];
43
+ if (utf8) {
44
+ try {
45
+ return decodeURIComponent(utf8.trim());
46
+ }
47
+ catch {
48
+ return utf8.trim();
49
+ }
50
+ }
51
+ return value.match(/filename="?([^";]+)"?/i)?.[1]?.trim();
52
+ }
53
+ function extensionFor(kind, mimeType) {
54
+ const normalized = mimeType.split(";")[0]?.trim().toLowerCase() || "";
55
+ const known = {
56
+ "image/jpeg": ".jpg",
57
+ "image/png": ".png",
58
+ "image/gif": ".gif",
59
+ "image/webp": ".webp",
60
+ "video/mp4": ".mp4",
61
+ "application/pdf": ".pdf",
62
+ "text/plain": ".txt",
63
+ };
64
+ return known[normalized] || (kind === "image" ? ".jpg" : kind === "video" ? ".mp4" : ".bin");
65
+ }
66
+ async function readResponseWithLimit(response, maxBytes) {
67
+ const declaredLength = Number(response.headers.get("content-length"));
68
+ if (Number.isFinite(declaredLength) && declaredLength > maxBytes + 64) {
69
+ throw new Error(`attachment exceeds the configured ${maxBytes}-byte limit`);
70
+ }
71
+ if (!response.body) {
72
+ return Buffer.alloc(0);
73
+ }
74
+ const reader = response.body.getReader();
75
+ const chunks = [];
76
+ let total = 0;
77
+ while (true) {
78
+ const next = await reader.read();
79
+ if (next.done) {
80
+ break;
81
+ }
82
+ total += next.value.byteLength;
83
+ if (total > maxBytes + 64) {
84
+ await reader.cancel().catch(() => undefined);
85
+ throw new Error(`attachment exceeds the configured ${maxBytes}-byte limit`);
86
+ }
87
+ chunks.push(next.value);
88
+ }
89
+ return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)), total);
90
+ }
91
+ export async function downloadWecomAttachment(descriptor, options) {
92
+ const maxBytes = getWecomInboundLimitBytes(descriptor.kind, options.env);
93
+ const controller = new AbortController();
94
+ const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? 60_000);
95
+ try {
96
+ const response = await (options.fetchImpl ?? fetch)(descriptor.url, {
97
+ signal: controller.signal,
98
+ });
99
+ if (!response.ok) {
100
+ throw new Error(`download returned HTTP ${response.status}`);
101
+ }
102
+ const encrypted = await readResponseWithLimit(response, maxBytes);
103
+ const decrypted = descriptor.aesKey ? decryptFile(encrypted, descriptor.aesKey) : encrypted;
104
+ if (decrypted.length > maxBytes) {
105
+ throw new Error(`attachment exceeds the configured ${maxBytes}-byte limit`);
106
+ }
107
+ const mimeType = response.headers.get("content-type")?.split(";")[0]?.trim();
108
+ const headerName = parseContentDispositionFileName(response.headers.get("content-disposition"));
109
+ const fallbackName = `${descriptor.kind}-${options.index + 1}${extensionFor(descriptor.kind, mimeType || "")}`;
110
+ const fileName = sanitizeFileName(descriptor.fileName || headerName || fallbackName, fallbackName);
111
+ const day = new Date().toISOString().slice(0, 10);
112
+ const directory = path.join(options.attachmentsDir ?? WECOM_INBOUND_ATTACHMENTS_DIR, day);
113
+ fs.mkdirSync(directory, { recursive: true });
114
+ const safeMessageId = options.messageId.replace(/[^a-zA-Z0-9._-]+/g, "-").slice(0, 80);
115
+ const filePath = path.join(directory, `${safeMessageId}-${options.index + 1}-${fileName}`);
116
+ fs.writeFileSync(filePath, decrypted);
117
+ return {
118
+ kind: descriptor.kind,
119
+ path: filePath,
120
+ fileName,
121
+ mimeType,
122
+ sizeBytes: decrypted.length,
123
+ };
124
+ }
125
+ finally {
126
+ clearTimeout(timer);
127
+ }
128
+ }
129
+ export async function downloadWecomInboundAttachments(parsed, options = {}) {
130
+ const attachments = [];
131
+ const failureNotes = [];
132
+ for (let index = 0; index < parsed.attachments.length; index += 1) {
133
+ const descriptor = parsed.attachments[index];
134
+ try {
135
+ attachments.push(await downloadWecomAttachment(descriptor, {
136
+ ...options,
137
+ messageId: parsed.id,
138
+ index,
139
+ }));
140
+ }
141
+ catch (error) {
142
+ const message = error instanceof Error ? error.message : String(error);
143
+ failureNotes.push(`[WeCom attachment unavailable: ${descriptor.kind}; ${message}]`);
144
+ }
145
+ }
146
+ return { attachments, failureNotes };
147
+ }
@@ -0,0 +1,149 @@
1
+ function appendMixedContent(items, textParts, attachments) {
2
+ for (const item of items ?? []) {
3
+ if (item.msgtype === "text" && item.text?.content) {
4
+ textParts.push(item.text.content);
5
+ }
6
+ else if (item.msgtype === "image" && item.image?.url) {
7
+ attachments.push({
8
+ kind: "image",
9
+ url: item.image.url,
10
+ aesKey: item.image.aeskey,
11
+ });
12
+ }
13
+ }
14
+ }
15
+ function appendQuote(quote, textParts, attachments) {
16
+ if (!quote) {
17
+ return;
18
+ }
19
+ const quotedText = quote.text?.content || quote.voice?.content;
20
+ if (quotedText) {
21
+ textParts.push(`[Quoted message]\n${quotedText}`);
22
+ }
23
+ if (quote.image?.url) {
24
+ attachments.push({
25
+ kind: "image",
26
+ url: quote.image.url,
27
+ aesKey: quote.image.aeskey,
28
+ });
29
+ }
30
+ if (quote.file?.url) {
31
+ attachments.push({
32
+ kind: "file",
33
+ url: quote.file.url,
34
+ aesKey: quote.file.aeskey,
35
+ });
36
+ }
37
+ const quotedVideo = quote.video;
38
+ if (quotedVideo?.url) {
39
+ attachments.push({
40
+ kind: "video",
41
+ url: quotedVideo.url,
42
+ aesKey: quotedVideo.aeskey,
43
+ });
44
+ }
45
+ appendMixedContent(quote.mixed?.msg_item, textParts, attachments);
46
+ }
47
+ export function parseWecomInboundFrame(frame) {
48
+ const body = frame.body;
49
+ if (!body?.msgid || !body.from?.userid || !frame.headers?.req_id) {
50
+ return null;
51
+ }
52
+ const textParts = [];
53
+ const attachments = [];
54
+ if (body.msgtype === "text" && body.text?.content) {
55
+ textParts.push(body.text.content);
56
+ }
57
+ else if (body.msgtype === "voice" && body.voice?.content) {
58
+ textParts.push(body.voice.content);
59
+ }
60
+ else if (body.msgtype === "mixed") {
61
+ appendMixedContent(body.mixed?.msg_item, textParts, attachments);
62
+ }
63
+ else if (body.msgtype === "image" && body.image?.url) {
64
+ attachments.push({
65
+ kind: "image",
66
+ url: body.image.url,
67
+ aesKey: body.image.aeskey,
68
+ });
69
+ }
70
+ else if (body.msgtype === "file" && body.file?.url) {
71
+ attachments.push({
72
+ kind: "file",
73
+ url: body.file.url,
74
+ aesKey: body.file.aeskey,
75
+ });
76
+ }
77
+ else if (body.msgtype === "video" && body.video?.url) {
78
+ attachments.push({
79
+ kind: "video",
80
+ url: body.video.url,
81
+ aesKey: body.video.aeskey,
82
+ });
83
+ }
84
+ appendQuote(body.quote, textParts, attachments);
85
+ const isGroup = body.chattype === "group" && Boolean(body.chatid);
86
+ const timestampMs = typeof body.create_time === "number" && body.create_time > 0
87
+ ? body.create_time * 1000
88
+ : Date.now();
89
+ return {
90
+ id: body.msgid,
91
+ reqId: frame.headers.req_id,
92
+ senderId: body.from.userid,
93
+ conversationId: isGroup ? body.chatid : body.from.userid,
94
+ chatType: isGroup ? "group" : "direct",
95
+ text: textParts.join("\n").trim(),
96
+ createdAt: new Date(timestampMs).toISOString(),
97
+ attachments,
98
+ };
99
+ }
100
+ export function toWecomChannelInboundMessage(parsed, attachments, accountId) {
101
+ return {
102
+ id: parsed.id,
103
+ conversation: {
104
+ channelId: "wecom",
105
+ accountId,
106
+ conversationId: parsed.conversationId,
107
+ recipientId: parsed.conversationId,
108
+ opaqueRef: parsed.reqId,
109
+ metadata: {
110
+ chatType: parsed.chatType,
111
+ messageId: parsed.id,
112
+ },
113
+ },
114
+ senderId: parsed.senderId,
115
+ text: parsed.text,
116
+ attachments,
117
+ createdAt: parsed.createdAt,
118
+ metadata: {
119
+ chatType: parsed.chatType,
120
+ reqId: parsed.reqId,
121
+ },
122
+ };
123
+ }
124
+ export function buildWecomInboundPrompt(text, attachments = []) {
125
+ const normalizedText = text.trim();
126
+ if (attachments.length === 0) {
127
+ return normalizedText;
128
+ }
129
+ const lines = [
130
+ "[WeCom inbound attachments — ACTION REQUIRED]",
131
+ "Use the local attachment paths below when the user asks you to inspect or process them.",
132
+ ];
133
+ attachments.forEach((attachment, index) => {
134
+ const metadata = [
135
+ `kind=${attachment.kind}`,
136
+ attachment.fileName ? `name=${attachment.fileName}` : "",
137
+ typeof attachment.sizeBytes === "number" ? `bytes=${attachment.sizeBytes}` : "",
138
+ ].filter(Boolean);
139
+ lines.push(`${index + 1}. ${metadata.join(" ")} path=${attachment.path}`);
140
+ });
141
+ return [normalizedText || "Received WeCom attachment(s).", lines.join("\n")]
142
+ .filter(Boolean)
143
+ .join("\n\n");
144
+ }
145
+ export function formatWecomVisibleText(text) {
146
+ return text
147
+ .replace(/\bWeChat\b/g, "WeCom")
148
+ .replace(/\bwechat-(codex|claude|opencode|pi|daemon|setup)\b/g, "wecom-$1");
149
+ }