weacpx 0.4.8 → 0.4.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/dist/cli.js +1620 -534
- package/dist/config/types.d.ts +1 -0
- package/dist/orchestration/orchestration-types.d.ts +7 -1
- package/dist/weixin/api/api.d.ts +28 -0
- package/dist/weixin/api/session-guard.d.ts +2 -0
- package/dist/weixin/api/types.d.ts +2 -0
- package/dist/weixin/auth/accounts.d.ts +7 -0
- package/dist/weixin/auth/login-qr.d.ts +6 -0
- package/dist/weixin/index.d.ts +1 -1
- package/dist/weixin/messaging/inbound.d.ts +18 -1
- package/dist/weixin/messaging/markdown-filter.d.ts +45 -0
- package/dist/weixin/messaging/send.d.ts +3 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2424,7 +2424,8 @@ function parseOrchestrationConfig(raw) {
|
|
|
2424
2424
|
allowWorkerChainedRequests: raw.allowWorkerChainedRequests === true,
|
|
2425
2425
|
allowedAgentRequestTargets: Array.isArray(raw.allowedAgentRequestTargets) ? raw.allowedAgentRequestTargets.filter((value) => typeof value === "string") : [...DEFAULT_ORCHESTRATION_CONFIG.allowedAgentRequestTargets],
|
|
2426
2426
|
allowedAgentRequestRoles: Array.isArray(raw.allowedAgentRequestRoles) ? raw.allowedAgentRequestRoles.filter((value) => typeof value === "string") : [...DEFAULT_ORCHESTRATION_CONFIG.allowedAgentRequestRoles],
|
|
2427
|
-
progressHeartbeatSeconds: typeof raw.progressHeartbeatSeconds === "number" && Number.isFinite(raw.progressHeartbeatSeconds) ? raw.progressHeartbeatSeconds : DEFAULT_ORCHESTRATION_CONFIG.progressHeartbeatSeconds
|
|
2427
|
+
progressHeartbeatSeconds: typeof raw.progressHeartbeatSeconds === "number" && Number.isFinite(raw.progressHeartbeatSeconds) ? raw.progressHeartbeatSeconds : DEFAULT_ORCHESTRATION_CONFIG.progressHeartbeatSeconds,
|
|
2428
|
+
maxParallelTasksPerAgent: typeof raw.maxParallelTasksPerAgent === "number" && Number.isFinite(raw.maxParallelTasksPerAgent) && raw.maxParallelTasksPerAgent >= 1 ? Math.floor(raw.maxParallelTasksPerAgent) : DEFAULT_ORCHESTRATION_CONFIG.maxParallelTasksPerAgent
|
|
2428
2429
|
};
|
|
2429
2430
|
}
|
|
2430
2431
|
var DEFAULT_PERF_LOG_CONFIG, DEFAULT_LOGGING_CONFIG, DEFAULT_PERMISSION_MODE = "approve-all", DEFAULT_NON_INTERACTIVE_PERMISSIONS = "deny", DEFAULT_CHANNEL_CONFIG, DEFAULT_ORCHESTRATION_CONFIG;
|
|
@@ -2452,7 +2453,8 @@ var init_load_config = __esm(() => {
|
|
|
2452
2453
|
allowWorkerChainedRequests: false,
|
|
2453
2454
|
allowedAgentRequestTargets: [],
|
|
2454
2455
|
allowedAgentRequestRoles: [],
|
|
2455
|
-
progressHeartbeatSeconds: 300
|
|
2456
|
+
progressHeartbeatSeconds: 300,
|
|
2457
|
+
maxParallelTasksPerAgent: 3
|
|
2456
2458
|
};
|
|
2457
2459
|
});
|
|
2458
2460
|
|
|
@@ -9645,6 +9647,34 @@ var init_quota_errors = __esm(() => {
|
|
|
9645
9647
|
};
|
|
9646
9648
|
});
|
|
9647
9649
|
|
|
9650
|
+
// src/commands/workspace-name.ts
|
|
9651
|
+
function sanitizeWorkspaceName(input, fallback = "workspace") {
|
|
9652
|
+
const sanitized = input.trim().replace(UNSAFE_RUN_RE, "-").replace(TRIM_DASHES_RE, "");
|
|
9653
|
+
return sanitized.length > 0 ? sanitized : fallback;
|
|
9654
|
+
}
|
|
9655
|
+
function allocateWorkspaceName(base, existing) {
|
|
9656
|
+
if (!Object.prototype.hasOwnProperty.call(existing, base))
|
|
9657
|
+
return base;
|
|
9658
|
+
let suffix = 2;
|
|
9659
|
+
while (Object.prototype.hasOwnProperty.call(existing, `${base}-${suffix}`))
|
|
9660
|
+
suffix += 1;
|
|
9661
|
+
return `${base}-${suffix}`;
|
|
9662
|
+
}
|
|
9663
|
+
function isWorkspaceNameValid(input) {
|
|
9664
|
+
return VALID_WORKSPACE_NAME_RE.test(input);
|
|
9665
|
+
}
|
|
9666
|
+
function quoteWorkspaceNameIfNeeded(input) {
|
|
9667
|
+
if (isWorkspaceNameValid(input))
|
|
9668
|
+
return input;
|
|
9669
|
+
return `"${input.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
9670
|
+
}
|
|
9671
|
+
var VALID_WORKSPACE_NAME_RE, UNSAFE_RUN_RE, TRIM_DASHES_RE;
|
|
9672
|
+
var init_workspace_name = __esm(() => {
|
|
9673
|
+
VALID_WORKSPACE_NAME_RE = /^[a-zA-Z0-9._-]+$/;
|
|
9674
|
+
UNSAFE_RUN_RE = /[^a-zA-Z0-9._-]+/g;
|
|
9675
|
+
TRIM_DASHES_RE = /^-+|-+$/g;
|
|
9676
|
+
});
|
|
9677
|
+
|
|
9648
9678
|
// src/orchestration/orchestration-types.ts
|
|
9649
9679
|
function createEmptyOrchestrationState() {
|
|
9650
9680
|
return {
|
|
@@ -9683,7 +9713,7 @@ function isOptionalBoolean(value) {
|
|
|
9683
9713
|
return value === undefined || typeof value === "boolean";
|
|
9684
9714
|
}
|
|
9685
9715
|
function isTaskStatus(value) {
|
|
9686
|
-
return value === "needs_confirmation" || value === "running" || value === "blocked" || value === "waiting_for_human" || value === "completed" || value === "failed" || value === "cancelled";
|
|
9716
|
+
return value === "needs_confirmation" || value === "queued" || value === "running" || value === "blocked" || value === "waiting_for_human" || value === "completed" || value === "failed" || value === "cancelled";
|
|
9687
9717
|
}
|
|
9688
9718
|
function isSourceKind(value) {
|
|
9689
9719
|
return value === "human" || value === "coordinator" || value === "worker";
|
|
@@ -9719,7 +9749,7 @@ function isTaskRecord(value) {
|
|
|
9719
9749
|
if (!isRecord2(value)) {
|
|
9720
9750
|
return false;
|
|
9721
9751
|
}
|
|
9722
|
-
return isString(value.taskId) && isString(value.sourceHandle) && isSourceKind(value.sourceKind) && isString(value.coordinatorSession) && isOptionalString(value.workerSession) && isString(value.workspace) && isOptionalString(value.cwd) && isString(value.targetAgent) && isOptionalString(value.role) && isString(value.task) && isTaskStatus(value.status) && isString(value.summary) && isString(value.resultText) && isString(value.createdAt) && isString(value.updatedAt) && isOptionalString(value.chatKey) && isOptionalString(value.replyContextToken) && isOptionalString(value.accountId) && isOptionalString(value.deliveryAccountId) && isOptionalString(value.coordinatorInjectedAt) && isOptionalString(value.cancelRequestedAt) && isOptionalString(value.cancelCompletedAt) && isOptionalString(value.lastCancelError) && isOptionalBoolean(value.noticePending) && isOptionalString(value.noticeSentAt) && isOptionalString(value.lastNoticeError) && isOptionalBoolean(value.injectionPending) && isOptionalString(value.injectionAppliedAt) && isOptionalString(value.lastInjectionError) && isOptionalString(value.lastProgressAt) && isOptionalString(value.lastProgressSummary) && isOptionalString(value.groupId) && (value.openQuestion === undefined || isOpenQuestionRecord(value.openQuestion)) && (value.reviewPending === undefined || isReviewPendingRecord(value.reviewPending)) && (value.correctionPending === undefined || isCorrectionPendingRecord(value.correctionPending)) && isOptionalNumber(value.eventSeq) && (value.events === undefined || Array.isArray(value.events) && value.events.every(isTaskEventRecord));
|
|
9752
|
+
return isString(value.taskId) && isString(value.sourceHandle) && isSourceKind(value.sourceKind) && isString(value.coordinatorSession) && isOptionalString(value.workerSession) && isString(value.workspace) && isOptionalString(value.cwd) && isString(value.targetAgent) && isOptionalString(value.role) && isString(value.task) && isTaskStatus(value.status) && isString(value.summary) && isString(value.resultText) && isString(value.createdAt) && isString(value.updatedAt) && isOptionalString(value.chatKey) && isOptionalString(value.replyContextToken) && isOptionalString(value.accountId) && isOptionalString(value.deliveryAccountId) && isOptionalString(value.coordinatorInjectedAt) && isOptionalString(value.cancelRequestedAt) && isOptionalString(value.cancelCompletedAt) && isOptionalString(value.lastCancelError) && isOptionalBoolean(value.noticePending) && isOptionalString(value.noticeSentAt) && isOptionalString(value.lastNoticeError) && isOptionalBoolean(value.injectionPending) && isOptionalBoolean(value.ephemeralWorkerSession) && isOptionalBoolean(value.ephemeralWorkerSessionClosed) && isOptionalString(value.injectionAppliedAt) && isOptionalString(value.lastInjectionError) && isOptionalString(value.lastProgressAt) && isOptionalString(value.lastProgressSummary) && isOptionalString(value.groupId) && (value.openQuestion === undefined || isOpenQuestionRecord(value.openQuestion)) && (value.reviewPending === undefined || isReviewPendingRecord(value.reviewPending)) && (value.correctionPending === undefined || isCorrectionPendingRecord(value.correctionPending)) && isOptionalNumber(value.eventSeq) && (value.events === undefined || Array.isArray(value.events) && value.events.every(isTaskEventRecord));
|
|
9723
9753
|
}
|
|
9724
9754
|
function isExternalCoordinatorRecord(value) {
|
|
9725
9755
|
if (!isRecord2(value)) {
|
|
@@ -9731,7 +9761,7 @@ function isWorkerBindingRecord(value) {
|
|
|
9731
9761
|
if (!isRecord2(value)) {
|
|
9732
9762
|
return false;
|
|
9733
9763
|
}
|
|
9734
|
-
return isString(value.sourceHandle) && isString(value.coordinatorSession) && isString(value.workspace) && isOptionalString(value.cwd) && isString(value.targetAgent) && isOptionalString(value.role);
|
|
9764
|
+
return isString(value.sourceHandle) && isString(value.coordinatorSession) && isString(value.workspace) && isOptionalString(value.cwd) && isString(value.targetAgent) && isOptionalString(value.role) && isOptionalBoolean(value.ephemeral);
|
|
9735
9765
|
}
|
|
9736
9766
|
function isGroupRecord(value) {
|
|
9737
9767
|
if (!isRecord2(value)) {
|
|
@@ -9987,12 +10017,25 @@ var init_state_store = __esm(() => {
|
|
|
9987
10017
|
import { mkdir as mkdir6, writeFile as writeFile5 } from "node:fs/promises";
|
|
9988
10018
|
import { homedir as homedir3 } from "node:os";
|
|
9989
10019
|
import { join as join3 } from "node:path";
|
|
10020
|
+
function coerceMissing(value) {
|
|
10021
|
+
if (value === undefined)
|
|
10022
|
+
return;
|
|
10023
|
+
const trimmed = value.trim();
|
|
10024
|
+
if (!trimmed)
|
|
10025
|
+
return;
|
|
10026
|
+
const lower = trimmed.toLowerCase();
|
|
10027
|
+
if (lower === "undefined" || lower === "null")
|
|
10028
|
+
return;
|
|
10029
|
+
return trimmed;
|
|
10030
|
+
}
|
|
9990
10031
|
function resolvePluginHome(input = {}) {
|
|
9991
|
-
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
|
|
9995
|
-
|
|
10032
|
+
const explicit = coerceMissing(input.pluginHome);
|
|
10033
|
+
if (explicit)
|
|
10034
|
+
return explicit;
|
|
10035
|
+
const envOverride = coerceMissing(process.env.WEACPX_PLUGIN_HOME);
|
|
10036
|
+
if (envOverride)
|
|
10037
|
+
return envOverride;
|
|
10038
|
+
const home = coerceMissing(input.home) ?? coerceMissing(process.env.HOME) ?? homedir3();
|
|
9996
10039
|
return join3(home, ".weacpx", "plugins");
|
|
9997
10040
|
}
|
|
9998
10041
|
async function ensurePluginHome(pluginHome) {
|
|
@@ -10202,6 +10245,28 @@ function loadConfigRouteTag(accountId) {
|
|
|
10202
10245
|
return;
|
|
10203
10246
|
}
|
|
10204
10247
|
}
|
|
10248
|
+
function loadConfigBotAgent(accountId) {
|
|
10249
|
+
try {
|
|
10250
|
+
const configPath = resolveConfigPath();
|
|
10251
|
+
if (!fs3.existsSync(configPath))
|
|
10252
|
+
return;
|
|
10253
|
+
const raw = fs3.readFileSync(configPath, "utf-8");
|
|
10254
|
+
const cfg = JSON.parse(raw);
|
|
10255
|
+
const channels = cfg.channels;
|
|
10256
|
+
const section = channels?.["openclaw-weixin"];
|
|
10257
|
+
if (!section)
|
|
10258
|
+
return;
|
|
10259
|
+
if (accountId) {
|
|
10260
|
+
const accounts = section.accounts;
|
|
10261
|
+
const agent = accounts?.[accountId]?.botAgent;
|
|
10262
|
+
if (typeof agent === "string" && agent.trim())
|
|
10263
|
+
return agent.trim();
|
|
10264
|
+
}
|
|
10265
|
+
return typeof section.botAgent === "string" && section.botAgent.trim() ? section.botAgent.trim() : undefined;
|
|
10266
|
+
} catch {
|
|
10267
|
+
return;
|
|
10268
|
+
}
|
|
10269
|
+
}
|
|
10205
10270
|
function listWeixinAccountIds() {
|
|
10206
10271
|
const indexed = listIndexedWeixinAccountIds();
|
|
10207
10272
|
if (indexed.length > 0)
|
|
@@ -10490,8 +10555,83 @@ var init_send_errors = __esm(() => {
|
|
|
10490
10555
|
|
|
10491
10556
|
// src/weixin/api/api.ts
|
|
10492
10557
|
import crypto from "node:crypto";
|
|
10558
|
+
function buildClientVersion(version2) {
|
|
10559
|
+
const parts = version2.split(".").map((p) => parseInt(p, 10));
|
|
10560
|
+
const major = parts[0] ?? 0;
|
|
10561
|
+
const minor = parts[1] ?? 0;
|
|
10562
|
+
const patch = parts[2] ?? 0;
|
|
10563
|
+
return (major & 255) << 16 | (minor & 255) << 8 | patch & 255;
|
|
10564
|
+
}
|
|
10565
|
+
function sanitizeBotAgent(raw) {
|
|
10566
|
+
if (!raw || typeof raw !== "string")
|
|
10567
|
+
return DEFAULT_BOT_AGENT;
|
|
10568
|
+
const trimmed = raw.trim();
|
|
10569
|
+
if (!trimmed)
|
|
10570
|
+
return DEFAULT_BOT_AGENT;
|
|
10571
|
+
const productRe = /^[A-Za-z0-9_.\-]{1,32}\/[A-Za-z0-9_.+\-]{1,32}$/;
|
|
10572
|
+
const commentCharRe = /^[\x20-\x27\x2A-\x7E]{1,64}$/;
|
|
10573
|
+
const rawTokens = trimmed.split(/\s+/);
|
|
10574
|
+
const tokens = [];
|
|
10575
|
+
for (let i = 0;i < rawTokens.length; i += 1) {
|
|
10576
|
+
const tok = rawTokens[i];
|
|
10577
|
+
if (tok.startsWith("(") && !tok.endsWith(")")) {
|
|
10578
|
+
let acc = tok;
|
|
10579
|
+
while (i + 1 < rawTokens.length && !acc.endsWith(")")) {
|
|
10580
|
+
i += 1;
|
|
10581
|
+
acc += " " + rawTokens[i];
|
|
10582
|
+
}
|
|
10583
|
+
tokens.push(acc);
|
|
10584
|
+
} else {
|
|
10585
|
+
tokens.push(tok);
|
|
10586
|
+
}
|
|
10587
|
+
}
|
|
10588
|
+
const accepted = [];
|
|
10589
|
+
let pendingProduct = null;
|
|
10590
|
+
for (const tok of tokens) {
|
|
10591
|
+
if (tok.startsWith("(") && tok.endsWith(")")) {
|
|
10592
|
+
const inner = tok.slice(1, -1);
|
|
10593
|
+
if (pendingProduct && commentCharRe.test(inner)) {
|
|
10594
|
+
accepted.push(`${pendingProduct} (${inner})`);
|
|
10595
|
+
pendingProduct = null;
|
|
10596
|
+
} else {
|
|
10597
|
+
if (pendingProduct) {
|
|
10598
|
+
accepted.push(pendingProduct);
|
|
10599
|
+
pendingProduct = null;
|
|
10600
|
+
}
|
|
10601
|
+
}
|
|
10602
|
+
continue;
|
|
10603
|
+
}
|
|
10604
|
+
if (pendingProduct) {
|
|
10605
|
+
accepted.push(pendingProduct);
|
|
10606
|
+
pendingProduct = null;
|
|
10607
|
+
}
|
|
10608
|
+
if (productRe.test(tok)) {
|
|
10609
|
+
pendingProduct = tok;
|
|
10610
|
+
}
|
|
10611
|
+
}
|
|
10612
|
+
if (pendingProduct)
|
|
10613
|
+
accepted.push(pendingProduct);
|
|
10614
|
+
if (accepted.length === 0)
|
|
10615
|
+
return DEFAULT_BOT_AGENT;
|
|
10616
|
+
const joined = accepted.join(" ");
|
|
10617
|
+
if (Buffer.byteLength(joined, "utf-8") <= BOT_AGENT_MAX_LEN)
|
|
10618
|
+
return joined;
|
|
10619
|
+
const truncated = [];
|
|
10620
|
+
let len = 0;
|
|
10621
|
+
for (const t of accepted) {
|
|
10622
|
+
const add = (truncated.length === 0 ? 0 : 1) + Buffer.byteLength(t, "utf-8");
|
|
10623
|
+
if (len + add > BOT_AGENT_MAX_LEN)
|
|
10624
|
+
break;
|
|
10625
|
+
truncated.push(t);
|
|
10626
|
+
len += add;
|
|
10627
|
+
}
|
|
10628
|
+
return truncated.length > 0 ? truncated.join(" ") : DEFAULT_BOT_AGENT;
|
|
10629
|
+
}
|
|
10493
10630
|
function buildBaseInfo() {
|
|
10494
|
-
return {
|
|
10631
|
+
return {
|
|
10632
|
+
channel_version: CHANNEL_VERSION,
|
|
10633
|
+
bot_agent: sanitizeBotAgent(loadConfigBotAgent())
|
|
10634
|
+
};
|
|
10495
10635
|
}
|
|
10496
10636
|
function ensureTrailingSlash(url) {
|
|
10497
10637
|
return url.endsWith("/") ? url : `${url}/`;
|
|
@@ -10502,6 +10642,9 @@ function randomWechatUin() {
|
|
|
10502
10642
|
}
|
|
10503
10643
|
function buildCommonHeaders() {
|
|
10504
10644
|
const headers = {};
|
|
10645
|
+
if (ILINK_APP_ID)
|
|
10646
|
+
headers["iLink-App-Id"] = ILINK_APP_ID;
|
|
10647
|
+
headers["iLink-App-ClientVersion"] = String(ILINK_APP_CLIENT_VERSION);
|
|
10505
10648
|
const routeTag = loadConfigRouteTag();
|
|
10506
10649
|
if (routeTag) {
|
|
10507
10650
|
headers.SKRouteTag = routeTag;
|
|
@@ -10547,6 +10690,35 @@ async function apiGetFetch(params) {
|
|
|
10547
10690
|
throw err;
|
|
10548
10691
|
}
|
|
10549
10692
|
}
|
|
10693
|
+
async function apiPostFetch(params) {
|
|
10694
|
+
const base = ensureTrailingSlash(params.baseUrl);
|
|
10695
|
+
const url = new URL(params.endpoint, base);
|
|
10696
|
+
const hdrs = buildCommonHeaders();
|
|
10697
|
+
hdrs["Content-Type"] = "application/json";
|
|
10698
|
+
logger.debug(`POST ${redactUrl(url.toString())} body=${redactBody(params.body)}`);
|
|
10699
|
+
const controller = new AbortController;
|
|
10700
|
+
const t = params.timeoutMs !== undefined ? setTimeout(() => controller.abort(), params.timeoutMs) : undefined;
|
|
10701
|
+
const onAbort = () => controller.abort();
|
|
10702
|
+
params.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
10703
|
+
try {
|
|
10704
|
+
const res = await fetch(url.toString(), {
|
|
10705
|
+
method: "POST",
|
|
10706
|
+
headers: hdrs,
|
|
10707
|
+
body: params.body,
|
|
10708
|
+
signal: controller.signal
|
|
10709
|
+
});
|
|
10710
|
+
const rawText = await res.text();
|
|
10711
|
+
logger.debug(`${params.label} status=${res.status} raw=${redactBody(rawText)}`);
|
|
10712
|
+
if (!res.ok) {
|
|
10713
|
+
throw new Error(`${params.label} ${res.status}: ${rawText}`);
|
|
10714
|
+
}
|
|
10715
|
+
return rawText;
|
|
10716
|
+
} finally {
|
|
10717
|
+
if (t !== undefined)
|
|
10718
|
+
clearTimeout(t);
|
|
10719
|
+
params.abortSignal?.removeEventListener("abort", onAbort);
|
|
10720
|
+
}
|
|
10721
|
+
}
|
|
10550
10722
|
async function apiFetch(params) {
|
|
10551
10723
|
const base = ensureTrailingSlash(params.baseUrl);
|
|
10552
10724
|
const url = new URL(params.endpoint, base);
|
|
@@ -10705,7 +10877,7 @@ async function sendTyping(params) {
|
|
|
10705
10877
|
label: "sendTyping"
|
|
10706
10878
|
});
|
|
10707
10879
|
}
|
|
10708
|
-
var CHANNEL_VERSION, DEFAULT_LONG_POLL_TIMEOUT_MS = 35000, DEFAULT_API_TIMEOUT_MS = 15000, DEFAULT_CONFIG_TIMEOUT_MS = 1e4;
|
|
10880
|
+
var CHANNEL_VERSION, ILINK_APP_CLIENT_VERSION, ILINK_APP_ID, DEFAULT_BOT_AGENT = "weacpx", BOT_AGENT_MAX_LEN = 256, DEFAULT_LONG_POLL_TIMEOUT_MS = 35000, DEFAULT_API_TIMEOUT_MS = 15000, DEFAULT_CONFIG_TIMEOUT_MS = 1e4;
|
|
10709
10881
|
var init_api = __esm(() => {
|
|
10710
10882
|
init_version();
|
|
10711
10883
|
init_accounts();
|
|
@@ -10713,6 +10885,8 @@ var init_api = __esm(() => {
|
|
|
10713
10885
|
init_redact();
|
|
10714
10886
|
init_send_errors();
|
|
10715
10887
|
CHANNEL_VERSION = readVersion();
|
|
10888
|
+
ILINK_APP_CLIENT_VERSION = buildClientVersion(CHANNEL_VERSION);
|
|
10889
|
+
ILINK_APP_ID = (process.env.WEACPX_ILINK_APP_ID ?? "").trim();
|
|
10716
10890
|
});
|
|
10717
10891
|
|
|
10718
10892
|
// node_modules/qrcode-terminal/vendor/QRCode/QRMode.js
|
|
@@ -11744,22 +11918,47 @@ function purgeExpiredLogins() {
|
|
|
11744
11918
|
}
|
|
11745
11919
|
}
|
|
11746
11920
|
}
|
|
11921
|
+
function getLocalBotTokenList() {
|
|
11922
|
+
const accountIds = listIndexedWeixinAccountIds();
|
|
11923
|
+
const tokens = [];
|
|
11924
|
+
for (let i = accountIds.length - 1;i >= 0 && tokens.length < 10; i--) {
|
|
11925
|
+
const accountId = accountIds[i];
|
|
11926
|
+
if (!accountId)
|
|
11927
|
+
continue;
|
|
11928
|
+
const data = loadWeixinAccount(accountId);
|
|
11929
|
+
const token = data?.token?.trim();
|
|
11930
|
+
if (token) {
|
|
11931
|
+
tokens.push(token);
|
|
11932
|
+
}
|
|
11933
|
+
}
|
|
11934
|
+
return tokens;
|
|
11935
|
+
}
|
|
11747
11936
|
async function fetchQRCode(apiBaseUrl, botType) {
|
|
11748
11937
|
logger.info(`Fetching QR code from: ${apiBaseUrl} bot_type=${botType}`);
|
|
11749
|
-
const
|
|
11938
|
+
const localTokenList = getLocalBotTokenList();
|
|
11939
|
+
logger.info(`fetchQRCode: local_token_list count=${localTokenList.length}`);
|
|
11940
|
+
const rawText = await apiPostFetch({
|
|
11750
11941
|
baseUrl: apiBaseUrl,
|
|
11751
11942
|
endpoint: `ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(botType)}`,
|
|
11752
|
-
|
|
11943
|
+
body: JSON.stringify({ local_token_list: localTokenList }),
|
|
11753
11944
|
label: "fetchQRCode"
|
|
11754
11945
|
});
|
|
11755
11946
|
return JSON.parse(rawText);
|
|
11756
11947
|
}
|
|
11757
|
-
|
|
11758
|
-
|
|
11948
|
+
function buildPollQRStatusEndpoint(qrcode, verifyCode) {
|
|
11949
|
+
let endpoint = `ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`;
|
|
11950
|
+
if (verifyCode) {
|
|
11951
|
+
endpoint += `&verify_code=${encodeURIComponent(verifyCode)}`;
|
|
11952
|
+
}
|
|
11953
|
+
return endpoint;
|
|
11954
|
+
}
|
|
11955
|
+
async function pollQRStatus(apiBaseUrl, qrcode, verifyCode) {
|
|
11956
|
+
logger.debug(`Long-poll QR status from: ${apiBaseUrl} qrcode=*** hasVerifyCode=${Boolean(verifyCode)}`);
|
|
11957
|
+
const endpoint = buildPollQRStatusEndpoint(qrcode, verifyCode);
|
|
11759
11958
|
try {
|
|
11760
11959
|
const rawText = await apiGetFetch({
|
|
11761
11960
|
baseUrl: apiBaseUrl,
|
|
11762
|
-
endpoint
|
|
11961
|
+
endpoint,
|
|
11763
11962
|
timeoutMs: QR_LONG_POLL_TIMEOUT_MS,
|
|
11764
11963
|
label: "pollQRStatus"
|
|
11765
11964
|
});
|
|
@@ -11774,6 +11973,28 @@ async function pollQRStatus(apiBaseUrl, qrcode) {
|
|
|
11774
11973
|
return { status: "wait" };
|
|
11775
11974
|
}
|
|
11776
11975
|
}
|
|
11976
|
+
async function readVerifyCodeFromStdin(prompt) {
|
|
11977
|
+
if (!process.stdin.isTTY) {
|
|
11978
|
+
throw new Error("verify code requested but stdin is not a TTY (running in daemon mode?)");
|
|
11979
|
+
}
|
|
11980
|
+
process.stdout.write(prompt);
|
|
11981
|
+
return new Promise((resolve) => {
|
|
11982
|
+
let input = "";
|
|
11983
|
+
const onData = (chunk) => {
|
|
11984
|
+
const str = chunk.toString();
|
|
11985
|
+
input += str;
|
|
11986
|
+
if (input.includes(`
|
|
11987
|
+
`)) {
|
|
11988
|
+
process.stdin.removeListener("data", onData);
|
|
11989
|
+
process.stdin.pause();
|
|
11990
|
+
resolve(input.trim());
|
|
11991
|
+
}
|
|
11992
|
+
};
|
|
11993
|
+
process.stdin.resume();
|
|
11994
|
+
process.stdin.setEncoding("utf-8");
|
|
11995
|
+
process.stdin.on("data", onData);
|
|
11996
|
+
});
|
|
11997
|
+
}
|
|
11777
11998
|
async function startWeixinLoginWithQr(opts) {
|
|
11778
11999
|
const sessionKey = opts.accountId || randomUUID2();
|
|
11779
12000
|
purgeExpiredLogins();
|
|
@@ -11812,6 +12033,36 @@ async function startWeixinLoginWithQr(opts) {
|
|
|
11812
12033
|
};
|
|
11813
12034
|
}
|
|
11814
12035
|
}
|
|
12036
|
+
async function refreshQRCode(activeLogin, botType, qrRefreshCount, onScannedReset) {
|
|
12037
|
+
try {
|
|
12038
|
+
const qrResponse = await fetchQRCode(FIXED_BASE_URL, botType);
|
|
12039
|
+
activeLogin.qrcode = qrResponse.qrcode;
|
|
12040
|
+
activeLogin.qrcodeUrl = qrResponse.qrcode_img_content;
|
|
12041
|
+
activeLogin.startedAt = Date.now();
|
|
12042
|
+
onScannedReset();
|
|
12043
|
+
logger.info(`refreshQRCode: new QR code obtained (${qrRefreshCount}/${MAX_QR_REFRESH_COUNT}) qrcode=${redactToken(qrResponse.qrcode)}`);
|
|
12044
|
+
process.stdout.write(`\uD83D\uDD04 新二维码已生成,请重新扫描
|
|
12045
|
+
|
|
12046
|
+
`);
|
|
12047
|
+
try {
|
|
12048
|
+
const qrterm = await Promise.resolve().then(() => __toESM(require_main(), 1));
|
|
12049
|
+
qrterm.default.generate(qrResponse.qrcode_img_content, { small: true });
|
|
12050
|
+
process.stdout.write(`如果二维码未能成功展示,请用浏览器打开以下链接扫码:
|
|
12051
|
+
`);
|
|
12052
|
+
process.stdout.write(`${qrResponse.qrcode_img_content}
|
|
12053
|
+
`);
|
|
12054
|
+
} catch {
|
|
12055
|
+
process.stdout.write(`二维码未加载成功,请用浏览器打开以下链接扫码:
|
|
12056
|
+
`);
|
|
12057
|
+
process.stdout.write(`${qrResponse.qrcode_img_content}
|
|
12058
|
+
`);
|
|
12059
|
+
}
|
|
12060
|
+
return { success: true };
|
|
12061
|
+
} catch (refreshErr) {
|
|
12062
|
+
logger.error(`refreshQRCode: failed to refresh QR code: ${String(refreshErr)}`);
|
|
12063
|
+
return { success: false, message: `刷新二维码失败: ${String(refreshErr)}` };
|
|
12064
|
+
}
|
|
12065
|
+
}
|
|
11815
12066
|
async function waitForWeixinLogin(opts) {
|
|
11816
12067
|
let activeLogin = activeLogins.get(opts.sessionKey);
|
|
11817
12068
|
if (!activeLogin) {
|
|
@@ -11838,7 +12089,7 @@ async function waitForWeixinLogin(opts) {
|
|
|
11838
12089
|
while (Date.now() < deadline) {
|
|
11839
12090
|
try {
|
|
11840
12091
|
const currentBaseUrl = activeLogin.currentApiBaseUrl ?? FIXED_BASE_URL;
|
|
11841
|
-
const statusResponse = await pollQRStatus(currentBaseUrl, activeLogin.qrcode);
|
|
12092
|
+
const statusResponse = await pollQRStatus(currentBaseUrl, activeLogin.qrcode, activeLogin.pendingVerifyCode);
|
|
11842
12093
|
logger.debug(`pollQRStatus: status=${statusResponse.status} hasBotToken=${Boolean(statusResponse.bot_token)} hasBotId=${Boolean(statusResponse.ilink_bot_id)}`);
|
|
11843
12094
|
activeLogin.status = statusResponse.status;
|
|
11844
12095
|
switch (statusResponse.status) {
|
|
@@ -11848,6 +12099,10 @@ async function waitForWeixinLogin(opts) {
|
|
|
11848
12099
|
}
|
|
11849
12100
|
break;
|
|
11850
12101
|
case "scaned":
|
|
12102
|
+
if (activeLogin.pendingVerifyCode) {
|
|
12103
|
+
logger.info("verify code accepted, resuming polling");
|
|
12104
|
+
activeLogin.pendingVerifyCode = undefined;
|
|
12105
|
+
}
|
|
11851
12106
|
if (!scannedPrinted) {
|
|
11852
12107
|
process.stdout.write(`
|
|
11853
12108
|
\uD83D\uDC40 已扫码,在微信继续操作...
|
|
@@ -11856,6 +12111,7 @@ async function waitForWeixinLogin(opts) {
|
|
|
11856
12111
|
}
|
|
11857
12112
|
break;
|
|
11858
12113
|
case "expired": {
|
|
12114
|
+
activeLogin.pendingVerifyCode = undefined;
|
|
11859
12115
|
qrRefreshCount++;
|
|
11860
12116
|
if (qrRefreshCount > MAX_QR_REFRESH_COUNT) {
|
|
11861
12117
|
logger.warn(`waitForWeixinLogin: QR expired ${MAX_QR_REFRESH_COUNT} times, giving up sessionKey=${opts.sessionKey}`);
|
|
@@ -11869,36 +12125,14 @@ async function waitForWeixinLogin(opts) {
|
|
|
11869
12125
|
⏳ 二维码已过期,正在刷新...(${qrRefreshCount}/${MAX_QR_REFRESH_COUNT})
|
|
11870
12126
|
`);
|
|
11871
12127
|
logger.info(`waitForWeixinLogin: QR expired, refreshing (${qrRefreshCount}/${MAX_QR_REFRESH_COUNT})`);
|
|
11872
|
-
|
|
11873
|
-
const botType = opts.botType || DEFAULT_ILINK_BOT_TYPE;
|
|
11874
|
-
const qrResponse = await fetchQRCode(FIXED_BASE_URL, botType);
|
|
11875
|
-
activeLogin.qrcode = qrResponse.qrcode;
|
|
11876
|
-
activeLogin.qrcodeUrl = qrResponse.qrcode_img_content;
|
|
11877
|
-
activeLogin.startedAt = Date.now();
|
|
12128
|
+
const expiredRefreshResult = await refreshQRCode(activeLogin, opts.botType || DEFAULT_ILINK_BOT_TYPE, qrRefreshCount, () => {
|
|
11878
12129
|
scannedPrinted = false;
|
|
11879
|
-
|
|
11880
|
-
|
|
11881
|
-
|
|
11882
|
-
`);
|
|
11883
|
-
try {
|
|
11884
|
-
const qrterm = await Promise.resolve().then(() => __toESM(require_main(), 1));
|
|
11885
|
-
qrterm.default.generate(qrResponse.qrcode_img_content, { small: true });
|
|
11886
|
-
process.stdout.write(`如果二维码未能成功展示,请用浏览器打开以下链接扫码:
|
|
11887
|
-
`);
|
|
11888
|
-
process.stdout.write(`${qrResponse.qrcode_img_content}
|
|
11889
|
-
`);
|
|
11890
|
-
} catch {
|
|
11891
|
-
process.stdout.write(`二维码未加载成功,请用浏览器打开以下链接扫码:
|
|
11892
|
-
`);
|
|
11893
|
-
process.stdout.write(`${qrResponse.qrcode_img_content}
|
|
11894
|
-
`);
|
|
11895
|
-
}
|
|
11896
|
-
} catch (refreshErr) {
|
|
11897
|
-
logger.error(`waitForWeixinLogin: failed to refresh QR code: ${String(refreshErr)}`);
|
|
12130
|
+
});
|
|
12131
|
+
if (!expiredRefreshResult.success) {
|
|
11898
12132
|
activeLogins.delete(opts.sessionKey);
|
|
11899
12133
|
return {
|
|
11900
12134
|
connected: false,
|
|
11901
|
-
message:
|
|
12135
|
+
message: expiredRefreshResult.message
|
|
11902
12136
|
};
|
|
11903
12137
|
}
|
|
11904
12138
|
break;
|
|
@@ -11914,6 +12148,49 @@ async function waitForWeixinLogin(opts) {
|
|
|
11914
12148
|
}
|
|
11915
12149
|
break;
|
|
11916
12150
|
}
|
|
12151
|
+
case "need_verifycode": {
|
|
12152
|
+
const verifyPrompt = activeLogin.pendingVerifyCode ? "❌ 你输入的数字不匹配,请重新输入:" : "输入手机微信显示的数字,以继续连接:";
|
|
12153
|
+
let code;
|
|
12154
|
+
try {
|
|
12155
|
+
code = await readVerifyCodeFromStdin(verifyPrompt);
|
|
12156
|
+
} catch (err) {
|
|
12157
|
+
logger.error(`waitForWeixinLogin: cannot read verify code (no TTY): ${String(err)}`);
|
|
12158
|
+
activeLogins.delete(opts.sessionKey);
|
|
12159
|
+
return {
|
|
12160
|
+
connected: false,
|
|
12161
|
+
message: "需要输入配对码,但当前环境没有交互式终端。请在前台运行 `weacpx login` 完成登录。"
|
|
12162
|
+
};
|
|
12163
|
+
}
|
|
12164
|
+
activeLogin.pendingVerifyCode = code;
|
|
12165
|
+
continue;
|
|
12166
|
+
}
|
|
12167
|
+
case "verify_code_blocked": {
|
|
12168
|
+
logger.warn(`waitForWeixinLogin: verify code blocked, qrRefreshCount=${qrRefreshCount} sessionKey=${opts.sessionKey}`);
|
|
12169
|
+
process.stdout.write(`
|
|
12170
|
+
⛔ 多次输入错误,请稍后再试。
|
|
12171
|
+
`);
|
|
12172
|
+
activeLogin.pendingVerifyCode = undefined;
|
|
12173
|
+
qrRefreshCount++;
|
|
12174
|
+
if (qrRefreshCount > MAX_QR_REFRESH_COUNT) {
|
|
12175
|
+
logger.warn(`waitForWeixinLogin: verify_code_blocked and QR refresh limit reached, giving up sessionKey=${opts.sessionKey}`);
|
|
12176
|
+
activeLogins.delete(opts.sessionKey);
|
|
12177
|
+
return {
|
|
12178
|
+
connected: false,
|
|
12179
|
+
message: "多次输入错误,连接流程已停止。请稍后再试。"
|
|
12180
|
+
};
|
|
12181
|
+
}
|
|
12182
|
+
const blockedRefreshResult = await refreshQRCode(activeLogin, opts.botType || DEFAULT_ILINK_BOT_TYPE, qrRefreshCount, () => {
|
|
12183
|
+
scannedPrinted = false;
|
|
12184
|
+
});
|
|
12185
|
+
if (!blockedRefreshResult.success) {
|
|
12186
|
+
activeLogins.delete(opts.sessionKey);
|
|
12187
|
+
return {
|
|
12188
|
+
connected: false,
|
|
12189
|
+
message: blockedRefreshResult.message
|
|
12190
|
+
};
|
|
12191
|
+
}
|
|
12192
|
+
break;
|
|
12193
|
+
}
|
|
11917
12194
|
case "confirmed": {
|
|
11918
12195
|
if (!statusResponse.ilink_bot_id) {
|
|
11919
12196
|
activeLogins.delete(opts.sessionKey);
|
|
@@ -11953,140 +12230,22 @@ async function waitForWeixinLogin(opts) {
|
|
|
11953
12230
|
message: "登录超时,请重试。"
|
|
11954
12231
|
};
|
|
11955
12232
|
}
|
|
11956
|
-
var ACTIVE_LOGIN_TTL_MS,
|
|
12233
|
+
var ACTIVE_LOGIN_TTL_MS, QR_LONG_POLL_TIMEOUT_MS = 35000, DEFAULT_ILINK_BOT_TYPE = "3", FIXED_BASE_URL = "https://ilinkai.weixin.qq.com", activeLogins, MAX_QR_REFRESH_COUNT = 3;
|
|
11957
12234
|
var init_login_qr = __esm(() => {
|
|
11958
12235
|
init_api();
|
|
12236
|
+
init_accounts();
|
|
11959
12237
|
init_logger();
|
|
11960
12238
|
init_redact();
|
|
11961
12239
|
ACTIVE_LOGIN_TTL_MS = 5 * 60000;
|
|
11962
12240
|
activeLogins = new Map;
|
|
11963
12241
|
});
|
|
11964
12242
|
|
|
11965
|
-
// src/weixin/
|
|
11966
|
-
|
|
11967
|
-
|
|
11968
|
-
|
|
11969
|
-
cache = new Map;
|
|
11970
|
-
constructor(apiOpts, log) {
|
|
11971
|
-
this.apiOpts = apiOpts;
|
|
11972
|
-
this.log = log;
|
|
11973
|
-
}
|
|
11974
|
-
async getForUser(userId, contextToken) {
|
|
11975
|
-
const now = Date.now();
|
|
11976
|
-
const entry = this.cache.get(userId);
|
|
11977
|
-
const shouldFetch = !entry || now >= entry.nextFetchAt;
|
|
11978
|
-
if (shouldFetch) {
|
|
11979
|
-
let fetchOk = false;
|
|
11980
|
-
try {
|
|
11981
|
-
const resp = await getConfig({
|
|
11982
|
-
baseUrl: this.apiOpts.baseUrl,
|
|
11983
|
-
token: this.apiOpts.token,
|
|
11984
|
-
ilinkUserId: userId,
|
|
11985
|
-
contextToken
|
|
11986
|
-
});
|
|
11987
|
-
if (resp.ret === 0) {
|
|
11988
|
-
this.cache.set(userId, {
|
|
11989
|
-
config: { typingTicket: resp.typing_ticket ?? "" },
|
|
11990
|
-
everSucceeded: true,
|
|
11991
|
-
nextFetchAt: now + Math.random() * CONFIG_CACHE_TTL_MS,
|
|
11992
|
-
retryDelayMs: CONFIG_CACHE_INITIAL_RETRY_MS
|
|
11993
|
-
});
|
|
11994
|
-
this.log(`[weixin] config ${entry?.everSucceeded ? "refreshed" : "cached"} for ${userId}`);
|
|
11995
|
-
fetchOk = true;
|
|
11996
|
-
}
|
|
11997
|
-
} catch (err) {
|
|
11998
|
-
this.log(`[weixin] getConfig failed for ${userId} (ignored): ${String(err)}`);
|
|
11999
|
-
}
|
|
12000
|
-
if (!fetchOk) {
|
|
12001
|
-
const prevDelay = entry?.retryDelayMs ?? CONFIG_CACHE_INITIAL_RETRY_MS;
|
|
12002
|
-
const nextDelay = Math.min(prevDelay * 2, CONFIG_CACHE_MAX_RETRY_MS);
|
|
12003
|
-
if (entry) {
|
|
12004
|
-
entry.nextFetchAt = now + nextDelay;
|
|
12005
|
-
entry.retryDelayMs = nextDelay;
|
|
12006
|
-
} else {
|
|
12007
|
-
this.cache.set(userId, {
|
|
12008
|
-
config: { typingTicket: "" },
|
|
12009
|
-
everSucceeded: false,
|
|
12010
|
-
nextFetchAt: now + CONFIG_CACHE_INITIAL_RETRY_MS,
|
|
12011
|
-
retryDelayMs: CONFIG_CACHE_INITIAL_RETRY_MS
|
|
12012
|
-
});
|
|
12013
|
-
}
|
|
12014
|
-
}
|
|
12015
|
-
}
|
|
12016
|
-
return this.cache.get(userId)?.config ?? { typingTicket: "" };
|
|
12017
|
-
}
|
|
12018
|
-
}
|
|
12019
|
-
var CONFIG_CACHE_TTL_MS, CONFIG_CACHE_INITIAL_RETRY_MS = 2000, CONFIG_CACHE_MAX_RETRY_MS;
|
|
12020
|
-
var init_config_cache = __esm(() => {
|
|
12021
|
-
init_api();
|
|
12022
|
-
CONFIG_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
12023
|
-
CONFIG_CACHE_MAX_RETRY_MS = 60 * 60 * 1000;
|
|
12024
|
-
});
|
|
12025
|
-
|
|
12026
|
-
// src/weixin/api/session-guard.ts
|
|
12027
|
-
function pauseSession(accountId) {
|
|
12028
|
-
const until = Date.now() + SESSION_PAUSE_DURATION_MS;
|
|
12029
|
-
pauseUntilMap.set(accountId, until);
|
|
12030
|
-
logger.info(`session-guard: paused accountId=${accountId} until=${new Date(until).toISOString()} (${SESSION_PAUSE_DURATION_MS / 1000}s)`);
|
|
12031
|
-
}
|
|
12032
|
-
function getRemainingPauseMs(accountId) {
|
|
12033
|
-
const until = pauseUntilMap.get(accountId);
|
|
12034
|
-
if (until === undefined)
|
|
12035
|
-
return 0;
|
|
12036
|
-
const remaining = until - Date.now();
|
|
12037
|
-
if (remaining <= 0) {
|
|
12038
|
-
pauseUntilMap.delete(accountId);
|
|
12039
|
-
return 0;
|
|
12040
|
-
}
|
|
12041
|
-
return remaining;
|
|
12042
|
-
}
|
|
12043
|
-
var SESSION_PAUSE_DURATION_MS, SESSION_EXPIRED_ERRCODE = -14, pauseUntilMap;
|
|
12044
|
-
var init_session_guard = __esm(() => {
|
|
12045
|
-
init_logger();
|
|
12046
|
-
SESSION_PAUSE_DURATION_MS = 60 * 60 * 1000;
|
|
12047
|
-
pauseUntilMap = new Map;
|
|
12048
|
-
});
|
|
12049
|
-
|
|
12050
|
-
// src/weixin/messaging/conversation-executor.ts
|
|
12051
|
-
function createConversationExecutor() {
|
|
12052
|
-
const states = new Map;
|
|
12053
|
-
const getState = (conversationId) => {
|
|
12054
|
-
const existing = states.get(conversationId);
|
|
12055
|
-
if (existing)
|
|
12056
|
-
return existing;
|
|
12057
|
-
const created = { activeControls: 0 };
|
|
12058
|
-
states.set(conversationId, created);
|
|
12059
|
-
return created;
|
|
12060
|
-
};
|
|
12061
|
-
const cleanupState = (conversationId, state) => {
|
|
12062
|
-
if (!state.normalTail && state.activeControls === 0) {
|
|
12063
|
-
states.delete(conversationId);
|
|
12064
|
-
}
|
|
12065
|
-
};
|
|
12066
|
-
return {
|
|
12067
|
-
run(conversationId, lane, task) {
|
|
12068
|
-
const state = getState(conversationId);
|
|
12069
|
-
if (lane === "control") {
|
|
12070
|
-
state.activeControls += 1;
|
|
12071
|
-
return Promise.resolve().then(task).finally(() => {
|
|
12072
|
-
state.activeControls -= 1;
|
|
12073
|
-
cleanupState(conversationId, state);
|
|
12074
|
-
});
|
|
12075
|
-
}
|
|
12076
|
-
const previous = state.normalTail ?? Promise.resolve();
|
|
12077
|
-
const next = previous.catch(() => {
|
|
12078
|
-
return;
|
|
12079
|
-
}).then(task);
|
|
12080
|
-
state.normalTail = next;
|
|
12081
|
-
return next.finally(() => {
|
|
12082
|
-
if (state.normalTail === next) {
|
|
12083
|
-
state.normalTail = undefined;
|
|
12084
|
-
}
|
|
12085
|
-
cleanupState(conversationId, state);
|
|
12086
|
-
});
|
|
12087
|
-
}
|
|
12088
|
-
};
|
|
12243
|
+
// src/weixin/util/random.ts
|
|
12244
|
+
import crypto2 from "node:crypto";
|
|
12245
|
+
function generateId(prefix) {
|
|
12246
|
+
return `${prefix}:${Date.now()}-${crypto2.randomBytes(4).toString("hex")}`;
|
|
12089
12247
|
}
|
|
12248
|
+
var init_random = () => {};
|
|
12090
12249
|
|
|
12091
12250
|
// src/weixin/api/types.ts
|
|
12092
12251
|
var UploadMediaType, MessageType, MessageItemType, MessageState, TypingStatus;
|
|
@@ -12121,9 +12280,272 @@ var init_types2 = __esm(() => {
|
|
|
12121
12280
|
};
|
|
12122
12281
|
});
|
|
12123
12282
|
|
|
12283
|
+
// src/weixin/messaging/inbound.ts
|
|
12284
|
+
import fs5 from "node:fs";
|
|
12285
|
+
import path6 from "node:path";
|
|
12286
|
+
function contextTokenKey(accountId, userId) {
|
|
12287
|
+
return `${accountId}:${userId}`;
|
|
12288
|
+
}
|
|
12289
|
+
function resolveContextTokenFilePath(accountId) {
|
|
12290
|
+
return path6.join(resolveStateDir(), "openclaw-weixin", "accounts", `${accountId}.context-tokens.json`);
|
|
12291
|
+
}
|
|
12292
|
+
function persistContextTokens(accountId) {
|
|
12293
|
+
const prefix = `${accountId}:`;
|
|
12294
|
+
const tokens = {};
|
|
12295
|
+
for (const [k, v] of contextTokenStore) {
|
|
12296
|
+
if (k.startsWith(prefix))
|
|
12297
|
+
tokens[k.slice(prefix.length)] = v;
|
|
12298
|
+
}
|
|
12299
|
+
const filePath = resolveContextTokenFilePath(accountId);
|
|
12300
|
+
try {
|
|
12301
|
+
fs5.mkdirSync(path6.dirname(filePath), { recursive: true });
|
|
12302
|
+
fs5.writeFileSync(filePath, JSON.stringify(tokens), "utf-8");
|
|
12303
|
+
} catch (err) {
|
|
12304
|
+
logger.warn(`persistContextTokens: failed to write ${filePath}: ${String(err)}`);
|
|
12305
|
+
}
|
|
12306
|
+
}
|
|
12307
|
+
function restoreContextTokens(accountId) {
|
|
12308
|
+
const filePath = resolveContextTokenFilePath(accountId);
|
|
12309
|
+
try {
|
|
12310
|
+
if (!fs5.existsSync(filePath))
|
|
12311
|
+
return;
|
|
12312
|
+
const raw = fs5.readFileSync(filePath, "utf-8");
|
|
12313
|
+
const tokens = JSON.parse(raw);
|
|
12314
|
+
let count = 0;
|
|
12315
|
+
for (const [userId, token] of Object.entries(tokens)) {
|
|
12316
|
+
if (typeof token === "string" && token) {
|
|
12317
|
+
contextTokenStore.set(contextTokenKey(accountId, userId), token);
|
|
12318
|
+
count++;
|
|
12319
|
+
}
|
|
12320
|
+
}
|
|
12321
|
+
logger.info(`restoreContextTokens: restored ${count} tokens for account=${accountId}`);
|
|
12322
|
+
} catch (err) {
|
|
12323
|
+
logger.warn(`restoreContextTokens: failed to read ${filePath}: ${String(err)}`);
|
|
12324
|
+
}
|
|
12325
|
+
}
|
|
12326
|
+
function clearContextTokensForAccount(accountId) {
|
|
12327
|
+
const prefix = `${accountId}:`;
|
|
12328
|
+
for (const k of [...contextTokenStore.keys()]) {
|
|
12329
|
+
if (k.startsWith(prefix))
|
|
12330
|
+
contextTokenStore.delete(k);
|
|
12331
|
+
}
|
|
12332
|
+
const filePath = resolveContextTokenFilePath(accountId);
|
|
12333
|
+
try {
|
|
12334
|
+
if (fs5.existsSync(filePath))
|
|
12335
|
+
fs5.unlinkSync(filePath);
|
|
12336
|
+
} catch (err) {
|
|
12337
|
+
logger.warn(`clearContextTokensForAccount: failed to remove ${filePath}: ${String(err)}`);
|
|
12338
|
+
}
|
|
12339
|
+
logger.info(`clearContextTokensForAccount: cleared tokens for account=${accountId}`);
|
|
12340
|
+
}
|
|
12341
|
+
function setContextToken(accountId, userId, token) {
|
|
12342
|
+
const k = contextTokenKey(accountId, userId);
|
|
12343
|
+
logger.debug(`setContextToken: key=${k}`);
|
|
12344
|
+
contextTokenStore.set(k, token);
|
|
12345
|
+
persistContextTokens(accountId);
|
|
12346
|
+
}
|
|
12347
|
+
function getContextToken(accountId, userId) {
|
|
12348
|
+
const k = contextTokenKey(accountId, normalizeWeixinUserIdFromChatKey(userId));
|
|
12349
|
+
const val = contextTokenStore.get(k);
|
|
12350
|
+
logger.debug(`getContextToken: key=${k} found=${val !== undefined} storeSize=${contextTokenStore.size}`);
|
|
12351
|
+
return val;
|
|
12352
|
+
}
|
|
12353
|
+
function normalizeWeixinUserIdFromChatKey(chatKey) {
|
|
12354
|
+
const parts = chatKey.split(":");
|
|
12355
|
+
if (parts[0] === "weixin" && parts[2]) {
|
|
12356
|
+
return parts.slice(2).join(":");
|
|
12357
|
+
}
|
|
12358
|
+
return chatKey;
|
|
12359
|
+
}
|
|
12360
|
+
function isMediaItem(item) {
|
|
12361
|
+
return item.type === MessageItemType.IMAGE || item.type === MessageItemType.VIDEO || item.type === MessageItemType.FILE || item.type === MessageItemType.VOICE;
|
|
12362
|
+
}
|
|
12363
|
+
function bodyFromItemList(itemList) {
|
|
12364
|
+
if (!itemList?.length)
|
|
12365
|
+
return "";
|
|
12366
|
+
for (const item of itemList) {
|
|
12367
|
+
if (item.type === MessageItemType.TEXT && item.text_item?.text != null) {
|
|
12368
|
+
const text = String(item.text_item.text);
|
|
12369
|
+
const ref = item.ref_msg;
|
|
12370
|
+
if (!ref)
|
|
12371
|
+
return text;
|
|
12372
|
+
if (ref.message_item && isMediaItem(ref.message_item))
|
|
12373
|
+
return text;
|
|
12374
|
+
const parts = [];
|
|
12375
|
+
if (ref.title)
|
|
12376
|
+
parts.push(ref.title);
|
|
12377
|
+
if (ref.message_item) {
|
|
12378
|
+
const refBody = bodyFromItemList([ref.message_item]);
|
|
12379
|
+
if (refBody)
|
|
12380
|
+
parts.push(refBody);
|
|
12381
|
+
}
|
|
12382
|
+
if (!parts.length)
|
|
12383
|
+
return text;
|
|
12384
|
+
return `[引用: ${parts.join(" | ")}]
|
|
12385
|
+
${text}`;
|
|
12386
|
+
}
|
|
12387
|
+
if (item.type === MessageItemType.VOICE && item.voice_item?.text) {
|
|
12388
|
+
return item.voice_item.text;
|
|
12389
|
+
}
|
|
12390
|
+
}
|
|
12391
|
+
return "";
|
|
12392
|
+
}
|
|
12393
|
+
function extractWeixinMediaDescriptors(itemList) {
|
|
12394
|
+
const out = [];
|
|
12395
|
+
for (const item of itemList ?? []) {
|
|
12396
|
+
const descriptor = descriptorFromItem(item);
|
|
12397
|
+
if (descriptor)
|
|
12398
|
+
out.push(descriptor);
|
|
12399
|
+
const ref = item.type === MessageItemType.TEXT ? item.ref_msg?.message_item : undefined;
|
|
12400
|
+
const refDescriptor = descriptorFromItem(ref);
|
|
12401
|
+
if (refDescriptor)
|
|
12402
|
+
out.push(refDescriptor);
|
|
12403
|
+
}
|
|
12404
|
+
return out;
|
|
12405
|
+
}
|
|
12406
|
+
function descriptorFromItem(item) {
|
|
12407
|
+
if (!item)
|
|
12408
|
+
return;
|
|
12409
|
+
if (item.type === MessageItemType.IMAGE)
|
|
12410
|
+
return { item, kind: "image" };
|
|
12411
|
+
if (item.type === MessageItemType.VIDEO)
|
|
12412
|
+
return { item, kind: "video" };
|
|
12413
|
+
if (item.type === MessageItemType.FILE)
|
|
12414
|
+
return { item, kind: "file", fileName: item.file_item?.file_name };
|
|
12415
|
+
if (item.type === MessageItemType.VOICE)
|
|
12416
|
+
return { item, kind: "audio" };
|
|
12417
|
+
return;
|
|
12418
|
+
}
|
|
12419
|
+
var contextTokenStore;
|
|
12420
|
+
var init_inbound = __esm(() => {
|
|
12421
|
+
init_logger();
|
|
12422
|
+
init_random();
|
|
12423
|
+
init_types2();
|
|
12424
|
+
init_state_dir();
|
|
12425
|
+
contextTokenStore = new Map;
|
|
12426
|
+
});
|
|
12427
|
+
|
|
12428
|
+
// src/weixin/api/config-cache.ts
|
|
12429
|
+
class WeixinConfigManager {
|
|
12430
|
+
apiOpts;
|
|
12431
|
+
log;
|
|
12432
|
+
cache = new Map;
|
|
12433
|
+
constructor(apiOpts, log) {
|
|
12434
|
+
this.apiOpts = apiOpts;
|
|
12435
|
+
this.log = log;
|
|
12436
|
+
}
|
|
12437
|
+
async getForUser(userId, contextToken) {
|
|
12438
|
+
const now = Date.now();
|
|
12439
|
+
const entry = this.cache.get(userId);
|
|
12440
|
+
const shouldFetch = !entry || now >= entry.nextFetchAt;
|
|
12441
|
+
if (shouldFetch) {
|
|
12442
|
+
let fetchOk = false;
|
|
12443
|
+
try {
|
|
12444
|
+
const resp = await getConfig({
|
|
12445
|
+
baseUrl: this.apiOpts.baseUrl,
|
|
12446
|
+
token: this.apiOpts.token,
|
|
12447
|
+
ilinkUserId: userId,
|
|
12448
|
+
contextToken
|
|
12449
|
+
});
|
|
12450
|
+
if (resp.ret === 0) {
|
|
12451
|
+
this.cache.set(userId, {
|
|
12452
|
+
config: { typingTicket: resp.typing_ticket ?? "" },
|
|
12453
|
+
everSucceeded: true,
|
|
12454
|
+
nextFetchAt: now + Math.random() * CONFIG_CACHE_TTL_MS,
|
|
12455
|
+
retryDelayMs: CONFIG_CACHE_INITIAL_RETRY_MS
|
|
12456
|
+
});
|
|
12457
|
+
this.log(`[weixin] config ${entry?.everSucceeded ? "refreshed" : "cached"} for ${userId}`);
|
|
12458
|
+
fetchOk = true;
|
|
12459
|
+
}
|
|
12460
|
+
} catch (err) {
|
|
12461
|
+
this.log(`[weixin] getConfig failed for ${userId} (ignored): ${String(err)}`);
|
|
12462
|
+
}
|
|
12463
|
+
if (!fetchOk) {
|
|
12464
|
+
const prevDelay = entry?.retryDelayMs ?? CONFIG_CACHE_INITIAL_RETRY_MS;
|
|
12465
|
+
const nextDelay = Math.min(prevDelay * 2, CONFIG_CACHE_MAX_RETRY_MS);
|
|
12466
|
+
if (entry) {
|
|
12467
|
+
entry.nextFetchAt = now + nextDelay;
|
|
12468
|
+
entry.retryDelayMs = nextDelay;
|
|
12469
|
+
} else {
|
|
12470
|
+
this.cache.set(userId, {
|
|
12471
|
+
config: { typingTicket: "" },
|
|
12472
|
+
everSucceeded: false,
|
|
12473
|
+
nextFetchAt: now + CONFIG_CACHE_INITIAL_RETRY_MS,
|
|
12474
|
+
retryDelayMs: CONFIG_CACHE_INITIAL_RETRY_MS
|
|
12475
|
+
});
|
|
12476
|
+
}
|
|
12477
|
+
}
|
|
12478
|
+
}
|
|
12479
|
+
return this.cache.get(userId)?.config ?? { typingTicket: "" };
|
|
12480
|
+
}
|
|
12481
|
+
}
|
|
12482
|
+
var CONFIG_CACHE_TTL_MS, CONFIG_CACHE_INITIAL_RETRY_MS = 2000, CONFIG_CACHE_MAX_RETRY_MS;
|
|
12483
|
+
var init_config_cache = __esm(() => {
|
|
12484
|
+
init_api();
|
|
12485
|
+
CONFIG_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
12486
|
+
CONFIG_CACHE_MAX_RETRY_MS = 60 * 60 * 1000;
|
|
12487
|
+
});
|
|
12488
|
+
|
|
12489
|
+
// src/weixin/api/session-guard.ts
|
|
12490
|
+
function pauseSession(accountId) {
|
|
12491
|
+
const until = Date.now() + SESSION_PAUSE_DURATION_MS;
|
|
12492
|
+
pauseUntilMap.set(accountId, until);
|
|
12493
|
+
logger.info(`session-guard: paused accountId=${accountId} until=${new Date(until).toISOString()} (${SESSION_PAUSE_DURATION_MS / 1000}s)`);
|
|
12494
|
+
}
|
|
12495
|
+
function resetSessionPause(accountId) {
|
|
12496
|
+
pauseUntilMap.delete(accountId);
|
|
12497
|
+
}
|
|
12498
|
+
var SESSION_PAUSE_DURATION_MS, SESSION_EXPIRED_ERRCODE = -14, pauseUntilMap;
|
|
12499
|
+
var init_session_guard = __esm(() => {
|
|
12500
|
+
init_logger();
|
|
12501
|
+
SESSION_PAUSE_DURATION_MS = 60 * 60 * 1000;
|
|
12502
|
+
pauseUntilMap = new Map;
|
|
12503
|
+
});
|
|
12504
|
+
|
|
12505
|
+
// src/weixin/messaging/conversation-executor.ts
|
|
12506
|
+
function createConversationExecutor() {
|
|
12507
|
+
const states = new Map;
|
|
12508
|
+
const getState = (conversationId) => {
|
|
12509
|
+
const existing = states.get(conversationId);
|
|
12510
|
+
if (existing)
|
|
12511
|
+
return existing;
|
|
12512
|
+
const created = { activeControls: 0 };
|
|
12513
|
+
states.set(conversationId, created);
|
|
12514
|
+
return created;
|
|
12515
|
+
};
|
|
12516
|
+
const cleanupState = (conversationId, state) => {
|
|
12517
|
+
if (!state.normalTail && state.activeControls === 0) {
|
|
12518
|
+
states.delete(conversationId);
|
|
12519
|
+
}
|
|
12520
|
+
};
|
|
12521
|
+
return {
|
|
12522
|
+
run(conversationId, lane, task) {
|
|
12523
|
+
const state = getState(conversationId);
|
|
12524
|
+
if (lane === "control") {
|
|
12525
|
+
state.activeControls += 1;
|
|
12526
|
+
return Promise.resolve().then(task).finally(() => {
|
|
12527
|
+
state.activeControls -= 1;
|
|
12528
|
+
cleanupState(conversationId, state);
|
|
12529
|
+
});
|
|
12530
|
+
}
|
|
12531
|
+
const previous = state.normalTail ?? Promise.resolve();
|
|
12532
|
+
const next = previous.catch(() => {
|
|
12533
|
+
return;
|
|
12534
|
+
}).then(task);
|
|
12535
|
+
state.normalTail = next;
|
|
12536
|
+
return next.finally(() => {
|
|
12537
|
+
if (state.normalTail === next) {
|
|
12538
|
+
state.normalTail = undefined;
|
|
12539
|
+
}
|
|
12540
|
+
cleanupState(conversationId, state);
|
|
12541
|
+
});
|
|
12542
|
+
}
|
|
12543
|
+
};
|
|
12544
|
+
}
|
|
12545
|
+
|
|
12124
12546
|
// src/channels/media-store.ts
|
|
12125
12547
|
import { access as access2, mkdir as mkdir7, readdir, rm as rm4, stat, writeFile as writeFile6 } from "node:fs/promises";
|
|
12126
|
-
import
|
|
12548
|
+
import path7 from "node:path";
|
|
12127
12549
|
|
|
12128
12550
|
class RuntimeMediaStore {
|
|
12129
12551
|
rootDir;
|
|
@@ -12139,10 +12561,10 @@ class RuntimeMediaStore {
|
|
|
12139
12561
|
const safeChatKey = safePathSegment(input.chatKey);
|
|
12140
12562
|
const safeMessageId = safePathSegment(input.messageId || "message");
|
|
12141
12563
|
const baseFileName = sanitizeMediaFileName(input.fileName ?? "attachment", input.mimeType);
|
|
12142
|
-
const dir =
|
|
12564
|
+
const dir = path7.join(this.rootDir, input.channelId, safeChatKey, safeMessageId);
|
|
12143
12565
|
await mkdir7(dir, { recursive: true });
|
|
12144
|
-
const resolvedRoot =
|
|
12145
|
-
const resolvedFile =
|
|
12566
|
+
const resolvedRoot = path7.resolve(this.rootDir);
|
|
12567
|
+
const resolvedFile = path7.resolve(path7.join(dir, await uniqueFileName(dir, baseFileName)));
|
|
12146
12568
|
if (!isPathInside(resolvedFile, resolvedRoot)) {
|
|
12147
12569
|
throw new Error("media path escapes runtime media root");
|
|
12148
12570
|
}
|
|
@@ -12151,7 +12573,7 @@ class RuntimeMediaStore {
|
|
|
12151
12573
|
kind: input.kind,
|
|
12152
12574
|
filePath: resolvedFile,
|
|
12153
12575
|
mimeType: input.mimeType,
|
|
12154
|
-
fileName:
|
|
12576
|
+
fileName: path7.basename(resolvedFile),
|
|
12155
12577
|
sizeBytes: input.buffer.byteLength,
|
|
12156
12578
|
source: {
|
|
12157
12579
|
channelId: input.channelId,
|
|
@@ -12167,10 +12589,10 @@ class RuntimeMediaStore {
|
|
|
12167
12589
|
}
|
|
12168
12590
|
}
|
|
12169
12591
|
function sanitizeMediaFileName(fileName, mimeType) {
|
|
12170
|
-
const base =
|
|
12592
|
+
const base = path7.basename(fileName.trim() || "attachment");
|
|
12171
12593
|
const replaced = base.replace(/[\\/:*?"<>|\s]+/g, "-").replace(/^-+|-+$/g, "");
|
|
12172
12594
|
const safe = replaced || "attachment";
|
|
12173
|
-
const ext =
|
|
12595
|
+
const ext = path7.extname(safe);
|
|
12174
12596
|
if (ext)
|
|
12175
12597
|
return safe;
|
|
12176
12598
|
return `${safe}${extensionFromMime(mimeType)}`;
|
|
@@ -12180,13 +12602,13 @@ function safePathSegment(value) {
|
|
|
12180
12602
|
return safe || "unknown";
|
|
12181
12603
|
}
|
|
12182
12604
|
async function uniqueFileName(dir, baseName) {
|
|
12183
|
-
const ext =
|
|
12605
|
+
const ext = path7.extname(baseName);
|
|
12184
12606
|
const stem = ext ? baseName.slice(0, -ext.length) : baseName;
|
|
12185
12607
|
let candidate = baseName;
|
|
12186
12608
|
let counter = 2;
|
|
12187
12609
|
while (true) {
|
|
12188
12610
|
try {
|
|
12189
|
-
await access2(
|
|
12611
|
+
await access2(path7.join(dir, candidate));
|
|
12190
12612
|
candidate = `${stem}-${counter}${ext}`;
|
|
12191
12613
|
counter += 1;
|
|
12192
12614
|
} catch {
|
|
@@ -12217,8 +12639,8 @@ function extensionFromMime(mimeType) {
|
|
|
12217
12639
|
return ".bin";
|
|
12218
12640
|
}
|
|
12219
12641
|
function isPathInside(candidate, root) {
|
|
12220
|
-
const relative =
|
|
12221
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
12642
|
+
const relative = path7.relative(root, candidate);
|
|
12643
|
+
return relative === "" || !relative.startsWith("..") && !path7.isAbsolute(relative);
|
|
12222
12644
|
}
|
|
12223
12645
|
async function cleanupDir(dir, cutoffMs) {
|
|
12224
12646
|
let entries;
|
|
@@ -12229,7 +12651,7 @@ async function cleanupDir(dir, cutoffMs) {
|
|
|
12229
12651
|
}
|
|
12230
12652
|
let empty = true;
|
|
12231
12653
|
for (const entry of entries) {
|
|
12232
|
-
const full =
|
|
12654
|
+
const full = path7.join(dir, entry.name);
|
|
12233
12655
|
if (entry.isDirectory()) {
|
|
12234
12656
|
const childEmpty = await cleanupDir(full, cutoffMs);
|
|
12235
12657
|
if (childEmpty) {
|
|
@@ -12258,18 +12680,18 @@ var init_media_store = __esm(() => {
|
|
|
12258
12680
|
});
|
|
12259
12681
|
|
|
12260
12682
|
// src/channels/outbound-media-safety.ts
|
|
12261
|
-
import
|
|
12262
|
-
import
|
|
12683
|
+
import fs6 from "node:fs/promises";
|
|
12684
|
+
import path8 from "node:path";
|
|
12263
12685
|
async function resolveSafeOutboundMediaPath(mediaPath, allowedRoots) {
|
|
12264
12686
|
if (mediaPath.startsWith("http://") || mediaPath.startsWith("https://")) {
|
|
12265
12687
|
return null;
|
|
12266
12688
|
}
|
|
12267
|
-
const candidate =
|
|
12689
|
+
const candidate = path8.isAbsolute(mediaPath) ? mediaPath : path8.resolve(mediaPath);
|
|
12268
12690
|
const realCandidate = await realpathOrNull(candidate);
|
|
12269
12691
|
if (!realCandidate) {
|
|
12270
12692
|
return null;
|
|
12271
12693
|
}
|
|
12272
|
-
const stat2 = await
|
|
12694
|
+
const stat2 = await fs6.stat(realCandidate).catch(() => null);
|
|
12273
12695
|
if (!stat2?.isFile()) {
|
|
12274
12696
|
return null;
|
|
12275
12697
|
}
|
|
@@ -12283,21 +12705,21 @@ async function resolveSafeOutboundMediaPath(mediaPath, allowedRoots) {
|
|
|
12283
12705
|
}
|
|
12284
12706
|
async function realpathOrNull(filePath) {
|
|
12285
12707
|
try {
|
|
12286
|
-
return await
|
|
12708
|
+
return await fs6.realpath(filePath);
|
|
12287
12709
|
} catch {
|
|
12288
12710
|
return null;
|
|
12289
12711
|
}
|
|
12290
12712
|
}
|
|
12291
12713
|
function isPathInside2(candidate, root) {
|
|
12292
|
-
const relative =
|
|
12293
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
12714
|
+
const relative = path8.relative(root, candidate);
|
|
12715
|
+
return relative === "" || !relative.startsWith("..") && !path8.isAbsolute(relative);
|
|
12294
12716
|
}
|
|
12295
12717
|
var init_outbound_media_safety = () => {};
|
|
12296
12718
|
|
|
12297
12719
|
// src/weixin/media/mime.ts
|
|
12298
|
-
import
|
|
12720
|
+
import path9 from "node:path";
|
|
12299
12721
|
function getMimeFromFilename(filename) {
|
|
12300
|
-
const ext =
|
|
12722
|
+
const ext = path9.extname(filename).toLowerCase();
|
|
12301
12723
|
return EXTENSION_TO_MIME[ext] ?? "application/octet-stream";
|
|
12302
12724
|
}
|
|
12303
12725
|
function getExtensionFromMime(mimeType) {
|
|
@@ -12635,115 +13057,361 @@ function buildFinalHeadsUp(input) {
|
|
|
12635
13057
|
\uD83D\uDCC4 结果共 ${total} 段,已发 ${sentSoFar} 段。回复 /jx 续看后 ${remaining} 段。`;
|
|
12636
13058
|
}
|
|
12637
13059
|
|
|
12638
|
-
// src/weixin/
|
|
12639
|
-
|
|
12640
|
-
|
|
12641
|
-
|
|
12642
|
-
|
|
12643
|
-
|
|
12644
|
-
|
|
12645
|
-
|
|
12646
|
-
|
|
12647
|
-
return `${accountId}:${userId}`;
|
|
12648
|
-
}
|
|
12649
|
-
function setContextToken(accountId, userId, token) {
|
|
12650
|
-
const k = contextTokenKey(accountId, userId);
|
|
12651
|
-
logger.debug(`setContextToken: key=${k}`);
|
|
12652
|
-
contextTokenStore.set(k, token);
|
|
12653
|
-
}
|
|
12654
|
-
function getContextToken(accountId, userId) {
|
|
12655
|
-
const k = contextTokenKey(accountId, normalizeWeixinUserIdFromChatKey(userId));
|
|
12656
|
-
const val = contextTokenStore.get(k);
|
|
12657
|
-
logger.debug(`getContextToken: key=${k} found=${val !== undefined} storeSize=${contextTokenStore.size}`);
|
|
12658
|
-
return val;
|
|
12659
|
-
}
|
|
12660
|
-
function normalizeWeixinUserIdFromChatKey(chatKey) {
|
|
12661
|
-
const parts = chatKey.split(":");
|
|
12662
|
-
if (parts[0] === "weixin" && parts[2]) {
|
|
12663
|
-
return parts.slice(2).join(":");
|
|
13060
|
+
// src/weixin/messaging/markdown-filter.ts
|
|
13061
|
+
class StreamingMarkdownFilter {
|
|
13062
|
+
buf = "";
|
|
13063
|
+
fence = false;
|
|
13064
|
+
sol = true;
|
|
13065
|
+
inl = null;
|
|
13066
|
+
feed(delta) {
|
|
13067
|
+
this.buf += delta;
|
|
13068
|
+
return this.pump(false);
|
|
12664
13069
|
}
|
|
12665
|
-
|
|
12666
|
-
|
|
12667
|
-
|
|
12668
|
-
|
|
12669
|
-
|
|
12670
|
-
|
|
12671
|
-
|
|
13070
|
+
flush() {
|
|
13071
|
+
return this.pump(true);
|
|
13072
|
+
}
|
|
13073
|
+
pump(eof) {
|
|
13074
|
+
let out = "";
|
|
13075
|
+
while (this.buf) {
|
|
13076
|
+
const sLen = this.buf.length;
|
|
13077
|
+
const sSol = this.sol;
|
|
13078
|
+
const sFence = this.fence;
|
|
13079
|
+
const sInl = this.inl;
|
|
13080
|
+
if (this.fence)
|
|
13081
|
+
out += this.pumpFence(eof);
|
|
13082
|
+
else if (this.inl)
|
|
13083
|
+
out += this.pumpInline(eof);
|
|
13084
|
+
else if (this.sol)
|
|
13085
|
+
out += this.pumpSOL(eof);
|
|
13086
|
+
else
|
|
13087
|
+
out += this.pumpBody(eof);
|
|
13088
|
+
if (this.buf.length === sLen && this.sol === sSol && this.fence === sFence && this.inl === sInl)
|
|
13089
|
+
break;
|
|
13090
|
+
}
|
|
13091
|
+
if (eof && this.inl) {
|
|
13092
|
+
const markers = { image: "![", bold3: "***", italic: "*", ubold3: "___", uitalic: "_" };
|
|
13093
|
+
out += (markers[this.inl.type] ?? "") + this.inl.acc;
|
|
13094
|
+
this.inl = null;
|
|
13095
|
+
}
|
|
13096
|
+
return out;
|
|
13097
|
+
}
|
|
13098
|
+
pumpFence(eof) {
|
|
13099
|
+
if (this.sol) {
|
|
13100
|
+
if (this.buf.length < 3 && !eof)
|
|
13101
|
+
return "";
|
|
13102
|
+
if (this.buf.startsWith("```")) {
|
|
13103
|
+
const nl2 = this.buf.indexOf(`
|
|
13104
|
+
`, 3);
|
|
13105
|
+
if (nl2 !== -1) {
|
|
13106
|
+
this.fence = false;
|
|
13107
|
+
const line = this.buf.slice(0, nl2 + 1);
|
|
13108
|
+
this.buf = this.buf.slice(nl2 + 1);
|
|
13109
|
+
this.sol = true;
|
|
13110
|
+
return line;
|
|
13111
|
+
}
|
|
13112
|
+
if (eof) {
|
|
13113
|
+
this.fence = false;
|
|
13114
|
+
const line = this.buf;
|
|
13115
|
+
this.buf = "";
|
|
13116
|
+
return line;
|
|
13117
|
+
}
|
|
13118
|
+
return "";
|
|
13119
|
+
}
|
|
13120
|
+
this.sol = false;
|
|
13121
|
+
}
|
|
13122
|
+
const nl = this.buf.indexOf(`
|
|
13123
|
+
`);
|
|
13124
|
+
if (nl !== -1) {
|
|
13125
|
+
const chunk2 = this.buf.slice(0, nl + 1);
|
|
13126
|
+
this.buf = this.buf.slice(nl + 1);
|
|
13127
|
+
this.sol = true;
|
|
13128
|
+
return chunk2;
|
|
13129
|
+
}
|
|
13130
|
+
const chunk = this.buf;
|
|
13131
|
+
this.buf = "";
|
|
13132
|
+
return chunk;
|
|
13133
|
+
}
|
|
13134
|
+
pumpSOL(eof) {
|
|
13135
|
+
const b = this.buf;
|
|
13136
|
+
if (b[0] === `
|
|
13137
|
+
`) {
|
|
13138
|
+
this.buf = b.slice(1);
|
|
13139
|
+
return `
|
|
13140
|
+
`;
|
|
13141
|
+
}
|
|
13142
|
+
if (b[0] === "`") {
|
|
13143
|
+
if (b.length < 3 && !eof)
|
|
13144
|
+
return "";
|
|
13145
|
+
if (b.startsWith("```")) {
|
|
13146
|
+
const nl = b.indexOf(`
|
|
13147
|
+
`, 3);
|
|
13148
|
+
if (nl !== -1) {
|
|
13149
|
+
this.fence = true;
|
|
13150
|
+
const line = b.slice(0, nl + 1);
|
|
13151
|
+
this.buf = b.slice(nl + 1);
|
|
13152
|
+
this.sol = true;
|
|
13153
|
+
return line;
|
|
13154
|
+
}
|
|
13155
|
+
if (eof) {
|
|
13156
|
+
this.buf = "";
|
|
13157
|
+
return b;
|
|
13158
|
+
}
|
|
13159
|
+
return "";
|
|
13160
|
+
}
|
|
13161
|
+
this.sol = false;
|
|
13162
|
+
return "";
|
|
13163
|
+
}
|
|
13164
|
+
if (b[0] === ">") {
|
|
13165
|
+
this.sol = false;
|
|
13166
|
+
return "";
|
|
13167
|
+
}
|
|
13168
|
+
if (b[0] === "#") {
|
|
13169
|
+
let n = 0;
|
|
13170
|
+
while (n < b.length && b[n] === "#")
|
|
13171
|
+
n++;
|
|
13172
|
+
if (n === b.length && !eof)
|
|
13173
|
+
return "";
|
|
13174
|
+
if (n >= 5 && n <= 6 && n < b.length && b[n] === " ") {
|
|
13175
|
+
this.buf = b.slice(n + 1);
|
|
13176
|
+
this.sol = false;
|
|
13177
|
+
return "";
|
|
13178
|
+
}
|
|
13179
|
+
this.sol = false;
|
|
13180
|
+
return "";
|
|
13181
|
+
}
|
|
13182
|
+
if (b[0] === " " || b[0] === "\t") {
|
|
13183
|
+
if (b.search(/[^ \t]/) === -1 && !eof)
|
|
13184
|
+
return "";
|
|
13185
|
+
this.sol = false;
|
|
13186
|
+
return "";
|
|
13187
|
+
}
|
|
13188
|
+
if (b[0] === "-" || b[0] === "*" || b[0] === "_") {
|
|
13189
|
+
const ch = b[0];
|
|
13190
|
+
let j = 0;
|
|
13191
|
+
while (j < b.length && (b[j] === ch || b[j] === " "))
|
|
13192
|
+
j++;
|
|
13193
|
+
if (j === b.length && !eof)
|
|
13194
|
+
return "";
|
|
13195
|
+
if (j === b.length || b[j] === `
|
|
13196
|
+
`) {
|
|
13197
|
+
let count = 0;
|
|
13198
|
+
for (let k = 0;k < j; k++)
|
|
13199
|
+
if (b[k] === ch)
|
|
13200
|
+
count++;
|
|
13201
|
+
if (count >= 3) {
|
|
13202
|
+
if (j < b.length) {
|
|
13203
|
+
this.buf = b.slice(j + 1);
|
|
13204
|
+
this.sol = true;
|
|
13205
|
+
return b.slice(0, j + 1);
|
|
13206
|
+
}
|
|
13207
|
+
this.buf = "";
|
|
13208
|
+
return b;
|
|
13209
|
+
}
|
|
13210
|
+
}
|
|
13211
|
+
this.sol = false;
|
|
13212
|
+
return "";
|
|
13213
|
+
}
|
|
13214
|
+
this.sol = false;
|
|
12672
13215
|
return "";
|
|
12673
|
-
|
|
12674
|
-
|
|
12675
|
-
|
|
12676
|
-
|
|
12677
|
-
|
|
12678
|
-
|
|
12679
|
-
if (
|
|
12680
|
-
|
|
12681
|
-
|
|
12682
|
-
|
|
12683
|
-
|
|
12684
|
-
|
|
12685
|
-
|
|
12686
|
-
|
|
12687
|
-
|
|
13216
|
+
}
|
|
13217
|
+
pumpBody(eof) {
|
|
13218
|
+
let out = "";
|
|
13219
|
+
let i = 0;
|
|
13220
|
+
while (i < this.buf.length) {
|
|
13221
|
+
const c = this.buf[i];
|
|
13222
|
+
if (c === `
|
|
13223
|
+
`) {
|
|
13224
|
+
out += this.buf.slice(0, i + 1);
|
|
13225
|
+
this.buf = this.buf.slice(i + 1);
|
|
13226
|
+
this.sol = true;
|
|
13227
|
+
return out;
|
|
13228
|
+
}
|
|
13229
|
+
if (c === "!" && i + 1 < this.buf.length && this.buf[i + 1] === "[") {
|
|
13230
|
+
out += this.buf.slice(0, i);
|
|
13231
|
+
this.buf = this.buf.slice(i + 2);
|
|
13232
|
+
this.inl = { type: "image", acc: "" };
|
|
13233
|
+
return out;
|
|
13234
|
+
}
|
|
13235
|
+
if (c === "~") {
|
|
13236
|
+
i++;
|
|
13237
|
+
continue;
|
|
12688
13238
|
}
|
|
12689
|
-
if (
|
|
12690
|
-
|
|
12691
|
-
|
|
12692
|
-
|
|
13239
|
+
if (c === "*") {
|
|
13240
|
+
if (i + 2 < this.buf.length && this.buf[i + 1] === "*" && this.buf[i + 2] === "*") {
|
|
13241
|
+
out += this.buf.slice(0, i);
|
|
13242
|
+
this.buf = this.buf.slice(i + 3);
|
|
13243
|
+
this.inl = { type: "bold3", acc: "" };
|
|
13244
|
+
return out;
|
|
13245
|
+
}
|
|
13246
|
+
if (i + 1 < this.buf.length && this.buf[i + 1] === "*") {
|
|
13247
|
+
i += 2;
|
|
13248
|
+
continue;
|
|
13249
|
+
}
|
|
13250
|
+
if (i + 1 < this.buf.length && this.buf[i + 1] !== " " && this.buf[i + 1] !== `
|
|
13251
|
+
`) {
|
|
13252
|
+
out += this.buf.slice(0, i);
|
|
13253
|
+
this.buf = this.buf.slice(i + 1);
|
|
13254
|
+
this.inl = { type: "italic", acc: "" };
|
|
13255
|
+
return out;
|
|
13256
|
+
}
|
|
13257
|
+
i++;
|
|
13258
|
+
continue;
|
|
13259
|
+
}
|
|
13260
|
+
if (c === "_") {
|
|
13261
|
+
if (i + 2 < this.buf.length && this.buf[i + 1] === "_" && this.buf[i + 2] === "_") {
|
|
13262
|
+
out += this.buf.slice(0, i);
|
|
13263
|
+
this.buf = this.buf.slice(i + 3);
|
|
13264
|
+
this.inl = { type: "ubold3", acc: "" };
|
|
13265
|
+
return out;
|
|
13266
|
+
}
|
|
13267
|
+
if (i + 1 < this.buf.length && this.buf[i + 1] === "_") {
|
|
13268
|
+
i += 2;
|
|
13269
|
+
continue;
|
|
13270
|
+
}
|
|
13271
|
+
if (i + 1 < this.buf.length && this.buf[i + 1] !== " " && this.buf[i + 1] !== `
|
|
13272
|
+
`) {
|
|
13273
|
+
out += this.buf.slice(0, i);
|
|
13274
|
+
this.buf = this.buf.slice(i + 1);
|
|
13275
|
+
this.inl = { type: "uitalic", acc: "" };
|
|
13276
|
+
return out;
|
|
13277
|
+
}
|
|
13278
|
+
i++;
|
|
13279
|
+
continue;
|
|
13280
|
+
}
|
|
13281
|
+
i++;
|
|
12693
13282
|
}
|
|
12694
|
-
|
|
12695
|
-
|
|
13283
|
+
let hold = 0;
|
|
13284
|
+
if (!eof) {
|
|
13285
|
+
if (this.buf.endsWith("**"))
|
|
13286
|
+
hold = 2;
|
|
13287
|
+
else if (this.buf.endsWith("__"))
|
|
13288
|
+
hold = 2;
|
|
13289
|
+
else if (this.buf.endsWith("*"))
|
|
13290
|
+
hold = 1;
|
|
13291
|
+
else if (this.buf.endsWith("_"))
|
|
13292
|
+
hold = 1;
|
|
13293
|
+
else if (this.buf.endsWith("!"))
|
|
13294
|
+
hold = 1;
|
|
13295
|
+
}
|
|
13296
|
+
out += this.buf.slice(0, this.buf.length - hold);
|
|
13297
|
+
this.buf = hold > 0 ? this.buf.slice(-hold) : "";
|
|
13298
|
+
return out;
|
|
13299
|
+
}
|
|
13300
|
+
pumpInline(_eof) {
|
|
13301
|
+
if (!this.inl)
|
|
13302
|
+
return "";
|
|
13303
|
+
this.inl.acc += this.buf;
|
|
13304
|
+
this.buf = "";
|
|
13305
|
+
switch (this.inl.type) {
|
|
13306
|
+
case "bold3": {
|
|
13307
|
+
const idx = this.inl.acc.indexOf("***");
|
|
13308
|
+
if (idx !== -1) {
|
|
13309
|
+
const content = this.inl.acc.slice(0, idx);
|
|
13310
|
+
this.buf = this.inl.acc.slice(idx + 3);
|
|
13311
|
+
this.inl = null;
|
|
13312
|
+
if (StreamingMarkdownFilter.containsCJK(content))
|
|
13313
|
+
return content;
|
|
13314
|
+
return `***${content}***`;
|
|
13315
|
+
}
|
|
13316
|
+
return "";
|
|
13317
|
+
}
|
|
13318
|
+
case "ubold3": {
|
|
13319
|
+
const idx = this.inl.acc.indexOf("___");
|
|
13320
|
+
if (idx !== -1) {
|
|
13321
|
+
const content = this.inl.acc.slice(0, idx);
|
|
13322
|
+
this.buf = this.inl.acc.slice(idx + 3);
|
|
13323
|
+
this.inl = null;
|
|
13324
|
+
if (StreamingMarkdownFilter.containsCJK(content))
|
|
13325
|
+
return content;
|
|
13326
|
+
return `___${content}___`;
|
|
13327
|
+
}
|
|
13328
|
+
return "";
|
|
13329
|
+
}
|
|
13330
|
+
case "italic": {
|
|
13331
|
+
for (let j = 0;j < this.inl.acc.length; j++) {
|
|
13332
|
+
if (this.inl.acc[j] === `
|
|
13333
|
+
`) {
|
|
13334
|
+
const r = "*" + this.inl.acc.slice(0, j + 1);
|
|
13335
|
+
this.buf = this.inl.acc.slice(j + 1);
|
|
13336
|
+
this.inl = null;
|
|
13337
|
+
this.sol = true;
|
|
13338
|
+
return r;
|
|
13339
|
+
}
|
|
13340
|
+
if (this.inl.acc[j] === "*") {
|
|
13341
|
+
if (j + 1 < this.inl.acc.length && this.inl.acc[j + 1] === "*") {
|
|
13342
|
+
j++;
|
|
13343
|
+
continue;
|
|
13344
|
+
}
|
|
13345
|
+
const content = this.inl.acc.slice(0, j);
|
|
13346
|
+
this.buf = this.inl.acc.slice(j + 1);
|
|
13347
|
+
this.inl = null;
|
|
13348
|
+
if (StreamingMarkdownFilter.containsCJK(content))
|
|
13349
|
+
return content;
|
|
13350
|
+
return `*${content}*`;
|
|
13351
|
+
}
|
|
13352
|
+
}
|
|
13353
|
+
return "";
|
|
13354
|
+
}
|
|
13355
|
+
case "uitalic": {
|
|
13356
|
+
for (let j = 0;j < this.inl.acc.length; j++) {
|
|
13357
|
+
if (this.inl.acc[j] === `
|
|
13358
|
+
`) {
|
|
13359
|
+
const r = "_" + this.inl.acc.slice(0, j + 1);
|
|
13360
|
+
this.buf = this.inl.acc.slice(j + 1);
|
|
13361
|
+
this.inl = null;
|
|
13362
|
+
this.sol = true;
|
|
13363
|
+
return r;
|
|
13364
|
+
}
|
|
13365
|
+
if (this.inl.acc[j] === "_") {
|
|
13366
|
+
if (j + 1 < this.inl.acc.length && this.inl.acc[j + 1] === "_") {
|
|
13367
|
+
j++;
|
|
13368
|
+
continue;
|
|
13369
|
+
}
|
|
13370
|
+
const content = this.inl.acc.slice(0, j);
|
|
13371
|
+
this.buf = this.inl.acc.slice(j + 1);
|
|
13372
|
+
this.inl = null;
|
|
13373
|
+
if (StreamingMarkdownFilter.containsCJK(content))
|
|
13374
|
+
return content;
|
|
13375
|
+
return `_${content}_`;
|
|
13376
|
+
}
|
|
13377
|
+
}
|
|
13378
|
+
return "";
|
|
13379
|
+
}
|
|
13380
|
+
case "image": {
|
|
13381
|
+
const cb = this.inl.acc.indexOf("]");
|
|
13382
|
+
if (cb === -1)
|
|
13383
|
+
return "";
|
|
13384
|
+
if (cb + 1 >= this.inl.acc.length)
|
|
13385
|
+
return "";
|
|
13386
|
+
if (this.inl.acc[cb + 1] !== "(") {
|
|
13387
|
+
const r = "![" + this.inl.acc.slice(0, cb + 1);
|
|
13388
|
+
this.buf = this.inl.acc.slice(cb + 1);
|
|
13389
|
+
this.inl = null;
|
|
13390
|
+
return r;
|
|
13391
|
+
}
|
|
13392
|
+
const cp = this.inl.acc.indexOf(")", cb + 2);
|
|
13393
|
+
if (cp !== -1) {
|
|
13394
|
+
this.buf = this.inl.acc.slice(cp + 1);
|
|
13395
|
+
this.inl = null;
|
|
13396
|
+
return "";
|
|
13397
|
+
}
|
|
13398
|
+
return "";
|
|
13399
|
+
}
|
|
12696
13400
|
}
|
|
13401
|
+
return "";
|
|
12697
13402
|
}
|
|
12698
|
-
|
|
12699
|
-
|
|
12700
|
-
function extractWeixinMediaDescriptors(itemList) {
|
|
12701
|
-
const out = [];
|
|
12702
|
-
for (const item of itemList ?? []) {
|
|
12703
|
-
const descriptor = descriptorFromItem(item);
|
|
12704
|
-
if (descriptor)
|
|
12705
|
-
out.push(descriptor);
|
|
12706
|
-
const ref = item.type === MessageItemType.TEXT ? item.ref_msg?.message_item : undefined;
|
|
12707
|
-
const refDescriptor = descriptorFromItem(ref);
|
|
12708
|
-
if (refDescriptor)
|
|
12709
|
-
out.push(refDescriptor);
|
|
13403
|
+
static containsCJK(text) {
|
|
13404
|
+
return /[\u2E80-\u9FFF\uAC00-\uD7AF\uF900-\uFAFF]/.test(text);
|
|
12710
13405
|
}
|
|
12711
|
-
return out;
|
|
12712
13406
|
}
|
|
12713
|
-
function descriptorFromItem(item) {
|
|
12714
|
-
if (!item)
|
|
12715
|
-
return;
|
|
12716
|
-
if (item.type === MessageItemType.IMAGE)
|
|
12717
|
-
return { item, kind: "image" };
|
|
12718
|
-
if (item.type === MessageItemType.VIDEO)
|
|
12719
|
-
return { item, kind: "video" };
|
|
12720
|
-
if (item.type === MessageItemType.FILE)
|
|
12721
|
-
return { item, kind: "file", fileName: item.file_item?.file_name };
|
|
12722
|
-
if (item.type === MessageItemType.VOICE)
|
|
12723
|
-
return { item, kind: "audio" };
|
|
12724
|
-
return;
|
|
12725
|
-
}
|
|
12726
|
-
var contextTokenStore;
|
|
12727
|
-
var init_inbound = __esm(() => {
|
|
12728
|
-
init_logger();
|
|
12729
|
-
init_random();
|
|
12730
|
-
init_types2();
|
|
12731
|
-
contextTokenStore = new Map;
|
|
12732
|
-
});
|
|
12733
13407
|
|
|
12734
13408
|
// src/weixin/messaging/send.ts
|
|
12735
13409
|
function generateClientId() {
|
|
12736
13410
|
return generateId("openclaw-weixin");
|
|
12737
13411
|
}
|
|
12738
13412
|
function markdownToPlainText(text) {
|
|
12739
|
-
|
|
12740
|
-
|
|
12741
|
-
result = result.replace(/!\[[^\]]*\]\([^)]*\)/g, "");
|
|
12742
|
-
result = result.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
|
|
12743
|
-
result = result.replace(/^\|[\s:|-]+\|$/gm, "");
|
|
12744
|
-
result = result.replace(/^\|(.+)\|$/gm, (_, inner) => inner.split("|").map((cell) => cell.trim()).join(" "));
|
|
12745
|
-
result = result.replace(/\*\*(.+?)\*\*/g, "$1").replace(/\*(.+?)\*/g, "$1").replace(/__(.+?)__/g, "$1").replace(/~~(.+?)~~/g, "$1").replace(/`(.+?)`/g, "$1");
|
|
12746
|
-
return result;
|
|
13413
|
+
const f = new StreamingMarkdownFilter;
|
|
13414
|
+
return f.feed(text) + f.flush();
|
|
12747
13415
|
}
|
|
12748
13416
|
function buildTextMessageReq(params) {
|
|
12749
13417
|
const { to, text, contextToken, clientId } = params;
|
|
@@ -12979,10 +13647,10 @@ var init_cdn_upload = __esm(() => {
|
|
|
12979
13647
|
|
|
12980
13648
|
// src/weixin/cdn/upload.ts
|
|
12981
13649
|
import crypto3 from "node:crypto";
|
|
12982
|
-
import
|
|
13650
|
+
import fs7 from "node:fs/promises";
|
|
12983
13651
|
async function uploadMediaToCdn(params) {
|
|
12984
13652
|
const { filePath, toUserId, opts, cdnBaseUrl, mediaType, label } = params;
|
|
12985
|
-
const plaintext = await
|
|
13653
|
+
const plaintext = await fs7.readFile(filePath);
|
|
12986
13654
|
const rawsize = plaintext.length;
|
|
12987
13655
|
const rawfilemd5 = crypto3.createHash("md5").update(plaintext).digest("hex");
|
|
12988
13656
|
const filesize = aesEcbPaddedSize(rawsize);
|
|
@@ -13055,7 +13723,7 @@ var init_upload = __esm(() => {
|
|
|
13055
13723
|
});
|
|
13056
13724
|
|
|
13057
13725
|
// src/weixin/messaging/send-media.ts
|
|
13058
|
-
import
|
|
13726
|
+
import path10 from "node:path";
|
|
13059
13727
|
async function sendWeixinMediaFile(params) {
|
|
13060
13728
|
const { media, filePath, to, text, opts, cdnBaseUrl } = params;
|
|
13061
13729
|
const mime = media?.mimeType ?? getMimeFromFilename(filePath);
|
|
@@ -13082,7 +13750,7 @@ async function sendWeixinMediaFile(params) {
|
|
|
13082
13750
|
logger.info(`[weixin] sendWeixinMediaFile: image upload done filekey=${uploaded2.filekey} size=${uploaded2.fileSize}`);
|
|
13083
13751
|
return sendImageMessageWeixin({ to, text, uploaded: uploaded2, opts });
|
|
13084
13752
|
}
|
|
13085
|
-
const fileName = media?.fileName ??
|
|
13753
|
+
const fileName = media?.fileName ?? path10.basename(filePath);
|
|
13086
13754
|
logger.info(`[weixin] sendWeixinMediaFile: uploading file attachment filePath=${filePath} name=${fileName} to=${to}`);
|
|
13087
13755
|
const uploaded = await uploadFileAttachmentToWeixin({
|
|
13088
13756
|
filePath,
|
|
@@ -13102,14 +13770,14 @@ var init_send_media = __esm(() => {
|
|
|
13102
13770
|
});
|
|
13103
13771
|
|
|
13104
13772
|
// src/weixin/messaging/debug-mode.ts
|
|
13105
|
-
import
|
|
13106
|
-
import
|
|
13773
|
+
import fs8 from "node:fs";
|
|
13774
|
+
import path11 from "node:path";
|
|
13107
13775
|
function resolveDebugModePath() {
|
|
13108
|
-
return
|
|
13776
|
+
return path11.join(resolveStateDir(), "openclaw-weixin", "debug-mode.json");
|
|
13109
13777
|
}
|
|
13110
13778
|
function loadState() {
|
|
13111
13779
|
try {
|
|
13112
|
-
const raw =
|
|
13780
|
+
const raw = fs8.readFileSync(resolveDebugModePath(), "utf-8");
|
|
13113
13781
|
const parsed = JSON.parse(raw);
|
|
13114
13782
|
if (parsed && typeof parsed.accounts === "object")
|
|
13115
13783
|
return parsed;
|
|
@@ -13118,8 +13786,8 @@ function loadState() {
|
|
|
13118
13786
|
}
|
|
13119
13787
|
function saveState(state) {
|
|
13120
13788
|
const filePath = resolveDebugModePath();
|
|
13121
|
-
ensureDirSync(
|
|
13122
|
-
|
|
13789
|
+
ensureDirSync(path11.dirname(filePath));
|
|
13790
|
+
fs8.writeFileSync(filePath, JSON.stringify(state, null, 2), "utf-8");
|
|
13123
13791
|
}
|
|
13124
13792
|
function toggleDebugMode(accountId) {
|
|
13125
13793
|
const state = loadState();
|
|
@@ -13576,9 +14244,9 @@ var init_perf_tracer = __esm(() => {
|
|
|
13576
14244
|
|
|
13577
14245
|
// src/weixin/messaging/handle-weixin-message-turn.ts
|
|
13578
14246
|
import crypto4 from "node:crypto";
|
|
13579
|
-
import
|
|
14247
|
+
import fs9 from "node:fs/promises";
|
|
13580
14248
|
import { tmpdir } from "node:os";
|
|
13581
|
-
import
|
|
14249
|
+
import path12 from "node:path";
|
|
13582
14250
|
function utf8ByteLength(s) {
|
|
13583
14251
|
return Buffer.byteLength(s, "utf8");
|
|
13584
14252
|
}
|
|
@@ -13668,24 +14336,24 @@ function hardCutByCodepoint(s, maxBytes) {
|
|
|
13668
14336
|
return out;
|
|
13669
14337
|
}
|
|
13670
14338
|
function resolveMediaTempDir(customRoot) {
|
|
13671
|
-
return customRoot ??
|
|
14339
|
+
return customRoot ?? path12.join(tmpdir(), "weacpx", "media");
|
|
13672
14340
|
}
|
|
13673
14341
|
function createSaveMediaBuffer(mediaTempDir) {
|
|
13674
14342
|
return async function saveMediaBuffer(buffer, contentType, subdir, maxBytes, originalFilename) {
|
|
13675
14343
|
if (maxBytes !== undefined && buffer.byteLength > maxBytes) {
|
|
13676
14344
|
throw new Error(`media exceeds ${maxBytes} bytes`);
|
|
13677
14345
|
}
|
|
13678
|
-
const dir =
|
|
13679
|
-
await
|
|
14346
|
+
const dir = path12.join(resolveMediaTempDir(mediaTempDir), subdir ?? "");
|
|
14347
|
+
await fs9.mkdir(dir, { recursive: true });
|
|
13680
14348
|
let ext = ".bin";
|
|
13681
14349
|
if (originalFilename) {
|
|
13682
|
-
ext =
|
|
14350
|
+
ext = path12.extname(originalFilename) || ".bin";
|
|
13683
14351
|
} else if (contentType) {
|
|
13684
14352
|
ext = getExtensionFromMime(contentType);
|
|
13685
14353
|
}
|
|
13686
14354
|
const name = `${Date.now()}-${crypto4.randomBytes(4).toString("hex")}${ext}`;
|
|
13687
|
-
const filePath =
|
|
13688
|
-
await
|
|
14355
|
+
const filePath = path12.join(dir, name);
|
|
14356
|
+
await fs9.writeFile(filePath, buffer);
|
|
13689
14357
|
return { path: filePath };
|
|
13690
14358
|
};
|
|
13691
14359
|
}
|
|
@@ -13837,7 +14505,7 @@ async function handleWeixinMessageTurn(full, deps) {
|
|
|
13837
14505
|
continue;
|
|
13838
14506
|
}
|
|
13839
14507
|
try {
|
|
13840
|
-
const buffer = await
|
|
14508
|
+
const buffer = await fs9.readFile(filePath);
|
|
13841
14509
|
const mimeType = downloaded.fileMediaType ?? downloaded.voiceMediaType ?? defaultWeixinMime(descriptor.kind);
|
|
13842
14510
|
media.push(await mediaStore.saveMediaBuffer({
|
|
13843
14511
|
channelId: "weixin",
|
|
@@ -13851,7 +14519,7 @@ async function handleWeixinMessageTurn(full, deps) {
|
|
|
13851
14519
|
maxBytes: descriptor.kind === "image" ? DEFAULT_IMAGE_MAX_BYTES : DEFAULT_ATTACHMENT_MAX_BYTES
|
|
13852
14520
|
}));
|
|
13853
14521
|
} finally {
|
|
13854
|
-
await
|
|
14522
|
+
await fs9.rm(filePath, { force: true }).catch(() => {});
|
|
13855
14523
|
}
|
|
13856
14524
|
} catch (err) {
|
|
13857
14525
|
deps.errLog(`media download failed: ${String(err)}`);
|
|
@@ -14085,20 +14753,20 @@ var init_handle_weixin_message_turn = __esm(() => {
|
|
|
14085
14753
|
});
|
|
14086
14754
|
|
|
14087
14755
|
// src/weixin/storage/sync-buf.ts
|
|
14088
|
-
import
|
|
14089
|
-
import
|
|
14756
|
+
import fs10 from "node:fs";
|
|
14757
|
+
import path13 from "node:path";
|
|
14090
14758
|
function resolveAccountsDir2() {
|
|
14091
|
-
return
|
|
14759
|
+
return path13.join(resolveStateDir(), "openclaw-weixin", "accounts");
|
|
14092
14760
|
}
|
|
14093
14761
|
function getSyncBufFilePath(accountId) {
|
|
14094
|
-
return
|
|
14762
|
+
return path13.join(resolveAccountsDir2(), `${accountId}.sync.json`);
|
|
14095
14763
|
}
|
|
14096
14764
|
function getLegacySyncBufDefaultJsonPath() {
|
|
14097
|
-
return
|
|
14765
|
+
return path13.join(resolveStateDir(), "agents", "default", "sessions", ".openclaw-weixin-sync", "default.json");
|
|
14098
14766
|
}
|
|
14099
14767
|
function readSyncBufFile(filePath) {
|
|
14100
14768
|
try {
|
|
14101
|
-
const raw =
|
|
14769
|
+
const raw = fs10.readFileSync(filePath, "utf-8");
|
|
14102
14770
|
const data = JSON.parse(raw);
|
|
14103
14771
|
if (typeof data.get_updates_buf === "string") {
|
|
14104
14772
|
return data.get_updates_buf;
|
|
@@ -14110,10 +14778,10 @@ function loadGetUpdatesBuf(filePath) {
|
|
|
14110
14778
|
const value = readSyncBufFile(filePath);
|
|
14111
14779
|
if (value !== undefined)
|
|
14112
14780
|
return value;
|
|
14113
|
-
const accountId =
|
|
14781
|
+
const accountId = path13.basename(filePath, ".sync.json");
|
|
14114
14782
|
const rawId = deriveRawAccountId(accountId);
|
|
14115
14783
|
if (rawId) {
|
|
14116
|
-
const compatPath =
|
|
14784
|
+
const compatPath = path13.join(resolveAccountsDir2(), `${rawId}.sync.json`);
|
|
14117
14785
|
const compatValue = readSyncBufFile(compatPath);
|
|
14118
14786
|
if (compatValue !== undefined)
|
|
14119
14787
|
return compatValue;
|
|
@@ -14121,9 +14789,9 @@ function loadGetUpdatesBuf(filePath) {
|
|
|
14121
14789
|
return readSyncBufFile(getLegacySyncBufDefaultJsonPath());
|
|
14122
14790
|
}
|
|
14123
14791
|
function saveGetUpdatesBuf(filePath, getUpdatesBuf) {
|
|
14124
|
-
const dir =
|
|
14792
|
+
const dir = path13.dirname(filePath);
|
|
14125
14793
|
ensureDirSync(dir);
|
|
14126
|
-
|
|
14794
|
+
fs10.writeFileSync(filePath, JSON.stringify({ get_updates_buf: getUpdatesBuf }, null, 0), "utf-8");
|
|
14127
14795
|
}
|
|
14128
14796
|
var init_sync_buf = __esm(() => {
|
|
14129
14797
|
init_accounts();
|
|
@@ -14157,23 +14825,23 @@ function shouldFetchTypingConfig(textBody) {
|
|
|
14157
14825
|
}
|
|
14158
14826
|
async function monitorWeixinProvider(opts) {
|
|
14159
14827
|
const {
|
|
14160
|
-
baseUrl,
|
|
14161
|
-
cdnBaseUrl,
|
|
14162
|
-
token,
|
|
14163
|
-
accountId,
|
|
14164
14828
|
agent,
|
|
14165
14829
|
abortSignal,
|
|
14166
14830
|
longPollTimeoutMs
|
|
14167
14831
|
} = opts;
|
|
14832
|
+
let baseUrl = opts.baseUrl;
|
|
14833
|
+
let cdnBaseUrl = opts.cdnBaseUrl;
|
|
14834
|
+
let token = opts.token;
|
|
14835
|
+
let accountId = opts.accountId;
|
|
14168
14836
|
const log = opts.log ?? ((msg) => console.log(msg));
|
|
14169
14837
|
const errLog = (msg) => {
|
|
14170
14838
|
log(msg);
|
|
14171
14839
|
logger.error(msg);
|
|
14172
14840
|
};
|
|
14173
|
-
|
|
14841
|
+
let aLog = logger.withAccount(accountId);
|
|
14174
14842
|
log(`[weixin] monitor started (${baseUrl}, account=${accountId})`);
|
|
14175
14843
|
aLog.info(`Monitor started: baseUrl=${baseUrl}`);
|
|
14176
|
-
|
|
14844
|
+
let syncFilePath = getSyncBufFilePath(accountId);
|
|
14177
14845
|
const previousGetUpdatesBuf = loadGetUpdatesBuf(syncFilePath);
|
|
14178
14846
|
let getUpdatesBuf = previousGetUpdatesBuf ?? "";
|
|
14179
14847
|
if (previousGetUpdatesBuf) {
|
|
@@ -14181,7 +14849,7 @@ async function monitorWeixinProvider(opts) {
|
|
|
14181
14849
|
} else {
|
|
14182
14850
|
log(`[weixin] no previous sync buf, starting fresh`);
|
|
14183
14851
|
}
|
|
14184
|
-
|
|
14852
|
+
let configManager = new WeixinConfigManager({ baseUrl, token }, log);
|
|
14185
14853
|
const conversationExecutor = createConversationExecutor();
|
|
14186
14854
|
const seenMessageIds = new Set;
|
|
14187
14855
|
const messageIdOrder = [];
|
|
@@ -14204,11 +14872,37 @@ async function monitorWeixinProvider(opts) {
|
|
|
14204
14872
|
if (isApiError) {
|
|
14205
14873
|
const isSessionExpired = resp.errcode === SESSION_EXPIRED_ERRCODE || resp.ret === SESSION_EXPIRED_ERRCODE;
|
|
14206
14874
|
if (isSessionExpired) {
|
|
14875
|
+
const staleToken = token;
|
|
14876
|
+
const staleAccountId = accountId;
|
|
14877
|
+
errLog(`[weixin] session expired (errcode ${SESSION_EXPIRED_ERRCODE}), entering credential recovery. Please run \`weacpx login\` to re-login.`);
|
|
14207
14878
|
pauseSession(accountId);
|
|
14208
|
-
const pauseMs = getRemainingPauseMs(accountId);
|
|
14209
|
-
errLog(`[weixin] session expired (errcode ${SESSION_EXPIRED_ERRCODE}), pausing for ${Math.ceil(pauseMs / 60000)} min. Please run \`npx weixin-acp login\` to re-login.`);
|
|
14210
14879
|
consecutiveFailures = 0;
|
|
14211
|
-
await
|
|
14880
|
+
const recovered = await pollForFreshCredentials(staleAccountId, staleToken, log, abortSignal);
|
|
14881
|
+
if (recovered === null) {
|
|
14882
|
+
aLog.info("Monitor stopped (aborted during credential recovery)");
|
|
14883
|
+
return;
|
|
14884
|
+
}
|
|
14885
|
+
const oldAccountId = accountId;
|
|
14886
|
+
accountId = recovered.accountId;
|
|
14887
|
+
baseUrl = recovered.baseUrl;
|
|
14888
|
+
cdnBaseUrl = recovered.cdnBaseUrl;
|
|
14889
|
+
token = recovered.token;
|
|
14890
|
+
aLog = logger.withAccount(accountId);
|
|
14891
|
+
syncFilePath = getSyncBufFilePath(accountId);
|
|
14892
|
+
const previousBuf = loadGetUpdatesBuf(syncFilePath);
|
|
14893
|
+
getUpdatesBuf = previousBuf ?? "";
|
|
14894
|
+
configManager = new WeixinConfigManager({ baseUrl, token }, log);
|
|
14895
|
+
seenMessageIds.clear();
|
|
14896
|
+
messageIdOrder.length = 0;
|
|
14897
|
+
consecutiveFailures = 0;
|
|
14898
|
+
nextTimeoutMs = longPollTimeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS2;
|
|
14899
|
+
resetSessionPause(oldAccountId);
|
|
14900
|
+
resetSessionPause(accountId);
|
|
14901
|
+
if (oldAccountId !== accountId) {
|
|
14902
|
+
clearContextTokensForAccount(oldAccountId);
|
|
14903
|
+
restoreContextTokens(accountId);
|
|
14904
|
+
}
|
|
14905
|
+
log(`[weixin] credential recovered, resuming monitor with account=${accountId}`);
|
|
14212
14906
|
continue;
|
|
14213
14907
|
}
|
|
14214
14908
|
consecutiveFailures += 1;
|
|
@@ -14302,7 +14996,43 @@ function sleep(ms, signal) {
|
|
|
14302
14996
|
}, { once: true });
|
|
14303
14997
|
});
|
|
14304
14998
|
}
|
|
14305
|
-
|
|
14999
|
+
async function pollForFreshCredentials(staleAccountId, staleToken, log, abortSignal) {
|
|
15000
|
+
let attempt = 0;
|
|
15001
|
+
while (!abortSignal?.aborted) {
|
|
15002
|
+
attempt += 1;
|
|
15003
|
+
const currentAccount = resolveWeixinAccount(staleAccountId);
|
|
15004
|
+
if (currentAccount.token && currentAccount.token !== staleToken) {
|
|
15005
|
+
log(`[weixin] credential recovery: fresh token detected for account=${staleAccountId}`);
|
|
15006
|
+
return {
|
|
15007
|
+
accountId: currentAccount.accountId,
|
|
15008
|
+
baseUrl: currentAccount.baseUrl,
|
|
15009
|
+
cdnBaseUrl: currentAccount.cdnBaseUrl,
|
|
15010
|
+
token: currentAccount.token
|
|
15011
|
+
};
|
|
15012
|
+
}
|
|
15013
|
+
const ids = listWeixinAccountIds();
|
|
15014
|
+
for (const id of ids) {
|
|
15015
|
+
if (id === staleAccountId)
|
|
15016
|
+
continue;
|
|
15017
|
+
const account = resolveWeixinAccount(id);
|
|
15018
|
+
if (account.configured && account.token) {
|
|
15019
|
+
log(`[weixin] credential recovery: new account detected, switching to account=${id}`);
|
|
15020
|
+
return {
|
|
15021
|
+
accountId: account.accountId,
|
|
15022
|
+
baseUrl: account.baseUrl,
|
|
15023
|
+
cdnBaseUrl: account.cdnBaseUrl,
|
|
15024
|
+
token: account.token
|
|
15025
|
+
};
|
|
15026
|
+
}
|
|
15027
|
+
}
|
|
15028
|
+
if (attempt % 10 === 0) {
|
|
15029
|
+
log(`[weixin] credential recovery: still waiting for fresh credentials (checked ${attempt} times)`);
|
|
15030
|
+
}
|
|
15031
|
+
await sleep(CREDENTIAL_RECOVERY_POLL_INTERVAL_MS, abortSignal);
|
|
15032
|
+
}
|
|
15033
|
+
return null;
|
|
15034
|
+
}
|
|
15035
|
+
var DEFAULT_LONG_POLL_TIMEOUT_MS2 = 35000, MAX_CONSECUTIVE_FAILURES = 3, BACKOFF_DELAY_MS = 30000, RETRY_DELAY_MS = 2000, CREDENTIAL_RECOVERY_POLL_INTERVAL_MS = 30000;
|
|
14306
15036
|
var init_monitor = __esm(() => {
|
|
14307
15037
|
init_api();
|
|
14308
15038
|
init_config_cache();
|
|
@@ -14311,6 +15041,9 @@ var init_monitor = __esm(() => {
|
|
|
14311
15041
|
init_types2();
|
|
14312
15042
|
init_sync_buf();
|
|
14313
15043
|
init_logger();
|
|
15044
|
+
init_accounts();
|
|
15045
|
+
init_session_guard();
|
|
15046
|
+
init_inbound();
|
|
14314
15047
|
});
|
|
14315
15048
|
|
|
14316
15049
|
// src/weixin/bot.ts
|
|
@@ -14369,6 +15102,8 @@ function logout(opts) {
|
|
|
14369
15102
|
log("当前没有已登录的账号");
|
|
14370
15103
|
return;
|
|
14371
15104
|
}
|
|
15105
|
+
for (const id of ids)
|
|
15106
|
+
clearContextTokensForAccount(id);
|
|
14372
15107
|
clearAllWeixinAccounts();
|
|
14373
15108
|
log("✅ 已退出登录");
|
|
14374
15109
|
}
|
|
@@ -14396,6 +15131,7 @@ async function start(agent, opts) {
|
|
|
14396
15131
|
if (!account.configured) {
|
|
14397
15132
|
throw new Error(`账号 ${accountId} 未配置 (缺少 token),请先运行 login`);
|
|
14398
15133
|
}
|
|
15134
|
+
restoreContextTokens(account.accountId);
|
|
14399
15135
|
log(`[weixin] 启动 bot, account=${account.accountId}`);
|
|
14400
15136
|
await monitorWeixinProvider({
|
|
14401
15137
|
baseUrl: account.baseUrl,
|
|
@@ -14421,6 +15157,7 @@ async function start(agent, opts) {
|
|
|
14421
15157
|
var init_bot = __esm(() => {
|
|
14422
15158
|
init_accounts();
|
|
14423
15159
|
init_login_qr();
|
|
15160
|
+
init_inbound();
|
|
14424
15161
|
init_monitor();
|
|
14425
15162
|
});
|
|
14426
15163
|
|
|
@@ -14751,9 +15488,9 @@ function createWeixinConsumerLock(options = {}) {
|
|
|
14751
15488
|
}
|
|
14752
15489
|
};
|
|
14753
15490
|
}
|
|
14754
|
-
async function loadLockMetadata(
|
|
15491
|
+
async function loadLockMetadata(path14) {
|
|
14755
15492
|
try {
|
|
14756
|
-
const raw = await readFile6(
|
|
15493
|
+
const raw = await readFile6(path14, "utf8");
|
|
14757
15494
|
const parsed = JSON.parse(raw);
|
|
14758
15495
|
if (!parsed || typeof parsed.pid !== "number" || !parsed.mode || !parsed.configPath || !parsed.statePath) {
|
|
14759
15496
|
return null;
|
|
@@ -15823,6 +16560,7 @@ function parseCommand(input) {
|
|
|
15823
16560
|
if (command === "/workspace" && parts[1] === "new" && parts[2]) {
|
|
15824
16561
|
const name = parts[2];
|
|
15825
16562
|
let cwd = "";
|
|
16563
|
+
let raw = false;
|
|
15826
16564
|
let invalid = false;
|
|
15827
16565
|
for (let index = 3;index < parts.length; index += 1) {
|
|
15828
16566
|
if (parts[index] === "--cwd" || parts[index] === "-d") {
|
|
@@ -15834,11 +16572,19 @@ function parseCommand(input) {
|
|
|
15834
16572
|
index += 1;
|
|
15835
16573
|
continue;
|
|
15836
16574
|
}
|
|
16575
|
+
if (parts[index] === "--raw") {
|
|
16576
|
+
if (raw) {
|
|
16577
|
+
invalid = true;
|
|
16578
|
+
break;
|
|
16579
|
+
}
|
|
16580
|
+
raw = true;
|
|
16581
|
+
continue;
|
|
16582
|
+
}
|
|
15837
16583
|
invalid = true;
|
|
15838
16584
|
break;
|
|
15839
16585
|
}
|
|
15840
16586
|
if (!invalid && name.trim().length > 0 && cwd.trim().length > 0) {
|
|
15841
|
-
return { kind: "workspace.new", name, cwd };
|
|
16587
|
+
return { kind: "workspace.new", name, cwd, ...raw ? { raw: true } : {} };
|
|
15842
16588
|
}
|
|
15843
16589
|
}
|
|
15844
16590
|
if (command === "/workspace" && parts[1] === "rm" && parts[2]) {
|
|
@@ -16298,26 +17044,26 @@ var init_permission_handler = __esm(() => {
|
|
|
16298
17044
|
|
|
16299
17045
|
// src/commands/handlers/config-handler.ts
|
|
16300
17046
|
function handleConfigShow(context) {
|
|
16301
|
-
const lines = ["支持修改的配置字段:", ...SUPPORTED_CONFIG_PATHS.map((
|
|
16302
|
-
lines.push("", "兼容旧配置:", ...LEGACY_CONFIG_PATHS.map((
|
|
17047
|
+
const lines = ["支持修改的配置字段:", ...SUPPORTED_CONFIG_PATHS.map((path14) => `- ${path14}`)];
|
|
17048
|
+
lines.push("", "兼容旧配置:", ...LEGACY_CONFIG_PATHS.map((path14) => `- ${path14}`));
|
|
16303
17049
|
if (context.config) {
|
|
16304
17050
|
lines.push("", "示例:", "- /config set channel.replyMode final", "- /config set logging.level debug");
|
|
16305
17051
|
}
|
|
16306
17052
|
return { text: lines.join(`
|
|
16307
17053
|
`) };
|
|
16308
17054
|
}
|
|
16309
|
-
async function handleConfigSet(context,
|
|
17055
|
+
async function handleConfigSet(context, path14, rawValue) {
|
|
16310
17056
|
if (!context.config || !context.configStore) {
|
|
16311
17057
|
return { text: "当前没有加载可写入的配置。" };
|
|
16312
17058
|
}
|
|
16313
17059
|
const previous = cloneAppConfig(context.config);
|
|
16314
17060
|
const updated = cloneAppConfig(context.config);
|
|
16315
|
-
const result = applySupportedConfigUpdate(updated,
|
|
17061
|
+
const result = applySupportedConfigUpdate(updated, path14, rawValue);
|
|
16316
17062
|
if ("error" in result) {
|
|
16317
17063
|
return { text: result.error };
|
|
16318
17064
|
}
|
|
16319
17065
|
await context.configStore.save(updated);
|
|
16320
|
-
if (
|
|
17066
|
+
if (path14 === "transport.permissionMode" || path14 === "transport.nonInteractivePermissions" || path14 === "transport.permissionPolicy") {
|
|
16321
17067
|
try {
|
|
16322
17068
|
await context.transport.updatePermissionPolicy?.(updated.transport);
|
|
16323
17069
|
} catch (error2) {
|
|
@@ -16327,10 +17073,10 @@ async function handleConfigSet(context, path13, rawValue) {
|
|
|
16327
17073
|
}
|
|
16328
17074
|
}
|
|
16329
17075
|
context.replaceConfig(updated);
|
|
16330
|
-
return { text: `配置已更新:${
|
|
17076
|
+
return { text: `配置已更新:${path14} = ${result.renderedValue}` };
|
|
16331
17077
|
}
|
|
16332
|
-
function applySupportedConfigUpdate(config2,
|
|
16333
|
-
switch (
|
|
17078
|
+
function applySupportedConfigUpdate(config2, path14, rawValue) {
|
|
17079
|
+
switch (path14) {
|
|
16334
17080
|
case "transport.type": {
|
|
16335
17081
|
const parsed = parseEnum(rawValue, ["acpx-cli", "acpx-bridge"]);
|
|
16336
17082
|
if (!parsed)
|
|
@@ -16418,18 +17164,18 @@ function applySupportedConfigUpdate(config2, path13, rawValue) {
|
|
|
16418
17164
|
};
|
|
16419
17165
|
}
|
|
16420
17166
|
}
|
|
16421
|
-
const agentMatch =
|
|
17167
|
+
const agentMatch = path14.match(/^agents\.([^.]+)\.(driver|command)$/);
|
|
16422
17168
|
if (agentMatch) {
|
|
16423
17169
|
const [, name, field] = agentMatch;
|
|
16424
17170
|
if (!name || !field) {
|
|
16425
|
-
return { error: `不支持修改这个配置路径:${
|
|
17171
|
+
return { error: `不支持修改这个配置路径:${path14}` };
|
|
16426
17172
|
}
|
|
16427
17173
|
const agent = config2.agents[name];
|
|
16428
17174
|
if (!agent) {
|
|
16429
17175
|
return { error: `Agent「${name}」不存在,请先创建。` };
|
|
16430
17176
|
}
|
|
16431
17177
|
if (!rawValue.trim()) {
|
|
16432
|
-
return { error: `${
|
|
17178
|
+
return { error: `${path14} 不能为空。` };
|
|
16433
17179
|
}
|
|
16434
17180
|
if (field === "driver") {
|
|
16435
17181
|
agent.driver = rawValue;
|
|
@@ -16438,18 +17184,18 @@ function applySupportedConfigUpdate(config2, path13, rawValue) {
|
|
|
16438
17184
|
}
|
|
16439
17185
|
return { renderedValue: rawValue };
|
|
16440
17186
|
}
|
|
16441
|
-
const workspaceMatch =
|
|
17187
|
+
const workspaceMatch = path14.match(/^workspaces\.([^.]+)\.(cwd|description)$/);
|
|
16442
17188
|
if (workspaceMatch) {
|
|
16443
17189
|
const [, name, field] = workspaceMatch;
|
|
16444
17190
|
if (!name || !field) {
|
|
16445
|
-
return { error: `不支持修改这个配置路径:${
|
|
17191
|
+
return { error: `不支持修改这个配置路径:${path14}` };
|
|
16446
17192
|
}
|
|
16447
17193
|
const workspace = config2.workspaces[name];
|
|
16448
17194
|
if (!workspace) {
|
|
16449
17195
|
return { error: `工作区「${name}」不存在,请先创建。` };
|
|
16450
17196
|
}
|
|
16451
17197
|
if (!rawValue.trim()) {
|
|
16452
|
-
return { error: `${
|
|
17198
|
+
return { error: `${path14} 不能为空。` };
|
|
16453
17199
|
}
|
|
16454
17200
|
if (field === "cwd") {
|
|
16455
17201
|
workspace.cwd = rawValue;
|
|
@@ -16458,15 +17204,15 @@ function applySupportedConfigUpdate(config2, path13, rawValue) {
|
|
|
16458
17204
|
}
|
|
16459
17205
|
return { renderedValue: rawValue };
|
|
16460
17206
|
}
|
|
16461
|
-
return { error: `不支持修改这个配置路径:${
|
|
17207
|
+
return { error: `不支持修改这个配置路径:${path14}` };
|
|
16462
17208
|
}
|
|
16463
17209
|
function parseEnum(value, allowed) {
|
|
16464
17210
|
return allowed.includes(value) ? value : null;
|
|
16465
17211
|
}
|
|
16466
|
-
function parsePositiveNumber(rawValue,
|
|
17212
|
+
function parsePositiveNumber(rawValue, path14) {
|
|
16467
17213
|
const value = Number(rawValue);
|
|
16468
17214
|
if (!Number.isFinite(value) || value <= 0) {
|
|
16469
|
-
return { error: `${
|
|
17215
|
+
return { error: `${path14} 必须是正数。` };
|
|
16470
17216
|
}
|
|
16471
17217
|
return { value };
|
|
16472
17218
|
}
|
|
@@ -16901,7 +17647,7 @@ async function handleSessionAttach(context, chatKey, alias, agent, workspace, tr
|
|
|
16901
17647
|
return {
|
|
16902
17648
|
text: [
|
|
16903
17649
|
"没有找到可绑定的已有会话。",
|
|
16904
|
-
`请确认会话名是否正确,然后重新执行:/session attach ${alias} --agent ${agent} --ws ${workspace} --name <会话名>`
|
|
17650
|
+
`请确认会话名是否正确,然后重新执行:/session attach ${alias} --agent ${agent} --ws ${quoteWorkspaceNameIfNeeded(workspace)} --name <会话名>`
|
|
16905
17651
|
].join(`
|
|
16906
17652
|
`)
|
|
16907
17653
|
};
|
|
@@ -17238,6 +17984,7 @@ var NO_CURRENT_SESSION_TEXT = "当前还没有选中的会话。请先执行 /se
|
|
|
17238
17984
|
var init_session_handler = __esm(() => {
|
|
17239
17985
|
init_build_coordinator_prompt();
|
|
17240
17986
|
init_channel_scope();
|
|
17987
|
+
init_workspace_name();
|
|
17241
17988
|
sessionHelp = {
|
|
17242
17989
|
topic: "session",
|
|
17243
17990
|
aliases: ["ss", "sessions"],
|
|
@@ -17885,7 +18632,7 @@ var init_agent_handler = __esm(() => {
|
|
|
17885
18632
|
function handleWorkspaces(context) {
|
|
17886
18633
|
return { text: context.config ? renderWorkspaces(context.config) : "No config loaded." };
|
|
17887
18634
|
}
|
|
17888
|
-
async function handleWorkspaceCreate(context, workspaceName, cwd) {
|
|
18635
|
+
async function handleWorkspaceCreate(context, workspaceName, cwd, options = {}) {
|
|
17889
18636
|
if (!context.config || !context.configStore) {
|
|
17890
18637
|
return { text: "当前没有加载可写入的配置。" };
|
|
17891
18638
|
}
|
|
@@ -17893,9 +18640,18 @@ async function handleWorkspaceCreate(context, workspaceName, cwd) {
|
|
|
17893
18640
|
if (!await pathExists(normalizedCwd)) {
|
|
17894
18641
|
return { text: `工作区路径不存在:${cwd}` };
|
|
17895
18642
|
}
|
|
17896
|
-
|
|
18643
|
+
let name = workspaceName;
|
|
18644
|
+
let notice;
|
|
18645
|
+
if (!options.raw && !isWorkspaceNameValid(workspaceName)) {
|
|
18646
|
+
const base = sanitizeWorkspaceName(workspaceName);
|
|
18647
|
+
name = allocateWorkspaceName(base, context.config.workspaces);
|
|
18648
|
+
notice = `名称 ${JSON.stringify(workspaceName)} 含有特殊字符,已保存为「${name}」。如需保留原名请加 --raw。`;
|
|
18649
|
+
}
|
|
18650
|
+
const updated = await context.configStore.upsertWorkspace(name, normalizedCwd);
|
|
17897
18651
|
context.replaceConfig(updated);
|
|
17898
|
-
|
|
18652
|
+
const savedLine = `工作区「${name}」已保存`;
|
|
18653
|
+
return { text: notice ? `${notice}
|
|
18654
|
+
${savedLine}` : savedLine };
|
|
17899
18655
|
}
|
|
17900
18656
|
async function handleWorkspaceRemove(context, workspaceName) {
|
|
17901
18657
|
if (!context.config || !context.configStore) {
|
|
@@ -17907,6 +18663,7 @@ async function handleWorkspaceRemove(context, workspaceName) {
|
|
|
17907
18663
|
}
|
|
17908
18664
|
var workspaceHelp;
|
|
17909
18665
|
var init_workspace_handler = __esm(() => {
|
|
18666
|
+
init_workspace_name();
|
|
17910
18667
|
init_workspace_path();
|
|
17911
18668
|
workspaceHelp = {
|
|
17912
18669
|
topic: "workspace",
|
|
@@ -17915,7 +18672,7 @@ var init_workspace_handler = __esm(() => {
|
|
|
17915
18672
|
commands: [
|
|
17916
18673
|
{ usage: "/workspaces", description: "查看当前已注册的工作区" },
|
|
17917
18674
|
{ usage: "/workspace 或 /ws", description: "查看工作区列表" },
|
|
17918
|
-
{ usage: "/ws new <name> -d <path>", description: "
|
|
18675
|
+
{ usage: "/ws new <name> -d <path> [--raw]", description: "添加工作区;含特殊字符的名称会被自动规范化,--raw 保留原名" },
|
|
17919
18676
|
{ usage: "/workspace rm <name>", description: "删除工作区" }
|
|
17920
18677
|
],
|
|
17921
18678
|
examples: ['/ws new backend -d "/tmp/backend"', "/workspace rm backend"]
|
|
@@ -18152,7 +18909,7 @@ async function resolveShortcutWorkspace(context, target) {
|
|
|
18152
18909
|
reused: true
|
|
18153
18910
|
};
|
|
18154
18911
|
}
|
|
18155
|
-
const workspaceName = allocateWorkspaceName(
|
|
18912
|
+
const workspaceName = allocateWorkspaceName(sanitizeWorkspaceName(basenameForWorkspacePath(cwd)), context.config?.workspaces ?? {});
|
|
18156
18913
|
const updated = await context.configStore.upsertWorkspace(workspaceName, cwd);
|
|
18157
18914
|
context.replaceConfig(updated);
|
|
18158
18915
|
return {
|
|
@@ -18161,16 +18918,6 @@ async function resolveShortcutWorkspace(context, target) {
|
|
|
18161
18918
|
reused: false
|
|
18162
18919
|
};
|
|
18163
18920
|
}
|
|
18164
|
-
function allocateWorkspaceName(context, baseName) {
|
|
18165
|
-
if (!context.config?.workspaces[baseName]) {
|
|
18166
|
-
return baseName;
|
|
18167
|
-
}
|
|
18168
|
-
let suffix = 2;
|
|
18169
|
-
while (context.config.workspaces[`${baseName}-${suffix}`]) {
|
|
18170
|
-
suffix += 1;
|
|
18171
|
-
}
|
|
18172
|
-
return `${baseName}-${suffix}`;
|
|
18173
|
-
}
|
|
18174
18921
|
async function allocateUniqueSessionAlias(context, baseAlias, chatKey) {
|
|
18175
18922
|
if (!await hasLogicalSession(context, baseAlias, chatKey)) {
|
|
18176
18923
|
return baseAlias;
|
|
@@ -18196,6 +18943,7 @@ function renderShortcutSessionCreationError(workspace, alias) {
|
|
|
18196
18943
|
};
|
|
18197
18944
|
}
|
|
18198
18945
|
var init_session_shortcut_handler = __esm(() => {
|
|
18946
|
+
init_workspace_name();
|
|
18199
18947
|
init_workspace_path();
|
|
18200
18948
|
init_errors();
|
|
18201
18949
|
init_channel_scope();
|
|
@@ -18208,8 +18956,8 @@ function renderTransportError(session, error2) {
|
|
|
18208
18956
|
return {
|
|
18209
18957
|
text: [
|
|
18210
18958
|
`当前会话「${session.alias}」暂时不可用。`,
|
|
18211
|
-
`请先在微信里重新执行:/session new ${session.alias} --agent ${session.agent} --ws ${session.workspace}`,
|
|
18212
|
-
`如果你要绑定一个已有会话,再执行:/session attach ${session.alias} --agent ${session.agent} --ws ${session.workspace} --name <会话名>`
|
|
18959
|
+
`请先在微信里重新执行:/session new ${session.alias} --agent ${session.agent} --ws ${quoteWorkspaceNameIfNeeded(session.workspace)}`,
|
|
18960
|
+
`如果你要绑定一个已有会话,再执行:/session attach ${session.alias} --agent ${session.agent} --ws ${quoteWorkspaceNameIfNeeded(session.workspace)} --name <会话名>`
|
|
18213
18961
|
].join(`
|
|
18214
18962
|
`)
|
|
18215
18963
|
};
|
|
@@ -18271,7 +19019,7 @@ function renderSessionCreationFailure(session, detail) {
|
|
|
18271
19019
|
text: [
|
|
18272
19020
|
"会话创建失败。",
|
|
18273
19021
|
`错误信息:${summarizeTransportError(detail)}`,
|
|
18274
|
-
`如果你要先绑定一个已有会话,可以执行:/session attach ${session.alias} --agent ${session.agent} --ws ${session.workspace} --name <会话名>`
|
|
19022
|
+
`如果你要先绑定一个已有会话,可以执行:/session attach ${session.alias} --agent ${session.agent} --ws ${quoteWorkspaceNameIfNeeded(session.workspace)} --name <会话名>`
|
|
18275
19023
|
].join(`
|
|
18276
19024
|
`)
|
|
18277
19025
|
};
|
|
@@ -18290,6 +19038,7 @@ async function tryRecoverMissingSession(ops, session, error2) {
|
|
|
18290
19038
|
}
|
|
18291
19039
|
var init_session_recovery_handler = __esm(() => {
|
|
18292
19040
|
init_errors();
|
|
19041
|
+
init_workspace_name();
|
|
18293
19042
|
});
|
|
18294
19043
|
|
|
18295
19044
|
// src/recovery/auto-install-optional-dep.ts
|
|
@@ -18415,10 +19164,10 @@ ${err.message}`, reason: "spawn" });
|
|
|
18415
19164
|
const dir = join10(homedir6(), ".weacpx", "logs");
|
|
18416
19165
|
await mkdir10(dir, { recursive: true });
|
|
18417
19166
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "").replace(/-/g, "");
|
|
18418
|
-
const
|
|
18419
|
-
const stream = createWriteStream(
|
|
19167
|
+
const path14 = join10(dir, `auto-install-${timestamp}.log`);
|
|
19168
|
+
const stream = createWriteStream(path14, { flags: "a" });
|
|
18420
19169
|
return {
|
|
18421
|
-
path:
|
|
19170
|
+
path: path14,
|
|
18422
19171
|
append: async (chunk) => {
|
|
18423
19172
|
await new Promise((resolve3, reject) => stream.write(chunk, (err) => err ? reject(err) : resolve3()));
|
|
18424
19173
|
},
|
|
@@ -18501,9 +19250,9 @@ function isUnder(child, parent) {
|
|
|
18501
19250
|
const p = parent.replace(/[\\/]+$/, "");
|
|
18502
19251
|
return c === p || c.startsWith(p + "/") || c.startsWith(p + "\\");
|
|
18503
19252
|
}
|
|
18504
|
-
async function defaultFsExists(
|
|
19253
|
+
async function defaultFsExists(path14) {
|
|
18505
19254
|
try {
|
|
18506
|
-
await access3(
|
|
19255
|
+
await access3(path14);
|
|
18507
19256
|
return true;
|
|
18508
19257
|
} catch {
|
|
18509
19258
|
return false;
|
|
@@ -18731,7 +19480,7 @@ class CommandRouter {
|
|
|
18731
19480
|
case "workspaces":
|
|
18732
19481
|
return handleWorkspaces(this.createHandlerContext());
|
|
18733
19482
|
case "workspace.new":
|
|
18734
|
-
return await handleWorkspaceCreate(this.createHandlerContext(), command.name, command.cwd);
|
|
19483
|
+
return await handleWorkspaceCreate(this.createHandlerContext(), command.name, command.cwd, command.raw ? { raw: true } : {});
|
|
18735
19484
|
case "workspace.rm":
|
|
18736
19485
|
return await handleWorkspaceRemove(this.createHandlerContext(), command.name);
|
|
18737
19486
|
case "sessions":
|
|
@@ -19193,7 +19942,7 @@ function resolveAcpxCommandMetadata(options = {}) {
|
|
|
19193
19942
|
}
|
|
19194
19943
|
const platform = options.platform ?? process.platform;
|
|
19195
19944
|
const resolvePackageJson = options.resolvePackageJson ?? ((id) => require3.resolve(id));
|
|
19196
|
-
const readPackageJson = options.readPackageJson ?? ((
|
|
19945
|
+
const readPackageJson = options.readPackageJson ?? ((path14) => JSON.parse(readFileSync(path14, "utf8")));
|
|
19197
19946
|
try {
|
|
19198
19947
|
const packageJsonPath = resolvePackageJson("acpx/package.json");
|
|
19199
19948
|
const pkg = readPackageJson(packageJsonPath);
|
|
@@ -19570,8 +20319,8 @@ class OrchestrationServer {
|
|
|
19570
20319
|
if (this.endpoint.kind !== "unix") {
|
|
19571
20320
|
return;
|
|
19572
20321
|
}
|
|
19573
|
-
const removeFile = this.deps.removeFile ?? (async (
|
|
19574
|
-
await rm7(
|
|
20322
|
+
const removeFile = this.deps.removeFile ?? (async (path14) => {
|
|
20323
|
+
await rm7(path14, { force: true });
|
|
19575
20324
|
});
|
|
19576
20325
|
await removeFile(this.endpoint.path);
|
|
19577
20326
|
}
|
|
@@ -19732,9 +20481,9 @@ function requireTaskQuestions(params, key) {
|
|
|
19732
20481
|
};
|
|
19733
20482
|
});
|
|
19734
20483
|
}
|
|
19735
|
-
async function canConnectToEndpoint(
|
|
20484
|
+
async function canConnectToEndpoint(path14) {
|
|
19736
20485
|
return await new Promise((resolve3) => {
|
|
19737
|
-
const socket = createConnection2(
|
|
20486
|
+
const socket = createConnection2(path14);
|
|
19738
20487
|
let settled = false;
|
|
19739
20488
|
const finish = (result) => {
|
|
19740
20489
|
if (settled) {
|
|
@@ -19755,7 +20504,7 @@ async function canConnectToEndpoint(path13) {
|
|
|
19755
20504
|
});
|
|
19756
20505
|
});
|
|
19757
20506
|
}
|
|
19758
|
-
async function listen(server,
|
|
20507
|
+
async function listen(server, path14) {
|
|
19759
20508
|
await new Promise((resolve3, reject) => {
|
|
19760
20509
|
const onError = (error2) => {
|
|
19761
20510
|
server.off("listening", onListening);
|
|
@@ -19767,7 +20516,7 @@ async function listen(server, path13) {
|
|
|
19767
20516
|
};
|
|
19768
20517
|
server.once("error", onError);
|
|
19769
20518
|
server.once("listening", onListening);
|
|
19770
|
-
server.listen(
|
|
20519
|
+
server.listen(path14);
|
|
19771
20520
|
});
|
|
19772
20521
|
}
|
|
19773
20522
|
function isServerNotRunningError(error2) {
|
|
@@ -19891,6 +20640,7 @@ class OrchestrationService {
|
|
|
19891
20640
|
stateMutex;
|
|
19892
20641
|
pendingWorkerSessions = new Map;
|
|
19893
20642
|
pendingLogicalTransportSessions = new Map;
|
|
20643
|
+
pendingParallelStarts = new Map;
|
|
19894
20644
|
constructor(deps) {
|
|
19895
20645
|
this.deps = deps;
|
|
19896
20646
|
this.stateMutex = deps.stateMutex ?? new AsyncMutex;
|
|
@@ -20055,84 +20805,139 @@ class OrchestrationService {
|
|
|
20055
20805
|
const normalizedGroupId = this.normalizeGroupId(input.groupId);
|
|
20056
20806
|
const taskId = this.deps.createId();
|
|
20057
20807
|
const workerSession = await this.resolveWorkerSession(input);
|
|
20058
|
-
|
|
20059
|
-
|
|
20060
|
-
let prepared;
|
|
20061
|
-
try {
|
|
20062
|
-
ensuredWorkerSession = await this.ensureReservedWorkerSession({
|
|
20063
|
-
workerSession,
|
|
20064
|
-
sourceHandle: input.sourceHandle,
|
|
20065
|
-
sourceKind: input.sourceKind,
|
|
20066
|
-
coordinatorSession: input.coordinatorSession,
|
|
20067
|
-
workspace: input.workspace,
|
|
20068
|
-
...input.cwd ? { cwd: input.cwd } : {},
|
|
20069
|
-
targetAgent: input.targetAgent,
|
|
20070
|
-
role
|
|
20071
|
-
});
|
|
20072
|
-
prepared = await this.mutate(async () => {
|
|
20808
|
+
if (input.parallel) {
|
|
20809
|
+
const queuedResult = await this.mutate(async () => {
|
|
20073
20810
|
const state = await this.deps.loadState();
|
|
20074
|
-
|
|
20075
|
-
|
|
20076
|
-
|
|
20811
|
+
if (this.canStartParallelTask(state, input.targetAgent)) {
|
|
20812
|
+
this.pendingParallelStarts.set(input.targetAgent, (this.pendingParallelStarts.get(input.targetAgent) ?? 0) + 1);
|
|
20813
|
+
return null;
|
|
20077
20814
|
}
|
|
20078
|
-
const
|
|
20815
|
+
const now = this.deps.now().toISOString();
|
|
20816
|
+
const queuedTask = {
|
|
20079
20817
|
taskId,
|
|
20080
20818
|
sourceHandle: input.sourceHandle,
|
|
20081
20819
|
sourceKind: input.sourceKind,
|
|
20082
20820
|
coordinatorSession: input.coordinatorSession,
|
|
20083
|
-
workerSession
|
|
20821
|
+
workerSession,
|
|
20084
20822
|
workspace: input.workspace,
|
|
20085
20823
|
...input.cwd ? { cwd: input.cwd } : {},
|
|
20086
20824
|
targetAgent: input.targetAgent,
|
|
20087
20825
|
...role ? { role } : {},
|
|
20088
20826
|
...normalizedGroupId ? { groupId: normalizedGroupId } : {},
|
|
20089
20827
|
task: input.task,
|
|
20090
|
-
status: "
|
|
20828
|
+
status: "queued",
|
|
20829
|
+
ephemeralWorkerSession: true,
|
|
20091
20830
|
summary: "",
|
|
20092
20831
|
resultText: "",
|
|
20093
20832
|
createdAt: now,
|
|
20094
20833
|
updatedAt: now,
|
|
20095
20834
|
eventSeq: 1,
|
|
20096
|
-
events: [{ seq: 1, at: now, type: "created", status: "
|
|
20835
|
+
events: [{ seq: 1, at: now, type: "created", status: "queued", message: "Task queued at parallel capacity" }],
|
|
20097
20836
|
...input.chatKey ? { chatKey: input.chatKey } : {},
|
|
20098
20837
|
...input.replyContextToken ? { replyContextToken: input.replyContextToken } : {},
|
|
20099
20838
|
...input.accountId ? { accountId: input.accountId } : {}
|
|
20100
20839
|
};
|
|
20101
|
-
|
|
20102
|
-
|
|
20103
|
-
|
|
20104
|
-
|
|
20105
|
-
|
|
20106
|
-
|
|
20107
|
-
|
|
20108
|
-
|
|
20109
|
-
|
|
20110
|
-
|
|
20111
|
-
|
|
20112
|
-
|
|
20113
|
-
this.
|
|
20114
|
-
|
|
20115
|
-
|
|
20116
|
-
|
|
20840
|
+
state.orchestration.tasks[taskId] = queuedTask;
|
|
20841
|
+
await this.deps.saveState(state);
|
|
20842
|
+
return { taskId, status: "queued", workerSession };
|
|
20843
|
+
});
|
|
20844
|
+
if (queuedResult) {
|
|
20845
|
+
this.logEvent("orchestration.task.queued", "parallel task queued at capacity", { taskId, targetAgent: input.targetAgent });
|
|
20846
|
+
return queuedResult;
|
|
20847
|
+
}
|
|
20848
|
+
}
|
|
20849
|
+
const releasePendingParallelStart = input.parallel ? () => {
|
|
20850
|
+
const count = this.pendingParallelStarts.get(input.targetAgent) ?? 0;
|
|
20851
|
+
if (count <= 1) {
|
|
20852
|
+
this.pendingParallelStarts.delete(input.targetAgent);
|
|
20853
|
+
} else {
|
|
20854
|
+
this.pendingParallelStarts.set(input.targetAgent, count - 1);
|
|
20855
|
+
}
|
|
20856
|
+
} : undefined;
|
|
20857
|
+
let ensuredWorkerSession = workerSession;
|
|
20858
|
+
let prepared;
|
|
20859
|
+
const releaseWorkerReservation = await this.reserveProposedWorkerSession(workerSession);
|
|
20860
|
+
try {
|
|
20861
|
+
try {
|
|
20862
|
+
ensuredWorkerSession = await this.ensureReservedWorkerSession({
|
|
20863
|
+
workerSession,
|
|
20864
|
+
sourceHandle: input.sourceHandle,
|
|
20865
|
+
sourceKind: input.sourceKind,
|
|
20117
20866
|
coordinatorSession: input.coordinatorSession,
|
|
20118
20867
|
workspace: input.workspace,
|
|
20119
20868
|
...input.cwd ? { cwd: input.cwd } : {},
|
|
20120
20869
|
targetAgent: input.targetAgent,
|
|
20121
20870
|
role
|
|
20122
|
-
};
|
|
20123
|
-
await this.
|
|
20124
|
-
|
|
20125
|
-
|
|
20126
|
-
|
|
20127
|
-
|
|
20128
|
-
|
|
20129
|
-
|
|
20130
|
-
|
|
20131
|
-
|
|
20871
|
+
});
|
|
20872
|
+
prepared = await this.mutate(async () => {
|
|
20873
|
+
const state = await this.deps.loadState();
|
|
20874
|
+
const now = this.deps.now().toISOString();
|
|
20875
|
+
if (normalizedGroupId) {
|
|
20876
|
+
this.assertGroupOwnership(this.ensureGroups(state)[normalizedGroupId], normalizedGroupId, input.coordinatorSession);
|
|
20877
|
+
}
|
|
20878
|
+
const task = {
|
|
20879
|
+
taskId,
|
|
20880
|
+
sourceHandle: input.sourceHandle,
|
|
20881
|
+
sourceKind: input.sourceKind,
|
|
20882
|
+
coordinatorSession: input.coordinatorSession,
|
|
20883
|
+
workerSession: ensuredWorkerSession,
|
|
20884
|
+
workspace: input.workspace,
|
|
20885
|
+
...input.cwd ? { cwd: input.cwd } : {},
|
|
20886
|
+
targetAgent: input.targetAgent,
|
|
20887
|
+
...role ? { role } : {},
|
|
20888
|
+
...normalizedGroupId ? { groupId: normalizedGroupId } : {},
|
|
20889
|
+
task: input.task,
|
|
20890
|
+
status: "running",
|
|
20891
|
+
summary: "",
|
|
20892
|
+
resultText: "",
|
|
20893
|
+
createdAt: now,
|
|
20894
|
+
updatedAt: now,
|
|
20895
|
+
eventSeq: 1,
|
|
20896
|
+
events: [{ seq: 1, at: now, type: "created", status: "running", message: "Task created" }],
|
|
20897
|
+
...input.chatKey ? { chatKey: input.chatKey } : {},
|
|
20898
|
+
...input.replyContextToken ? { replyContextToken: input.replyContextToken } : {},
|
|
20899
|
+
...input.accountId ? { accountId: input.accountId } : {},
|
|
20900
|
+
...input.parallel ? { ephemeralWorkerSession: true } : {}
|
|
20901
|
+
};
|
|
20902
|
+
let previousGroup;
|
|
20903
|
+
if (normalizedGroupId) {
|
|
20904
|
+
const group = this.ensureGroups(state)[normalizedGroupId];
|
|
20905
|
+
previousGroup = { ...group };
|
|
20906
|
+
group.updatedAt = now;
|
|
20907
|
+
group.coordinatorInjectedAt = undefined;
|
|
20908
|
+
group.injectionPending = undefined;
|
|
20909
|
+
group.injectionAppliedAt = undefined;
|
|
20910
|
+
group.lastInjectionError = undefined;
|
|
20911
|
+
}
|
|
20912
|
+
const previousBinding = state.orchestration.workerBindings[ensuredWorkerSession];
|
|
20913
|
+
this.assertWorkerSessionDoesNotConflictExternalCoordinator(state, ensuredWorkerSession);
|
|
20914
|
+
this.assertWorkerSessionAvailable(state, ensuredWorkerSession, undefined, { allowCurrentReservation: true });
|
|
20915
|
+
state.orchestration.tasks[taskId] = task;
|
|
20916
|
+
state.orchestration.workerBindings[ensuredWorkerSession] = {
|
|
20917
|
+
sourceHandle: ensuredWorkerSession,
|
|
20918
|
+
coordinatorSession: input.coordinatorSession,
|
|
20919
|
+
workspace: input.workspace,
|
|
20920
|
+
...input.cwd ? { cwd: input.cwd } : {},
|
|
20921
|
+
targetAgent: input.targetAgent,
|
|
20922
|
+
role,
|
|
20923
|
+
...input.parallel ? { ephemeral: true } : {}
|
|
20924
|
+
};
|
|
20925
|
+
await this.deps.saveState(state);
|
|
20926
|
+
return {
|
|
20927
|
+
task: { ...task },
|
|
20928
|
+
previousBinding,
|
|
20929
|
+
previousGroup,
|
|
20930
|
+
normalizedGroupId
|
|
20931
|
+
};
|
|
20932
|
+
});
|
|
20933
|
+
} catch (error2) {
|
|
20934
|
+
await releaseWorkerReservation();
|
|
20935
|
+
throw error2;
|
|
20936
|
+
}
|
|
20132
20937
|
await releaseWorkerReservation();
|
|
20133
|
-
|
|
20938
|
+
} finally {
|
|
20939
|
+
releasePendingParallelStart?.();
|
|
20134
20940
|
}
|
|
20135
|
-
await releaseWorkerReservation();
|
|
20136
20941
|
try {
|
|
20137
20942
|
await this.deps.dispatchWorkerTask({
|
|
20138
20943
|
taskId,
|
|
@@ -20182,6 +20987,7 @@ class OrchestrationService {
|
|
|
20182
20987
|
return { sourceContext, targetLocation, role, normalizedGroupId };
|
|
20183
20988
|
});
|
|
20184
20989
|
const autoRun = preflight.sourceContext.sourceKind === "coordinator";
|
|
20990
|
+
const taskId = this.deps.createId();
|
|
20185
20991
|
const workerSessionName = await this.resolveWorkerSession({
|
|
20186
20992
|
sourceHandle: input.sourceHandle,
|
|
20187
20993
|
sourceKind: preflight.sourceContext.sourceKind,
|
|
@@ -20190,18 +20996,18 @@ class OrchestrationService {
|
|
|
20190
20996
|
...preflight.targetLocation.cwd ? { cwd: preflight.targetLocation.cwd } : {},
|
|
20191
20997
|
targetAgent: input.targetAgent,
|
|
20192
20998
|
task: input.task,
|
|
20193
|
-
...preflight.role ? { role: preflight.role } : {}
|
|
20999
|
+
...preflight.role ? { role: preflight.role } : {},
|
|
21000
|
+
...input.parallel ? { parallel: true } : {}
|
|
20194
21001
|
});
|
|
20195
|
-
|
|
20196
|
-
|
|
20197
|
-
try {
|
|
20198
|
-
prepared = await this.mutate(async () => {
|
|
21002
|
+
if (input.parallel && autoRun) {
|
|
21003
|
+
const queuedResult = await this.mutate(async () => {
|
|
20199
21004
|
const state = await this.deps.loadState();
|
|
20200
|
-
this.
|
|
21005
|
+
if (this.canStartParallelTask(state, input.targetAgent)) {
|
|
21006
|
+
this.pendingParallelStarts.set(input.targetAgent, (this.pendingParallelStarts.get(input.targetAgent) ?? 0) + 1);
|
|
21007
|
+
return null;
|
|
21008
|
+
}
|
|
20201
21009
|
const now = this.deps.now().toISOString();
|
|
20202
|
-
const
|
|
20203
|
-
const status = autoRun ? "running" : "needs_confirmation";
|
|
20204
|
-
const task = {
|
|
21010
|
+
const queuedTask = {
|
|
20205
21011
|
taskId,
|
|
20206
21012
|
sourceHandle: input.sourceHandle,
|
|
20207
21013
|
sourceKind: preflight.sourceContext.sourceKind,
|
|
@@ -20213,49 +21019,101 @@ class OrchestrationService {
|
|
|
20213
21019
|
...preflight.role ? { role: preflight.role } : {},
|
|
20214
21020
|
...preflight.normalizedGroupId ? { groupId: preflight.normalizedGroupId } : {},
|
|
20215
21021
|
task: input.task,
|
|
20216
|
-
status,
|
|
21022
|
+
status: "queued",
|
|
21023
|
+
ephemeralWorkerSession: true,
|
|
20217
21024
|
summary: "",
|
|
20218
21025
|
resultText: "",
|
|
20219
21026
|
createdAt: now,
|
|
20220
21027
|
updatedAt: now,
|
|
20221
21028
|
eventSeq: 1,
|
|
20222
|
-
events: [{ seq: 1, at: now, type: "created", status, message: "Task
|
|
21029
|
+
events: [{ seq: 1, at: now, type: "created", status: "queued", message: "Task queued at parallel capacity" }]
|
|
20223
21030
|
};
|
|
20224
|
-
|
|
20225
|
-
|
|
20226
|
-
|
|
20227
|
-
|
|
20228
|
-
|
|
20229
|
-
|
|
20230
|
-
|
|
20231
|
-
|
|
20232
|
-
|
|
20233
|
-
|
|
20234
|
-
|
|
20235
|
-
|
|
20236
|
-
|
|
20237
|
-
|
|
20238
|
-
|
|
20239
|
-
|
|
21031
|
+
state.orchestration.tasks[taskId] = queuedTask;
|
|
21032
|
+
await this.deps.saveState(state);
|
|
21033
|
+
return { taskId, status: "queued", workerSession: workerSessionName };
|
|
21034
|
+
});
|
|
21035
|
+
if (queuedResult) {
|
|
21036
|
+
this.logEvent("orchestration.task.queued", "parallel task queued at capacity", { taskId, targetAgent: input.targetAgent });
|
|
21037
|
+
return queuedResult;
|
|
21038
|
+
}
|
|
21039
|
+
}
|
|
21040
|
+
const releasePendingParallelStart = input.parallel && autoRun ? () => {
|
|
21041
|
+
const count = this.pendingParallelStarts.get(input.targetAgent) ?? 0;
|
|
21042
|
+
if (count <= 1) {
|
|
21043
|
+
this.pendingParallelStarts.delete(input.targetAgent);
|
|
21044
|
+
} else {
|
|
21045
|
+
this.pendingParallelStarts.set(input.targetAgent, count - 1);
|
|
21046
|
+
}
|
|
21047
|
+
} : undefined;
|
|
21048
|
+
let prepared;
|
|
21049
|
+
const releaseWorkerReservation = await this.reserveProposedWorkerSession(workerSessionName);
|
|
21050
|
+
try {
|
|
21051
|
+
try {
|
|
21052
|
+
prepared = await this.mutate(async () => {
|
|
21053
|
+
const state = await this.deps.loadState();
|
|
21054
|
+
this.assertRpcRequestAllowed(state, preflight.sourceContext.sourceKind, preflight.sourceContext.coordinatorSession, input.targetAgent, preflight.role);
|
|
21055
|
+
const now = this.deps.now().toISOString();
|
|
21056
|
+
const status = autoRun ? "running" : "needs_confirmation";
|
|
21057
|
+
const task = {
|
|
21058
|
+
taskId,
|
|
21059
|
+
sourceHandle: input.sourceHandle,
|
|
21060
|
+
sourceKind: preflight.sourceContext.sourceKind,
|
|
20240
21061
|
coordinatorSession: preflight.sourceContext.coordinatorSession,
|
|
21062
|
+
workerSession: workerSessionName,
|
|
20241
21063
|
workspace: preflight.targetLocation.workspace,
|
|
20242
21064
|
...preflight.targetLocation.cwd ? { cwd: preflight.targetLocation.cwd } : {},
|
|
20243
21065
|
targetAgent: input.targetAgent,
|
|
20244
|
-
role: preflight.role
|
|
21066
|
+
...preflight.role ? { role: preflight.role } : {},
|
|
21067
|
+
...preflight.normalizedGroupId ? { groupId: preflight.normalizedGroupId } : {},
|
|
21068
|
+
task: input.task,
|
|
21069
|
+
status,
|
|
21070
|
+
summary: "",
|
|
21071
|
+
resultText: "",
|
|
21072
|
+
createdAt: now,
|
|
21073
|
+
updatedAt: now,
|
|
21074
|
+
eventSeq: 1,
|
|
21075
|
+
events: [{ seq: 1, at: now, type: "created", status, message: "Task created" }],
|
|
21076
|
+
...input.parallel ? { ephemeralWorkerSession: true } : {}
|
|
20245
21077
|
};
|
|
20246
|
-
|
|
20247
|
-
|
|
20248
|
-
|
|
20249
|
-
|
|
20250
|
-
|
|
20251
|
-
|
|
20252
|
-
|
|
20253
|
-
|
|
20254
|
-
|
|
21078
|
+
if (preflight.normalizedGroupId) {
|
|
21079
|
+
const group = this.ensureGroups(state)[preflight.normalizedGroupId];
|
|
21080
|
+
group.updatedAt = now;
|
|
21081
|
+
group.coordinatorInjectedAt = undefined;
|
|
21082
|
+
group.injectionPending = undefined;
|
|
21083
|
+
group.injectionAppliedAt = undefined;
|
|
21084
|
+
group.lastInjectionError = undefined;
|
|
21085
|
+
}
|
|
21086
|
+
let previousBinding;
|
|
21087
|
+
if (autoRun) {
|
|
21088
|
+
previousBinding = state.orchestration.workerBindings[workerSessionName];
|
|
21089
|
+
this.assertWorkerSessionDoesNotConflictExternalCoordinator(state, workerSessionName);
|
|
21090
|
+
this.assertWorkerSessionAvailable(state, workerSessionName, undefined, { allowCurrentReservation: true });
|
|
21091
|
+
state.orchestration.tasks[taskId] = task;
|
|
21092
|
+
state.orchestration.workerBindings[workerSessionName] = {
|
|
21093
|
+
sourceHandle: workerSessionName,
|
|
21094
|
+
coordinatorSession: preflight.sourceContext.coordinatorSession,
|
|
21095
|
+
workspace: preflight.targetLocation.workspace,
|
|
21096
|
+
...preflight.targetLocation.cwd ? { cwd: preflight.targetLocation.cwd } : {},
|
|
21097
|
+
targetAgent: input.targetAgent,
|
|
21098
|
+
role: preflight.role,
|
|
21099
|
+
...input.parallel ? { ephemeral: true } : {}
|
|
21100
|
+
};
|
|
21101
|
+
} else {
|
|
21102
|
+
this.assertWorkerSessionDoesNotConflictExternalCoordinator(state, workerSessionName);
|
|
21103
|
+
this.assertWorkerSessionAvailable(state, workerSessionName, undefined, { allowCurrentReservation: true });
|
|
21104
|
+
state.orchestration.tasks[taskId] = task;
|
|
21105
|
+
}
|
|
21106
|
+
await this.deps.saveState(state);
|
|
21107
|
+
return { task: { ...task }, status, previousBinding, normalizedGroupId: preflight.normalizedGroupId };
|
|
21108
|
+
});
|
|
21109
|
+
} catch (error2) {
|
|
21110
|
+
await releaseWorkerReservation();
|
|
21111
|
+
throw error2;
|
|
21112
|
+
}
|
|
20255
21113
|
await releaseWorkerReservation();
|
|
20256
|
-
|
|
21114
|
+
} finally {
|
|
21115
|
+
releasePendingParallelStart?.();
|
|
20257
21116
|
}
|
|
20258
|
-
await releaseWorkerReservation();
|
|
20259
21117
|
if (autoRun) {
|
|
20260
21118
|
this.runAutoRunRpcWorkerTask({
|
|
20261
21119
|
task: prepared.task,
|
|
@@ -21224,6 +22082,16 @@ class OrchestrationService {
|
|
|
21224
22082
|
await this.recordOpenQuestionWakeError(prepared.task.taskId, prepared.replacementQuestionId, error2 instanceof Error ? error2.message : String(error2));
|
|
21225
22083
|
}
|
|
21226
22084
|
}
|
|
22085
|
+
if (input.decision === "accept") {
|
|
22086
|
+
try {
|
|
22087
|
+
await this.reconcileParallelSlots();
|
|
22088
|
+
} catch (error2) {
|
|
22089
|
+
this.logEvent("orchestration.parallel.reconcile_failed", "reconcile failed after contested result accepted", {
|
|
22090
|
+
taskId: prepared.task.taskId,
|
|
22091
|
+
message: error2 instanceof Error ? error2.message : String(error2)
|
|
22092
|
+
});
|
|
22093
|
+
}
|
|
22094
|
+
}
|
|
21227
22095
|
return prepared.task;
|
|
21228
22096
|
}
|
|
21229
22097
|
async listTasks(filter) {
|
|
@@ -21690,6 +22558,16 @@ class OrchestrationService {
|
|
|
21690
22558
|
if (prepared.closedPackageId) {
|
|
21691
22559
|
await this.handoffQueuedQuestions(prepared.task.coordinatorSession, prepared.closedPackageId);
|
|
21692
22560
|
}
|
|
22561
|
+
if (!prepared.shouldPropagate && this.isTerminalStatus(prepared.task.status)) {
|
|
22562
|
+
try {
|
|
22563
|
+
await this.reconcileParallelSlots();
|
|
22564
|
+
} catch (error2) {
|
|
22565
|
+
this.logEvent("orchestration.parallel.reconcile_failed", "reconcile failed after non-running cancel", {
|
|
22566
|
+
taskId: prepared.task.taskId,
|
|
22567
|
+
message: error2 instanceof Error ? error2.message : String(error2)
|
|
22568
|
+
});
|
|
22569
|
+
}
|
|
22570
|
+
}
|
|
21693
22571
|
return prepared.task;
|
|
21694
22572
|
}
|
|
21695
22573
|
async completeTaskCancellation(taskId) {
|
|
@@ -21752,6 +22630,14 @@ class OrchestrationService {
|
|
|
21752
22630
|
return prepared.task;
|
|
21753
22631
|
}
|
|
21754
22632
|
this.logEvent("orchestration.task.cancel_completed", "task cancellation completed", this.taskContext(prepared.task));
|
|
22633
|
+
try {
|
|
22634
|
+
await this.reconcileParallelSlots();
|
|
22635
|
+
} catch (error2) {
|
|
22636
|
+
this.logEvent("orchestration.parallel.reconcile_failed", "reconcile failed after cancel completion", {
|
|
22637
|
+
taskId: prepared.task.taskId,
|
|
22638
|
+
message: error2 instanceof Error ? error2.message : String(error2)
|
|
22639
|
+
});
|
|
22640
|
+
}
|
|
21755
22641
|
return prepared.task;
|
|
21756
22642
|
}
|
|
21757
22643
|
async failTaskCancellation(taskId, errorMessage) {
|
|
@@ -21796,6 +22682,34 @@ class OrchestrationService {
|
|
|
21796
22682
|
task: currentTask.task,
|
|
21797
22683
|
...currentTask.role ? { role: currentTask.role } : {}
|
|
21798
22684
|
});
|
|
22685
|
+
if (currentTask.ephemeralWorkerSession === true) {
|
|
22686
|
+
const queuedResult = await this.mutate(async () => {
|
|
22687
|
+
const state = await this.deps.loadState();
|
|
22688
|
+
const task = state.orchestration.tasks[input.taskId];
|
|
22689
|
+
if (!task) {
|
|
22690
|
+
throw new Error(`task "${input.taskId}" does not exist`);
|
|
22691
|
+
}
|
|
22692
|
+
this.assertCoordinatorOwnership(task, input.coordinatorSession);
|
|
22693
|
+
this.assertNeedsConfirmation(task);
|
|
22694
|
+
if (this.canStartParallelTask(state, task.targetAgent)) {
|
|
22695
|
+
return null;
|
|
22696
|
+
}
|
|
22697
|
+
const now = this.deps.now().toISOString();
|
|
22698
|
+
task.workerSession = workerSession;
|
|
22699
|
+
task.status = "queued";
|
|
22700
|
+
task.updatedAt = now;
|
|
22701
|
+
this.appendTaskEvent(task, now, "status_changed", {
|
|
22702
|
+
status: "queued",
|
|
22703
|
+
message: "Task queued at parallel capacity"
|
|
22704
|
+
});
|
|
22705
|
+
await this.deps.saveState(state);
|
|
22706
|
+
return { ...task };
|
|
22707
|
+
});
|
|
22708
|
+
if (queuedResult) {
|
|
22709
|
+
this.logEvent("orchestration.task.queued", "parallel task queued at capacity on approve", { taskId: input.taskId, targetAgent: currentTask.targetAgent });
|
|
22710
|
+
return queuedResult;
|
|
22711
|
+
}
|
|
22712
|
+
}
|
|
21799
22713
|
const releaseWorkerReservation = await this.reserveProposedWorkerSession(workerSession, input.taskId);
|
|
21800
22714
|
let ensuredWorkerSession = workerSession;
|
|
21801
22715
|
let prepared;
|
|
@@ -21837,7 +22751,8 @@ class OrchestrationService {
|
|
|
21837
22751
|
workspace: task.workspace,
|
|
21838
22752
|
...task.cwd ? { cwd: task.cwd } : {},
|
|
21839
22753
|
targetAgent: task.targetAgent,
|
|
21840
|
-
role: task.role
|
|
22754
|
+
role: task.role,
|
|
22755
|
+
...task.ephemeralWorkerSession ? { ephemeral: true } : {}
|
|
21841
22756
|
};
|
|
21842
22757
|
await this.deps.saveState(state);
|
|
21843
22758
|
return {
|
|
@@ -21891,6 +22806,10 @@ class OrchestrationService {
|
|
|
21891
22806
|
}
|
|
21892
22807
|
async resolveWorkerSession(input) {
|
|
21893
22808
|
const role = this.normalizeRole(input.role);
|
|
22809
|
+
const baseName = [input.workspace, input.cwd ? this.cwdWorkerSessionPart(input.cwd) : undefined, input.targetAgent, role, input.coordinatorSession].filter((part) => typeof part === "string" && part.trim().length > 0).map((part) => part.trim()).join(":");
|
|
22810
|
+
if (input.parallel) {
|
|
22811
|
+
return `${baseName}:p-${this.deps.createId()}`;
|
|
22812
|
+
}
|
|
21894
22813
|
const reusable = await this.deps.findReusableWorkerSession?.({
|
|
21895
22814
|
sourceHandle: input.sourceHandle,
|
|
21896
22815
|
sourceKind: input.sourceKind,
|
|
@@ -21903,7 +22822,7 @@ class OrchestrationService {
|
|
|
21903
22822
|
if (reusable && reusable.trim().length > 0) {
|
|
21904
22823
|
return reusable.trim();
|
|
21905
22824
|
}
|
|
21906
|
-
return
|
|
22825
|
+
return baseName;
|
|
21907
22826
|
}
|
|
21908
22827
|
async reserveProposedWorkerSession(workerSession, excludingTaskId) {
|
|
21909
22828
|
await this.mutate(async () => {
|
|
@@ -22061,7 +22980,7 @@ class OrchestrationService {
|
|
|
22061
22980
|
if (role && policy.allowedAgentRequestRoles.length > 0 && !policy.allowedAgentRequestRoles.includes(role)) {
|
|
22062
22981
|
throw new Error(`role "${role}" is not allowed for agent-requested delegation`);
|
|
22063
22982
|
}
|
|
22064
|
-
const outstandingRequests = Object.values(state.orchestration.tasks).filter((task) => task.coordinatorSession === coordinatorSession && task.sourceKind !== "human" && (task.status === "needs_confirmation" || task.status === "running"));
|
|
22983
|
+
const outstandingRequests = Object.values(state.orchestration.tasks).filter((task) => task.coordinatorSession === coordinatorSession && task.sourceKind !== "human" && (task.status === "needs_confirmation" || task.status === "running" || task.status === "queued"));
|
|
22065
22984
|
if (outstandingRequests.length >= policy.maxPendingAgentRequestsPerCoordinator) {
|
|
22066
22985
|
throw new Error("agent-requested delegation quota exceeded for this coordinator");
|
|
22067
22986
|
}
|
|
@@ -22226,6 +23145,137 @@ class OrchestrationService {
|
|
|
22226
23145
|
hasActiveTaskWorkerSession(state, workerSession, excludingTaskId) {
|
|
22227
23146
|
return Object.values(state.orchestration.tasks).some((task) => task.taskId !== excludingTaskId && task.workerSession === workerSession && (!this.isTerminalStatus(task.status) || task.reviewPending !== undefined));
|
|
22228
23147
|
}
|
|
23148
|
+
countActiveParallelSlots(state, targetAgent) {
|
|
23149
|
+
const persisted = Object.values(state.orchestration.tasks).filter((task) => task.ephemeralWorkerSession === true && task.targetAgent === targetAgent && (task.status === "running" || task.status === "blocked" || task.status === "waiting_for_human")).length;
|
|
23150
|
+
const pending = this.pendingParallelStarts.get(targetAgent) ?? 0;
|
|
23151
|
+
return persisted + pending;
|
|
23152
|
+
}
|
|
23153
|
+
canStartParallelTask(state, targetAgent) {
|
|
23154
|
+
const cap = this.deps.config.orchestration.maxParallelTasksPerAgent;
|
|
23155
|
+
return this.countActiveParallelSlots(state, targetAgent) < cap;
|
|
23156
|
+
}
|
|
23157
|
+
async reconcileParallelSlots() {
|
|
23158
|
+
const toClose = await this.mutate(async () => {
|
|
23159
|
+
const state = await this.deps.loadState();
|
|
23160
|
+
const collected = [];
|
|
23161
|
+
for (const task of Object.values(state.orchestration.tasks)) {
|
|
23162
|
+
if (task.ephemeralWorkerSession === true && task.ephemeralWorkerSessionClosed !== true && task.workerSession && task.reviewPending === undefined && this.isTerminalStatus(task.status)) {
|
|
23163
|
+
task.ephemeralWorkerSessionClosed = true;
|
|
23164
|
+
if (state.orchestration.workerBindings[task.workerSession] !== undefined) {
|
|
23165
|
+
delete state.orchestration.workerBindings[task.workerSession];
|
|
23166
|
+
collected.push({
|
|
23167
|
+
workerSession: task.workerSession,
|
|
23168
|
+
coordinatorSession: task.coordinatorSession,
|
|
23169
|
+
workspace: task.workspace,
|
|
23170
|
+
...task.cwd ? { cwd: task.cwd } : {},
|
|
23171
|
+
targetAgent: task.targetAgent,
|
|
23172
|
+
...task.role ? { role: task.role } : {}
|
|
23173
|
+
});
|
|
23174
|
+
}
|
|
23175
|
+
}
|
|
23176
|
+
}
|
|
23177
|
+
if (collected.length > 0) {
|
|
23178
|
+
await this.deps.saveState(state);
|
|
23179
|
+
}
|
|
23180
|
+
return collected;
|
|
23181
|
+
});
|
|
23182
|
+
for (const req of toClose) {
|
|
23183
|
+
try {
|
|
23184
|
+
await this.deps.closeWorkerSession?.(req);
|
|
23185
|
+
} catch (error2) {
|
|
23186
|
+
this.logEvent("orchestration.parallel.close_failed", "failed to close ephemeral worker session", {
|
|
23187
|
+
workerSession: req.workerSession,
|
|
23188
|
+
message: error2 instanceof Error ? error2.message : String(error2)
|
|
23189
|
+
});
|
|
23190
|
+
}
|
|
23191
|
+
}
|
|
23192
|
+
for (;; ) {
|
|
23193
|
+
const next = await this.mutate(async () => {
|
|
23194
|
+
const state = await this.deps.loadState();
|
|
23195
|
+
const queued = Object.values(state.orchestration.tasks).filter((t) => t.status === "queued" && t.ephemeralWorkerSession === true).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
23196
|
+
for (const task of queued) {
|
|
23197
|
+
if (!this.canStartParallelTask(state, task.targetAgent)) {
|
|
23198
|
+
continue;
|
|
23199
|
+
}
|
|
23200
|
+
task.status = "running";
|
|
23201
|
+
task.updatedAt = this.deps.now().toISOString();
|
|
23202
|
+
state.orchestration.workerBindings[task.workerSession] = {
|
|
23203
|
+
sourceHandle: task.workerSession,
|
|
23204
|
+
coordinatorSession: task.coordinatorSession,
|
|
23205
|
+
workspace: task.workspace,
|
|
23206
|
+
...task.cwd ? { cwd: task.cwd } : {},
|
|
23207
|
+
targetAgent: task.targetAgent,
|
|
23208
|
+
...task.role ? { role: task.role } : {},
|
|
23209
|
+
ephemeral: true
|
|
23210
|
+
};
|
|
23211
|
+
await this.deps.saveState(state);
|
|
23212
|
+
return { ...task };
|
|
23213
|
+
}
|
|
23214
|
+
return null;
|
|
23215
|
+
});
|
|
23216
|
+
if (!next) {
|
|
23217
|
+
break;
|
|
23218
|
+
}
|
|
23219
|
+
try {
|
|
23220
|
+
await this.ensureReservedWorkerSession({
|
|
23221
|
+
workerSession: next.workerSession,
|
|
23222
|
+
sourceHandle: next.sourceHandle,
|
|
23223
|
+
sourceKind: next.sourceKind,
|
|
23224
|
+
coordinatorSession: next.coordinatorSession,
|
|
23225
|
+
workspace: next.workspace,
|
|
23226
|
+
...next.cwd ? { cwd: next.cwd } : {},
|
|
23227
|
+
targetAgent: next.targetAgent,
|
|
23228
|
+
...next.role ? { role: next.role } : {}
|
|
23229
|
+
});
|
|
23230
|
+
await this.deps.dispatchWorkerTask({
|
|
23231
|
+
taskId: next.taskId,
|
|
23232
|
+
workerSession: next.workerSession,
|
|
23233
|
+
coordinatorSession: next.coordinatorSession,
|
|
23234
|
+
workspace: next.workspace,
|
|
23235
|
+
...next.cwd ? { cwd: next.cwd } : {},
|
|
23236
|
+
targetAgent: next.targetAgent,
|
|
23237
|
+
...next.role ? { role: next.role } : {},
|
|
23238
|
+
task: next.task
|
|
23239
|
+
});
|
|
23240
|
+
} catch (error2) {
|
|
23241
|
+
await this.mutate(async () => {
|
|
23242
|
+
const state = await this.deps.loadState();
|
|
23243
|
+
const task = state.orchestration.tasks[next.taskId];
|
|
23244
|
+
if (task && task.status === "running") {
|
|
23245
|
+
task.status = "queued";
|
|
23246
|
+
task.updatedAt = this.deps.now().toISOString();
|
|
23247
|
+
delete state.orchestration.workerBindings[next.workerSession];
|
|
23248
|
+
this.appendTaskEvent(task, task.updatedAt, "status_changed", {
|
|
23249
|
+
status: "queued",
|
|
23250
|
+
message: "Task re-queued after drain failure"
|
|
23251
|
+
});
|
|
23252
|
+
await this.deps.saveState(state);
|
|
23253
|
+
}
|
|
23254
|
+
});
|
|
23255
|
+
this.logEvent("orchestration.parallel.drain_failed", "failed to drain queued parallel task", {
|
|
23256
|
+
taskId: next.taskId,
|
|
23257
|
+
workerSession: next.workerSession,
|
|
23258
|
+
message: error2 instanceof Error ? error2.message : String(error2)
|
|
23259
|
+
});
|
|
23260
|
+
break;
|
|
23261
|
+
}
|
|
23262
|
+
await this.mutate(async () => {
|
|
23263
|
+
const state = await this.deps.loadState();
|
|
23264
|
+
const task = state.orchestration.tasks[next.taskId];
|
|
23265
|
+
if (task && task.status === "running") {
|
|
23266
|
+
this.appendTaskEvent(task, task.updatedAt, "status_changed", {
|
|
23267
|
+
status: "running",
|
|
23268
|
+
message: "Task drained from parallel queue"
|
|
23269
|
+
});
|
|
23270
|
+
await this.deps.saveState(state);
|
|
23271
|
+
}
|
|
23272
|
+
});
|
|
23273
|
+
this.logEvent("orchestration.task.drained", "parallel task drained from queue", {
|
|
23274
|
+
taskId: next.taskId,
|
|
23275
|
+
targetAgent: next.targetAgent
|
|
23276
|
+
});
|
|
23277
|
+
}
|
|
23278
|
+
}
|
|
22229
23279
|
async assertProposedWorkerSessionDoesNotConflictExternalCoordinator(workerSession) {
|
|
22230
23280
|
const state = await this.deps.loadState();
|
|
22231
23281
|
this.assertWorkerSessionDoesNotConflictExternalCoordinator(state, workerSession);
|
|
@@ -23236,6 +24286,13 @@ async function runConsole(paths, deps) {
|
|
|
23236
24286
|
trigger: "startup"
|
|
23237
24287
|
});
|
|
23238
24288
|
} catch {}
|
|
24289
|
+
try {
|
|
24290
|
+
await runtime.orchestration.service.reconcileParallelSlots();
|
|
24291
|
+
} catch (reconcileError) {
|
|
24292
|
+
await runtime.logger.error("orchestration.parallel.reconcile_failed", "failed to reconcile parallel slots at startup", {
|
|
24293
|
+
message: reconcileError instanceof Error ? reconcileError.message : String(reconcileError)
|
|
24294
|
+
});
|
|
24295
|
+
}
|
|
23239
24296
|
consumerLock = deps.consumerLock ?? deps.consumerLockFactory?.(runtime);
|
|
23240
24297
|
if (consumerLock) {
|
|
23241
24298
|
const lockMeta = {
|
|
@@ -23994,7 +25051,7 @@ var init_spawn_command = __esm(() => {
|
|
|
23994
25051
|
// src/transport/prompt-media.ts
|
|
23995
25052
|
import { mkdtemp, open as open3, rm as rm8, writeFile as writeFile8 } from "node:fs/promises";
|
|
23996
25053
|
import { tmpdir as defaultTmpdir } from "node:os";
|
|
23997
|
-
import
|
|
25054
|
+
import path14 from "node:path";
|
|
23998
25055
|
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
23999
25056
|
async function createStructuredPromptFile(text, media, deps = defaultStructuredPromptFileDeps) {
|
|
24000
25057
|
const mediaList = normalizePromptMedia(media);
|
|
@@ -24028,7 +25085,7 @@ async function createStructuredPromptFile(text, media, deps = defaultStructuredP
|
|
|
24028
25085
|
type: "resource",
|
|
24029
25086
|
resource: {
|
|
24030
25087
|
uri: pathToFileURL2(item.filePath).toString(),
|
|
24031
|
-
text: `${item.fileName ??
|
|
25088
|
+
text: `${item.fileName ?? path14.basename(item.filePath)} ${item.mimeType} ${item.type}`
|
|
24032
25089
|
}
|
|
24033
25090
|
});
|
|
24034
25091
|
}
|
|
@@ -24042,7 +25099,7 @@ function normalizePromptMedia(media) {
|
|
|
24042
25099
|
function buildAttachmentSummary(items) {
|
|
24043
25100
|
const lines = ["Attachments available as local files:"];
|
|
24044
25101
|
for (const [index, item] of items.entries()) {
|
|
24045
|
-
lines.push(`${index + 1}. ${item.type} ${item.fileName ??
|
|
25102
|
+
lines.push(`${index + 1}. ${item.type} ${item.fileName ?? path14.basename(item.filePath)} ${item.mimeType} ${item.filePath}`);
|
|
24046
25103
|
}
|
|
24047
25104
|
return lines.join(`
|
|
24048
25105
|
`);
|
|
@@ -24050,8 +25107,8 @@ function buildAttachmentSummary(items) {
|
|
|
24050
25107
|
async function writeStructuredPromptBlocks(blocks, deps) {
|
|
24051
25108
|
let dir = "";
|
|
24052
25109
|
try {
|
|
24053
|
-
dir = await deps.mkdtemp(
|
|
24054
|
-
const filePath =
|
|
25110
|
+
dir = await deps.mkdtemp(path14.join(deps.tmpdir(), "weacpx-acp-prompt-"));
|
|
25111
|
+
const filePath = path14.join(dir, "prompt.json");
|
|
24055
25112
|
await deps.writeFile(filePath, JSON.stringify(blocks), "utf8");
|
|
24056
25113
|
return { filePath, cleanup: async () => deps.rm(dir, { recursive: true, force: true }) };
|
|
24057
25114
|
} catch (error2) {
|
|
@@ -25595,6 +26652,14 @@ async function buildApp(paths, deps = {}) {
|
|
|
25595
26652
|
resultText: ""
|
|
25596
26653
|
});
|
|
25597
26654
|
}
|
|
26655
|
+
try {
|
|
26656
|
+
await orchestration.reconcileParallelSlots();
|
|
26657
|
+
} catch (reconcileError) {
|
|
26658
|
+
await logger2.error("orchestration.parallel.reconcile_failed", "failed to reconcile parallel slots after worker turn", {
|
|
26659
|
+
taskId: input.taskId,
|
|
26660
|
+
message: reconcileError instanceof Error ? reconcileError.message : String(reconcileError)
|
|
26661
|
+
});
|
|
26662
|
+
}
|
|
25598
26663
|
if (taskRecord && shouldNotifyTaskCompletion(taskRecord)) {
|
|
25599
26664
|
try {
|
|
25600
26665
|
await sendCompletionNotice(taskRecord);
|
|
@@ -25662,6 +26727,13 @@ async function buildApp(paths, deps = {}) {
|
|
|
25662
26727
|
throw new Error(result.message || "worker task cancel was not acknowledged");
|
|
25663
26728
|
}
|
|
25664
26729
|
},
|
|
26730
|
+
closeWorkerSession: async ({ workerSession, targetAgent, workspace, cwd }) => {
|
|
26731
|
+
if (!transport.removeSession) {
|
|
26732
|
+
return;
|
|
26733
|
+
}
|
|
26734
|
+
const session = resolveWorkerRuntimeSession({ workerSession, targetAgent, workspace, ...cwd ? { cwd } : {} });
|
|
26735
|
+
await transport.removeSession(session);
|
|
26736
|
+
},
|
|
25665
26737
|
resumeWorkerTask: async ({ taskId, workerSession, coordinatorSession, targetAgent, workspace, cwd, answer }) => {
|
|
25666
26738
|
launchWorkerTurn({
|
|
25667
26739
|
taskId,
|
|
@@ -25687,7 +26759,7 @@ async function buildApp(paths, deps = {}) {
|
|
|
25687
26759
|
}
|
|
25688
26760
|
},
|
|
25689
26761
|
findReusableWorkerSession: async ({ coordinatorSession, workspace, cwd, targetAgent, role }) => {
|
|
25690
|
-
const binding = Object.entries(state.orchestration.workerBindings).find(([, current]) => current.coordinatorSession === coordinatorSession && current.workspace === workspace && current.cwd === cwd && current.targetAgent === targetAgent && current.role === role);
|
|
26762
|
+
const binding = Object.entries(state.orchestration.workerBindings).find(([, current]) => current.ephemeral !== true && current.coordinatorSession === coordinatorSession && current.workspace === workspace && current.cwd === cwd && current.targetAgent === targetAgent && current.role === role);
|
|
25691
26763
|
return binding?.[0] ?? null;
|
|
25692
26764
|
},
|
|
25693
26765
|
logger: logger2
|
|
@@ -26247,107 +27319,107 @@ async function checkRuntime(options = {}) {
|
|
|
26247
27319
|
}
|
|
26248
27320
|
function createRuntimeFsProbe() {
|
|
26249
27321
|
return {
|
|
26250
|
-
stat: async (
|
|
26251
|
-
access: async (
|
|
27322
|
+
stat: async (path15) => await stat3(path15),
|
|
27323
|
+
access: async (path15, mode) => await access4(path15, mode)
|
|
26252
27324
|
};
|
|
26253
27325
|
}
|
|
26254
|
-
async function checkDirectoryCreatable(label,
|
|
27326
|
+
async function checkDirectoryCreatable(label, path15, probe, platform) {
|
|
26255
27327
|
try {
|
|
26256
|
-
const stats = await probe.stat(
|
|
27328
|
+
const stats = await probe.stat(path15);
|
|
26257
27329
|
if (!stats.isDirectory()) {
|
|
26258
27330
|
return {
|
|
26259
27331
|
ok: false,
|
|
26260
|
-
detail: `${label}: ${
|
|
27332
|
+
detail: `${label}: ${path15} (exists but is not a directory)`
|
|
26261
27333
|
};
|
|
26262
27334
|
}
|
|
26263
|
-
await probe.access(
|
|
27335
|
+
await probe.access(path15, directoryAccessMode(platform));
|
|
26264
27336
|
return {
|
|
26265
27337
|
ok: true,
|
|
26266
|
-
detail: `${label}: ${
|
|
27338
|
+
detail: `${label}: ${path15} (writable)`
|
|
26267
27339
|
};
|
|
26268
27340
|
} catch (error2) {
|
|
26269
27341
|
if (!isMissingPathError(error2)) {
|
|
26270
27342
|
return {
|
|
26271
27343
|
ok: false,
|
|
26272
|
-
detail: `${label}: ${
|
|
27344
|
+
detail: `${label}: ${path15} (unusable: ${formatError6(error2)})`
|
|
26273
27345
|
};
|
|
26274
27346
|
}
|
|
26275
|
-
const parentCheck = await checkCreatableAncestorDirectory(
|
|
27347
|
+
const parentCheck = await checkCreatableAncestorDirectory(path15, probe, platform);
|
|
26276
27348
|
if (!parentCheck.ok) {
|
|
26277
27349
|
return {
|
|
26278
27350
|
ok: false,
|
|
26279
|
-
detail: `${label}: ${
|
|
27351
|
+
detail: `${label}: ${path15} (parent not writable: ${parentCheck.blockingPath})`
|
|
26280
27352
|
};
|
|
26281
27353
|
}
|
|
26282
27354
|
return {
|
|
26283
27355
|
ok: true,
|
|
26284
|
-
detail: `${label}: ${
|
|
27356
|
+
detail: `${label}: ${path15} (creatable via ${parentCheck.creatableFrom})`
|
|
26285
27357
|
};
|
|
26286
27358
|
}
|
|
26287
27359
|
}
|
|
26288
|
-
async function checkFileCreatable(label,
|
|
27360
|
+
async function checkFileCreatable(label, path15, probe, platform) {
|
|
26289
27361
|
try {
|
|
26290
|
-
const stats = await probe.stat(
|
|
27362
|
+
const stats = await probe.stat(path15);
|
|
26291
27363
|
if (stats.isDirectory()) {
|
|
26292
27364
|
return {
|
|
26293
27365
|
ok: false,
|
|
26294
|
-
detail: `${label}: ${
|
|
27366
|
+
detail: `${label}: ${path15} (exists but is a directory)`
|
|
26295
27367
|
};
|
|
26296
27368
|
}
|
|
26297
|
-
await probe.access(
|
|
27369
|
+
await probe.access(path15, constants.W_OK);
|
|
26298
27370
|
return {
|
|
26299
27371
|
ok: true,
|
|
26300
|
-
detail: `${label}: ${
|
|
27372
|
+
detail: `${label}: ${path15} (writable)`
|
|
26301
27373
|
};
|
|
26302
27374
|
} catch (error2) {
|
|
26303
27375
|
if (!isMissingPathError(error2)) {
|
|
26304
27376
|
return {
|
|
26305
27377
|
ok: false,
|
|
26306
|
-
detail: `${label}: ${
|
|
27378
|
+
detail: `${label}: ${path15} (unusable: ${formatError6(error2)})`
|
|
26307
27379
|
};
|
|
26308
27380
|
}
|
|
26309
|
-
const parentCheck = await checkCreatableAncestorDirectory(dirname14(
|
|
27381
|
+
const parentCheck = await checkCreatableAncestorDirectory(dirname14(path15), probe, platform);
|
|
26310
27382
|
if (!parentCheck.ok) {
|
|
26311
27383
|
return {
|
|
26312
27384
|
ok: false,
|
|
26313
|
-
detail: `${label}: ${
|
|
27385
|
+
detail: `${label}: ${path15} (parent not writable: ${parentCheck.blockingPath})`
|
|
26314
27386
|
};
|
|
26315
27387
|
}
|
|
26316
27388
|
return {
|
|
26317
27389
|
ok: true,
|
|
26318
|
-
detail: `${label}: ${
|
|
27390
|
+
detail: `${label}: ${path15} (creatable via ${parentCheck.creatableFrom})`
|
|
26319
27391
|
};
|
|
26320
27392
|
}
|
|
26321
27393
|
}
|
|
26322
|
-
async function checkCreatableAncestorDirectory(
|
|
27394
|
+
async function checkCreatableAncestorDirectory(path15, probe, platform) {
|
|
26323
27395
|
try {
|
|
26324
|
-
const stats = await probe.stat(
|
|
27396
|
+
const stats = await probe.stat(path15);
|
|
26325
27397
|
if (!stats.isDirectory()) {
|
|
26326
27398
|
return {
|
|
26327
27399
|
ok: false,
|
|
26328
|
-
creatableFrom:
|
|
26329
|
-
blockingPath:
|
|
27400
|
+
creatableFrom: path15,
|
|
27401
|
+
blockingPath: path15
|
|
26330
27402
|
};
|
|
26331
27403
|
}
|
|
26332
|
-
await probe.access(
|
|
27404
|
+
await probe.access(path15, directoryAccessMode(platform));
|
|
26333
27405
|
return {
|
|
26334
27406
|
ok: true,
|
|
26335
|
-
creatableFrom:
|
|
27407
|
+
creatableFrom: path15
|
|
26336
27408
|
};
|
|
26337
27409
|
} catch (error2) {
|
|
26338
27410
|
if (!isMissingPathError(error2)) {
|
|
26339
27411
|
return {
|
|
26340
27412
|
ok: false,
|
|
26341
|
-
creatableFrom:
|
|
26342
|
-
blockingPath:
|
|
27413
|
+
creatableFrom: path15,
|
|
27414
|
+
blockingPath: path15
|
|
26343
27415
|
};
|
|
26344
27416
|
}
|
|
26345
|
-
const parent = dirname14(
|
|
26346
|
-
if (parent ===
|
|
27417
|
+
const parent = dirname14(path15);
|
|
27418
|
+
if (parent === path15) {
|
|
26347
27419
|
return {
|
|
26348
27420
|
ok: false,
|
|
26349
|
-
creatableFrom:
|
|
26350
|
-
blockingPath:
|
|
27421
|
+
creatableFrom: path15,
|
|
27422
|
+
blockingPath: path15
|
|
26351
27423
|
};
|
|
26352
27424
|
}
|
|
26353
27425
|
const parentCheck = await checkCreatableAncestorDirectory(parent, probe, platform);
|
|
@@ -39393,7 +40465,8 @@ function buildWeacpxMcpToolRegistry(input) {
|
|
|
39393
40465
|
task: exports_external.string().min(1),
|
|
39394
40466
|
workingDirectory: exports_external.string().min(1).optional(),
|
|
39395
40467
|
role: exports_external.string().min(1).optional(),
|
|
39396
|
-
groupId: exports_external.string().min(1).optional()
|
|
40468
|
+
groupId: exports_external.string().min(1).optional(),
|
|
40469
|
+
parallel: exports_external.boolean().describe("Set to true to run this task in its own ephemeral session, concurrently with other in-flight tasks for the same agent.").optional()
|
|
39397
40470
|
}).strict(),
|
|
39398
40471
|
handler: async (args) => await asToolResult(async () => {
|
|
39399
40472
|
const input2 = args;
|
|
@@ -39414,7 +40487,8 @@ function buildWeacpxMcpToolRegistry(input) {
|
|
|
39414
40487
|
targetAgent: exports_external.string().min(1),
|
|
39415
40488
|
task: exports_external.string().min(1),
|
|
39416
40489
|
workingDirectory: exports_external.string().min(1).optional(),
|
|
39417
|
-
role: exports_external.string().min(1).optional()
|
|
40490
|
+
role: exports_external.string().min(1).optional(),
|
|
40491
|
+
parallel: exports_external.boolean().describe("Set to true to run this task in its own ephemeral session, concurrently with other in-flight tasks for the same agent.").optional()
|
|
39418
40492
|
}).strict()).min(1)
|
|
39419
40493
|
}).strict(),
|
|
39420
40494
|
handler: async (args) => await asToolResult(async () => {
|
|
@@ -39433,7 +40507,8 @@ function buildWeacpxMcpToolRegistry(input) {
|
|
|
39433
40507
|
task: entry.task,
|
|
39434
40508
|
...entry.workingDirectory ? { workingDirectory: entry.workingDirectory } : {},
|
|
39435
40509
|
...entry.role ? { role: entry.role } : {},
|
|
39436
|
-
...groupId ? { groupId } : {}
|
|
40510
|
+
...groupId ? { groupId } : {},
|
|
40511
|
+
...entry.parallel !== undefined ? { parallel: entry.parallel } : {}
|
|
39437
40512
|
});
|
|
39438
40513
|
results.push({ index, taskId: result.taskId, status: result.status });
|
|
39439
40514
|
} catch (error2) {
|
|
@@ -39657,7 +40732,7 @@ function createErrorResult(message) {
|
|
|
39657
40732
|
};
|
|
39658
40733
|
}
|
|
39659
40734
|
function renderDelegateSuccess(result) {
|
|
39660
|
-
const next = result.status === "needs_confirmation" ? `Next: this delegation requires user approval. Tell the user, then call task_approve or task_cancel based on their response.` : `Next: task "${result.taskId}" is running. Return this taskId to the user, call task_get/task_list for non-blocking progress snapshots, or task_watch to long-poll for the next event or terminal state.`;
|
|
40735
|
+
const next = result.status === "needs_confirmation" ? `Next: this delegation requires user approval. Tell the user, then call task_approve or task_cancel based on their response.` : result.status === "queued" ? `Next: task "${result.taskId}" is queued (agent at parallel capacity). It will start automatically when a slot frees. Call task_watch to long-poll for the transition to running, or task_get/task_list for non-blocking snapshots.` : `Next: task "${result.taskId}" is running. Return this taskId to the user, call task_get/task_list for non-blocking progress snapshots, or task_watch to long-poll for the next event or terminal state.`;
|
|
39661
40736
|
return [`Delegation task "${result.taskId}" created.`, `- Status: ${result.status}`, next].join(`
|
|
39662
40737
|
`);
|
|
39663
40738
|
}
|
|
@@ -39928,7 +41003,8 @@ function createOrchestrationTransport(endpoint, deps = {}) {
|
|
|
39928
41003
|
task: input.task,
|
|
39929
41004
|
...input.workingDirectory !== undefined ? { cwd: input.workingDirectory } : {},
|
|
39930
41005
|
...input.role !== undefined ? { role: input.role } : {},
|
|
39931
|
-
...input.groupId !== undefined ? { groupId: input.groupId } : {}
|
|
41006
|
+
...input.groupId !== undefined ? { groupId: input.groupId } : {},
|
|
41007
|
+
...input.parallel !== undefined ? { parallel: input.parallel } : {}
|
|
39932
41008
|
}),
|
|
39933
41009
|
createGroup: async (input) => await client.createGroup(input),
|
|
39934
41010
|
getTask: async (input) => await client.getTaskForCoordinator(input),
|
|
@@ -40641,10 +41717,12 @@ function parseSourceHandle(args, env = process.env) {
|
|
|
40641
41717
|
|
|
40642
41718
|
// src/cli.ts
|
|
40643
41719
|
init_workspace_path();
|
|
41720
|
+
init_workspace_name();
|
|
40644
41721
|
init_state_store();
|
|
40645
41722
|
|
|
40646
41723
|
// src/onboarding.ts
|
|
40647
41724
|
init_workspace_path();
|
|
41725
|
+
init_workspace_name();
|
|
40648
41726
|
init_agent_templates();
|
|
40649
41727
|
function isFirstUse(config2, state) {
|
|
40650
41728
|
return Object.keys(state.sessions ?? {}).length === 0 && Object.keys(config2.workspaces ?? {}).length === 0 && (config2.plugins ?? []).length === 0;
|
|
@@ -40655,7 +41733,7 @@ async function maybeRunFirstUseOnboarding(input) {
|
|
|
40655
41733
|
if (!input.deps.isInteractive())
|
|
40656
41734
|
return { created: false };
|
|
40657
41735
|
const cwd = normalizeWorkspacePath(input.deps.cwd());
|
|
40658
|
-
const workspaceName =
|
|
41736
|
+
const workspaceName = allocateWorkspaceName(sanitizeWorkspaceName(basenameForWorkspacePath(cwd)), input.config.workspaces);
|
|
40659
41737
|
const yes = (await input.deps.promptText(`检测到首次使用 weacpx。是否将当前目录创建为工作区「${workspaceName}」?[Y/n] `)).trim().toLowerCase();
|
|
40660
41738
|
if (yes === "n" || yes === "no")
|
|
40661
41739
|
return { created: false };
|
|
@@ -40696,18 +41774,6 @@ function resolveTemplateChoice(answer, names) {
|
|
|
40696
41774
|
return names[index - 1];
|
|
40697
41775
|
return names.includes(answer) ? answer : null;
|
|
40698
41776
|
}
|
|
40699
|
-
function allocateName(base, existing) {
|
|
40700
|
-
if (!existing[base])
|
|
40701
|
-
return base;
|
|
40702
|
-
let suffix = 2;
|
|
40703
|
-
while (existing[`${base}-${suffix}`])
|
|
40704
|
-
suffix += 1;
|
|
40705
|
-
return `${base}-${suffix}`;
|
|
40706
|
-
}
|
|
40707
|
-
function sanitizeName(input, fallback) {
|
|
40708
|
-
const sanitized = input.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
40709
|
-
return sanitized || fallback;
|
|
40710
|
-
}
|
|
40711
41777
|
|
|
40712
41778
|
// src/cli-update.ts
|
|
40713
41779
|
init_plugin_home();
|
|
@@ -42371,7 +43437,7 @@ var HELP_LINES = [
|
|
|
42371
43437
|
"weacpx doctor - 运行诊断",
|
|
42372
43438
|
"weacpx version - 查看版本",
|
|
42373
43439
|
"weacpx agent|agents list|add|rm|templates - 管理本机 Agent",
|
|
42374
|
-
"weacpx workspace list|add|rm - 管理本机工作区(别名:ws)",
|
|
43440
|
+
"weacpx workspace list|add [name] [--raw]|rm <name> - 管理本机工作区(别名:ws)",
|
|
42375
43441
|
"weacpx mcp-stdio [--coordinator-session <session>] [--source-handle <handle>] [--workspace <name>] - 启动 MCP stdio 服务"
|
|
42376
43442
|
];
|
|
42377
43443
|
function getUsageText() {
|
|
@@ -42635,10 +43701,23 @@ async function handleWorkspaceCli(args, deps) {
|
|
|
42635
43701
|
if (args.length !== 1)
|
|
42636
43702
|
return null;
|
|
42637
43703
|
return await workspaceList(deps.print);
|
|
42638
|
-
case "add":
|
|
42639
|
-
|
|
42640
|
-
|
|
42641
|
-
|
|
43704
|
+
case "add": {
|
|
43705
|
+
const rest = args.slice(1);
|
|
43706
|
+
let rawFlag = false;
|
|
43707
|
+
let explicit;
|
|
43708
|
+
for (const token of rest) {
|
|
43709
|
+
if (token === "--raw") {
|
|
43710
|
+
if (rawFlag)
|
|
43711
|
+
return null;
|
|
43712
|
+
rawFlag = true;
|
|
43713
|
+
continue;
|
|
43714
|
+
}
|
|
43715
|
+
if (explicit !== undefined)
|
|
43716
|
+
return null;
|
|
43717
|
+
explicit = token;
|
|
43718
|
+
}
|
|
43719
|
+
return await workspaceAdd(explicit, { ...deps, raw: rawFlag });
|
|
43720
|
+
}
|
|
42642
43721
|
case "rm":
|
|
42643
43722
|
if (args.length !== 2 || !args[1])
|
|
42644
43723
|
return null;
|
|
@@ -42663,13 +43742,20 @@ async function workspaceList(print) {
|
|
|
42663
43742
|
}
|
|
42664
43743
|
async function workspaceAdd(rawName, deps) {
|
|
42665
43744
|
const cwd = normalizeWorkspacePath(deps.cwd());
|
|
42666
|
-
const
|
|
42667
|
-
if (
|
|
43745
|
+
const input = rawName === undefined ? basenameForWorkspacePath(cwd) : rawName.trim();
|
|
43746
|
+
if (input.length === 0) {
|
|
42668
43747
|
deps.print("工作区名称不能为空。");
|
|
42669
43748
|
return 1;
|
|
42670
43749
|
}
|
|
42671
43750
|
const store = await createCliConfigStore();
|
|
42672
43751
|
const config2 = await store.load();
|
|
43752
|
+
let name = input;
|
|
43753
|
+
if (!deps.raw && !isWorkspaceNameValid(input)) {
|
|
43754
|
+
const base = sanitizeWorkspaceName(input);
|
|
43755
|
+
name = allocateWorkspaceName(base, config2.workspaces);
|
|
43756
|
+
const sourceLabel = rawName === undefined ? "目录名" : "名称";
|
|
43757
|
+
deps.print(`${sourceLabel} ${JSON.stringify(input)} 含有特殊字符,已保存为「${name}」。如需保留原名请加 --raw。`);
|
|
43758
|
+
}
|
|
42673
43759
|
const existing = config2.workspaces[name];
|
|
42674
43760
|
if (existing) {
|
|
42675
43761
|
if (sameWorkspacePath(existing.cwd, cwd)) {
|
|
@@ -42677,7 +43763,7 @@ async function workspaceAdd(rawName, deps) {
|
|
|
42677
43763
|
return 0;
|
|
42678
43764
|
}
|
|
42679
43765
|
deps.print(`工作区「${name}」已存在,但路径不同:${existing.cwd}`);
|
|
42680
|
-
deps.print(`请换一个名称,或先执行:weacpx workspace rm ${name}`);
|
|
43766
|
+
deps.print(`请换一个名称,或先执行:weacpx workspace rm ${quoteWorkspaceNameIfNeeded(name)}`);
|
|
42681
43767
|
return 1;
|
|
42682
43768
|
}
|
|
42683
43769
|
await store.upsertWorkspace(name, cwd);
|