openzca 0.1.57 → 0.1.59
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 +18 -6
- package/dist/cli.js +421 -95
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -125,6 +125,7 @@ You can also open the saved file manually (for example: `open qr.png` on macOS).
|
|
|
125
125
|
|
|
126
126
|
Media commands accept local files, `file://` paths, and repeatable `--url` options. Add `--group` for group threads.
|
|
127
127
|
`openzca msg video` attempts native video send for a single `.mp4` input by uploading the video and thumbnail to Zalo first. If `ffmpeg` is unavailable, the input is not a single `.mp4`, or native send fails, it falls back to the normal attachment send path. Use `--thumbnail <path-or-url>` to supply the preview image explicitly.
|
|
128
|
+
`openzca msg voice` sends `--url` inputs directly. For local voice files, if both `ffmpeg` and `OPENZCA_VOICE_PUBLISH_CMD` are available, `openzca` normalizes the file to `.m4a`, runs the publish command with the normalized temp file path, expects one public `http(s)` URL on stdout, and sends that URL. Otherwise it falls back to the legacy Zalo upload flow.
|
|
128
129
|
Local paths using `~` are expanded automatically (for positional file args, `--url`, and `OPENZCA_LISTEN_MEDIA_DIR`).
|
|
129
130
|
Group text sends via `openzca msg send --group` resolve unique `@Name` or `@userId` mentions against the current group member list using member ids, display names, and usernames. Mention offsets are computed after formatting markers are parsed, so messages like `**@Alice Nguyen** hello` work. If multiple members share the same label, the command fails instead of guessing.
|
|
130
131
|
When formatted text would produce an oversized outbound payload, `openzca msg send` automatically splits it into multiple sequential text messages using the final outbound text and rebased style/mention offsets. The split happens after formatting is parsed, using both rendered text length and estimated request payload size rather than the raw input string.
|
|
@@ -176,11 +177,11 @@ For media debugging, grep these events in the debug log:
|
|
|
176
177
|
| `openzca group info <groupId>` | Get group details |
|
|
177
178
|
| `openzca group members <groupId>` | List members |
|
|
178
179
|
| `openzca group create <name> <members...>` | Create a group |
|
|
179
|
-
| `openzca group poll create <groupId>` | Create a poll (`--question`, repeatable `--option`, optional poll flags) |
|
|
180
|
-
| `openzca group poll detail <pollId>` | Get poll details |
|
|
181
|
-
| `openzca group poll vote <pollId>` | Vote on a poll with repeatable `--option <id>` |
|
|
182
|
-
| `openzca group poll lock <pollId>` | Close a poll |
|
|
183
|
-
| `openzca group poll share <pollId>` | Share a poll |
|
|
180
|
+
| `openzca group poll create <groupId>` | Create a poll (`--question`, repeatable `--option`, optional poll flags, `--json`) |
|
|
181
|
+
| `openzca group poll detail <pollId>` | Get poll details (`--json`) |
|
|
182
|
+
| `openzca group poll vote <pollId>` | Vote on a poll with repeatable `--option <id>` (`--json`) |
|
|
183
|
+
| `openzca group poll lock <pollId>` | Close a poll (`--json`) |
|
|
184
|
+
| `openzca group poll share <pollId>` | Share a poll (`--json`) |
|
|
184
185
|
| `openzca group rename <groupId> <name>` | Rename group |
|
|
185
186
|
| `openzca group avatar <groupId> <file>` | Change group avatar |
|
|
186
187
|
| `openzca group settings <groupId>` | Update settings (`--lock-name`, `--sign-admin`, etc.) |
|
|
@@ -201,7 +202,7 @@ For media debugging, grep these events in the debug log:
|
|
|
201
202
|
| `openzca group leave <groupId>` | Leave group |
|
|
202
203
|
| `openzca group disperse <groupId>` | Disperse group |
|
|
203
204
|
|
|
204
|
-
Poll creation currently targets group threads only and maps to the existing `zca-js` group poll APIs. `group poll create` requires `--question` plus at least two `--option` values, and also supports `--multi`, `--allow-add-option`, `--hide-vote-preview`, `--anonymous`,
|
|
205
|
+
Poll creation currently targets group threads only and maps to the existing `zca-js` group poll APIs. `group poll create` requires `--question` plus at least two `--option` values, and also supports `--multi`, `--allow-add-option`, `--hide-vote-preview`, `--anonymous`, `--expire-ms`, and `--json`. Use `--json` to reliably read the returned `poll_id` and option `option_id` values for follow-up detail or vote commands.
|
|
205
206
|
|
|
206
207
|
### friend — Friend management
|
|
207
208
|
|
|
@@ -301,6 +302,7 @@ Notes:
|
|
|
301
302
|
| `openzca listen --db` | Force DB writes for this listener session |
|
|
302
303
|
| `openzca listen --no-db` | Disable DB writes for this listener session |
|
|
303
304
|
| `openzca listen --keep-alive` | Auto-reconnect on disconnect |
|
|
305
|
+
| `openzca listen --self` | Include events produced by the logged-in account |
|
|
304
306
|
| `openzca listen --supervised --raw` | Supervisor mode with lifecycle JSON events (`session_id`, `connected`, `heartbeat`, `error`, `closed`) |
|
|
305
307
|
| `openzca listen --keep-alive --recycle-ms <ms>` | Periodically recycle listener process to avoid stale sessions |
|
|
306
308
|
|
|
@@ -317,8 +319,12 @@ It also includes stable routing fields for downstream tools:
|
|
|
317
319
|
- `senderId`, `toId`, `chatType`, `msgType`, `timestamp`
|
|
318
320
|
- `mentions` (normalized mention entities: `uid`, `pos`, `len`, `type`, optional `text`)
|
|
319
321
|
- `mentionIds` (flattened mention user IDs)
|
|
322
|
+
- `pollId`, `pollTitle`, `pollOptionIds`, and `poll` when a message or group event carries poll metadata
|
|
323
|
+
- `rawMessage` for poll message payloads, and `rawGroupEvent` for poll group-event payloads
|
|
320
324
|
- `metadata.threadId`, `metadata.targetId`, `metadata.senderId`, `metadata.toId`
|
|
321
325
|
- `metadata.mentions`, `metadata.mentionIds`, `metadata.mentionCount`
|
|
326
|
+
- `metadata.pollId`, `metadata.pollTitle`, `metadata.pollOptionIds`, and `metadata.poll`
|
|
327
|
+
- `metadata.rawMessage` / `metadata.rawGroupEvent` for poll payload schema debugging
|
|
322
328
|
- `quote` and `metadata.quote` when the inbound message is a reply to a previous message
|
|
323
329
|
- Includes parsed `quote.attach` and extracted `quote.mediaUrls` when attachment URLs are present.
|
|
324
330
|
- `quoteMediaPath`, `quoteMediaPaths`, `quoteMediaUrl`, `quoteMediaUrls`, `quoteMediaType`, `quoteMediaTypes`
|
|
@@ -326,6 +332,8 @@ It also includes stable routing fields for downstream tools:
|
|
|
326
332
|
|
|
327
333
|
For direct messages, `metadata.senderName` is intentionally omitted so consumers can prefer numeric IDs for routing instead of display-name targets.
|
|
328
334
|
|
|
335
|
+
By default, zca-js suppresses events produced by the logged-in account. Use `listen --self --raw` when a caller needs to observe its own actions, such as a poll it just created.
|
|
336
|
+
|
|
329
337
|
When a reply/quoted message is detected, `content` also appends a compact line:
|
|
330
338
|
|
|
331
339
|
```text
|
|
@@ -433,6 +441,10 @@ Upload/listener coordination overrides:
|
|
|
433
441
|
- `OPENZCA_UPLOAD_GROUP_PROBE`: allow `msg upload` to probe `getGroupInfo` when auto thread-type detection is enabled.
|
|
434
442
|
- Default: enabled.
|
|
435
443
|
- Set to `0` to skip probe and rely only on cache matches.
|
|
444
|
+
- `OPENZCA_VOICE_PUBLISH_CMD`: optional command used by `msg voice` for local files.
|
|
445
|
+
- `openzca` passes one normalized `.m4a` temp file path as the first argument.
|
|
446
|
+
- The command must print exactly one public `http(s)` URL to stdout.
|
|
447
|
+
- Requires `ffmpeg`; if unset or `ffmpeg` is unavailable, local voice files keep using the legacy Zalo upload flow.
|
|
436
448
|
|
|
437
449
|
### account — Multi-account profiles
|
|
438
450
|
|
package/dist/cli.js
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
import { createRequire as createRequire2 } from "module";
|
|
5
5
|
import { spawn as spawn2 } from "child_process";
|
|
6
6
|
import fsSync from "fs";
|
|
7
|
-
import
|
|
7
|
+
import fs7 from "fs/promises";
|
|
8
8
|
import net from "net";
|
|
9
|
-
import
|
|
10
|
-
import
|
|
9
|
+
import os5 from "os";
|
|
10
|
+
import path7 from "path";
|
|
11
11
|
import readline from "readline/promises";
|
|
12
12
|
import util from "util";
|
|
13
13
|
import { Command } from "commander";
|
|
@@ -1735,10 +1735,11 @@ async function imageMetadataGetter(filePath) {
|
|
|
1735
1735
|
size: data.length
|
|
1736
1736
|
};
|
|
1737
1737
|
}
|
|
1738
|
-
function createZaloClient() {
|
|
1738
|
+
function createZaloClient(options) {
|
|
1739
1739
|
return new Zalo({
|
|
1740
1740
|
imageMetadataGetter,
|
|
1741
|
-
logging: false
|
|
1741
|
+
logging: false,
|
|
1742
|
+
...options
|
|
1742
1743
|
});
|
|
1743
1744
|
}
|
|
1744
1745
|
function toCredentials(value) {
|
|
@@ -1749,14 +1750,14 @@ function toCredentials(value) {
|
|
|
1749
1750
|
language: value.language
|
|
1750
1751
|
};
|
|
1751
1752
|
}
|
|
1752
|
-
async function loginWithStoredCredentials(profileName) {
|
|
1753
|
+
async function loginWithStoredCredentials(profileName, options) {
|
|
1753
1754
|
const stored = await loadCredentials(profileName);
|
|
1754
1755
|
if (!stored) {
|
|
1755
1756
|
throw new Error(
|
|
1756
1757
|
`Profile "${profileName}" has no credentials. Run: auth login`
|
|
1757
1758
|
);
|
|
1758
1759
|
}
|
|
1759
|
-
const zalo = createZaloClient();
|
|
1760
|
+
const zalo = createZaloClient(options);
|
|
1760
1761
|
return zalo.login(toCredentials(stored));
|
|
1761
1762
|
}
|
|
1762
1763
|
async function loginWithCredentialPayload(profileName, credentials) {
|
|
@@ -2035,6 +2036,96 @@ function buildCreatePollOptions(options) {
|
|
|
2035
2036
|
};
|
|
2036
2037
|
}
|
|
2037
2038
|
|
|
2039
|
+
// src/lib/listen-poll.ts
|
|
2040
|
+
var POLL_ID_KEYS = /* @__PURE__ */ new Set(["pollId", "poll_id", "pollID", "pollid"]);
|
|
2041
|
+
var OPTION_ID_KEYS = /* @__PURE__ */ new Set(["optionId", "option_id", "optionID", "optionid"]);
|
|
2042
|
+
var TITLE_KEYS = ["question", "title"];
|
|
2043
|
+
function looksLikeStructuredJsonString(value) {
|
|
2044
|
+
const trimmed = value.trim();
|
|
2045
|
+
if (trimmed.length < 2) return false;
|
|
2046
|
+
const first = trimmed[0];
|
|
2047
|
+
const last = trimmed[trimmed.length - 1];
|
|
2048
|
+
return first === "{" && last === "}" || first === "[" && last === "]";
|
|
2049
|
+
}
|
|
2050
|
+
function parseStructuredJsonString(value) {
|
|
2051
|
+
if (!looksLikeStructuredJsonString(value)) return void 0;
|
|
2052
|
+
try {
|
|
2053
|
+
return JSON.parse(value);
|
|
2054
|
+
} catch {
|
|
2055
|
+
return void 0;
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
function normalizeIdentifier(value) {
|
|
2059
|
+
if (typeof value === "number" && Number.isInteger(value) && value > 0) {
|
|
2060
|
+
return Number.isSafeInteger(value) ? value : String(value);
|
|
2061
|
+
}
|
|
2062
|
+
if (typeof value !== "string") return void 0;
|
|
2063
|
+
const normalized = value.trim();
|
|
2064
|
+
if (!/^[1-9]\d*$/.test(normalized)) return void 0;
|
|
2065
|
+
const numeric = Number(normalized);
|
|
2066
|
+
return Number.isSafeInteger(numeric) ? numeric : normalized;
|
|
2067
|
+
}
|
|
2068
|
+
function firstString(record, keys) {
|
|
2069
|
+
for (const key of keys) {
|
|
2070
|
+
const value = record[key];
|
|
2071
|
+
if (typeof value === "string" && value.trim()) {
|
|
2072
|
+
return value.trim();
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
return void 0;
|
|
2076
|
+
}
|
|
2077
|
+
function asRecord(value) {
|
|
2078
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
2079
|
+
return value;
|
|
2080
|
+
}
|
|
2081
|
+
function extractInboundPollInfo(...values) {
|
|
2082
|
+
const optionIds = [];
|
|
2083
|
+
const seenOptionIds = /* @__PURE__ */ new Set();
|
|
2084
|
+
let pollId;
|
|
2085
|
+
let title;
|
|
2086
|
+
const pushOptionId = (value) => {
|
|
2087
|
+
const optionId = normalizeIdentifier(value);
|
|
2088
|
+
if (!optionId) return;
|
|
2089
|
+
const key = String(optionId);
|
|
2090
|
+
if (seenOptionIds.has(key)) return;
|
|
2091
|
+
seenOptionIds.add(key);
|
|
2092
|
+
optionIds.push(optionId);
|
|
2093
|
+
};
|
|
2094
|
+
const visit = (value, depth = 0) => {
|
|
2095
|
+
if (depth > 8 || value === null || value === void 0) return;
|
|
2096
|
+
if (typeof value === "string") {
|
|
2097
|
+
const parsed = parseStructuredJsonString(value);
|
|
2098
|
+
if (parsed !== void 0) visit(parsed, depth + 1);
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
if (Array.isArray(value)) {
|
|
2102
|
+
for (const entry of value) visit(entry, depth + 1);
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
const record = asRecord(value);
|
|
2106
|
+
if (!record) return;
|
|
2107
|
+
for (const [key, nested] of Object.entries(record)) {
|
|
2108
|
+
if (!pollId && POLL_ID_KEYS.has(key)) {
|
|
2109
|
+
pollId = normalizeIdentifier(nested);
|
|
2110
|
+
}
|
|
2111
|
+
if (OPTION_ID_KEYS.has(key)) {
|
|
2112
|
+
pushOptionId(nested);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
title ??= firstString(record, TITLE_KEYS);
|
|
2116
|
+
for (const nested of Object.values(record)) {
|
|
2117
|
+
visit(nested, depth + 1);
|
|
2118
|
+
}
|
|
2119
|
+
};
|
|
2120
|
+
for (const value of values) visit(value);
|
|
2121
|
+
if (!pollId) return null;
|
|
2122
|
+
return {
|
|
2123
|
+
pollId,
|
|
2124
|
+
...title ? { title } : {},
|
|
2125
|
+
...optionIds.length > 0 ? { optionIds } : {}
|
|
2126
|
+
};
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2038
2129
|
// src/lib/time-range.ts
|
|
2039
2130
|
var DURATION_PART_RE = /(\d+)\s*(ms|s|m|h|d|w)/gi;
|
|
2040
2131
|
function durationToMs(input) {
|
|
@@ -3066,6 +3157,110 @@ async function sendNativeVideo(params) {
|
|
|
3066
3157
|
}
|
|
3067
3158
|
}
|
|
3068
3159
|
|
|
3160
|
+
// src/lib/voice-send.ts
|
|
3161
|
+
import { execFile as execFile2 } from "child_process";
|
|
3162
|
+
import fs6 from "fs/promises";
|
|
3163
|
+
import os4 from "os";
|
|
3164
|
+
import path6 from "path";
|
|
3165
|
+
import { promisify as promisify2 } from "util";
|
|
3166
|
+
var execFileAsync2 = promisify2(execFile2);
|
|
3167
|
+
function sanitizeOutputBasename(filePath) {
|
|
3168
|
+
const parsed = path6.parse(filePath);
|
|
3169
|
+
const base = parsed.name.trim() || "voice";
|
|
3170
|
+
const sanitized = base.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3171
|
+
return sanitized || "voice";
|
|
3172
|
+
}
|
|
3173
|
+
async function runBinary2(command, args) {
|
|
3174
|
+
try {
|
|
3175
|
+
await execFileAsync2(command, args, {
|
|
3176
|
+
encoding: "utf8",
|
|
3177
|
+
maxBuffer: 10 * 1024 * 1024
|
|
3178
|
+
});
|
|
3179
|
+
} catch (error) {
|
|
3180
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
3181
|
+
throw new Error(`${command} is required for voice publish mode`);
|
|
3182
|
+
}
|
|
3183
|
+
if (error instanceof Error && "stderr" in error) {
|
|
3184
|
+
const stderr = typeof error.stderr === "string" ? error.stderr.trim() : "";
|
|
3185
|
+
throw new Error(stderr ? `${command} failed: ${stderr}` : `${command} failed: ${error.message}`);
|
|
3186
|
+
}
|
|
3187
|
+
throw error;
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
function getVoicePublishCommandFromEnv(env = process.env) {
|
|
3191
|
+
const configured = env.OPENZCA_VOICE_PUBLISH_CMD?.trim();
|
|
3192
|
+
return configured && configured.length > 0 ? configured : null;
|
|
3193
|
+
}
|
|
3194
|
+
function extractPublishedVoiceUrl(stdout) {
|
|
3195
|
+
const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
3196
|
+
const candidate = lines.at(-1);
|
|
3197
|
+
if (!candidate) {
|
|
3198
|
+
throw new Error("Voice publish command did not print a public URL to stdout");
|
|
3199
|
+
}
|
|
3200
|
+
if (!/^https?:\/\/\S+$/i.test(candidate)) {
|
|
3201
|
+
throw new Error(`Voice publish command returned an invalid URL: ${candidate}`);
|
|
3202
|
+
}
|
|
3203
|
+
return candidate;
|
|
3204
|
+
}
|
|
3205
|
+
async function normalizeVoiceForPublish(inputPath) {
|
|
3206
|
+
const dir = await fs6.mkdtemp(path6.join(os4.tmpdir(), "openzca-voice-"));
|
|
3207
|
+
const outputPath = path6.join(dir, `${sanitizeOutputBasename(inputPath)}.m4a`);
|
|
3208
|
+
try {
|
|
3209
|
+
await runBinary2("ffmpeg", [
|
|
3210
|
+
"-y",
|
|
3211
|
+
"-v",
|
|
3212
|
+
"error",
|
|
3213
|
+
"-i",
|
|
3214
|
+
inputPath,
|
|
3215
|
+
"-vn",
|
|
3216
|
+
"-map_metadata",
|
|
3217
|
+
"-1",
|
|
3218
|
+
"-ac",
|
|
3219
|
+
"1",
|
|
3220
|
+
"-ar",
|
|
3221
|
+
"44100",
|
|
3222
|
+
"-c:a",
|
|
3223
|
+
"aac",
|
|
3224
|
+
"-b:a",
|
|
3225
|
+
"64k",
|
|
3226
|
+
"-movflags",
|
|
3227
|
+
"+faststart",
|
|
3228
|
+
outputPath
|
|
3229
|
+
]);
|
|
3230
|
+
await fs6.access(outputPath);
|
|
3231
|
+
} catch (error) {
|
|
3232
|
+
await fs6.rm(dir, { recursive: true, force: true });
|
|
3233
|
+
throw error;
|
|
3234
|
+
}
|
|
3235
|
+
return {
|
|
3236
|
+
path: outputPath,
|
|
3237
|
+
cleanup: async () => {
|
|
3238
|
+
await fs6.rm(dir, { recursive: true, force: true });
|
|
3239
|
+
}
|
|
3240
|
+
};
|
|
3241
|
+
}
|
|
3242
|
+
async function publishVoiceFile(command, filePath) {
|
|
3243
|
+
try {
|
|
3244
|
+
const { stdout } = await execFileAsync2(
|
|
3245
|
+
"sh",
|
|
3246
|
+
["-c", `${command} "$1"`, "openzca-voice-publish", filePath],
|
|
3247
|
+
{
|
|
3248
|
+
encoding: "utf8",
|
|
3249
|
+
maxBuffer: 10 * 1024 * 1024
|
|
3250
|
+
}
|
|
3251
|
+
);
|
|
3252
|
+
return extractPublishedVoiceUrl(stdout);
|
|
3253
|
+
} catch (error) {
|
|
3254
|
+
if (error instanceof Error && "stderr" in error) {
|
|
3255
|
+
const stderr = typeof error.stderr === "string" ? error.stderr.trim() : "";
|
|
3256
|
+
throw new Error(
|
|
3257
|
+
stderr ? `Voice publish command failed: ${stderr}` : `Voice publish command failed: ${error.message}`
|
|
3258
|
+
);
|
|
3259
|
+
}
|
|
3260
|
+
throw error;
|
|
3261
|
+
}
|
|
3262
|
+
}
|
|
3263
|
+
|
|
3069
3264
|
// src/lib/reply.ts
|
|
3070
3265
|
import { ThreadType as ThreadType2 } from "zca-js";
|
|
3071
3266
|
function prepareReplyMessage(value, params) {
|
|
@@ -3133,7 +3328,7 @@ function prepareStoredReplyMessage(value, params) {
|
|
|
3133
3328
|
if (storedThreadType !== void 0 && storedThreadType !== params.threadType) {
|
|
3134
3329
|
throw new Error("Reply source thread type does not match --group.");
|
|
3135
3330
|
}
|
|
3136
|
-
const storedThreadId =
|
|
3331
|
+
const storedThreadId = firstString2([record.threadId, record.rawThreadId]) ?? void 0;
|
|
3137
3332
|
if (storedThreadId && storedThreadId !== params.threadId) {
|
|
3138
3333
|
throw new Error("Reply source belongs to a different thread.");
|
|
3139
3334
|
}
|
|
@@ -3203,7 +3398,7 @@ function parseReplyMessageTtl(value) {
|
|
|
3203
3398
|
return Math.trunc(parsed);
|
|
3204
3399
|
}
|
|
3205
3400
|
function requireStringLike(values, label) {
|
|
3206
|
-
const value =
|
|
3401
|
+
const value = firstString2(values);
|
|
3207
3402
|
if (!value) {
|
|
3208
3403
|
throw new Error(`Missing ${label}.`);
|
|
3209
3404
|
}
|
|
@@ -3220,7 +3415,7 @@ function requireTsString(values, label) {
|
|
|
3220
3415
|
}
|
|
3221
3416
|
throw new Error(`Missing ${label}.`);
|
|
3222
3417
|
}
|
|
3223
|
-
function
|
|
3418
|
+
function firstString2(values) {
|
|
3224
3419
|
for (const value of values) {
|
|
3225
3420
|
if (typeof value === "string" && value.trim()) {
|
|
3226
3421
|
return value.trim();
|
|
@@ -3241,7 +3436,7 @@ function isLikelyOpenzcaListenPayload(record) {
|
|
|
3241
3436
|
return typeof record.threadId === "string" && (typeof record.senderId === "string" || typeof record.chatType === "string" || typeof record.metadata === "object");
|
|
3242
3437
|
}
|
|
3243
3438
|
function inferReplyMessageThreadId(params) {
|
|
3244
|
-
const directThreadId =
|
|
3439
|
+
const directThreadId = firstString2([
|
|
3245
3440
|
params.sourceRecord.threadId,
|
|
3246
3441
|
params.sourceRecord.targetId,
|
|
3247
3442
|
params.sourceRecord.conversationId,
|
|
@@ -3254,7 +3449,7 @@ function inferReplyMessageThreadId(params) {
|
|
|
3254
3449
|
if (params.threadType === void 0) {
|
|
3255
3450
|
return void 0;
|
|
3256
3451
|
}
|
|
3257
|
-
const idTo =
|
|
3452
|
+
const idTo = firstString2([
|
|
3258
3453
|
params.canonicalRecord.idTo,
|
|
3259
3454
|
params.sourceRecord.idTo,
|
|
3260
3455
|
params.sourceRecord.toId,
|
|
@@ -3263,7 +3458,7 @@ function inferReplyMessageThreadId(params) {
|
|
|
3263
3458
|
if (params.threadType === ThreadType2.Group) {
|
|
3264
3459
|
return idTo;
|
|
3265
3460
|
}
|
|
3266
|
-
const uidFrom =
|
|
3461
|
+
const uidFrom = firstString2([
|
|
3267
3462
|
params.canonicalRecord.uidFrom,
|
|
3268
3463
|
params.sourceRecord.uidFrom,
|
|
3269
3464
|
params.sourceRecord.senderId,
|
|
@@ -3491,9 +3686,9 @@ function resolveDebugEnabled(command) {
|
|
|
3491
3686
|
}
|
|
3492
3687
|
function resolveDebugFilePath(command) {
|
|
3493
3688
|
const options = getDebugOptions(command);
|
|
3494
|
-
const configured = options.debugFile?.trim() || process.env.OPENZCA_DEBUG_FILE?.trim() ||
|
|
3689
|
+
const configured = options.debugFile?.trim() || process.env.OPENZCA_DEBUG_FILE?.trim() || path7.join(APP_HOME, "logs", "openzca-debug.log");
|
|
3495
3690
|
const normalized = normalizeMediaInput(configured);
|
|
3496
|
-
return
|
|
3691
|
+
return path7.isAbsolute(normalized) ? normalized : path7.resolve(process.cwd(), normalized);
|
|
3497
3692
|
}
|
|
3498
3693
|
function writeDebugLine(event, details, command) {
|
|
3499
3694
|
if (!resolveDebugEnabled(command)) {
|
|
@@ -3504,7 +3699,7 @@ function writeDebugLine(event, details, command) {
|
|
|
3504
3699
|
`;
|
|
3505
3700
|
const filePath = resolveDebugFilePath(command);
|
|
3506
3701
|
try {
|
|
3507
|
-
fsSync.mkdirSync(
|
|
3702
|
+
fsSync.mkdirSync(path7.dirname(filePath), { recursive: true });
|
|
3508
3703
|
fsSync.appendFileSync(filePath, line, "utf8");
|
|
3509
3704
|
} catch {
|
|
3510
3705
|
}
|
|
@@ -3611,14 +3806,14 @@ function collectIdsFromCacheEntries(entries, keys) {
|
|
|
3611
3806
|
return ids;
|
|
3612
3807
|
}
|
|
3613
3808
|
function getListenerOwnerLockPath(profile) {
|
|
3614
|
-
return
|
|
3809
|
+
return path7.join(getProfileDir(profile), "listener-owner.json");
|
|
3615
3810
|
}
|
|
3616
3811
|
function getListenIpcSocketPath(profile) {
|
|
3617
3812
|
if (process.platform === "win32") {
|
|
3618
3813
|
const safe = profile.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
3619
3814
|
return `\\\\.\\pipe\\openzca-listen-${safe}`;
|
|
3620
3815
|
}
|
|
3621
|
-
return
|
|
3816
|
+
return path7.join(getProfileDir(profile), "listen.sock");
|
|
3622
3817
|
}
|
|
3623
3818
|
function parsePositiveIntFromUnknown(value) {
|
|
3624
3819
|
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
@@ -3702,7 +3897,7 @@ function isProcessAlive(pid) {
|
|
|
3702
3897
|
}
|
|
3703
3898
|
async function readListenerOwnerRecord(lockPath) {
|
|
3704
3899
|
try {
|
|
3705
|
-
const raw = await
|
|
3900
|
+
const raw = await fs7.readFile(lockPath, "utf8");
|
|
3706
3901
|
const parsed = JSON.parse(raw);
|
|
3707
3902
|
const pid = parsePositiveIntFromUnknown(parsed.pid);
|
|
3708
3903
|
if (!pid) return null;
|
|
@@ -3722,11 +3917,11 @@ async function readActiveListenerOwner(profile) {
|
|
|
3722
3917
|
const lockPath = getListenerOwnerLockPath(profile);
|
|
3723
3918
|
const record = await readListenerOwnerRecord(lockPath);
|
|
3724
3919
|
if (!record) {
|
|
3725
|
-
await
|
|
3920
|
+
await fs7.rm(lockPath, { force: true });
|
|
3726
3921
|
return null;
|
|
3727
3922
|
}
|
|
3728
3923
|
if (!isProcessAlive(record.pid)) {
|
|
3729
|
-
await
|
|
3924
|
+
await fs7.rm(lockPath, { force: true });
|
|
3730
3925
|
return null;
|
|
3731
3926
|
}
|
|
3732
3927
|
return record;
|
|
@@ -3742,7 +3937,7 @@ async function acquireListenerOwnerLock(profile, sessionId, command) {
|
|
|
3742
3937
|
};
|
|
3743
3938
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
3744
3939
|
try {
|
|
3745
|
-
await
|
|
3940
|
+
await fs7.writeFile(lockPath, `${JSON.stringify(record, null, 2)}
|
|
3746
3941
|
`, {
|
|
3747
3942
|
encoding: "utf8",
|
|
3748
3943
|
flag: "wx"
|
|
@@ -3755,7 +3950,7 @@ async function acquireListenerOwnerLock(profile, sessionId, command) {
|
|
|
3755
3950
|
released = true;
|
|
3756
3951
|
const current = await readListenerOwnerRecord(lockPath);
|
|
3757
3952
|
if (current && current.pid !== process.pid) return;
|
|
3758
|
-
await
|
|
3953
|
+
await fs7.rm(lockPath, { force: true });
|
|
3759
3954
|
writeDebugLine(
|
|
3760
3955
|
"listen.owner.released",
|
|
3761
3956
|
{
|
|
@@ -3776,7 +3971,7 @@ async function acquireListenerOwnerLock(profile, sessionId, command) {
|
|
|
3776
3971
|
`Another openzca listener already owns profile "${profile}" (pid ${owner.pid}).`
|
|
3777
3972
|
);
|
|
3778
3973
|
}
|
|
3779
|
-
await
|
|
3974
|
+
await fs7.rm(lockPath, { force: true });
|
|
3780
3975
|
}
|
|
3781
3976
|
}
|
|
3782
3977
|
throw new Error(`Unable to acquire listener ownership for profile "${profile}".`);
|
|
@@ -3794,7 +3989,7 @@ async function startListenerIpcServer(api, profile, sessionId, command) {
|
|
|
3794
3989
|
}
|
|
3795
3990
|
const socketPath = getListenIpcSocketPath(profile);
|
|
3796
3991
|
if (process.platform !== "win32") {
|
|
3797
|
-
await
|
|
3992
|
+
await fs7.rm(socketPath, { force: true });
|
|
3798
3993
|
}
|
|
3799
3994
|
const uploadTimeoutMs = parsePositiveIntFromEnv(
|
|
3800
3995
|
"OPENZCA_UPLOAD_IPC_HANDLER_TIMEOUT_MS",
|
|
@@ -3975,7 +4170,7 @@ async function startListenerIpcServer(api, profile, sessionId, command) {
|
|
|
3975
4170
|
server.close(() => resolve());
|
|
3976
4171
|
});
|
|
3977
4172
|
if (process.platform !== "win32") {
|
|
3978
|
-
await
|
|
4173
|
+
await fs7.rm(socketPath, { force: true });
|
|
3979
4174
|
}
|
|
3980
4175
|
writeDebugLine(
|
|
3981
4176
|
"listen.ipc.stopped",
|
|
@@ -4241,9 +4436,9 @@ async function profileForLogin() {
|
|
|
4241
4436
|
await ensureProfile(fallback);
|
|
4242
4437
|
return fallback;
|
|
4243
4438
|
}
|
|
4244
|
-
async function requireApi(command) {
|
|
4439
|
+
async function requireApi(command, options) {
|
|
4245
4440
|
const profile = await currentProfile(command);
|
|
4246
|
-
const api = await loginWithStoredCredentials(profile);
|
|
4441
|
+
const api = await loginWithStoredCredentials(profile, options);
|
|
4247
4442
|
return { profile, api };
|
|
4248
4443
|
}
|
|
4249
4444
|
function toDbThreadType(groupFlag) {
|
|
@@ -6022,7 +6217,7 @@ function toDbRecordFromRecentMessage(params) {
|
|
|
6022
6217
|
});
|
|
6023
6218
|
}
|
|
6024
6219
|
async function parseCredentialFile(filePath) {
|
|
6025
|
-
const raw = await
|
|
6220
|
+
const raw = await fs7.readFile(filePath, "utf8");
|
|
6026
6221
|
const parsed = JSON.parse(raw);
|
|
6027
6222
|
if (!parsed.imei || !parsed.cookie || !parsed.userAgent) {
|
|
6028
6223
|
throw new Error("Credential file must include imei, cookie, and userAgent.");
|
|
@@ -6043,7 +6238,7 @@ async function waitForFileContent(filePath, timeoutMs) {
|
|
|
6043
6238
|
const startedAt = Date.now();
|
|
6044
6239
|
while (Date.now() - startedAt < timeoutMs) {
|
|
6045
6240
|
try {
|
|
6046
|
-
const data = await
|
|
6241
|
+
const data = await fs7.readFile(filePath);
|
|
6047
6242
|
if (data.length > 0) {
|
|
6048
6243
|
return data;
|
|
6049
6244
|
}
|
|
@@ -6058,8 +6253,8 @@ async function emitQrBase64FromDetachedLogin(profile, qrPath) {
|
|
|
6058
6253
|
if (!scriptPath) {
|
|
6059
6254
|
throw new Error("Cannot resolve CLI entrypoint for QR base64 mode.");
|
|
6060
6255
|
}
|
|
6061
|
-
const tempDir = await
|
|
6062
|
-
const targetPath =
|
|
6256
|
+
const tempDir = await fs7.mkdtemp(path7.join(os5.tmpdir(), "openzca-qr-"));
|
|
6257
|
+
const targetPath = path7.resolve(qrPath ?? path7.join(tempDir, "qr.png"));
|
|
6063
6258
|
const child = spawn2(
|
|
6064
6259
|
process.execPath,
|
|
6065
6260
|
[scriptPath, "--profile", profile, "auth", "login", "--qr-path", targetPath],
|
|
@@ -6099,7 +6294,7 @@ function normalizeMessageType(value) {
|
|
|
6099
6294
|
if (typeof value !== "string") return "";
|
|
6100
6295
|
return value.trim().toLowerCase();
|
|
6101
6296
|
}
|
|
6102
|
-
function
|
|
6297
|
+
function looksLikeStructuredJsonString2(value) {
|
|
6103
6298
|
const trimmed = value.trim();
|
|
6104
6299
|
if (trimmed.length < 2) return false;
|
|
6105
6300
|
const first = trimmed[0];
|
|
@@ -6114,7 +6309,7 @@ function normalizeStructuredContent(value, depth = 0) {
|
|
|
6114
6309
|
}
|
|
6115
6310
|
if (typeof value === "string") {
|
|
6116
6311
|
const trimmed = value.trim();
|
|
6117
|
-
if (!
|
|
6312
|
+
if (!looksLikeStructuredJsonString2(trimmed)) {
|
|
6118
6313
|
return value;
|
|
6119
6314
|
}
|
|
6120
6315
|
try {
|
|
@@ -6337,7 +6532,7 @@ function mediaExtFromTypeOrUrl(mediaType, mediaUrl, kind) {
|
|
|
6337
6532
|
if (fromType) return fromType;
|
|
6338
6533
|
try {
|
|
6339
6534
|
const parsedUrl = new URL(mediaUrl);
|
|
6340
|
-
const ext =
|
|
6535
|
+
const ext = path7.extname(parsedUrl.pathname);
|
|
6341
6536
|
if (ext) return ext;
|
|
6342
6537
|
} catch {
|
|
6343
6538
|
}
|
|
@@ -6368,20 +6563,20 @@ function parseInboundMediaFetchTimeoutMs() {
|
|
|
6368
6563
|
return Math.trunc(parsed);
|
|
6369
6564
|
}
|
|
6370
6565
|
function resolveOpenClawMediaDir() {
|
|
6371
|
-
const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() || process.env.CLAWDBOT_STATE_DIR?.trim() ||
|
|
6372
|
-
return
|
|
6566
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() || process.env.CLAWDBOT_STATE_DIR?.trim() || path7.join(os5.homedir(), ".openclaw");
|
|
6567
|
+
return path7.join(stateDir, "media");
|
|
6373
6568
|
}
|
|
6374
6569
|
function resolveInboundMediaDir(profile) {
|
|
6375
6570
|
const configuredRaw = process.env.OPENZCA_LISTEN_MEDIA_DIR?.trim();
|
|
6376
6571
|
if (configuredRaw) {
|
|
6377
6572
|
const configured = normalizeMediaInput(configuredRaw);
|
|
6378
|
-
return
|
|
6573
|
+
return path7.isAbsolute(configured) ? configured : path7.resolve(process.cwd(), configured);
|
|
6379
6574
|
}
|
|
6380
6575
|
const legacyRequested = process.env.OPENZCA_LISTEN_MEDIA_LEGACY_DIR?.trim() === "1";
|
|
6381
6576
|
if (legacyRequested) {
|
|
6382
|
-
return
|
|
6577
|
+
return path7.join(getProfileDir(profile), "inbound-media");
|
|
6383
6578
|
}
|
|
6384
|
-
return
|
|
6579
|
+
return path7.join(resolveOpenClawMediaDir(), "openzca", profile, "inbound");
|
|
6385
6580
|
}
|
|
6386
6581
|
async function cacheInboundMediaToProfile(profile, mediaUrl, kind) {
|
|
6387
6582
|
const maxBytes = parseMaxInboundMediaBytes();
|
|
@@ -6415,11 +6610,11 @@ async function cacheInboundMediaToProfile(profile, mediaUrl, kind) {
|
|
|
6415
6610
|
return null;
|
|
6416
6611
|
}
|
|
6417
6612
|
const dir = resolveInboundMediaDir(profile);
|
|
6418
|
-
await
|
|
6613
|
+
await fs7.mkdir(dir, { recursive: true });
|
|
6419
6614
|
const ext = mediaExtFromTypeOrUrl(mediaType, mediaUrl, kind);
|
|
6420
6615
|
const id = `${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
|
|
6421
|
-
const mediaPath =
|
|
6422
|
-
await
|
|
6616
|
+
const mediaPath = path7.join(dir, `${id}${ext}`);
|
|
6617
|
+
await fs7.writeFile(mediaPath, data);
|
|
6423
6618
|
return { mediaPath, mediaType };
|
|
6424
6619
|
}
|
|
6425
6620
|
async function cacheRemoteMediaEntries(params) {
|
|
@@ -6640,7 +6835,7 @@ function collectInboundMentions(value, sink, rawText, depth = 0) {
|
|
|
6640
6835
|
return;
|
|
6641
6836
|
}
|
|
6642
6837
|
if (typeof value === "string") {
|
|
6643
|
-
if (!
|
|
6838
|
+
if (!looksLikeStructuredJsonString2(value)) return;
|
|
6644
6839
|
try {
|
|
6645
6840
|
const parsed = JSON.parse(value);
|
|
6646
6841
|
collectInboundMentions(parsed, sink, rawText, depth + 1);
|
|
@@ -6929,7 +7124,7 @@ auth.command("login").description("Login with QR code").option("-q, --qr-path <p
|
|
|
6929
7124
|
auth.command("login-cred [file]").alias("login-creds").description("Login using credential JSON file").action(
|
|
6930
7125
|
wrapAction(async (file, command) => {
|
|
6931
7126
|
const profile = await currentProfile(command);
|
|
6932
|
-
const credentials = file ? await parseCredentialFile(
|
|
7127
|
+
const credentials = file ? await parseCredentialFile(path7.resolve(normalizeMediaInput(file))) : toCredentials(
|
|
6933
7128
|
await loadCredentials(profile) ?? (() => {
|
|
6934
7129
|
throw new Error(
|
|
6935
7130
|
`No saved credentials for profile "${profile}". Run: openzca auth login`
|
|
@@ -7036,7 +7231,7 @@ dbCmd.command("reset").option("-y, --yes", "Delete the SQLite DB file for the ac
|
|
|
7036
7231
|
const removedPaths = [];
|
|
7037
7232
|
const deleteIfExists = async (filename) => {
|
|
7038
7233
|
try {
|
|
7039
|
-
await
|
|
7234
|
+
await fs7.unlink(filename);
|
|
7040
7235
|
removedPaths.push(filename);
|
|
7041
7236
|
} catch (error) {
|
|
7042
7237
|
if (error.code !== "ENOENT") {
|
|
@@ -7749,53 +7944,108 @@ msg.command("voice <threadId> [file]").option("-u, --url <url>", "Voice URL (rep
|
|
|
7749
7944
|
}
|
|
7750
7945
|
const urlInputs = files.filter((entry) => isHttpUrl(entry));
|
|
7751
7946
|
const localInputs = files.filter((entry) => !isHttpUrl(entry));
|
|
7947
|
+
const publishCommand = getVoicePublishCommandFromEnv();
|
|
7948
|
+
const ffmpegAvailable = localInputs.length > 0 && publishCommand ? await isFfmpegAvailable() : false;
|
|
7949
|
+
const usePublishFlow = localInputs.length > 0 && Boolean(publishCommand) && ffmpegAvailable;
|
|
7752
7950
|
writeDebugLine(
|
|
7753
7951
|
"msg.voice.inputs",
|
|
7754
7952
|
{
|
|
7755
7953
|
threadId,
|
|
7756
7954
|
isGroup: Boolean(opts.group),
|
|
7757
7955
|
localInputs,
|
|
7758
|
-
urlInputs
|
|
7956
|
+
urlInputs,
|
|
7957
|
+
publishConfigured: Boolean(publishCommand),
|
|
7958
|
+
ffmpegAvailable: localInputs.length > 0 ? ffmpegAvailable : void 0,
|
|
7959
|
+
mode: usePublishFlow ? "publish" : "legacy"
|
|
7759
7960
|
},
|
|
7760
7961
|
command
|
|
7761
7962
|
);
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
const
|
|
7767
|
-
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
|
|
7963
|
+
await assertFilesExist(localInputs);
|
|
7964
|
+
const publishedLocals = [];
|
|
7965
|
+
let uploadedLocals = [];
|
|
7966
|
+
if (usePublishFlow) {
|
|
7967
|
+
for (const localInput of localInputs) {
|
|
7968
|
+
const normalized = await normalizeVoiceForPublish(localInput);
|
|
7969
|
+
try {
|
|
7970
|
+
const mediaUrl = await publishVoiceFile(publishCommand, normalized.path);
|
|
7971
|
+
publishedLocals.push({
|
|
7972
|
+
mediaPath: localInput,
|
|
7973
|
+
mediaUrl
|
|
7974
|
+
});
|
|
7975
|
+
} finally {
|
|
7976
|
+
await normalized.cleanup();
|
|
7771
7977
|
}
|
|
7772
7978
|
}
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
7979
|
+
} else if (localInputs.length > 0) {
|
|
7980
|
+
uploadedLocals = await withUploadListener(
|
|
7981
|
+
api,
|
|
7982
|
+
command,
|
|
7983
|
+
async () => api.uploadAttachment(localInputs, threadId, type)
|
|
7984
|
+
);
|
|
7985
|
+
}
|
|
7986
|
+
const pendingPublished = [...publishedLocals];
|
|
7987
|
+
const pendingUploaded = [...uploadedLocals];
|
|
7988
|
+
const outboundVoices = [];
|
|
7989
|
+
for (const entry of files) {
|
|
7990
|
+
if (isHttpUrl(entry)) {
|
|
7991
|
+
outboundVoices.push({
|
|
7992
|
+
mediaUrl: entry
|
|
7993
|
+
});
|
|
7994
|
+
continue;
|
|
7777
7995
|
}
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7996
|
+
if (usePublishFlow) {
|
|
7997
|
+
const nextPublished = pendingPublished.shift();
|
|
7998
|
+
if (!nextPublished) {
|
|
7999
|
+
throw new Error(`Voice publish flow lost local file mapping for: ${entry}`);
|
|
8000
|
+
}
|
|
8001
|
+
outboundVoices.push(nextPublished);
|
|
8002
|
+
continue;
|
|
8003
|
+
}
|
|
8004
|
+
const nextUploaded = pendingUploaded.shift();
|
|
8005
|
+
if (!nextUploaded) {
|
|
8006
|
+
throw new Error(`Voice upload flow lost local file mapping for: ${entry}`);
|
|
8007
|
+
}
|
|
8008
|
+
if (nextUploaded.fileType === "others" || nextUploaded.fileType === "video") {
|
|
8009
|
+
outboundVoices.push({
|
|
8010
|
+
mediaPath: entry,
|
|
8011
|
+
mediaUrl: nextUploaded.fileUrl,
|
|
8012
|
+
rawJson: JSON.stringify(nextUploaded)
|
|
7795
8013
|
});
|
|
7796
8014
|
}
|
|
7797
|
-
}
|
|
7798
|
-
|
|
8015
|
+
}
|
|
8016
|
+
if (outboundVoices.length === 0) {
|
|
8017
|
+
throw new Error(
|
|
8018
|
+
"No valid voice attachment generated. Use an audio file (e.g. .aac, .mp3, .m4a, .wav, .ogg)."
|
|
8019
|
+
);
|
|
8020
|
+
}
|
|
8021
|
+
const results = [];
|
|
8022
|
+
for (const item of outboundVoices) {
|
|
8023
|
+
results.push(await sendVoice({ voiceUrl: item.mediaUrl }, threadId, type));
|
|
8024
|
+
}
|
|
8025
|
+
output(results, false);
|
|
8026
|
+
if (await shouldWriteToDb(profile)) {
|
|
8027
|
+
scheduleDbWrite(profile, command, "msg.voice.db.persist_error", async () => {
|
|
8028
|
+
await persistOutgoingMessageBestEffort({
|
|
8029
|
+
profile,
|
|
8030
|
+
api,
|
|
8031
|
+
threadId,
|
|
8032
|
+
group: opts.group,
|
|
8033
|
+
msgType: "voice",
|
|
8034
|
+
response: results,
|
|
8035
|
+
rawPayload: {
|
|
8036
|
+
mode: usePublishFlow ? "publish" : "legacy",
|
|
8037
|
+
directUrls: urlInputs,
|
|
8038
|
+
published: publishedLocals,
|
|
8039
|
+
uploaded: uploadedLocals
|
|
8040
|
+
},
|
|
8041
|
+
media: outboundVoices.map((item) => ({
|
|
8042
|
+
mediaKind: "voice",
|
|
8043
|
+
mediaPath: item.mediaPath,
|
|
8044
|
+
mediaUrl: item.mediaUrl,
|
|
8045
|
+
rawJson: item.rawJson
|
|
8046
|
+
}))
|
|
8047
|
+
});
|
|
8048
|
+
});
|
|
7799
8049
|
}
|
|
7800
8050
|
}
|
|
7801
8051
|
)
|
|
@@ -8255,23 +8505,23 @@ group.command("create <name> <members...>").description("Create new group").acti
|
|
|
8255
8505
|
})
|
|
8256
8506
|
);
|
|
8257
8507
|
var groupPoll = group.command("poll").description("Group poll management");
|
|
8258
|
-
groupPoll.command("create <groupId>").requiredOption("-q, --question <text>", "Poll question").requiredOption("-o, --option <text>", "Poll option (repeatable)", collectValues, []).option("--multi", "Allow multiple choices").option("--allow-add-option", "Allow members to add new options").option("--hide-vote-preview", "Hide results until the member votes").option("--anonymous", "Hide voters").option("--expire-ms <ms>", "Poll expiration time in milliseconds").description("Create a poll in a group").action(
|
|
8508
|
+
groupPoll.command("create <groupId>").requiredOption("-q, --question <text>", "Poll question").requiredOption("-o, --option <text>", "Poll option (repeatable)", collectValues, []).option("-j, --json", "JSON output").option("--multi", "Allow multiple choices").option("--allow-add-option", "Allow members to add new options").option("--hide-vote-preview", "Hide results until the member votes").option("--anonymous", "Hide voters").option("--expire-ms <ms>", "Poll expiration time in milliseconds").description("Create a poll in a group").action(
|
|
8259
8509
|
wrapAction(
|
|
8260
8510
|
async (groupId, opts, command) => {
|
|
8261
8511
|
const pollOptions = buildCreatePollOptions(opts);
|
|
8262
8512
|
const { api } = await requireApi(command);
|
|
8263
|
-
output(await api.createPoll(pollOptions, groupId),
|
|
8513
|
+
output(await api.createPoll(pollOptions, groupId), shouldOutputJson(opts));
|
|
8264
8514
|
}
|
|
8265
8515
|
)
|
|
8266
8516
|
);
|
|
8267
|
-
groupPoll.command("detail <pollId>").description("Get poll detail").action(
|
|
8268
|
-
wrapAction(async (pollId, command) => {
|
|
8517
|
+
groupPoll.command("detail <pollId>").option("-j, --json", "JSON output").description("Get poll detail").action(
|
|
8518
|
+
wrapAction(async (pollId, opts, command) => {
|
|
8269
8519
|
const normalizedPollId = parsePollId(pollId);
|
|
8270
8520
|
const { api } = await requireApi(command);
|
|
8271
|
-
output(await api.getPollDetail(normalizedPollId),
|
|
8521
|
+
output(await api.getPollDetail(normalizedPollId), shouldOutputJson(opts));
|
|
8272
8522
|
})
|
|
8273
8523
|
);
|
|
8274
|
-
groupPoll.command("vote <pollId>").requiredOption("-o, --option <id>", "Poll option id (repeatable)", collectValues, []).description("Vote on a group poll").action(
|
|
8524
|
+
groupPoll.command("vote <pollId>").requiredOption("-o, --option <id>", "Poll option id (repeatable)", collectValues, []).option("-j, --json", "JSON output").description("Vote on a group poll").action(
|
|
8275
8525
|
wrapAction(
|
|
8276
8526
|
async (pollId, opts, command) => {
|
|
8277
8527
|
const normalizedPollId = parsePollId(pollId);
|
|
@@ -8281,22 +8531,22 @@ groupPoll.command("vote <pollId>").requiredOption("-o, --option <id>", "Poll opt
|
|
|
8281
8531
|
normalizedPollId,
|
|
8282
8532
|
optionIds.length === 1 ? optionIds[0] : optionIds
|
|
8283
8533
|
);
|
|
8284
|
-
output(response,
|
|
8534
|
+
output(response, shouldOutputJson(opts));
|
|
8285
8535
|
}
|
|
8286
8536
|
)
|
|
8287
8537
|
);
|
|
8288
|
-
groupPoll.command("lock <pollId>").description("Close a poll").action(
|
|
8289
|
-
wrapAction(async (pollId, command) => {
|
|
8538
|
+
groupPoll.command("lock <pollId>").option("-j, --json", "JSON output").description("Close a poll").action(
|
|
8539
|
+
wrapAction(async (pollId, opts, command) => {
|
|
8290
8540
|
const normalizedPollId = parsePollId(pollId);
|
|
8291
8541
|
const { api } = await requireApi(command);
|
|
8292
|
-
output(await api.lockPoll(normalizedPollId),
|
|
8542
|
+
output(await api.lockPoll(normalizedPollId), shouldOutputJson(opts));
|
|
8293
8543
|
})
|
|
8294
8544
|
);
|
|
8295
|
-
groupPoll.command("share <pollId>").description("Share a poll").action(
|
|
8296
|
-
wrapAction(async (pollId, command) => {
|
|
8545
|
+
groupPoll.command("share <pollId>").option("-j, --json", "JSON output").description("Share a poll").action(
|
|
8546
|
+
wrapAction(async (pollId, opts, command) => {
|
|
8297
8547
|
const normalizedPollId = parsePollId(pollId);
|
|
8298
8548
|
const { api } = await requireApi(command);
|
|
8299
|
-
output(await api.sharePoll(normalizedPollId),
|
|
8549
|
+
output(await api.sharePoll(normalizedPollId), shouldOutputJson(opts));
|
|
8300
8550
|
})
|
|
8301
8551
|
);
|
|
8302
8552
|
group.command("rename <groupId> <name>").description("Rename group").action(
|
|
@@ -8751,7 +9001,7 @@ me.command("last-online <userId>").description("Get last online of a user").acti
|
|
|
8751
9001
|
output(await api.lastOnline(userId), false);
|
|
8752
9002
|
})
|
|
8753
9003
|
);
|
|
8754
|
-
program.command("listen").description("Listen for real-time incoming messages").option("-e, --echo", "Echo incoming text message").option("-p, --prefix <prefix>", "Only process text starting with prefix").option("-w, --webhook <url>", "POST message payload to webhook").option("-r, --raw", "Output JSON line payload").option("--db", "Force DB persistence for this listener session").option("--no-db", "Disable DB persistence for this listener session").option("-k, --keep-alive", "Auto restart listener on disconnect").option(
|
|
9004
|
+
program.command("listen").description("Listen for real-time incoming messages").option("-e, --echo", "Echo incoming text message").option("-p, --prefix <prefix>", "Only process text starting with prefix").option("-w, --webhook <url>", "POST message payload to webhook").option("-r, --raw", "Output JSON line payload").option("--self", "Include events produced by the logged-in account").option("--db", "Force DB persistence for this listener session").option("--no-db", "Disable DB persistence for this listener session").option("-k, --keep-alive", "Auto restart listener on disconnect").option(
|
|
8755
9005
|
"--supervised",
|
|
8756
9006
|
"Supervisor mode (disable internal retry ownership; emit lifecycle events in --raw)"
|
|
8757
9007
|
).option(
|
|
@@ -8763,7 +9013,8 @@ program.command("listen").description("Listen for real-time incoming messages").
|
|
|
8763
9013
|
).action(
|
|
8764
9014
|
wrapAction(
|
|
8765
9015
|
async (opts, command) => {
|
|
8766
|
-
const
|
|
9016
|
+
const selfListen = Boolean(opts.self);
|
|
9017
|
+
const { profile, api } = await requireApi(command, { selfListen });
|
|
8767
9018
|
const supervised = Boolean(opts.supervised);
|
|
8768
9019
|
const defaultRecycleMs = 30 * 60 * 1e3;
|
|
8769
9020
|
const recycleMs = parseNonNegativeIntOption("--recycle-ms", opts.recycleMs) ?? parseNonNegativeIntOption(
|
|
@@ -8856,6 +9107,7 @@ program.command("listen").description("Listen for real-time incoming messages").
|
|
|
8856
9107
|
maxMediaFiles: parseMaxInboundMediaFiles(),
|
|
8857
9108
|
includeMediaUrl: process.env.OPENZCA_LISTEN_INCLUDE_MEDIA_URL?.trim() ?? null,
|
|
8858
9109
|
keepAlive: Boolean(opts.keepAlive),
|
|
9110
|
+
selfListen,
|
|
8859
9111
|
keepAliveRestartDelayMs: Boolean(opts.keepAlive) ? keepAliveRestartDelayMs : void 0,
|
|
8860
9112
|
keepAliveRestartOnAnyClose: Boolean(opts.keepAlive) ? keepAliveRestartOnAnyClose : void 0,
|
|
8861
9113
|
supervised,
|
|
@@ -9020,6 +9272,7 @@ ${replyContextText}` : replyContextText;
|
|
|
9020
9272
|
rawText
|
|
9021
9273
|
});
|
|
9022
9274
|
const mentionIds = mentions.map((item) => item.uid);
|
|
9275
|
+
const poll = extractInboundPollInfo(messageData, parsedContent);
|
|
9023
9276
|
const timestamp = toEpochSeconds(message.data.ts);
|
|
9024
9277
|
const timestampMs = toEpochMs(message.data.ts);
|
|
9025
9278
|
const payload = {
|
|
@@ -9048,6 +9301,11 @@ ${replyContextText}` : replyContextText;
|
|
|
9048
9301
|
mediaKind: mediaKind ?? void 0,
|
|
9049
9302
|
mentions: mentions.length > 0 ? mentions : void 0,
|
|
9050
9303
|
mentionIds: mentionIds.length > 0 ? mentionIds : void 0,
|
|
9304
|
+
poll: poll ?? void 0,
|
|
9305
|
+
pollId: poll?.pollId,
|
|
9306
|
+
pollTitle: poll?.title,
|
|
9307
|
+
pollOptionIds: poll?.optionIds,
|
|
9308
|
+
rawMessage: poll ? message.data : void 0,
|
|
9051
9309
|
metadata: {
|
|
9052
9310
|
isGroup: message.type === ThreadType3.Group,
|
|
9053
9311
|
chatType,
|
|
@@ -9077,7 +9335,12 @@ ${replyContextText}` : replyContextText;
|
|
|
9077
9335
|
mediaKind: mediaKind ?? void 0,
|
|
9078
9336
|
mentions: mentions.length > 0 ? mentions : void 0,
|
|
9079
9337
|
mentionIds: mentionIds.length > 0 ? mentionIds : void 0,
|
|
9080
|
-
mentionCount: mentions.length > 0 ? mentions.length : void 0
|
|
9338
|
+
mentionCount: mentions.length > 0 ? mentions.length : void 0,
|
|
9339
|
+
poll: poll ?? void 0,
|
|
9340
|
+
pollId: poll?.pollId,
|
|
9341
|
+
pollTitle: poll?.title,
|
|
9342
|
+
pollOptionIds: poll?.optionIds,
|
|
9343
|
+
rawMessage: poll ? message.data : void 0
|
|
9081
9344
|
},
|
|
9082
9345
|
// Backward-compatible convenience fields.
|
|
9083
9346
|
chatType,
|
|
@@ -9180,6 +9443,69 @@ ${replyContextText}` : replyContextText;
|
|
|
9180
9443
|
}
|
|
9181
9444
|
}
|
|
9182
9445
|
});
|
|
9446
|
+
api.listener.on("group_event", async (event) => {
|
|
9447
|
+
const poll = extractInboundPollInfo(event);
|
|
9448
|
+
if (!poll) return;
|
|
9449
|
+
const eventData = asObject(event.data);
|
|
9450
|
+
const groupTopic = asObject(eventData?.groupTopic);
|
|
9451
|
+
const actorId = getStringCandidate(eventData ?? {}, ["sourceId", "creatorId", "actorId", "editorId"]) || getStringCandidate(groupTopic ?? {}, ["creatorId", "editorId"]);
|
|
9452
|
+
const threadName = getStringCandidate(eventData ?? {}, ["groupName"]);
|
|
9453
|
+
const timestampSource = eventData?.time ?? groupTopic?.createTime ?? groupTopic?.editTime ?? Date.now();
|
|
9454
|
+
const timestamp = toEpochSeconds(timestampSource);
|
|
9455
|
+
const payload = {
|
|
9456
|
+
kind: "group_event",
|
|
9457
|
+
event: "poll",
|
|
9458
|
+
threadId: event.threadId,
|
|
9459
|
+
targetId: event.threadId,
|
|
9460
|
+
conversationId: event.threadId,
|
|
9461
|
+
type: ThreadType3.Group,
|
|
9462
|
+
timestamp,
|
|
9463
|
+
groupEventType: event.type,
|
|
9464
|
+
act: event.act,
|
|
9465
|
+
poll,
|
|
9466
|
+
pollId: poll.pollId,
|
|
9467
|
+
pollTitle: poll.title,
|
|
9468
|
+
pollOptionIds: poll.optionIds,
|
|
9469
|
+
rawGroupEvent: event,
|
|
9470
|
+
metadata: {
|
|
9471
|
+
isGroup: true,
|
|
9472
|
+
chatType: "group",
|
|
9473
|
+
threadId: event.threadId,
|
|
9474
|
+
targetId: event.threadId,
|
|
9475
|
+
threadName: threadName || void 0,
|
|
9476
|
+
senderId: actorId || void 0,
|
|
9477
|
+
fromId: actorId || void 0,
|
|
9478
|
+
timestamp,
|
|
9479
|
+
groupEventType: event.type,
|
|
9480
|
+
act: event.act,
|
|
9481
|
+
poll,
|
|
9482
|
+
pollId: poll.pollId,
|
|
9483
|
+
pollTitle: poll.title,
|
|
9484
|
+
pollOptionIds: poll.optionIds,
|
|
9485
|
+
rawGroupEvent: event
|
|
9486
|
+
},
|
|
9487
|
+
chatType: "group",
|
|
9488
|
+
senderId: actorId || void 0,
|
|
9489
|
+
senderName: void 0,
|
|
9490
|
+
senderDisplayName: void 0
|
|
9491
|
+
};
|
|
9492
|
+
writeDebugLine(
|
|
9493
|
+
"listen.group_event.poll",
|
|
9494
|
+
{
|
|
9495
|
+
profile,
|
|
9496
|
+
threadId: event.threadId,
|
|
9497
|
+
groupEventType: event.type,
|
|
9498
|
+
act: event.act,
|
|
9499
|
+
pollId: poll.pollId,
|
|
9500
|
+
pollTitle: poll.title,
|
|
9501
|
+
sessionId
|
|
9502
|
+
},
|
|
9503
|
+
command
|
|
9504
|
+
);
|
|
9505
|
+
if (opts.raw) {
|
|
9506
|
+
console.log(JSON.stringify(payload));
|
|
9507
|
+
}
|
|
9508
|
+
});
|
|
9183
9509
|
api.listener.on("error", (error) => {
|
|
9184
9510
|
writeDebugLine(
|
|
9185
9511
|
"listen.error",
|