weacpx 0.4.8 → 0.4.10
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/bridge/bridge-main.js +20 -3
- package/dist/cli.js +1935 -590
- package/dist/config/types.d.ts +8 -0
- package/dist/orchestration/orchestration-types.d.ts +7 -1
- package/dist/util/private-file.d.ts +26 -0
- 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
|
@@ -1999,6 +1999,7 @@ var require_lib = __commonJS((exports, module) => {
|
|
|
1999
1999
|
|
|
2000
2000
|
// src/util/private-file.ts
|
|
2001
2001
|
import { chmod, mkdir, writeFile } from "node:fs/promises";
|
|
2002
|
+
import { chmodSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2002
2003
|
import { dirname } from "node:path";
|
|
2003
2004
|
async function writePrivateFileAtomic(path, content) {
|
|
2004
2005
|
await mkdir(dirname(path), { recursive: true });
|
|
@@ -2031,6 +2032,25 @@ async function writePrivateFileAtomic(path, content) {
|
|
|
2031
2032
|
await release();
|
|
2032
2033
|
}
|
|
2033
2034
|
}
|
|
2035
|
+
function writePrivateFileSync(path, content, deps = {}) {
|
|
2036
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2037
|
+
const platform = deps.platform ?? process.platform;
|
|
2038
|
+
const atomicWrite = deps.atomicWrite ?? ((p, c) => import_write_file_atomic.default.sync(p, c, { mode: PRIVATE_FILE_MODE, encoding: "utf8", fsync: true }));
|
|
2039
|
+
try {
|
|
2040
|
+
atomicWrite(path, content);
|
|
2041
|
+
} catch (error) {
|
|
2042
|
+
if (!isTransientWriteError(error, platform)) {
|
|
2043
|
+
throw error;
|
|
2044
|
+
}
|
|
2045
|
+
const directWrite = deps.directWrite ?? ((p, c) => {
|
|
2046
|
+
writeFileSync(p, c, { encoding: "utf8", mode: PRIVATE_FILE_MODE });
|
|
2047
|
+
try {
|
|
2048
|
+
chmodSync(p, PRIVATE_FILE_MODE);
|
|
2049
|
+
} catch {}
|
|
2050
|
+
});
|
|
2051
|
+
directWrite(path, content);
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2034
2054
|
async function retryTransientWriteErrors(run, options = {}) {
|
|
2035
2055
|
const platform = options.platform ?? process.platform;
|
|
2036
2056
|
const maxAttempts = options.maxAttempts ?? WRITE_RETRY_MAX_ATTEMPTS;
|
|
@@ -2206,6 +2226,9 @@ function parseConfig(raw, options = {}) {
|
|
|
2206
2226
|
throw new Error("transport.permissionPolicy must be a non-empty string");
|
|
2207
2227
|
}
|
|
2208
2228
|
}
|
|
2229
|
+
if ("queueOwnerTtlSeconds" in transport && (typeof transport.queueOwnerTtlSeconds !== "number" || !Number.isFinite(transport.queueOwnerTtlSeconds) || transport.queueOwnerTtlSeconds < 0)) {
|
|
2230
|
+
throw new Error("transport.queueOwnerTtlSeconds must be a non-negative number (0 = keep alive forever)");
|
|
2231
|
+
}
|
|
2209
2232
|
if (!isRecord(raw.agents)) {
|
|
2210
2233
|
throw new Error("agents must be an object");
|
|
2211
2234
|
}
|
|
@@ -2302,7 +2325,8 @@ function parseConfig(raw, options = {}) {
|
|
|
2302
2325
|
...typeof transport.permissionPolicy === "string" ? { permissionPolicy: transport.permissionPolicy } : {},
|
|
2303
2326
|
type: transportType,
|
|
2304
2327
|
permissionMode,
|
|
2305
|
-
nonInteractivePermissions
|
|
2328
|
+
nonInteractivePermissions,
|
|
2329
|
+
queueOwnerTtlSeconds: typeof transport.queueOwnerTtlSeconds === "number" ? transport.queueOwnerTtlSeconds : DEFAULT_QUEUE_OWNER_TTL_SECONDS
|
|
2306
2330
|
},
|
|
2307
2331
|
logging: {
|
|
2308
2332
|
level: resolvedLoggingLevel,
|
|
@@ -2424,10 +2448,11 @@ function parseOrchestrationConfig(raw) {
|
|
|
2424
2448
|
allowWorkerChainedRequests: raw.allowWorkerChainedRequests === true,
|
|
2425
2449
|
allowedAgentRequestTargets: Array.isArray(raw.allowedAgentRequestTargets) ? raw.allowedAgentRequestTargets.filter((value) => typeof value === "string") : [...DEFAULT_ORCHESTRATION_CONFIG.allowedAgentRequestTargets],
|
|
2426
2450
|
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
|
|
2451
|
+
progressHeartbeatSeconds: typeof raw.progressHeartbeatSeconds === "number" && Number.isFinite(raw.progressHeartbeatSeconds) ? raw.progressHeartbeatSeconds : DEFAULT_ORCHESTRATION_CONFIG.progressHeartbeatSeconds,
|
|
2452
|
+
maxParallelTasksPerAgent: typeof raw.maxParallelTasksPerAgent === "number" && Number.isFinite(raw.maxParallelTasksPerAgent) && raw.maxParallelTasksPerAgent >= 1 ? Math.floor(raw.maxParallelTasksPerAgent) : DEFAULT_ORCHESTRATION_CONFIG.maxParallelTasksPerAgent
|
|
2428
2453
|
};
|
|
2429
2454
|
}
|
|
2430
|
-
var DEFAULT_PERF_LOG_CONFIG, DEFAULT_LOGGING_CONFIG, DEFAULT_PERMISSION_MODE = "approve-all", DEFAULT_NON_INTERACTIVE_PERMISSIONS = "deny", DEFAULT_CHANNEL_CONFIG, DEFAULT_ORCHESTRATION_CONFIG;
|
|
2455
|
+
var DEFAULT_PERF_LOG_CONFIG, DEFAULT_LOGGING_CONFIG, DEFAULT_PERMISSION_MODE = "approve-all", DEFAULT_NON_INTERACTIVE_PERMISSIONS = "deny", DEFAULT_QUEUE_OWNER_TTL_SECONDS = 1800, DEFAULT_CHANNEL_CONFIG, DEFAULT_ORCHESTRATION_CONFIG;
|
|
2431
2456
|
var init_load_config = __esm(() => {
|
|
2432
2457
|
init_workspace_path();
|
|
2433
2458
|
DEFAULT_PERF_LOG_CONFIG = {
|
|
@@ -2452,7 +2477,8 @@ var init_load_config = __esm(() => {
|
|
|
2452
2477
|
allowWorkerChainedRequests: false,
|
|
2453
2478
|
allowedAgentRequestTargets: [],
|
|
2454
2479
|
allowedAgentRequestRoles: [],
|
|
2455
|
-
progressHeartbeatSeconds: 300
|
|
2480
|
+
progressHeartbeatSeconds: 300,
|
|
2481
|
+
maxParallelTasksPerAgent: 3
|
|
2456
2482
|
};
|
|
2457
2483
|
});
|
|
2458
2484
|
|
|
@@ -2727,7 +2753,7 @@ class DaemonStatusStore {
|
|
|
2727
2753
|
var init_daemon_status = () => {};
|
|
2728
2754
|
|
|
2729
2755
|
// src/daemon/daemon-controller.ts
|
|
2730
|
-
import { mkdir as mkdir3, readFile as readFile4, rm as rm2
|
|
2756
|
+
import { mkdir as mkdir3, open, readFile as readFile4, rm as rm2 } from "node:fs/promises";
|
|
2731
2757
|
import { dirname as dirname3 } from "node:path";
|
|
2732
2758
|
|
|
2733
2759
|
class DaemonController {
|
|
@@ -2786,9 +2812,19 @@ class DaemonController {
|
|
|
2786
2812
|
if (current.state === "indeterminate") {
|
|
2787
2813
|
throw new Error(`weacpx daemon process is already running (pid ${current.pid}) but status metadata is missing`);
|
|
2788
2814
|
}
|
|
2789
|
-
await this.
|
|
2790
|
-
|
|
2791
|
-
|
|
2815
|
+
const pidHandle = await this.openPidFileExclusive();
|
|
2816
|
+
let pid;
|
|
2817
|
+
try {
|
|
2818
|
+
await this.statusStore.clear();
|
|
2819
|
+
pid = await this.deps.spawnDetached(options);
|
|
2820
|
+
await pidHandle.write(`${pid}
|
|
2821
|
+
`);
|
|
2822
|
+
} catch (error) {
|
|
2823
|
+
await pidHandle.close().catch(() => {});
|
|
2824
|
+
await rm2(this.paths.pidFile, { force: true }).catch(() => {});
|
|
2825
|
+
throw error;
|
|
2826
|
+
}
|
|
2827
|
+
await pidHandle.close();
|
|
2792
2828
|
await this.waitForStartupMetadata(pid, options.firstRunOnboarding ? this.onboardingStartupTimeoutMs : this.startupTimeoutMs, options.startupWait);
|
|
2793
2829
|
return { state: "started", pid };
|
|
2794
2830
|
}
|
|
@@ -2816,10 +2852,16 @@ class DaemonController {
|
|
|
2816
2852
|
throw error;
|
|
2817
2853
|
}
|
|
2818
2854
|
}
|
|
2819
|
-
async
|
|
2855
|
+
async openPidFileExclusive() {
|
|
2820
2856
|
await mkdir3(dirname3(this.paths.pidFile), { recursive: true });
|
|
2821
|
-
|
|
2822
|
-
|
|
2857
|
+
try {
|
|
2858
|
+
return await open(this.paths.pidFile, "wx", 384);
|
|
2859
|
+
} catch (error) {
|
|
2860
|
+
if (error.code === "EEXIST") {
|
|
2861
|
+
throw new Error(`weacpx daemon pid file already exists (${this.paths.pidFile}); another start may be in progress`);
|
|
2862
|
+
}
|
|
2863
|
+
throw error;
|
|
2864
|
+
}
|
|
2823
2865
|
}
|
|
2824
2866
|
async clearRuntimeFiles() {
|
|
2825
2867
|
await rm2(this.paths.pidFile, { force: true });
|
|
@@ -2916,15 +2958,17 @@ async function defaultRunProcessCommand(command, args) {
|
|
|
2916
2958
|
var init_terminate_process_tree = () => {};
|
|
2917
2959
|
|
|
2918
2960
|
// src/daemon/create-daemon-controller.ts
|
|
2919
|
-
import { mkdir as mkdir4, open } from "node:fs/promises";
|
|
2961
|
+
import { mkdir as mkdir4, open as open2 } from "node:fs/promises";
|
|
2920
2962
|
import { spawn as spawn2 } from "node:child_process";
|
|
2921
2963
|
function createDaemonController(paths, options) {
|
|
2922
2964
|
return new DaemonController(paths, {
|
|
2923
2965
|
isProcessRunning: options.isProcessRunning ?? defaultIsProcessRunning2,
|
|
2924
2966
|
spawnDetached: async (spawnOptions) => {
|
|
2925
2967
|
await mkdir4(paths.runtimeDir, { recursive: true });
|
|
2926
|
-
const stdoutHandle = await
|
|
2927
|
-
const stderrHandle = await
|
|
2968
|
+
const stdoutHandle = await open2(paths.stdoutLog, "a", 384);
|
|
2969
|
+
const stderrHandle = await open2(paths.stderrLog, "a", 384);
|
|
2970
|
+
await stdoutHandle.chmod(384).catch(() => {});
|
|
2971
|
+
await stderrHandle.chmod(384).catch(() => {});
|
|
2928
2972
|
try {
|
|
2929
2973
|
return await (options.spawnProcess ?? defaultSpawnProcess)(buildSpawnRequest(paths, options, stdoutHandle.fd, stderrHandle.fd, spawnOptions));
|
|
2930
2974
|
} finally {
|
|
@@ -9645,6 +9689,34 @@ var init_quota_errors = __esm(() => {
|
|
|
9645
9689
|
};
|
|
9646
9690
|
});
|
|
9647
9691
|
|
|
9692
|
+
// src/commands/workspace-name.ts
|
|
9693
|
+
function sanitizeWorkspaceName(input, fallback = "workspace") {
|
|
9694
|
+
const sanitized = input.trim().replace(UNSAFE_RUN_RE, "-").replace(TRIM_DASHES_RE, "");
|
|
9695
|
+
return sanitized.length > 0 ? sanitized : fallback;
|
|
9696
|
+
}
|
|
9697
|
+
function allocateWorkspaceName(base, existing) {
|
|
9698
|
+
if (!Object.prototype.hasOwnProperty.call(existing, base))
|
|
9699
|
+
return base;
|
|
9700
|
+
let suffix = 2;
|
|
9701
|
+
while (Object.prototype.hasOwnProperty.call(existing, `${base}-${suffix}`))
|
|
9702
|
+
suffix += 1;
|
|
9703
|
+
return `${base}-${suffix}`;
|
|
9704
|
+
}
|
|
9705
|
+
function isWorkspaceNameValid(input) {
|
|
9706
|
+
return VALID_WORKSPACE_NAME_RE.test(input);
|
|
9707
|
+
}
|
|
9708
|
+
function quoteWorkspaceNameIfNeeded(input) {
|
|
9709
|
+
if (isWorkspaceNameValid(input))
|
|
9710
|
+
return input;
|
|
9711
|
+
return `"${input.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
|
|
9712
|
+
}
|
|
9713
|
+
var VALID_WORKSPACE_NAME_RE, UNSAFE_RUN_RE, TRIM_DASHES_RE;
|
|
9714
|
+
var init_workspace_name = __esm(() => {
|
|
9715
|
+
VALID_WORKSPACE_NAME_RE = /^[a-zA-Z0-9._-]+$/;
|
|
9716
|
+
UNSAFE_RUN_RE = /[^a-zA-Z0-9._-]+/g;
|
|
9717
|
+
TRIM_DASHES_RE = /^-+|-+$/g;
|
|
9718
|
+
});
|
|
9719
|
+
|
|
9648
9720
|
// src/orchestration/orchestration-types.ts
|
|
9649
9721
|
function createEmptyOrchestrationState() {
|
|
9650
9722
|
return {
|
|
@@ -9683,7 +9755,7 @@ function isOptionalBoolean(value) {
|
|
|
9683
9755
|
return value === undefined || typeof value === "boolean";
|
|
9684
9756
|
}
|
|
9685
9757
|
function isTaskStatus(value) {
|
|
9686
|
-
return value === "needs_confirmation" || value === "running" || value === "blocked" || value === "waiting_for_human" || value === "completed" || value === "failed" || value === "cancelled";
|
|
9758
|
+
return value === "needs_confirmation" || value === "queued" || value === "running" || value === "blocked" || value === "waiting_for_human" || value === "completed" || value === "failed" || value === "cancelled";
|
|
9687
9759
|
}
|
|
9688
9760
|
function isSourceKind(value) {
|
|
9689
9761
|
return value === "human" || value === "coordinator" || value === "worker";
|
|
@@ -9719,7 +9791,7 @@ function isTaskRecord(value) {
|
|
|
9719
9791
|
if (!isRecord2(value)) {
|
|
9720
9792
|
return false;
|
|
9721
9793
|
}
|
|
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));
|
|
9794
|
+
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
9795
|
}
|
|
9724
9796
|
function isExternalCoordinatorRecord(value) {
|
|
9725
9797
|
if (!isRecord2(value)) {
|
|
@@ -9731,7 +9803,7 @@ function isWorkerBindingRecord(value) {
|
|
|
9731
9803
|
if (!isRecord2(value)) {
|
|
9732
9804
|
return false;
|
|
9733
9805
|
}
|
|
9734
|
-
return isString(value.sourceHandle) && isString(value.coordinatorSession) && isString(value.workspace) && isOptionalString(value.cwd) && isString(value.targetAgent) && isOptionalString(value.role);
|
|
9806
|
+
return isString(value.sourceHandle) && isString(value.coordinatorSession) && isString(value.workspace) && isOptionalString(value.cwd) && isString(value.targetAgent) && isOptionalString(value.role) && isOptionalBoolean(value.ephemeral);
|
|
9735
9807
|
}
|
|
9736
9808
|
function isGroupRecord(value) {
|
|
9737
9809
|
if (!isRecord2(value)) {
|
|
@@ -9984,20 +10056,33 @@ var init_state_store = __esm(() => {
|
|
|
9984
10056
|
});
|
|
9985
10057
|
|
|
9986
10058
|
// src/plugins/plugin-home.ts
|
|
9987
|
-
import { mkdir as mkdir6, writeFile as
|
|
10059
|
+
import { mkdir as mkdir6, writeFile as writeFile4 } from "node:fs/promises";
|
|
9988
10060
|
import { homedir as homedir3 } from "node:os";
|
|
9989
10061
|
import { join as join3 } from "node:path";
|
|
10062
|
+
function coerceMissing(value) {
|
|
10063
|
+
if (value === undefined)
|
|
10064
|
+
return;
|
|
10065
|
+
const trimmed = value.trim();
|
|
10066
|
+
if (!trimmed)
|
|
10067
|
+
return;
|
|
10068
|
+
const lower = trimmed.toLowerCase();
|
|
10069
|
+
if (lower === "undefined" || lower === "null")
|
|
10070
|
+
return;
|
|
10071
|
+
return trimmed;
|
|
10072
|
+
}
|
|
9990
10073
|
function resolvePluginHome(input = {}) {
|
|
9991
|
-
|
|
9992
|
-
|
|
9993
|
-
|
|
9994
|
-
|
|
9995
|
-
|
|
10074
|
+
const explicit = coerceMissing(input.pluginHome);
|
|
10075
|
+
if (explicit)
|
|
10076
|
+
return explicit;
|
|
10077
|
+
const envOverride = coerceMissing(process.env.WEACPX_PLUGIN_HOME);
|
|
10078
|
+
if (envOverride)
|
|
10079
|
+
return envOverride;
|
|
10080
|
+
const home = coerceMissing(input.home) ?? coerceMissing(process.env.HOME) ?? homedir3();
|
|
9996
10081
|
return join3(home, ".weacpx", "plugins");
|
|
9997
10082
|
}
|
|
9998
10083
|
async function ensurePluginHome(pluginHome) {
|
|
9999
10084
|
await mkdir6(pluginHome, { recursive: true, mode: 448 });
|
|
10000
|
-
await
|
|
10085
|
+
await writeFile4(join3(pluginHome, "package.json"), JSON.stringify({ private: true, type: "module" }, null, 2) + `
|
|
10001
10086
|
`, { flag: "wx" }).catch((error2) => {
|
|
10002
10087
|
if (error2.code !== "EEXIST")
|
|
10003
10088
|
throw error2;
|
|
@@ -10136,8 +10221,6 @@ function loadWeixinAccount(accountId) {
|
|
|
10136
10221
|
return null;
|
|
10137
10222
|
}
|
|
10138
10223
|
function saveWeixinAccount(accountId, update) {
|
|
10139
|
-
const dir = resolveAccountsDir();
|
|
10140
|
-
ensureDirSync(dir);
|
|
10141
10224
|
const existing = loadWeixinAccount(accountId) ?? {};
|
|
10142
10225
|
const token = update.token?.trim() || existing.token;
|
|
10143
10226
|
const baseUrl = update.baseUrl?.trim() || existing.baseUrl;
|
|
@@ -10147,11 +10230,7 @@ function saveWeixinAccount(accountId, update) {
|
|
|
10147
10230
|
...baseUrl ? { baseUrl } : {},
|
|
10148
10231
|
...userId ? { userId } : {}
|
|
10149
10232
|
};
|
|
10150
|
-
|
|
10151
|
-
fs3.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
|
|
10152
|
-
try {
|
|
10153
|
-
fs3.chmodSync(filePath, 384);
|
|
10154
|
-
} catch {}
|
|
10233
|
+
writePrivateFileSync(resolveAccountPath(accountId), JSON.stringify(data, null, 2));
|
|
10155
10234
|
}
|
|
10156
10235
|
function clearWeixinAccount(accountId) {
|
|
10157
10236
|
try {
|
|
@@ -10202,6 +10281,28 @@ function loadConfigRouteTag(accountId) {
|
|
|
10202
10281
|
return;
|
|
10203
10282
|
}
|
|
10204
10283
|
}
|
|
10284
|
+
function loadConfigBotAgent(accountId) {
|
|
10285
|
+
try {
|
|
10286
|
+
const configPath = resolveConfigPath();
|
|
10287
|
+
if (!fs3.existsSync(configPath))
|
|
10288
|
+
return;
|
|
10289
|
+
const raw = fs3.readFileSync(configPath, "utf-8");
|
|
10290
|
+
const cfg = JSON.parse(raw);
|
|
10291
|
+
const channels = cfg.channels;
|
|
10292
|
+
const section = channels?.["openclaw-weixin"];
|
|
10293
|
+
if (!section)
|
|
10294
|
+
return;
|
|
10295
|
+
if (accountId) {
|
|
10296
|
+
const accounts = section.accounts;
|
|
10297
|
+
const agent = accounts?.[accountId]?.botAgent;
|
|
10298
|
+
if (typeof agent === "string" && agent.trim())
|
|
10299
|
+
return agent.trim();
|
|
10300
|
+
}
|
|
10301
|
+
return typeof section.botAgent === "string" && section.botAgent.trim() ? section.botAgent.trim() : undefined;
|
|
10302
|
+
} catch {
|
|
10303
|
+
return;
|
|
10304
|
+
}
|
|
10305
|
+
}
|
|
10205
10306
|
function listWeixinAccountIds() {
|
|
10206
10307
|
const indexed = listIndexedWeixinAccountIds();
|
|
10207
10308
|
if (indexed.length > 0)
|
|
@@ -10230,6 +10331,7 @@ var DEFAULT_BASE_URL = "https://ilinkai.weixin.qq.com", CDN_BASE_URL = "https://
|
|
|
10230
10331
|
var init_accounts = __esm(() => {
|
|
10231
10332
|
init_ensure_dir();
|
|
10232
10333
|
init_state_dir();
|
|
10334
|
+
init_private_file();
|
|
10233
10335
|
});
|
|
10234
10336
|
|
|
10235
10337
|
// src/weixin/util/logger.ts
|
|
@@ -10490,8 +10592,83 @@ var init_send_errors = __esm(() => {
|
|
|
10490
10592
|
|
|
10491
10593
|
// src/weixin/api/api.ts
|
|
10492
10594
|
import crypto from "node:crypto";
|
|
10595
|
+
function buildClientVersion(version2) {
|
|
10596
|
+
const parts = version2.split(".").map((p) => parseInt(p, 10));
|
|
10597
|
+
const major = parts[0] ?? 0;
|
|
10598
|
+
const minor = parts[1] ?? 0;
|
|
10599
|
+
const patch = parts[2] ?? 0;
|
|
10600
|
+
return (major & 255) << 16 | (minor & 255) << 8 | patch & 255;
|
|
10601
|
+
}
|
|
10602
|
+
function sanitizeBotAgent(raw) {
|
|
10603
|
+
if (!raw || typeof raw !== "string")
|
|
10604
|
+
return DEFAULT_BOT_AGENT;
|
|
10605
|
+
const trimmed = raw.trim();
|
|
10606
|
+
if (!trimmed)
|
|
10607
|
+
return DEFAULT_BOT_AGENT;
|
|
10608
|
+
const productRe = /^[A-Za-z0-9_.\-]{1,32}\/[A-Za-z0-9_.+\-]{1,32}$/;
|
|
10609
|
+
const commentCharRe = /^[\x20-\x27\x2A-\x7E]{1,64}$/;
|
|
10610
|
+
const rawTokens = trimmed.split(/\s+/);
|
|
10611
|
+
const tokens = [];
|
|
10612
|
+
for (let i = 0;i < rawTokens.length; i += 1) {
|
|
10613
|
+
const tok = rawTokens[i];
|
|
10614
|
+
if (tok.startsWith("(") && !tok.endsWith(")")) {
|
|
10615
|
+
let acc = tok;
|
|
10616
|
+
while (i + 1 < rawTokens.length && !acc.endsWith(")")) {
|
|
10617
|
+
i += 1;
|
|
10618
|
+
acc += " " + rawTokens[i];
|
|
10619
|
+
}
|
|
10620
|
+
tokens.push(acc);
|
|
10621
|
+
} else {
|
|
10622
|
+
tokens.push(tok);
|
|
10623
|
+
}
|
|
10624
|
+
}
|
|
10625
|
+
const accepted = [];
|
|
10626
|
+
let pendingProduct = null;
|
|
10627
|
+
for (const tok of tokens) {
|
|
10628
|
+
if (tok.startsWith("(") && tok.endsWith(")")) {
|
|
10629
|
+
const inner = tok.slice(1, -1);
|
|
10630
|
+
if (pendingProduct && commentCharRe.test(inner)) {
|
|
10631
|
+
accepted.push(`${pendingProduct} (${inner})`);
|
|
10632
|
+
pendingProduct = null;
|
|
10633
|
+
} else {
|
|
10634
|
+
if (pendingProduct) {
|
|
10635
|
+
accepted.push(pendingProduct);
|
|
10636
|
+
pendingProduct = null;
|
|
10637
|
+
}
|
|
10638
|
+
}
|
|
10639
|
+
continue;
|
|
10640
|
+
}
|
|
10641
|
+
if (pendingProduct) {
|
|
10642
|
+
accepted.push(pendingProduct);
|
|
10643
|
+
pendingProduct = null;
|
|
10644
|
+
}
|
|
10645
|
+
if (productRe.test(tok)) {
|
|
10646
|
+
pendingProduct = tok;
|
|
10647
|
+
}
|
|
10648
|
+
}
|
|
10649
|
+
if (pendingProduct)
|
|
10650
|
+
accepted.push(pendingProduct);
|
|
10651
|
+
if (accepted.length === 0)
|
|
10652
|
+
return DEFAULT_BOT_AGENT;
|
|
10653
|
+
const joined = accepted.join(" ");
|
|
10654
|
+
if (Buffer.byteLength(joined, "utf-8") <= BOT_AGENT_MAX_LEN)
|
|
10655
|
+
return joined;
|
|
10656
|
+
const truncated = [];
|
|
10657
|
+
let len = 0;
|
|
10658
|
+
for (const t of accepted) {
|
|
10659
|
+
const add = (truncated.length === 0 ? 0 : 1) + Buffer.byteLength(t, "utf-8");
|
|
10660
|
+
if (len + add > BOT_AGENT_MAX_LEN)
|
|
10661
|
+
break;
|
|
10662
|
+
truncated.push(t);
|
|
10663
|
+
len += add;
|
|
10664
|
+
}
|
|
10665
|
+
return truncated.length > 0 ? truncated.join(" ") : DEFAULT_BOT_AGENT;
|
|
10666
|
+
}
|
|
10493
10667
|
function buildBaseInfo() {
|
|
10494
|
-
return {
|
|
10668
|
+
return {
|
|
10669
|
+
channel_version: CHANNEL_VERSION,
|
|
10670
|
+
bot_agent: sanitizeBotAgent(loadConfigBotAgent())
|
|
10671
|
+
};
|
|
10495
10672
|
}
|
|
10496
10673
|
function ensureTrailingSlash(url) {
|
|
10497
10674
|
return url.endsWith("/") ? url : `${url}/`;
|
|
@@ -10502,6 +10679,9 @@ function randomWechatUin() {
|
|
|
10502
10679
|
}
|
|
10503
10680
|
function buildCommonHeaders() {
|
|
10504
10681
|
const headers = {};
|
|
10682
|
+
if (ILINK_APP_ID)
|
|
10683
|
+
headers["iLink-App-Id"] = ILINK_APP_ID;
|
|
10684
|
+
headers["iLink-App-ClientVersion"] = String(ILINK_APP_CLIENT_VERSION);
|
|
10505
10685
|
const routeTag = loadConfigRouteTag();
|
|
10506
10686
|
if (routeTag) {
|
|
10507
10687
|
headers.SKRouteTag = routeTag;
|
|
@@ -10547,6 +10727,35 @@ async function apiGetFetch(params) {
|
|
|
10547
10727
|
throw err;
|
|
10548
10728
|
}
|
|
10549
10729
|
}
|
|
10730
|
+
async function apiPostFetch(params) {
|
|
10731
|
+
const base = ensureTrailingSlash(params.baseUrl);
|
|
10732
|
+
const url = new URL(params.endpoint, base);
|
|
10733
|
+
const hdrs = buildCommonHeaders();
|
|
10734
|
+
hdrs["Content-Type"] = "application/json";
|
|
10735
|
+
logger.debug(`POST ${redactUrl(url.toString())} body=${redactBody(params.body)}`);
|
|
10736
|
+
const controller = new AbortController;
|
|
10737
|
+
const t = params.timeoutMs !== undefined ? setTimeout(() => controller.abort(), params.timeoutMs) : undefined;
|
|
10738
|
+
const onAbort = () => controller.abort();
|
|
10739
|
+
params.abortSignal?.addEventListener("abort", onAbort, { once: true });
|
|
10740
|
+
try {
|
|
10741
|
+
const res = await fetch(url.toString(), {
|
|
10742
|
+
method: "POST",
|
|
10743
|
+
headers: hdrs,
|
|
10744
|
+
body: params.body,
|
|
10745
|
+
signal: controller.signal
|
|
10746
|
+
});
|
|
10747
|
+
const rawText = await res.text();
|
|
10748
|
+
logger.debug(`${params.label} status=${res.status} raw=${redactBody(rawText)}`);
|
|
10749
|
+
if (!res.ok) {
|
|
10750
|
+
throw new Error(`${params.label} ${res.status}: ${rawText}`);
|
|
10751
|
+
}
|
|
10752
|
+
return rawText;
|
|
10753
|
+
} finally {
|
|
10754
|
+
if (t !== undefined)
|
|
10755
|
+
clearTimeout(t);
|
|
10756
|
+
params.abortSignal?.removeEventListener("abort", onAbort);
|
|
10757
|
+
}
|
|
10758
|
+
}
|
|
10550
10759
|
async function apiFetch(params) {
|
|
10551
10760
|
const base = ensureTrailingSlash(params.baseUrl);
|
|
10552
10761
|
const url = new URL(params.endpoint, base);
|
|
@@ -10705,7 +10914,7 @@ async function sendTyping(params) {
|
|
|
10705
10914
|
label: "sendTyping"
|
|
10706
10915
|
});
|
|
10707
10916
|
}
|
|
10708
|
-
var CHANNEL_VERSION, DEFAULT_LONG_POLL_TIMEOUT_MS = 35000, DEFAULT_API_TIMEOUT_MS = 15000, DEFAULT_CONFIG_TIMEOUT_MS = 1e4;
|
|
10917
|
+
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
10918
|
var init_api = __esm(() => {
|
|
10710
10919
|
init_version();
|
|
10711
10920
|
init_accounts();
|
|
@@ -10713,6 +10922,8 @@ var init_api = __esm(() => {
|
|
|
10713
10922
|
init_redact();
|
|
10714
10923
|
init_send_errors();
|
|
10715
10924
|
CHANNEL_VERSION = readVersion();
|
|
10925
|
+
ILINK_APP_CLIENT_VERSION = buildClientVersion(CHANNEL_VERSION);
|
|
10926
|
+
ILINK_APP_ID = (process.env.WEACPX_ILINK_APP_ID ?? "").trim();
|
|
10716
10927
|
});
|
|
10717
10928
|
|
|
10718
10929
|
// node_modules/qrcode-terminal/vendor/QRCode/QRMode.js
|
|
@@ -11744,22 +11955,47 @@ function purgeExpiredLogins() {
|
|
|
11744
11955
|
}
|
|
11745
11956
|
}
|
|
11746
11957
|
}
|
|
11958
|
+
function getLocalBotTokenList() {
|
|
11959
|
+
const accountIds = listIndexedWeixinAccountIds();
|
|
11960
|
+
const tokens = [];
|
|
11961
|
+
for (let i = accountIds.length - 1;i >= 0 && tokens.length < 10; i--) {
|
|
11962
|
+
const accountId = accountIds[i];
|
|
11963
|
+
if (!accountId)
|
|
11964
|
+
continue;
|
|
11965
|
+
const data = loadWeixinAccount(accountId);
|
|
11966
|
+
const token = data?.token?.trim();
|
|
11967
|
+
if (token) {
|
|
11968
|
+
tokens.push(token);
|
|
11969
|
+
}
|
|
11970
|
+
}
|
|
11971
|
+
return tokens;
|
|
11972
|
+
}
|
|
11747
11973
|
async function fetchQRCode(apiBaseUrl, botType) {
|
|
11748
11974
|
logger.info(`Fetching QR code from: ${apiBaseUrl} bot_type=${botType}`);
|
|
11749
|
-
const
|
|
11975
|
+
const localTokenList = getLocalBotTokenList();
|
|
11976
|
+
logger.info(`fetchQRCode: local_token_list count=${localTokenList.length}`);
|
|
11977
|
+
const rawText = await apiPostFetch({
|
|
11750
11978
|
baseUrl: apiBaseUrl,
|
|
11751
11979
|
endpoint: `ilink/bot/get_bot_qrcode?bot_type=${encodeURIComponent(botType)}`,
|
|
11752
|
-
|
|
11980
|
+
body: JSON.stringify({ local_token_list: localTokenList }),
|
|
11753
11981
|
label: "fetchQRCode"
|
|
11754
11982
|
});
|
|
11755
11983
|
return JSON.parse(rawText);
|
|
11756
11984
|
}
|
|
11757
|
-
|
|
11758
|
-
|
|
11985
|
+
function buildPollQRStatusEndpoint(qrcode, verifyCode) {
|
|
11986
|
+
let endpoint = `ilink/bot/get_qrcode_status?qrcode=${encodeURIComponent(qrcode)}`;
|
|
11987
|
+
if (verifyCode) {
|
|
11988
|
+
endpoint += `&verify_code=${encodeURIComponent(verifyCode)}`;
|
|
11989
|
+
}
|
|
11990
|
+
return endpoint;
|
|
11991
|
+
}
|
|
11992
|
+
async function pollQRStatus(apiBaseUrl, qrcode, verifyCode) {
|
|
11993
|
+
logger.debug(`Long-poll QR status from: ${apiBaseUrl} qrcode=*** hasVerifyCode=${Boolean(verifyCode)}`);
|
|
11994
|
+
const endpoint = buildPollQRStatusEndpoint(qrcode, verifyCode);
|
|
11759
11995
|
try {
|
|
11760
11996
|
const rawText = await apiGetFetch({
|
|
11761
11997
|
baseUrl: apiBaseUrl,
|
|
11762
|
-
endpoint
|
|
11998
|
+
endpoint,
|
|
11763
11999
|
timeoutMs: QR_LONG_POLL_TIMEOUT_MS,
|
|
11764
12000
|
label: "pollQRStatus"
|
|
11765
12001
|
});
|
|
@@ -11774,6 +12010,28 @@ async function pollQRStatus(apiBaseUrl, qrcode) {
|
|
|
11774
12010
|
return { status: "wait" };
|
|
11775
12011
|
}
|
|
11776
12012
|
}
|
|
12013
|
+
async function readVerifyCodeFromStdin(prompt) {
|
|
12014
|
+
if (!process.stdin.isTTY) {
|
|
12015
|
+
throw new Error("verify code requested but stdin is not a TTY (running in daemon mode?)");
|
|
12016
|
+
}
|
|
12017
|
+
process.stdout.write(prompt);
|
|
12018
|
+
return new Promise((resolve) => {
|
|
12019
|
+
let input = "";
|
|
12020
|
+
const onData = (chunk) => {
|
|
12021
|
+
const str = chunk.toString();
|
|
12022
|
+
input += str;
|
|
12023
|
+
if (input.includes(`
|
|
12024
|
+
`)) {
|
|
12025
|
+
process.stdin.removeListener("data", onData);
|
|
12026
|
+
process.stdin.pause();
|
|
12027
|
+
resolve(input.trim());
|
|
12028
|
+
}
|
|
12029
|
+
};
|
|
12030
|
+
process.stdin.resume();
|
|
12031
|
+
process.stdin.setEncoding("utf-8");
|
|
12032
|
+
process.stdin.on("data", onData);
|
|
12033
|
+
});
|
|
12034
|
+
}
|
|
11777
12035
|
async function startWeixinLoginWithQr(opts) {
|
|
11778
12036
|
const sessionKey = opts.accountId || randomUUID2();
|
|
11779
12037
|
purgeExpiredLogins();
|
|
@@ -11812,6 +12070,36 @@ async function startWeixinLoginWithQr(opts) {
|
|
|
11812
12070
|
};
|
|
11813
12071
|
}
|
|
11814
12072
|
}
|
|
12073
|
+
async function refreshQRCode(activeLogin, botType, qrRefreshCount, onScannedReset) {
|
|
12074
|
+
try {
|
|
12075
|
+
const qrResponse = await fetchQRCode(FIXED_BASE_URL, botType);
|
|
12076
|
+
activeLogin.qrcode = qrResponse.qrcode;
|
|
12077
|
+
activeLogin.qrcodeUrl = qrResponse.qrcode_img_content;
|
|
12078
|
+
activeLogin.startedAt = Date.now();
|
|
12079
|
+
onScannedReset();
|
|
12080
|
+
logger.info(`refreshQRCode: new QR code obtained (${qrRefreshCount}/${MAX_QR_REFRESH_COUNT}) qrcode=${redactToken(qrResponse.qrcode)}`);
|
|
12081
|
+
process.stdout.write(`\uD83D\uDD04 新二维码已生成,请重新扫描
|
|
12082
|
+
|
|
12083
|
+
`);
|
|
12084
|
+
try {
|
|
12085
|
+
const qrterm = await Promise.resolve().then(() => __toESM(require_main(), 1));
|
|
12086
|
+
qrterm.default.generate(qrResponse.qrcode_img_content, { small: true });
|
|
12087
|
+
process.stdout.write(`如果二维码未能成功展示,请用浏览器打开以下链接扫码:
|
|
12088
|
+
`);
|
|
12089
|
+
process.stdout.write(`${qrResponse.qrcode_img_content}
|
|
12090
|
+
`);
|
|
12091
|
+
} catch {
|
|
12092
|
+
process.stdout.write(`二维码未加载成功,请用浏览器打开以下链接扫码:
|
|
12093
|
+
`);
|
|
12094
|
+
process.stdout.write(`${qrResponse.qrcode_img_content}
|
|
12095
|
+
`);
|
|
12096
|
+
}
|
|
12097
|
+
return { success: true };
|
|
12098
|
+
} catch (refreshErr) {
|
|
12099
|
+
logger.error(`refreshQRCode: failed to refresh QR code: ${String(refreshErr)}`);
|
|
12100
|
+
return { success: false, message: `刷新二维码失败: ${String(refreshErr)}` };
|
|
12101
|
+
}
|
|
12102
|
+
}
|
|
11815
12103
|
async function waitForWeixinLogin(opts) {
|
|
11816
12104
|
let activeLogin = activeLogins.get(opts.sessionKey);
|
|
11817
12105
|
if (!activeLogin) {
|
|
@@ -11838,7 +12126,7 @@ async function waitForWeixinLogin(opts) {
|
|
|
11838
12126
|
while (Date.now() < deadline) {
|
|
11839
12127
|
try {
|
|
11840
12128
|
const currentBaseUrl = activeLogin.currentApiBaseUrl ?? FIXED_BASE_URL;
|
|
11841
|
-
const statusResponse = await pollQRStatus(currentBaseUrl, activeLogin.qrcode);
|
|
12129
|
+
const statusResponse = await pollQRStatus(currentBaseUrl, activeLogin.qrcode, activeLogin.pendingVerifyCode);
|
|
11842
12130
|
logger.debug(`pollQRStatus: status=${statusResponse.status} hasBotToken=${Boolean(statusResponse.bot_token)} hasBotId=${Boolean(statusResponse.ilink_bot_id)}`);
|
|
11843
12131
|
activeLogin.status = statusResponse.status;
|
|
11844
12132
|
switch (statusResponse.status) {
|
|
@@ -11848,6 +12136,10 @@ async function waitForWeixinLogin(opts) {
|
|
|
11848
12136
|
}
|
|
11849
12137
|
break;
|
|
11850
12138
|
case "scaned":
|
|
12139
|
+
if (activeLogin.pendingVerifyCode) {
|
|
12140
|
+
logger.info("verify code accepted, resuming polling");
|
|
12141
|
+
activeLogin.pendingVerifyCode = undefined;
|
|
12142
|
+
}
|
|
11851
12143
|
if (!scannedPrinted) {
|
|
11852
12144
|
process.stdout.write(`
|
|
11853
12145
|
\uD83D\uDC40 已扫码,在微信继续操作...
|
|
@@ -11856,6 +12148,7 @@ async function waitForWeixinLogin(opts) {
|
|
|
11856
12148
|
}
|
|
11857
12149
|
break;
|
|
11858
12150
|
case "expired": {
|
|
12151
|
+
activeLogin.pendingVerifyCode = undefined;
|
|
11859
12152
|
qrRefreshCount++;
|
|
11860
12153
|
if (qrRefreshCount > MAX_QR_REFRESH_COUNT) {
|
|
11861
12154
|
logger.warn(`waitForWeixinLogin: QR expired ${MAX_QR_REFRESH_COUNT} times, giving up sessionKey=${opts.sessionKey}`);
|
|
@@ -11869,36 +12162,14 @@ async function waitForWeixinLogin(opts) {
|
|
|
11869
12162
|
⏳ 二维码已过期,正在刷新...(${qrRefreshCount}/${MAX_QR_REFRESH_COUNT})
|
|
11870
12163
|
`);
|
|
11871
12164
|
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();
|
|
12165
|
+
const expiredRefreshResult = await refreshQRCode(activeLogin, opts.botType || DEFAULT_ILINK_BOT_TYPE, qrRefreshCount, () => {
|
|
11878
12166
|
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)}`);
|
|
12167
|
+
});
|
|
12168
|
+
if (!expiredRefreshResult.success) {
|
|
11898
12169
|
activeLogins.delete(opts.sessionKey);
|
|
11899
12170
|
return {
|
|
11900
12171
|
connected: false,
|
|
11901
|
-
message:
|
|
12172
|
+
message: expiredRefreshResult.message
|
|
11902
12173
|
};
|
|
11903
12174
|
}
|
|
11904
12175
|
break;
|
|
@@ -11914,6 +12185,49 @@ async function waitForWeixinLogin(opts) {
|
|
|
11914
12185
|
}
|
|
11915
12186
|
break;
|
|
11916
12187
|
}
|
|
12188
|
+
case "need_verifycode": {
|
|
12189
|
+
const verifyPrompt = activeLogin.pendingVerifyCode ? "❌ 你输入的数字不匹配,请重新输入:" : "输入手机微信显示的数字,以继续连接:";
|
|
12190
|
+
let code;
|
|
12191
|
+
try {
|
|
12192
|
+
code = await readVerifyCodeFromStdin(verifyPrompt);
|
|
12193
|
+
} catch (err) {
|
|
12194
|
+
logger.error(`waitForWeixinLogin: cannot read verify code (no TTY): ${String(err)}`);
|
|
12195
|
+
activeLogins.delete(opts.sessionKey);
|
|
12196
|
+
return {
|
|
12197
|
+
connected: false,
|
|
12198
|
+
message: "需要输入配对码,但当前环境没有交互式终端。请在前台运行 `weacpx login` 完成登录。"
|
|
12199
|
+
};
|
|
12200
|
+
}
|
|
12201
|
+
activeLogin.pendingVerifyCode = code;
|
|
12202
|
+
continue;
|
|
12203
|
+
}
|
|
12204
|
+
case "verify_code_blocked": {
|
|
12205
|
+
logger.warn(`waitForWeixinLogin: verify code blocked, qrRefreshCount=${qrRefreshCount} sessionKey=${opts.sessionKey}`);
|
|
12206
|
+
process.stdout.write(`
|
|
12207
|
+
⛔ 多次输入错误,请稍后再试。
|
|
12208
|
+
`);
|
|
12209
|
+
activeLogin.pendingVerifyCode = undefined;
|
|
12210
|
+
qrRefreshCount++;
|
|
12211
|
+
if (qrRefreshCount > MAX_QR_REFRESH_COUNT) {
|
|
12212
|
+
logger.warn(`waitForWeixinLogin: verify_code_blocked and QR refresh limit reached, giving up sessionKey=${opts.sessionKey}`);
|
|
12213
|
+
activeLogins.delete(opts.sessionKey);
|
|
12214
|
+
return {
|
|
12215
|
+
connected: false,
|
|
12216
|
+
message: "多次输入错误,连接流程已停止。请稍后再试。"
|
|
12217
|
+
};
|
|
12218
|
+
}
|
|
12219
|
+
const blockedRefreshResult = await refreshQRCode(activeLogin, opts.botType || DEFAULT_ILINK_BOT_TYPE, qrRefreshCount, () => {
|
|
12220
|
+
scannedPrinted = false;
|
|
12221
|
+
});
|
|
12222
|
+
if (!blockedRefreshResult.success) {
|
|
12223
|
+
activeLogins.delete(opts.sessionKey);
|
|
12224
|
+
return {
|
|
12225
|
+
connected: false,
|
|
12226
|
+
message: blockedRefreshResult.message
|
|
12227
|
+
};
|
|
12228
|
+
}
|
|
12229
|
+
break;
|
|
12230
|
+
}
|
|
11917
12231
|
case "confirmed": {
|
|
11918
12232
|
if (!statusResponse.ilink_bot_id) {
|
|
11919
12233
|
activeLogins.delete(opts.sessionKey);
|
|
@@ -11953,140 +12267,22 @@ async function waitForWeixinLogin(opts) {
|
|
|
11953
12267
|
message: "登录超时,请重试。"
|
|
11954
12268
|
};
|
|
11955
12269
|
}
|
|
11956
|
-
var ACTIVE_LOGIN_TTL_MS,
|
|
12270
|
+
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
12271
|
var init_login_qr = __esm(() => {
|
|
11958
12272
|
init_api();
|
|
12273
|
+
init_accounts();
|
|
11959
12274
|
init_logger();
|
|
11960
12275
|
init_redact();
|
|
11961
12276
|
ACTIVE_LOGIN_TTL_MS = 5 * 60000;
|
|
11962
12277
|
activeLogins = new Map;
|
|
11963
12278
|
});
|
|
11964
12279
|
|
|
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
|
-
};
|
|
12280
|
+
// src/weixin/util/random.ts
|
|
12281
|
+
import crypto2 from "node:crypto";
|
|
12282
|
+
function generateId(prefix) {
|
|
12283
|
+
return `${prefix}:${Date.now()}-${crypto2.randomBytes(4).toString("hex")}`;
|
|
12089
12284
|
}
|
|
12285
|
+
var init_random = () => {};
|
|
12090
12286
|
|
|
12091
12287
|
// src/weixin/api/types.ts
|
|
12092
12288
|
var UploadMediaType, MessageType, MessageItemType, MessageState, TypingStatus;
|
|
@@ -12121,9 +12317,272 @@ var init_types2 = __esm(() => {
|
|
|
12121
12317
|
};
|
|
12122
12318
|
});
|
|
12123
12319
|
|
|
12124
|
-
// src/
|
|
12125
|
-
import
|
|
12320
|
+
// src/weixin/messaging/inbound.ts
|
|
12321
|
+
import fs5 from "node:fs";
|
|
12126
12322
|
import path6 from "node:path";
|
|
12323
|
+
function contextTokenKey(accountId, userId) {
|
|
12324
|
+
return `${accountId}:${userId}`;
|
|
12325
|
+
}
|
|
12326
|
+
function resolveContextTokenFilePath(accountId) {
|
|
12327
|
+
return path6.join(resolveStateDir(), "openclaw-weixin", "accounts", `${accountId}.context-tokens.json`);
|
|
12328
|
+
}
|
|
12329
|
+
function persistContextTokens(accountId) {
|
|
12330
|
+
const prefix = `${accountId}:`;
|
|
12331
|
+
const tokens = {};
|
|
12332
|
+
for (const [k, v] of contextTokenStore) {
|
|
12333
|
+
if (k.startsWith(prefix))
|
|
12334
|
+
tokens[k.slice(prefix.length)] = v;
|
|
12335
|
+
}
|
|
12336
|
+
const filePath = resolveContextTokenFilePath(accountId);
|
|
12337
|
+
try {
|
|
12338
|
+
writePrivateFileSync(filePath, JSON.stringify(tokens));
|
|
12339
|
+
} catch (err) {
|
|
12340
|
+
logger.warn(`persistContextTokens: failed to write ${filePath}: ${String(err)}`);
|
|
12341
|
+
}
|
|
12342
|
+
}
|
|
12343
|
+
function restoreContextTokens(accountId) {
|
|
12344
|
+
const filePath = resolveContextTokenFilePath(accountId);
|
|
12345
|
+
try {
|
|
12346
|
+
if (!fs5.existsSync(filePath))
|
|
12347
|
+
return;
|
|
12348
|
+
const raw = fs5.readFileSync(filePath, "utf-8");
|
|
12349
|
+
const tokens = JSON.parse(raw);
|
|
12350
|
+
let count = 0;
|
|
12351
|
+
for (const [userId, token] of Object.entries(tokens)) {
|
|
12352
|
+
if (typeof token === "string" && token) {
|
|
12353
|
+
contextTokenStore.set(contextTokenKey(accountId, userId), token);
|
|
12354
|
+
count++;
|
|
12355
|
+
}
|
|
12356
|
+
}
|
|
12357
|
+
logger.info(`restoreContextTokens: restored ${count} tokens for account=${accountId}`);
|
|
12358
|
+
} catch (err) {
|
|
12359
|
+
logger.warn(`restoreContextTokens: failed to read ${filePath}: ${String(err)}`);
|
|
12360
|
+
}
|
|
12361
|
+
}
|
|
12362
|
+
function clearContextTokensForAccount(accountId) {
|
|
12363
|
+
const prefix = `${accountId}:`;
|
|
12364
|
+
for (const k of [...contextTokenStore.keys()]) {
|
|
12365
|
+
if (k.startsWith(prefix))
|
|
12366
|
+
contextTokenStore.delete(k);
|
|
12367
|
+
}
|
|
12368
|
+
const filePath = resolveContextTokenFilePath(accountId);
|
|
12369
|
+
try {
|
|
12370
|
+
if (fs5.existsSync(filePath))
|
|
12371
|
+
fs5.unlinkSync(filePath);
|
|
12372
|
+
} catch (err) {
|
|
12373
|
+
logger.warn(`clearContextTokensForAccount: failed to remove ${filePath}: ${String(err)}`);
|
|
12374
|
+
}
|
|
12375
|
+
logger.info(`clearContextTokensForAccount: cleared tokens for account=${accountId}`);
|
|
12376
|
+
}
|
|
12377
|
+
function setContextToken(accountId, userId, token) {
|
|
12378
|
+
const k = contextTokenKey(accountId, userId);
|
|
12379
|
+
logger.debug(`setContextToken: key=${k}`);
|
|
12380
|
+
contextTokenStore.set(k, token);
|
|
12381
|
+
persistContextTokens(accountId);
|
|
12382
|
+
}
|
|
12383
|
+
function getContextToken(accountId, userId) {
|
|
12384
|
+
const k = contextTokenKey(accountId, normalizeWeixinUserIdFromChatKey(userId));
|
|
12385
|
+
const val = contextTokenStore.get(k);
|
|
12386
|
+
logger.debug(`getContextToken: key=${k} found=${val !== undefined} storeSize=${contextTokenStore.size}`);
|
|
12387
|
+
return val;
|
|
12388
|
+
}
|
|
12389
|
+
function normalizeWeixinUserIdFromChatKey(chatKey) {
|
|
12390
|
+
const parts = chatKey.split(":");
|
|
12391
|
+
if (parts[0] === "weixin" && parts[2]) {
|
|
12392
|
+
return parts.slice(2).join(":");
|
|
12393
|
+
}
|
|
12394
|
+
return chatKey;
|
|
12395
|
+
}
|
|
12396
|
+
function isMediaItem(item) {
|
|
12397
|
+
return item.type === MessageItemType.IMAGE || item.type === MessageItemType.VIDEO || item.type === MessageItemType.FILE || item.type === MessageItemType.VOICE;
|
|
12398
|
+
}
|
|
12399
|
+
function bodyFromItemList(itemList) {
|
|
12400
|
+
if (!itemList?.length)
|
|
12401
|
+
return "";
|
|
12402
|
+
for (const item of itemList) {
|
|
12403
|
+
if (item.type === MessageItemType.TEXT && item.text_item?.text != null) {
|
|
12404
|
+
const text = String(item.text_item.text);
|
|
12405
|
+
const ref = item.ref_msg;
|
|
12406
|
+
if (!ref)
|
|
12407
|
+
return text;
|
|
12408
|
+
if (ref.message_item && isMediaItem(ref.message_item))
|
|
12409
|
+
return text;
|
|
12410
|
+
const parts = [];
|
|
12411
|
+
if (ref.title)
|
|
12412
|
+
parts.push(ref.title);
|
|
12413
|
+
if (ref.message_item) {
|
|
12414
|
+
const refBody = bodyFromItemList([ref.message_item]);
|
|
12415
|
+
if (refBody)
|
|
12416
|
+
parts.push(refBody);
|
|
12417
|
+
}
|
|
12418
|
+
if (!parts.length)
|
|
12419
|
+
return text;
|
|
12420
|
+
return `[引用: ${parts.join(" | ")}]
|
|
12421
|
+
${text}`;
|
|
12422
|
+
}
|
|
12423
|
+
if (item.type === MessageItemType.VOICE && item.voice_item?.text) {
|
|
12424
|
+
return item.voice_item.text;
|
|
12425
|
+
}
|
|
12426
|
+
}
|
|
12427
|
+
return "";
|
|
12428
|
+
}
|
|
12429
|
+
function extractWeixinMediaDescriptors(itemList) {
|
|
12430
|
+
const out = [];
|
|
12431
|
+
for (const item of itemList ?? []) {
|
|
12432
|
+
const descriptor = descriptorFromItem(item);
|
|
12433
|
+
if (descriptor)
|
|
12434
|
+
out.push(descriptor);
|
|
12435
|
+
const ref = item.type === MessageItemType.TEXT ? item.ref_msg?.message_item : undefined;
|
|
12436
|
+
const refDescriptor = descriptorFromItem(ref);
|
|
12437
|
+
if (refDescriptor)
|
|
12438
|
+
out.push(refDescriptor);
|
|
12439
|
+
}
|
|
12440
|
+
return out;
|
|
12441
|
+
}
|
|
12442
|
+
function descriptorFromItem(item) {
|
|
12443
|
+
if (!item)
|
|
12444
|
+
return;
|
|
12445
|
+
if (item.type === MessageItemType.IMAGE)
|
|
12446
|
+
return { item, kind: "image" };
|
|
12447
|
+
if (item.type === MessageItemType.VIDEO)
|
|
12448
|
+
return { item, kind: "video" };
|
|
12449
|
+
if (item.type === MessageItemType.FILE)
|
|
12450
|
+
return { item, kind: "file", fileName: item.file_item?.file_name };
|
|
12451
|
+
if (item.type === MessageItemType.VOICE)
|
|
12452
|
+
return { item, kind: "audio" };
|
|
12453
|
+
return;
|
|
12454
|
+
}
|
|
12455
|
+
var contextTokenStore;
|
|
12456
|
+
var init_inbound = __esm(() => {
|
|
12457
|
+
init_logger();
|
|
12458
|
+
init_random();
|
|
12459
|
+
init_types2();
|
|
12460
|
+
init_state_dir();
|
|
12461
|
+
init_private_file();
|
|
12462
|
+
contextTokenStore = new Map;
|
|
12463
|
+
});
|
|
12464
|
+
|
|
12465
|
+
// src/weixin/api/config-cache.ts
|
|
12466
|
+
class WeixinConfigManager {
|
|
12467
|
+
apiOpts;
|
|
12468
|
+
log;
|
|
12469
|
+
cache = new Map;
|
|
12470
|
+
constructor(apiOpts, log) {
|
|
12471
|
+
this.apiOpts = apiOpts;
|
|
12472
|
+
this.log = log;
|
|
12473
|
+
}
|
|
12474
|
+
async getForUser(userId, contextToken) {
|
|
12475
|
+
const now = Date.now();
|
|
12476
|
+
const entry = this.cache.get(userId);
|
|
12477
|
+
const shouldFetch = !entry || now >= entry.nextFetchAt;
|
|
12478
|
+
if (shouldFetch) {
|
|
12479
|
+
let fetchOk = false;
|
|
12480
|
+
try {
|
|
12481
|
+
const resp = await getConfig({
|
|
12482
|
+
baseUrl: this.apiOpts.baseUrl,
|
|
12483
|
+
token: this.apiOpts.token,
|
|
12484
|
+
ilinkUserId: userId,
|
|
12485
|
+
contextToken
|
|
12486
|
+
});
|
|
12487
|
+
if (resp.ret === 0) {
|
|
12488
|
+
this.cache.set(userId, {
|
|
12489
|
+
config: { typingTicket: resp.typing_ticket ?? "" },
|
|
12490
|
+
everSucceeded: true,
|
|
12491
|
+
nextFetchAt: now + Math.random() * CONFIG_CACHE_TTL_MS,
|
|
12492
|
+
retryDelayMs: CONFIG_CACHE_INITIAL_RETRY_MS
|
|
12493
|
+
});
|
|
12494
|
+
this.log(`[weixin] config ${entry?.everSucceeded ? "refreshed" : "cached"} for ${userId}`);
|
|
12495
|
+
fetchOk = true;
|
|
12496
|
+
}
|
|
12497
|
+
} catch (err) {
|
|
12498
|
+
this.log(`[weixin] getConfig failed for ${userId} (ignored): ${String(err)}`);
|
|
12499
|
+
}
|
|
12500
|
+
if (!fetchOk) {
|
|
12501
|
+
const prevDelay = entry?.retryDelayMs ?? CONFIG_CACHE_INITIAL_RETRY_MS;
|
|
12502
|
+
const nextDelay = Math.min(prevDelay * 2, CONFIG_CACHE_MAX_RETRY_MS);
|
|
12503
|
+
if (entry) {
|
|
12504
|
+
entry.nextFetchAt = now + nextDelay;
|
|
12505
|
+
entry.retryDelayMs = nextDelay;
|
|
12506
|
+
} else {
|
|
12507
|
+
this.cache.set(userId, {
|
|
12508
|
+
config: { typingTicket: "" },
|
|
12509
|
+
everSucceeded: false,
|
|
12510
|
+
nextFetchAt: now + CONFIG_CACHE_INITIAL_RETRY_MS,
|
|
12511
|
+
retryDelayMs: CONFIG_CACHE_INITIAL_RETRY_MS
|
|
12512
|
+
});
|
|
12513
|
+
}
|
|
12514
|
+
}
|
|
12515
|
+
}
|
|
12516
|
+
return this.cache.get(userId)?.config ?? { typingTicket: "" };
|
|
12517
|
+
}
|
|
12518
|
+
}
|
|
12519
|
+
var CONFIG_CACHE_TTL_MS, CONFIG_CACHE_INITIAL_RETRY_MS = 2000, CONFIG_CACHE_MAX_RETRY_MS;
|
|
12520
|
+
var init_config_cache = __esm(() => {
|
|
12521
|
+
init_api();
|
|
12522
|
+
CONFIG_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
12523
|
+
CONFIG_CACHE_MAX_RETRY_MS = 60 * 60 * 1000;
|
|
12524
|
+
});
|
|
12525
|
+
|
|
12526
|
+
// src/weixin/api/session-guard.ts
|
|
12527
|
+
function pauseSession(accountId) {
|
|
12528
|
+
const until = Date.now() + SESSION_PAUSE_DURATION_MS;
|
|
12529
|
+
pauseUntilMap.set(accountId, until);
|
|
12530
|
+
logger.info(`session-guard: paused accountId=${accountId} until=${new Date(until).toISOString()} (${SESSION_PAUSE_DURATION_MS / 1000}s)`);
|
|
12531
|
+
}
|
|
12532
|
+
function resetSessionPause(accountId) {
|
|
12533
|
+
pauseUntilMap.delete(accountId);
|
|
12534
|
+
}
|
|
12535
|
+
var SESSION_PAUSE_DURATION_MS, SESSION_EXPIRED_ERRCODE = -14, pauseUntilMap;
|
|
12536
|
+
var init_session_guard = __esm(() => {
|
|
12537
|
+
init_logger();
|
|
12538
|
+
SESSION_PAUSE_DURATION_MS = 60 * 60 * 1000;
|
|
12539
|
+
pauseUntilMap = new Map;
|
|
12540
|
+
});
|
|
12541
|
+
|
|
12542
|
+
// src/weixin/messaging/conversation-executor.ts
|
|
12543
|
+
function createConversationExecutor() {
|
|
12544
|
+
const states = new Map;
|
|
12545
|
+
const getState = (conversationId) => {
|
|
12546
|
+
const existing = states.get(conversationId);
|
|
12547
|
+
if (existing)
|
|
12548
|
+
return existing;
|
|
12549
|
+
const created = { activeControls: 0 };
|
|
12550
|
+
states.set(conversationId, created);
|
|
12551
|
+
return created;
|
|
12552
|
+
};
|
|
12553
|
+
const cleanupState = (conversationId, state) => {
|
|
12554
|
+
if (!state.normalTail && state.activeControls === 0) {
|
|
12555
|
+
states.delete(conversationId);
|
|
12556
|
+
}
|
|
12557
|
+
};
|
|
12558
|
+
return {
|
|
12559
|
+
run(conversationId, lane, task) {
|
|
12560
|
+
const state = getState(conversationId);
|
|
12561
|
+
if (lane === "control") {
|
|
12562
|
+
state.activeControls += 1;
|
|
12563
|
+
return Promise.resolve().then(task).finally(() => {
|
|
12564
|
+
state.activeControls -= 1;
|
|
12565
|
+
cleanupState(conversationId, state);
|
|
12566
|
+
});
|
|
12567
|
+
}
|
|
12568
|
+
const previous = state.normalTail ?? Promise.resolve();
|
|
12569
|
+
const next = previous.catch(() => {
|
|
12570
|
+
return;
|
|
12571
|
+
}).then(task);
|
|
12572
|
+
state.normalTail = next;
|
|
12573
|
+
return next.finally(() => {
|
|
12574
|
+
if (state.normalTail === next) {
|
|
12575
|
+
state.normalTail = undefined;
|
|
12576
|
+
}
|
|
12577
|
+
cleanupState(conversationId, state);
|
|
12578
|
+
});
|
|
12579
|
+
}
|
|
12580
|
+
};
|
|
12581
|
+
}
|
|
12582
|
+
|
|
12583
|
+
// src/channels/media-store.ts
|
|
12584
|
+
import { access as access2, mkdir as mkdir7, readdir, rm as rm4, stat, writeFile as writeFile5 } from "node:fs/promises";
|
|
12585
|
+
import path7 from "node:path";
|
|
12127
12586
|
|
|
12128
12587
|
class RuntimeMediaStore {
|
|
12129
12588
|
rootDir;
|
|
@@ -12139,19 +12598,19 @@ class RuntimeMediaStore {
|
|
|
12139
12598
|
const safeChatKey = safePathSegment(input.chatKey);
|
|
12140
12599
|
const safeMessageId = safePathSegment(input.messageId || "message");
|
|
12141
12600
|
const baseFileName = sanitizeMediaFileName(input.fileName ?? "attachment", input.mimeType);
|
|
12142
|
-
const dir =
|
|
12601
|
+
const dir = path7.join(this.rootDir, input.channelId, safeChatKey, safeMessageId);
|
|
12143
12602
|
await mkdir7(dir, { recursive: true });
|
|
12144
|
-
const resolvedRoot =
|
|
12145
|
-
const resolvedFile =
|
|
12603
|
+
const resolvedRoot = path7.resolve(this.rootDir);
|
|
12604
|
+
const resolvedFile = path7.resolve(path7.join(dir, await uniqueFileName(dir, baseFileName)));
|
|
12146
12605
|
if (!isPathInside(resolvedFile, resolvedRoot)) {
|
|
12147
12606
|
throw new Error("media path escapes runtime media root");
|
|
12148
12607
|
}
|
|
12149
|
-
await
|
|
12608
|
+
await writeFile5(resolvedFile, input.buffer);
|
|
12150
12609
|
return {
|
|
12151
12610
|
kind: input.kind,
|
|
12152
12611
|
filePath: resolvedFile,
|
|
12153
12612
|
mimeType: input.mimeType,
|
|
12154
|
-
fileName:
|
|
12613
|
+
fileName: path7.basename(resolvedFile),
|
|
12155
12614
|
sizeBytes: input.buffer.byteLength,
|
|
12156
12615
|
source: {
|
|
12157
12616
|
channelId: input.channelId,
|
|
@@ -12167,10 +12626,10 @@ class RuntimeMediaStore {
|
|
|
12167
12626
|
}
|
|
12168
12627
|
}
|
|
12169
12628
|
function sanitizeMediaFileName(fileName, mimeType) {
|
|
12170
|
-
const base =
|
|
12629
|
+
const base = path7.basename(fileName.trim() || "attachment");
|
|
12171
12630
|
const replaced = base.replace(/[\\/:*?"<>|\s]+/g, "-").replace(/^-+|-+$/g, "");
|
|
12172
12631
|
const safe = replaced || "attachment";
|
|
12173
|
-
const ext =
|
|
12632
|
+
const ext = path7.extname(safe);
|
|
12174
12633
|
if (ext)
|
|
12175
12634
|
return safe;
|
|
12176
12635
|
return `${safe}${extensionFromMime(mimeType)}`;
|
|
@@ -12180,13 +12639,13 @@ function safePathSegment(value) {
|
|
|
12180
12639
|
return safe || "unknown";
|
|
12181
12640
|
}
|
|
12182
12641
|
async function uniqueFileName(dir, baseName) {
|
|
12183
|
-
const ext =
|
|
12642
|
+
const ext = path7.extname(baseName);
|
|
12184
12643
|
const stem = ext ? baseName.slice(0, -ext.length) : baseName;
|
|
12185
12644
|
let candidate = baseName;
|
|
12186
12645
|
let counter = 2;
|
|
12187
12646
|
while (true) {
|
|
12188
12647
|
try {
|
|
12189
|
-
await access2(
|
|
12648
|
+
await access2(path7.join(dir, candidate));
|
|
12190
12649
|
candidate = `${stem}-${counter}${ext}`;
|
|
12191
12650
|
counter += 1;
|
|
12192
12651
|
} catch {
|
|
@@ -12217,8 +12676,8 @@ function extensionFromMime(mimeType) {
|
|
|
12217
12676
|
return ".bin";
|
|
12218
12677
|
}
|
|
12219
12678
|
function isPathInside(candidate, root) {
|
|
12220
|
-
const relative =
|
|
12221
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
12679
|
+
const relative = path7.relative(root, candidate);
|
|
12680
|
+
return relative === "" || !relative.startsWith("..") && !path7.isAbsolute(relative);
|
|
12222
12681
|
}
|
|
12223
12682
|
async function cleanupDir(dir, cutoffMs) {
|
|
12224
12683
|
let entries;
|
|
@@ -12229,7 +12688,7 @@ async function cleanupDir(dir, cutoffMs) {
|
|
|
12229
12688
|
}
|
|
12230
12689
|
let empty = true;
|
|
12231
12690
|
for (const entry of entries) {
|
|
12232
|
-
const full =
|
|
12691
|
+
const full = path7.join(dir, entry.name);
|
|
12233
12692
|
if (entry.isDirectory()) {
|
|
12234
12693
|
const childEmpty = await cleanupDir(full, cutoffMs);
|
|
12235
12694
|
if (childEmpty) {
|
|
@@ -12258,18 +12717,18 @@ var init_media_store = __esm(() => {
|
|
|
12258
12717
|
});
|
|
12259
12718
|
|
|
12260
12719
|
// src/channels/outbound-media-safety.ts
|
|
12261
|
-
import
|
|
12262
|
-
import
|
|
12720
|
+
import fs6 from "node:fs/promises";
|
|
12721
|
+
import path8 from "node:path";
|
|
12263
12722
|
async function resolveSafeOutboundMediaPath(mediaPath, allowedRoots) {
|
|
12264
12723
|
if (mediaPath.startsWith("http://") || mediaPath.startsWith("https://")) {
|
|
12265
12724
|
return null;
|
|
12266
12725
|
}
|
|
12267
|
-
const candidate =
|
|
12726
|
+
const candidate = path8.isAbsolute(mediaPath) ? mediaPath : path8.resolve(mediaPath);
|
|
12268
12727
|
const realCandidate = await realpathOrNull(candidate);
|
|
12269
12728
|
if (!realCandidate) {
|
|
12270
12729
|
return null;
|
|
12271
12730
|
}
|
|
12272
|
-
const stat2 = await
|
|
12731
|
+
const stat2 = await fs6.stat(realCandidate).catch(() => null);
|
|
12273
12732
|
if (!stat2?.isFile()) {
|
|
12274
12733
|
return null;
|
|
12275
12734
|
}
|
|
@@ -12283,21 +12742,21 @@ async function resolveSafeOutboundMediaPath(mediaPath, allowedRoots) {
|
|
|
12283
12742
|
}
|
|
12284
12743
|
async function realpathOrNull(filePath) {
|
|
12285
12744
|
try {
|
|
12286
|
-
return await
|
|
12745
|
+
return await fs6.realpath(filePath);
|
|
12287
12746
|
} catch {
|
|
12288
12747
|
return null;
|
|
12289
12748
|
}
|
|
12290
12749
|
}
|
|
12291
12750
|
function isPathInside2(candidate, root) {
|
|
12292
|
-
const relative =
|
|
12293
|
-
return relative === "" || !relative.startsWith("..") && !
|
|
12751
|
+
const relative = path8.relative(root, candidate);
|
|
12752
|
+
return relative === "" || !relative.startsWith("..") && !path8.isAbsolute(relative);
|
|
12294
12753
|
}
|
|
12295
12754
|
var init_outbound_media_safety = () => {};
|
|
12296
12755
|
|
|
12297
12756
|
// src/weixin/media/mime.ts
|
|
12298
|
-
import
|
|
12757
|
+
import path9 from "node:path";
|
|
12299
12758
|
function getMimeFromFilename(filename) {
|
|
12300
|
-
const ext =
|
|
12759
|
+
const ext = path9.extname(filename).toLowerCase();
|
|
12301
12760
|
return EXTENSION_TO_MIME[ext] ?? "application/octet-stream";
|
|
12302
12761
|
}
|
|
12303
12762
|
function getExtensionFromMime(mimeType) {
|
|
@@ -12635,115 +13094,361 @@ function buildFinalHeadsUp(input) {
|
|
|
12635
13094
|
\uD83D\uDCC4 结果共 ${total} 段,已发 ${sentSoFar} 段。回复 /jx 续看后 ${remaining} 段。`;
|
|
12636
13095
|
}
|
|
12637
13096
|
|
|
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(":");
|
|
13097
|
+
// src/weixin/messaging/markdown-filter.ts
|
|
13098
|
+
class StreamingMarkdownFilter {
|
|
13099
|
+
buf = "";
|
|
13100
|
+
fence = false;
|
|
13101
|
+
sol = true;
|
|
13102
|
+
inl = null;
|
|
13103
|
+
feed(delta) {
|
|
13104
|
+
this.buf += delta;
|
|
13105
|
+
return this.pump(false);
|
|
12664
13106
|
}
|
|
12665
|
-
|
|
12666
|
-
|
|
12667
|
-
|
|
12668
|
-
|
|
12669
|
-
|
|
12670
|
-
|
|
12671
|
-
|
|
13107
|
+
flush() {
|
|
13108
|
+
return this.pump(true);
|
|
13109
|
+
}
|
|
13110
|
+
pump(eof) {
|
|
13111
|
+
let out = "";
|
|
13112
|
+
while (this.buf) {
|
|
13113
|
+
const sLen = this.buf.length;
|
|
13114
|
+
const sSol = this.sol;
|
|
13115
|
+
const sFence = this.fence;
|
|
13116
|
+
const sInl = this.inl;
|
|
13117
|
+
if (this.fence)
|
|
13118
|
+
out += this.pumpFence(eof);
|
|
13119
|
+
else if (this.inl)
|
|
13120
|
+
out += this.pumpInline(eof);
|
|
13121
|
+
else if (this.sol)
|
|
13122
|
+
out += this.pumpSOL(eof);
|
|
13123
|
+
else
|
|
13124
|
+
out += this.pumpBody(eof);
|
|
13125
|
+
if (this.buf.length === sLen && this.sol === sSol && this.fence === sFence && this.inl === sInl)
|
|
13126
|
+
break;
|
|
13127
|
+
}
|
|
13128
|
+
if (eof && this.inl) {
|
|
13129
|
+
const markers = { image: "![", bold3: "***", italic: "*", ubold3: "___", uitalic: "_" };
|
|
13130
|
+
out += (markers[this.inl.type] ?? "") + this.inl.acc;
|
|
13131
|
+
this.inl = null;
|
|
13132
|
+
}
|
|
13133
|
+
return out;
|
|
13134
|
+
}
|
|
13135
|
+
pumpFence(eof) {
|
|
13136
|
+
if (this.sol) {
|
|
13137
|
+
if (this.buf.length < 3 && !eof)
|
|
13138
|
+
return "";
|
|
13139
|
+
if (this.buf.startsWith("```")) {
|
|
13140
|
+
const nl2 = this.buf.indexOf(`
|
|
13141
|
+
`, 3);
|
|
13142
|
+
if (nl2 !== -1) {
|
|
13143
|
+
this.fence = false;
|
|
13144
|
+
const line = this.buf.slice(0, nl2 + 1);
|
|
13145
|
+
this.buf = this.buf.slice(nl2 + 1);
|
|
13146
|
+
this.sol = true;
|
|
13147
|
+
return line;
|
|
13148
|
+
}
|
|
13149
|
+
if (eof) {
|
|
13150
|
+
this.fence = false;
|
|
13151
|
+
const line = this.buf;
|
|
13152
|
+
this.buf = "";
|
|
13153
|
+
return line;
|
|
13154
|
+
}
|
|
13155
|
+
return "";
|
|
13156
|
+
}
|
|
13157
|
+
this.sol = false;
|
|
13158
|
+
}
|
|
13159
|
+
const nl = this.buf.indexOf(`
|
|
13160
|
+
`);
|
|
13161
|
+
if (nl !== -1) {
|
|
13162
|
+
const chunk2 = this.buf.slice(0, nl + 1);
|
|
13163
|
+
this.buf = this.buf.slice(nl + 1);
|
|
13164
|
+
this.sol = true;
|
|
13165
|
+
return chunk2;
|
|
13166
|
+
}
|
|
13167
|
+
const chunk = this.buf;
|
|
13168
|
+
this.buf = "";
|
|
13169
|
+
return chunk;
|
|
13170
|
+
}
|
|
13171
|
+
pumpSOL(eof) {
|
|
13172
|
+
const b = this.buf;
|
|
13173
|
+
if (b[0] === `
|
|
13174
|
+
`) {
|
|
13175
|
+
this.buf = b.slice(1);
|
|
13176
|
+
return `
|
|
13177
|
+
`;
|
|
13178
|
+
}
|
|
13179
|
+
if (b[0] === "`") {
|
|
13180
|
+
if (b.length < 3 && !eof)
|
|
13181
|
+
return "";
|
|
13182
|
+
if (b.startsWith("```")) {
|
|
13183
|
+
const nl = b.indexOf(`
|
|
13184
|
+
`, 3);
|
|
13185
|
+
if (nl !== -1) {
|
|
13186
|
+
this.fence = true;
|
|
13187
|
+
const line = b.slice(0, nl + 1);
|
|
13188
|
+
this.buf = b.slice(nl + 1);
|
|
13189
|
+
this.sol = true;
|
|
13190
|
+
return line;
|
|
13191
|
+
}
|
|
13192
|
+
if (eof) {
|
|
13193
|
+
this.buf = "";
|
|
13194
|
+
return b;
|
|
13195
|
+
}
|
|
13196
|
+
return "";
|
|
13197
|
+
}
|
|
13198
|
+
this.sol = false;
|
|
13199
|
+
return "";
|
|
13200
|
+
}
|
|
13201
|
+
if (b[0] === ">") {
|
|
13202
|
+
this.sol = false;
|
|
13203
|
+
return "";
|
|
13204
|
+
}
|
|
13205
|
+
if (b[0] === "#") {
|
|
13206
|
+
let n = 0;
|
|
13207
|
+
while (n < b.length && b[n] === "#")
|
|
13208
|
+
n++;
|
|
13209
|
+
if (n === b.length && !eof)
|
|
13210
|
+
return "";
|
|
13211
|
+
if (n >= 5 && n <= 6 && n < b.length && b[n] === " ") {
|
|
13212
|
+
this.buf = b.slice(n + 1);
|
|
13213
|
+
this.sol = false;
|
|
13214
|
+
return "";
|
|
13215
|
+
}
|
|
13216
|
+
this.sol = false;
|
|
13217
|
+
return "";
|
|
13218
|
+
}
|
|
13219
|
+
if (b[0] === " " || b[0] === "\t") {
|
|
13220
|
+
if (b.search(/[^ \t]/) === -1 && !eof)
|
|
13221
|
+
return "";
|
|
13222
|
+
this.sol = false;
|
|
13223
|
+
return "";
|
|
13224
|
+
}
|
|
13225
|
+
if (b[0] === "-" || b[0] === "*" || b[0] === "_") {
|
|
13226
|
+
const ch = b[0];
|
|
13227
|
+
let j = 0;
|
|
13228
|
+
while (j < b.length && (b[j] === ch || b[j] === " "))
|
|
13229
|
+
j++;
|
|
13230
|
+
if (j === b.length && !eof)
|
|
13231
|
+
return "";
|
|
13232
|
+
if (j === b.length || b[j] === `
|
|
13233
|
+
`) {
|
|
13234
|
+
let count = 0;
|
|
13235
|
+
for (let k = 0;k < j; k++)
|
|
13236
|
+
if (b[k] === ch)
|
|
13237
|
+
count++;
|
|
13238
|
+
if (count >= 3) {
|
|
13239
|
+
if (j < b.length) {
|
|
13240
|
+
this.buf = b.slice(j + 1);
|
|
13241
|
+
this.sol = true;
|
|
13242
|
+
return b.slice(0, j + 1);
|
|
13243
|
+
}
|
|
13244
|
+
this.buf = "";
|
|
13245
|
+
return b;
|
|
13246
|
+
}
|
|
13247
|
+
}
|
|
13248
|
+
this.sol = false;
|
|
13249
|
+
return "";
|
|
13250
|
+
}
|
|
13251
|
+
this.sol = false;
|
|
12672
13252
|
return "";
|
|
12673
|
-
|
|
12674
|
-
|
|
12675
|
-
|
|
12676
|
-
|
|
12677
|
-
|
|
12678
|
-
|
|
12679
|
-
if (
|
|
12680
|
-
|
|
12681
|
-
|
|
12682
|
-
|
|
12683
|
-
|
|
12684
|
-
|
|
12685
|
-
|
|
12686
|
-
|
|
12687
|
-
|
|
13253
|
+
}
|
|
13254
|
+
pumpBody(eof) {
|
|
13255
|
+
let out = "";
|
|
13256
|
+
let i = 0;
|
|
13257
|
+
while (i < this.buf.length) {
|
|
13258
|
+
const c = this.buf[i];
|
|
13259
|
+
if (c === `
|
|
13260
|
+
`) {
|
|
13261
|
+
out += this.buf.slice(0, i + 1);
|
|
13262
|
+
this.buf = this.buf.slice(i + 1);
|
|
13263
|
+
this.sol = true;
|
|
13264
|
+
return out;
|
|
13265
|
+
}
|
|
13266
|
+
if (c === "!" && i + 1 < this.buf.length && this.buf[i + 1] === "[") {
|
|
13267
|
+
out += this.buf.slice(0, i);
|
|
13268
|
+
this.buf = this.buf.slice(i + 2);
|
|
13269
|
+
this.inl = { type: "image", acc: "" };
|
|
13270
|
+
return out;
|
|
13271
|
+
}
|
|
13272
|
+
if (c === "~") {
|
|
13273
|
+
i++;
|
|
13274
|
+
continue;
|
|
12688
13275
|
}
|
|
12689
|
-
if (
|
|
12690
|
-
|
|
12691
|
-
|
|
12692
|
-
|
|
13276
|
+
if (c === "*") {
|
|
13277
|
+
if (i + 2 < this.buf.length && this.buf[i + 1] === "*" && this.buf[i + 2] === "*") {
|
|
13278
|
+
out += this.buf.slice(0, i);
|
|
13279
|
+
this.buf = this.buf.slice(i + 3);
|
|
13280
|
+
this.inl = { type: "bold3", acc: "" };
|
|
13281
|
+
return out;
|
|
13282
|
+
}
|
|
13283
|
+
if (i + 1 < this.buf.length && this.buf[i + 1] === "*") {
|
|
13284
|
+
i += 2;
|
|
13285
|
+
continue;
|
|
13286
|
+
}
|
|
13287
|
+
if (i + 1 < this.buf.length && this.buf[i + 1] !== " " && this.buf[i + 1] !== `
|
|
13288
|
+
`) {
|
|
13289
|
+
out += this.buf.slice(0, i);
|
|
13290
|
+
this.buf = this.buf.slice(i + 1);
|
|
13291
|
+
this.inl = { type: "italic", acc: "" };
|
|
13292
|
+
return out;
|
|
13293
|
+
}
|
|
13294
|
+
i++;
|
|
13295
|
+
continue;
|
|
13296
|
+
}
|
|
13297
|
+
if (c === "_") {
|
|
13298
|
+
if (i + 2 < this.buf.length && this.buf[i + 1] === "_" && this.buf[i + 2] === "_") {
|
|
13299
|
+
out += this.buf.slice(0, i);
|
|
13300
|
+
this.buf = this.buf.slice(i + 3);
|
|
13301
|
+
this.inl = { type: "ubold3", acc: "" };
|
|
13302
|
+
return out;
|
|
13303
|
+
}
|
|
13304
|
+
if (i + 1 < this.buf.length && this.buf[i + 1] === "_") {
|
|
13305
|
+
i += 2;
|
|
13306
|
+
continue;
|
|
13307
|
+
}
|
|
13308
|
+
if (i + 1 < this.buf.length && this.buf[i + 1] !== " " && this.buf[i + 1] !== `
|
|
13309
|
+
`) {
|
|
13310
|
+
out += this.buf.slice(0, i);
|
|
13311
|
+
this.buf = this.buf.slice(i + 1);
|
|
13312
|
+
this.inl = { type: "uitalic", acc: "" };
|
|
13313
|
+
return out;
|
|
13314
|
+
}
|
|
13315
|
+
i++;
|
|
13316
|
+
continue;
|
|
13317
|
+
}
|
|
13318
|
+
i++;
|
|
12693
13319
|
}
|
|
12694
|
-
|
|
12695
|
-
|
|
13320
|
+
let hold = 0;
|
|
13321
|
+
if (!eof) {
|
|
13322
|
+
if (this.buf.endsWith("**"))
|
|
13323
|
+
hold = 2;
|
|
13324
|
+
else if (this.buf.endsWith("__"))
|
|
13325
|
+
hold = 2;
|
|
13326
|
+
else if (this.buf.endsWith("*"))
|
|
13327
|
+
hold = 1;
|
|
13328
|
+
else if (this.buf.endsWith("_"))
|
|
13329
|
+
hold = 1;
|
|
13330
|
+
else if (this.buf.endsWith("!"))
|
|
13331
|
+
hold = 1;
|
|
13332
|
+
}
|
|
13333
|
+
out += this.buf.slice(0, this.buf.length - hold);
|
|
13334
|
+
this.buf = hold > 0 ? this.buf.slice(-hold) : "";
|
|
13335
|
+
return out;
|
|
13336
|
+
}
|
|
13337
|
+
pumpInline(_eof) {
|
|
13338
|
+
if (!this.inl)
|
|
13339
|
+
return "";
|
|
13340
|
+
this.inl.acc += this.buf;
|
|
13341
|
+
this.buf = "";
|
|
13342
|
+
switch (this.inl.type) {
|
|
13343
|
+
case "bold3": {
|
|
13344
|
+
const idx = this.inl.acc.indexOf("***");
|
|
13345
|
+
if (idx !== -1) {
|
|
13346
|
+
const content = this.inl.acc.slice(0, idx);
|
|
13347
|
+
this.buf = this.inl.acc.slice(idx + 3);
|
|
13348
|
+
this.inl = null;
|
|
13349
|
+
if (StreamingMarkdownFilter.containsCJK(content))
|
|
13350
|
+
return content;
|
|
13351
|
+
return `***${content}***`;
|
|
13352
|
+
}
|
|
13353
|
+
return "";
|
|
13354
|
+
}
|
|
13355
|
+
case "ubold3": {
|
|
13356
|
+
const idx = this.inl.acc.indexOf("___");
|
|
13357
|
+
if (idx !== -1) {
|
|
13358
|
+
const content = this.inl.acc.slice(0, idx);
|
|
13359
|
+
this.buf = this.inl.acc.slice(idx + 3);
|
|
13360
|
+
this.inl = null;
|
|
13361
|
+
if (StreamingMarkdownFilter.containsCJK(content))
|
|
13362
|
+
return content;
|
|
13363
|
+
return `___${content}___`;
|
|
13364
|
+
}
|
|
13365
|
+
return "";
|
|
13366
|
+
}
|
|
13367
|
+
case "italic": {
|
|
13368
|
+
for (let j = 0;j < this.inl.acc.length; j++) {
|
|
13369
|
+
if (this.inl.acc[j] === `
|
|
13370
|
+
`) {
|
|
13371
|
+
const r = "*" + this.inl.acc.slice(0, j + 1);
|
|
13372
|
+
this.buf = this.inl.acc.slice(j + 1);
|
|
13373
|
+
this.inl = null;
|
|
13374
|
+
this.sol = true;
|
|
13375
|
+
return r;
|
|
13376
|
+
}
|
|
13377
|
+
if (this.inl.acc[j] === "*") {
|
|
13378
|
+
if (j + 1 < this.inl.acc.length && this.inl.acc[j + 1] === "*") {
|
|
13379
|
+
j++;
|
|
13380
|
+
continue;
|
|
13381
|
+
}
|
|
13382
|
+
const content = this.inl.acc.slice(0, j);
|
|
13383
|
+
this.buf = this.inl.acc.slice(j + 1);
|
|
13384
|
+
this.inl = null;
|
|
13385
|
+
if (StreamingMarkdownFilter.containsCJK(content))
|
|
13386
|
+
return content;
|
|
13387
|
+
return `*${content}*`;
|
|
13388
|
+
}
|
|
13389
|
+
}
|
|
13390
|
+
return "";
|
|
13391
|
+
}
|
|
13392
|
+
case "uitalic": {
|
|
13393
|
+
for (let j = 0;j < this.inl.acc.length; j++) {
|
|
13394
|
+
if (this.inl.acc[j] === `
|
|
13395
|
+
`) {
|
|
13396
|
+
const r = "_" + this.inl.acc.slice(0, j + 1);
|
|
13397
|
+
this.buf = this.inl.acc.slice(j + 1);
|
|
13398
|
+
this.inl = null;
|
|
13399
|
+
this.sol = true;
|
|
13400
|
+
return r;
|
|
13401
|
+
}
|
|
13402
|
+
if (this.inl.acc[j] === "_") {
|
|
13403
|
+
if (j + 1 < this.inl.acc.length && this.inl.acc[j + 1] === "_") {
|
|
13404
|
+
j++;
|
|
13405
|
+
continue;
|
|
13406
|
+
}
|
|
13407
|
+
const content = this.inl.acc.slice(0, j);
|
|
13408
|
+
this.buf = this.inl.acc.slice(j + 1);
|
|
13409
|
+
this.inl = null;
|
|
13410
|
+
if (StreamingMarkdownFilter.containsCJK(content))
|
|
13411
|
+
return content;
|
|
13412
|
+
return `_${content}_`;
|
|
13413
|
+
}
|
|
13414
|
+
}
|
|
13415
|
+
return "";
|
|
13416
|
+
}
|
|
13417
|
+
case "image": {
|
|
13418
|
+
const cb = this.inl.acc.indexOf("]");
|
|
13419
|
+
if (cb === -1)
|
|
13420
|
+
return "";
|
|
13421
|
+
if (cb + 1 >= this.inl.acc.length)
|
|
13422
|
+
return "";
|
|
13423
|
+
if (this.inl.acc[cb + 1] !== "(") {
|
|
13424
|
+
const r = "![" + this.inl.acc.slice(0, cb + 1);
|
|
13425
|
+
this.buf = this.inl.acc.slice(cb + 1);
|
|
13426
|
+
this.inl = null;
|
|
13427
|
+
return r;
|
|
13428
|
+
}
|
|
13429
|
+
const cp = this.inl.acc.indexOf(")", cb + 2);
|
|
13430
|
+
if (cp !== -1) {
|
|
13431
|
+
this.buf = this.inl.acc.slice(cp + 1);
|
|
13432
|
+
this.inl = null;
|
|
13433
|
+
return "";
|
|
13434
|
+
}
|
|
13435
|
+
return "";
|
|
13436
|
+
}
|
|
12696
13437
|
}
|
|
13438
|
+
return "";
|
|
12697
13439
|
}
|
|
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);
|
|
13440
|
+
static containsCJK(text) {
|
|
13441
|
+
return /[\u2E80-\u9FFF\uAC00-\uD7AF\uF900-\uFAFF]/.test(text);
|
|
12710
13442
|
}
|
|
12711
|
-
return out;
|
|
12712
13443
|
}
|
|
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
13444
|
|
|
12734
13445
|
// src/weixin/messaging/send.ts
|
|
12735
13446
|
function generateClientId() {
|
|
12736
13447
|
return generateId("openclaw-weixin");
|
|
12737
13448
|
}
|
|
12738
13449
|
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;
|
|
13450
|
+
const f = new StreamingMarkdownFilter;
|
|
13451
|
+
return f.feed(text) + f.flush();
|
|
12747
13452
|
}
|
|
12748
13453
|
function buildTextMessageReq(params) {
|
|
12749
13454
|
const { to, text, contextToken, clientId } = params;
|
|
@@ -12979,10 +13684,10 @@ var init_cdn_upload = __esm(() => {
|
|
|
12979
13684
|
|
|
12980
13685
|
// src/weixin/cdn/upload.ts
|
|
12981
13686
|
import crypto3 from "node:crypto";
|
|
12982
|
-
import
|
|
13687
|
+
import fs7 from "node:fs/promises";
|
|
12983
13688
|
async function uploadMediaToCdn(params) {
|
|
12984
13689
|
const { filePath, toUserId, opts, cdnBaseUrl, mediaType, label } = params;
|
|
12985
|
-
const plaintext = await
|
|
13690
|
+
const plaintext = await fs7.readFile(filePath);
|
|
12986
13691
|
const rawsize = plaintext.length;
|
|
12987
13692
|
const rawfilemd5 = crypto3.createHash("md5").update(plaintext).digest("hex");
|
|
12988
13693
|
const filesize = aesEcbPaddedSize(rawsize);
|
|
@@ -13055,7 +13760,7 @@ var init_upload = __esm(() => {
|
|
|
13055
13760
|
});
|
|
13056
13761
|
|
|
13057
13762
|
// src/weixin/messaging/send-media.ts
|
|
13058
|
-
import
|
|
13763
|
+
import path10 from "node:path";
|
|
13059
13764
|
async function sendWeixinMediaFile(params) {
|
|
13060
13765
|
const { media, filePath, to, text, opts, cdnBaseUrl } = params;
|
|
13061
13766
|
const mime = media?.mimeType ?? getMimeFromFilename(filePath);
|
|
@@ -13082,7 +13787,7 @@ async function sendWeixinMediaFile(params) {
|
|
|
13082
13787
|
logger.info(`[weixin] sendWeixinMediaFile: image upload done filekey=${uploaded2.filekey} size=${uploaded2.fileSize}`);
|
|
13083
13788
|
return sendImageMessageWeixin({ to, text, uploaded: uploaded2, opts });
|
|
13084
13789
|
}
|
|
13085
|
-
const fileName = media?.fileName ??
|
|
13790
|
+
const fileName = media?.fileName ?? path10.basename(filePath);
|
|
13086
13791
|
logger.info(`[weixin] sendWeixinMediaFile: uploading file attachment filePath=${filePath} name=${fileName} to=${to}`);
|
|
13087
13792
|
const uploaded = await uploadFileAttachmentToWeixin({
|
|
13088
13793
|
filePath,
|
|
@@ -13102,14 +13807,14 @@ var init_send_media = __esm(() => {
|
|
|
13102
13807
|
});
|
|
13103
13808
|
|
|
13104
13809
|
// src/weixin/messaging/debug-mode.ts
|
|
13105
|
-
import
|
|
13106
|
-
import
|
|
13810
|
+
import fs8 from "node:fs";
|
|
13811
|
+
import path11 from "node:path";
|
|
13107
13812
|
function resolveDebugModePath() {
|
|
13108
|
-
return
|
|
13813
|
+
return path11.join(resolveStateDir(), "openclaw-weixin", "debug-mode.json");
|
|
13109
13814
|
}
|
|
13110
13815
|
function loadState() {
|
|
13111
13816
|
try {
|
|
13112
|
-
const raw =
|
|
13817
|
+
const raw = fs8.readFileSync(resolveDebugModePath(), "utf-8");
|
|
13113
13818
|
const parsed = JSON.parse(raw);
|
|
13114
13819
|
if (parsed && typeof parsed.accounts === "object")
|
|
13115
13820
|
return parsed;
|
|
@@ -13118,8 +13823,8 @@ function loadState() {
|
|
|
13118
13823
|
}
|
|
13119
13824
|
function saveState(state) {
|
|
13120
13825
|
const filePath = resolveDebugModePath();
|
|
13121
|
-
ensureDirSync(
|
|
13122
|
-
|
|
13826
|
+
ensureDirSync(path11.dirname(filePath));
|
|
13827
|
+
fs8.writeFileSync(filePath, JSON.stringify(state, null, 2), "utf-8");
|
|
13123
13828
|
}
|
|
13124
13829
|
function toggleDebugMode(accountId) {
|
|
13125
13830
|
const state = loadState();
|
|
@@ -13576,9 +14281,9 @@ var init_perf_tracer = __esm(() => {
|
|
|
13576
14281
|
|
|
13577
14282
|
// src/weixin/messaging/handle-weixin-message-turn.ts
|
|
13578
14283
|
import crypto4 from "node:crypto";
|
|
13579
|
-
import
|
|
14284
|
+
import fs9 from "node:fs/promises";
|
|
13580
14285
|
import { tmpdir } from "node:os";
|
|
13581
|
-
import
|
|
14286
|
+
import path12 from "node:path";
|
|
13582
14287
|
function utf8ByteLength(s) {
|
|
13583
14288
|
return Buffer.byteLength(s, "utf8");
|
|
13584
14289
|
}
|
|
@@ -13668,24 +14373,24 @@ function hardCutByCodepoint(s, maxBytes) {
|
|
|
13668
14373
|
return out;
|
|
13669
14374
|
}
|
|
13670
14375
|
function resolveMediaTempDir(customRoot) {
|
|
13671
|
-
return customRoot ??
|
|
14376
|
+
return customRoot ?? path12.join(tmpdir(), "weacpx", "media");
|
|
13672
14377
|
}
|
|
13673
14378
|
function createSaveMediaBuffer(mediaTempDir) {
|
|
13674
14379
|
return async function saveMediaBuffer(buffer, contentType, subdir, maxBytes, originalFilename) {
|
|
13675
14380
|
if (maxBytes !== undefined && buffer.byteLength > maxBytes) {
|
|
13676
14381
|
throw new Error(`media exceeds ${maxBytes} bytes`);
|
|
13677
14382
|
}
|
|
13678
|
-
const dir =
|
|
13679
|
-
await
|
|
14383
|
+
const dir = path12.join(resolveMediaTempDir(mediaTempDir), subdir ?? "");
|
|
14384
|
+
await fs9.mkdir(dir, { recursive: true });
|
|
13680
14385
|
let ext = ".bin";
|
|
13681
14386
|
if (originalFilename) {
|
|
13682
|
-
ext =
|
|
14387
|
+
ext = path12.extname(originalFilename) || ".bin";
|
|
13683
14388
|
} else if (contentType) {
|
|
13684
14389
|
ext = getExtensionFromMime(contentType);
|
|
13685
14390
|
}
|
|
13686
14391
|
const name = `${Date.now()}-${crypto4.randomBytes(4).toString("hex")}${ext}`;
|
|
13687
|
-
const filePath =
|
|
13688
|
-
await
|
|
14392
|
+
const filePath = path12.join(dir, name);
|
|
14393
|
+
await fs9.writeFile(filePath, buffer);
|
|
13689
14394
|
return { path: filePath };
|
|
13690
14395
|
};
|
|
13691
14396
|
}
|
|
@@ -13837,7 +14542,7 @@ async function handleWeixinMessageTurn(full, deps) {
|
|
|
13837
14542
|
continue;
|
|
13838
14543
|
}
|
|
13839
14544
|
try {
|
|
13840
|
-
const buffer = await
|
|
14545
|
+
const buffer = await fs9.readFile(filePath);
|
|
13841
14546
|
const mimeType = downloaded.fileMediaType ?? downloaded.voiceMediaType ?? defaultWeixinMime(descriptor.kind);
|
|
13842
14547
|
media.push(await mediaStore.saveMediaBuffer({
|
|
13843
14548
|
channelId: "weixin",
|
|
@@ -13851,7 +14556,7 @@ async function handleWeixinMessageTurn(full, deps) {
|
|
|
13851
14556
|
maxBytes: descriptor.kind === "image" ? DEFAULT_IMAGE_MAX_BYTES : DEFAULT_ATTACHMENT_MAX_BYTES
|
|
13852
14557
|
}));
|
|
13853
14558
|
} finally {
|
|
13854
|
-
await
|
|
14559
|
+
await fs9.rm(filePath, { force: true }).catch(() => {});
|
|
13855
14560
|
}
|
|
13856
14561
|
} catch (err) {
|
|
13857
14562
|
deps.errLog(`media download failed: ${String(err)}`);
|
|
@@ -14085,20 +14790,20 @@ var init_handle_weixin_message_turn = __esm(() => {
|
|
|
14085
14790
|
});
|
|
14086
14791
|
|
|
14087
14792
|
// src/weixin/storage/sync-buf.ts
|
|
14088
|
-
import
|
|
14089
|
-
import
|
|
14793
|
+
import fs10 from "node:fs";
|
|
14794
|
+
import path13 from "node:path";
|
|
14090
14795
|
function resolveAccountsDir2() {
|
|
14091
|
-
return
|
|
14796
|
+
return path13.join(resolveStateDir(), "openclaw-weixin", "accounts");
|
|
14092
14797
|
}
|
|
14093
14798
|
function getSyncBufFilePath(accountId) {
|
|
14094
|
-
return
|
|
14799
|
+
return path13.join(resolveAccountsDir2(), `${accountId}.sync.json`);
|
|
14095
14800
|
}
|
|
14096
14801
|
function getLegacySyncBufDefaultJsonPath() {
|
|
14097
|
-
return
|
|
14802
|
+
return path13.join(resolveStateDir(), "agents", "default", "sessions", ".openclaw-weixin-sync", "default.json");
|
|
14098
14803
|
}
|
|
14099
14804
|
function readSyncBufFile(filePath) {
|
|
14100
14805
|
try {
|
|
14101
|
-
const raw =
|
|
14806
|
+
const raw = fs10.readFileSync(filePath, "utf-8");
|
|
14102
14807
|
const data = JSON.parse(raw);
|
|
14103
14808
|
if (typeof data.get_updates_buf === "string") {
|
|
14104
14809
|
return data.get_updates_buf;
|
|
@@ -14110,10 +14815,10 @@ function loadGetUpdatesBuf(filePath) {
|
|
|
14110
14815
|
const value = readSyncBufFile(filePath);
|
|
14111
14816
|
if (value !== undefined)
|
|
14112
14817
|
return value;
|
|
14113
|
-
const accountId =
|
|
14818
|
+
const accountId = path13.basename(filePath, ".sync.json");
|
|
14114
14819
|
const rawId = deriveRawAccountId(accountId);
|
|
14115
14820
|
if (rawId) {
|
|
14116
|
-
const compatPath =
|
|
14821
|
+
const compatPath = path13.join(resolveAccountsDir2(), `${rawId}.sync.json`);
|
|
14117
14822
|
const compatValue = readSyncBufFile(compatPath);
|
|
14118
14823
|
if (compatValue !== undefined)
|
|
14119
14824
|
return compatValue;
|
|
@@ -14121,13 +14826,11 @@ function loadGetUpdatesBuf(filePath) {
|
|
|
14121
14826
|
return readSyncBufFile(getLegacySyncBufDefaultJsonPath());
|
|
14122
14827
|
}
|
|
14123
14828
|
function saveGetUpdatesBuf(filePath, getUpdatesBuf) {
|
|
14124
|
-
|
|
14125
|
-
ensureDirSync(dir);
|
|
14126
|
-
fs9.writeFileSync(filePath, JSON.stringify({ get_updates_buf: getUpdatesBuf }, null, 0), "utf-8");
|
|
14829
|
+
writePrivateFileSync(filePath, JSON.stringify({ get_updates_buf: getUpdatesBuf }, null, 0));
|
|
14127
14830
|
}
|
|
14128
14831
|
var init_sync_buf = __esm(() => {
|
|
14129
14832
|
init_accounts();
|
|
14130
|
-
|
|
14833
|
+
init_private_file();
|
|
14131
14834
|
init_state_dir();
|
|
14132
14835
|
});
|
|
14133
14836
|
|
|
@@ -14157,23 +14860,23 @@ function shouldFetchTypingConfig(textBody) {
|
|
|
14157
14860
|
}
|
|
14158
14861
|
async function monitorWeixinProvider(opts) {
|
|
14159
14862
|
const {
|
|
14160
|
-
baseUrl,
|
|
14161
|
-
cdnBaseUrl,
|
|
14162
|
-
token,
|
|
14163
|
-
accountId,
|
|
14164
14863
|
agent,
|
|
14165
14864
|
abortSignal,
|
|
14166
14865
|
longPollTimeoutMs
|
|
14167
14866
|
} = opts;
|
|
14867
|
+
let baseUrl = opts.baseUrl;
|
|
14868
|
+
let cdnBaseUrl = opts.cdnBaseUrl;
|
|
14869
|
+
let token = opts.token;
|
|
14870
|
+
let accountId = opts.accountId;
|
|
14168
14871
|
const log = opts.log ?? ((msg) => console.log(msg));
|
|
14169
14872
|
const errLog = (msg) => {
|
|
14170
14873
|
log(msg);
|
|
14171
14874
|
logger.error(msg);
|
|
14172
14875
|
};
|
|
14173
|
-
|
|
14876
|
+
let aLog = logger.withAccount(accountId);
|
|
14174
14877
|
log(`[weixin] monitor started (${baseUrl}, account=${accountId})`);
|
|
14175
14878
|
aLog.info(`Monitor started: baseUrl=${baseUrl}`);
|
|
14176
|
-
|
|
14879
|
+
let syncFilePath = getSyncBufFilePath(accountId);
|
|
14177
14880
|
const previousGetUpdatesBuf = loadGetUpdatesBuf(syncFilePath);
|
|
14178
14881
|
let getUpdatesBuf = previousGetUpdatesBuf ?? "";
|
|
14179
14882
|
if (previousGetUpdatesBuf) {
|
|
@@ -14181,7 +14884,7 @@ async function monitorWeixinProvider(opts) {
|
|
|
14181
14884
|
} else {
|
|
14182
14885
|
log(`[weixin] no previous sync buf, starting fresh`);
|
|
14183
14886
|
}
|
|
14184
|
-
|
|
14887
|
+
let configManager = new WeixinConfigManager({ baseUrl, token }, log);
|
|
14185
14888
|
const conversationExecutor = createConversationExecutor();
|
|
14186
14889
|
const seenMessageIds = new Set;
|
|
14187
14890
|
const messageIdOrder = [];
|
|
@@ -14204,11 +14907,37 @@ async function monitorWeixinProvider(opts) {
|
|
|
14204
14907
|
if (isApiError) {
|
|
14205
14908
|
const isSessionExpired = resp.errcode === SESSION_EXPIRED_ERRCODE || resp.ret === SESSION_EXPIRED_ERRCODE;
|
|
14206
14909
|
if (isSessionExpired) {
|
|
14910
|
+
const staleToken = token;
|
|
14911
|
+
const staleAccountId = accountId;
|
|
14912
|
+
errLog(`[weixin] session expired (errcode ${SESSION_EXPIRED_ERRCODE}), entering credential recovery. Please run \`weacpx login\` to re-login.`);
|
|
14207
14913
|
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
14914
|
consecutiveFailures = 0;
|
|
14211
|
-
await
|
|
14915
|
+
const recovered = await pollForFreshCredentials(staleAccountId, staleToken, log, abortSignal);
|
|
14916
|
+
if (recovered === null) {
|
|
14917
|
+
aLog.info("Monitor stopped (aborted during credential recovery)");
|
|
14918
|
+
return;
|
|
14919
|
+
}
|
|
14920
|
+
const oldAccountId = accountId;
|
|
14921
|
+
accountId = recovered.accountId;
|
|
14922
|
+
baseUrl = recovered.baseUrl;
|
|
14923
|
+
cdnBaseUrl = recovered.cdnBaseUrl;
|
|
14924
|
+
token = recovered.token;
|
|
14925
|
+
aLog = logger.withAccount(accountId);
|
|
14926
|
+
syncFilePath = getSyncBufFilePath(accountId);
|
|
14927
|
+
const previousBuf = loadGetUpdatesBuf(syncFilePath);
|
|
14928
|
+
getUpdatesBuf = previousBuf ?? "";
|
|
14929
|
+
configManager = new WeixinConfigManager({ baseUrl, token }, log);
|
|
14930
|
+
seenMessageIds.clear();
|
|
14931
|
+
messageIdOrder.length = 0;
|
|
14932
|
+
consecutiveFailures = 0;
|
|
14933
|
+
nextTimeoutMs = longPollTimeoutMs ?? DEFAULT_LONG_POLL_TIMEOUT_MS2;
|
|
14934
|
+
resetSessionPause(oldAccountId);
|
|
14935
|
+
resetSessionPause(accountId);
|
|
14936
|
+
if (oldAccountId !== accountId) {
|
|
14937
|
+
clearContextTokensForAccount(oldAccountId);
|
|
14938
|
+
restoreContextTokens(accountId);
|
|
14939
|
+
}
|
|
14940
|
+
log(`[weixin] credential recovered, resuming monitor with account=${accountId}`);
|
|
14212
14941
|
continue;
|
|
14213
14942
|
}
|
|
14214
14943
|
consecutiveFailures += 1;
|
|
@@ -14302,7 +15031,43 @@ function sleep(ms, signal) {
|
|
|
14302
15031
|
}, { once: true });
|
|
14303
15032
|
});
|
|
14304
15033
|
}
|
|
14305
|
-
|
|
15034
|
+
async function pollForFreshCredentials(staleAccountId, staleToken, log, abortSignal) {
|
|
15035
|
+
let attempt = 0;
|
|
15036
|
+
while (!abortSignal?.aborted) {
|
|
15037
|
+
attempt += 1;
|
|
15038
|
+
const currentAccount = resolveWeixinAccount(staleAccountId);
|
|
15039
|
+
if (currentAccount.token && currentAccount.token !== staleToken) {
|
|
15040
|
+
log(`[weixin] credential recovery: fresh token detected for account=${staleAccountId}`);
|
|
15041
|
+
return {
|
|
15042
|
+
accountId: currentAccount.accountId,
|
|
15043
|
+
baseUrl: currentAccount.baseUrl,
|
|
15044
|
+
cdnBaseUrl: currentAccount.cdnBaseUrl,
|
|
15045
|
+
token: currentAccount.token
|
|
15046
|
+
};
|
|
15047
|
+
}
|
|
15048
|
+
const ids = listWeixinAccountIds();
|
|
15049
|
+
for (const id of ids) {
|
|
15050
|
+
if (id === staleAccountId)
|
|
15051
|
+
continue;
|
|
15052
|
+
const account = resolveWeixinAccount(id);
|
|
15053
|
+
if (account.configured && account.token) {
|
|
15054
|
+
log(`[weixin] credential recovery: new account detected, switching to account=${id}`);
|
|
15055
|
+
return {
|
|
15056
|
+
accountId: account.accountId,
|
|
15057
|
+
baseUrl: account.baseUrl,
|
|
15058
|
+
cdnBaseUrl: account.cdnBaseUrl,
|
|
15059
|
+
token: account.token
|
|
15060
|
+
};
|
|
15061
|
+
}
|
|
15062
|
+
}
|
|
15063
|
+
if (attempt % 10 === 0) {
|
|
15064
|
+
log(`[weixin] credential recovery: still waiting for fresh credentials (checked ${attempt} times)`);
|
|
15065
|
+
}
|
|
15066
|
+
await sleep(CREDENTIAL_RECOVERY_POLL_INTERVAL_MS, abortSignal);
|
|
15067
|
+
}
|
|
15068
|
+
return null;
|
|
15069
|
+
}
|
|
15070
|
+
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
15071
|
var init_monitor = __esm(() => {
|
|
14307
15072
|
init_api();
|
|
14308
15073
|
init_config_cache();
|
|
@@ -14311,6 +15076,9 @@ var init_monitor = __esm(() => {
|
|
|
14311
15076
|
init_types2();
|
|
14312
15077
|
init_sync_buf();
|
|
14313
15078
|
init_logger();
|
|
15079
|
+
init_accounts();
|
|
15080
|
+
init_session_guard();
|
|
15081
|
+
init_inbound();
|
|
14314
15082
|
});
|
|
14315
15083
|
|
|
14316
15084
|
// src/weixin/bot.ts
|
|
@@ -14369,6 +15137,8 @@ function logout(opts) {
|
|
|
14369
15137
|
log("当前没有已登录的账号");
|
|
14370
15138
|
return;
|
|
14371
15139
|
}
|
|
15140
|
+
for (const id of ids)
|
|
15141
|
+
clearContextTokensForAccount(id);
|
|
14372
15142
|
clearAllWeixinAccounts();
|
|
14373
15143
|
log("✅ 已退出登录");
|
|
14374
15144
|
}
|
|
@@ -14396,6 +15166,7 @@ async function start(agent, opts) {
|
|
|
14396
15166
|
if (!account.configured) {
|
|
14397
15167
|
throw new Error(`账号 ${accountId} 未配置 (缺少 token),请先运行 login`);
|
|
14398
15168
|
}
|
|
15169
|
+
restoreContextTokens(account.accountId);
|
|
14399
15170
|
log(`[weixin] 启动 bot, account=${account.accountId}`);
|
|
14400
15171
|
await monitorWeixinProvider({
|
|
14401
15172
|
baseUrl: account.baseUrl,
|
|
@@ -14421,6 +15192,7 @@ async function start(agent, opts) {
|
|
|
14421
15192
|
var init_bot = __esm(() => {
|
|
14422
15193
|
init_accounts();
|
|
14423
15194
|
init_login_qr();
|
|
15195
|
+
init_inbound();
|
|
14424
15196
|
init_monitor();
|
|
14425
15197
|
});
|
|
14426
15198
|
|
|
@@ -14671,7 +15443,7 @@ var init_deliver_coordinator_message = __esm(() => {
|
|
|
14671
15443
|
});
|
|
14672
15444
|
|
|
14673
15445
|
// src/weixin/monitor/consumer-lock.ts
|
|
14674
|
-
import { mkdir as mkdir8, open as
|
|
15446
|
+
import { mkdir as mkdir8, open as open3, readFile as readFile6, rm as rm6 } from "node:fs/promises";
|
|
14675
15447
|
import { dirname as dirname8, join as join5 } from "node:path";
|
|
14676
15448
|
import { homedir as homedir4 } from "node:os";
|
|
14677
15449
|
function createWeixinConsumerLock(options = {}) {
|
|
@@ -14683,7 +15455,7 @@ function createWeixinConsumerLock(options = {}) {
|
|
|
14683
15455
|
await mkdir8(dirname8(lockFilePath), { recursive: true });
|
|
14684
15456
|
while (true) {
|
|
14685
15457
|
try {
|
|
14686
|
-
const handle = await
|
|
15458
|
+
const handle = await open3(lockFilePath, "wx");
|
|
14687
15459
|
try {
|
|
14688
15460
|
await handle.writeFile(`${JSON.stringify(meta2, null, 2)}
|
|
14689
15461
|
`, "utf8");
|
|
@@ -14751,9 +15523,9 @@ function createWeixinConsumerLock(options = {}) {
|
|
|
14751
15523
|
}
|
|
14752
15524
|
};
|
|
14753
15525
|
}
|
|
14754
|
-
async function loadLockMetadata(
|
|
15526
|
+
async function loadLockMetadata(path14) {
|
|
14755
15527
|
try {
|
|
14756
|
-
const raw = await readFile6(
|
|
15528
|
+
const raw = await readFile6(path14, "utf8");
|
|
14757
15529
|
const parsed = JSON.parse(raw);
|
|
14758
15530
|
if (!parsed || typeof parsed.pid !== "number" || !parsed.mode || !parsed.configPath || !parsed.statePath) {
|
|
14759
15531
|
return null;
|
|
@@ -15368,21 +16140,27 @@ async function loadConfiguredPlugins(input) {
|
|
|
15368
16140
|
const importPlugin = input.importPlugin ?? importPluginFromHome;
|
|
15369
16141
|
const loaded = [];
|
|
15370
16142
|
for (const config2 of enabled) {
|
|
15371
|
-
let moduleValue;
|
|
15372
16143
|
try {
|
|
15373
|
-
moduleValue
|
|
16144
|
+
let moduleValue;
|
|
16145
|
+
try {
|
|
16146
|
+
moduleValue = await importPlugin(config2.name, pluginHome);
|
|
16147
|
+
} catch (error2) {
|
|
16148
|
+
const message = error2 instanceof Error ? error2.message : String(error2);
|
|
16149
|
+
throw new Error(`failed to load plugin ${config2.name}: ${message}`);
|
|
16150
|
+
}
|
|
16151
|
+
const plugin = validateWeacpxPlugin(moduleValue, config2.name, {
|
|
16152
|
+
...input.currentWeacpxVersion !== undefined ? { currentWeacpxVersion: input.currentWeacpxVersion } : {}
|
|
16153
|
+
});
|
|
16154
|
+
const channels = plugin.channels ?? [];
|
|
16155
|
+
for (const channel of channels) {
|
|
16156
|
+
registerChannelPlugin(channel);
|
|
16157
|
+
}
|
|
16158
|
+
loaded.push({ name: config2.name, channels: channels.map((channel) => channel.type) });
|
|
15374
16159
|
} catch (error2) {
|
|
15375
|
-
|
|
15376
|
-
|
|
15377
|
-
|
|
15378
|
-
const plugin = validateWeacpxPlugin(moduleValue, config2.name, {
|
|
15379
|
-
...input.currentWeacpxVersion !== undefined ? { currentWeacpxVersion: input.currentWeacpxVersion } : {}
|
|
15380
|
-
});
|
|
15381
|
-
const channels = plugin.channels ?? [];
|
|
15382
|
-
for (const channel of channels) {
|
|
15383
|
-
registerChannelPlugin(channel);
|
|
16160
|
+
if (!input.onPluginError)
|
|
16161
|
+
throw error2;
|
|
16162
|
+
input.onPluginError({ name: config2.name, error: error2 });
|
|
15384
16163
|
}
|
|
15385
|
-
loaded.push({ name: config2.name, channels: channels.map((channel) => channel.type) });
|
|
15386
16164
|
}
|
|
15387
16165
|
return loaded;
|
|
15388
16166
|
}
|
|
@@ -15403,7 +16181,7 @@ var init_bootstrap = __esm(() => {
|
|
|
15403
16181
|
});
|
|
15404
16182
|
|
|
15405
16183
|
// src/logging/app-logger.ts
|
|
15406
|
-
import { appendFile, mkdir as mkdir9 } from "node:fs/promises";
|
|
16184
|
+
import { appendFile, chmod as chmod2, mkdir as mkdir9 } from "node:fs/promises";
|
|
15407
16185
|
import { dirname as dirname10 } from "node:path";
|
|
15408
16186
|
function createNoopAppLogger() {
|
|
15409
16187
|
return {
|
|
@@ -15417,6 +16195,7 @@ function createNoopAppLogger() {
|
|
|
15417
16195
|
function createAppLogger(options) {
|
|
15418
16196
|
const now = options.now ?? (() => new Date);
|
|
15419
16197
|
let writeChain = Promise.resolve();
|
|
16198
|
+
let modeEnsured = false;
|
|
15420
16199
|
return {
|
|
15421
16200
|
debug: async (event, message, context) => {
|
|
15422
16201
|
await enqueueWrite("debug", event, message, context);
|
|
@@ -15445,8 +16224,12 @@ function createAppLogger(options) {
|
|
|
15445
16224
|
}
|
|
15446
16225
|
const line = formatLogLine(now(), level, event, message, context);
|
|
15447
16226
|
await mkdir9(dirname10(options.filePath), { recursive: true });
|
|
16227
|
+
if (!modeEnsured) {
|
|
16228
|
+
modeEnsured = true;
|
|
16229
|
+
await chmod2(options.filePath, 384).catch(() => {});
|
|
16230
|
+
}
|
|
15448
16231
|
await rotateIfNeeded(options.filePath, Buffer.byteLength(line), options.maxSizeBytes, options.maxFiles);
|
|
15449
|
-
await appendFile(options.filePath, line, "utf8");
|
|
16232
|
+
await appendFile(options.filePath, line, { encoding: "utf8", mode: 384 });
|
|
15450
16233
|
}
|
|
15451
16234
|
}
|
|
15452
16235
|
function formatLogLine(time3, level, event, message, context) {
|
|
@@ -15823,6 +16606,7 @@ function parseCommand(input) {
|
|
|
15823
16606
|
if (command === "/workspace" && parts[1] === "new" && parts[2]) {
|
|
15824
16607
|
const name = parts[2];
|
|
15825
16608
|
let cwd = "";
|
|
16609
|
+
let raw = false;
|
|
15826
16610
|
let invalid = false;
|
|
15827
16611
|
for (let index = 3;index < parts.length; index += 1) {
|
|
15828
16612
|
if (parts[index] === "--cwd" || parts[index] === "-d") {
|
|
@@ -15834,11 +16618,19 @@ function parseCommand(input) {
|
|
|
15834
16618
|
index += 1;
|
|
15835
16619
|
continue;
|
|
15836
16620
|
}
|
|
16621
|
+
if (parts[index] === "--raw") {
|
|
16622
|
+
if (raw) {
|
|
16623
|
+
invalid = true;
|
|
16624
|
+
break;
|
|
16625
|
+
}
|
|
16626
|
+
raw = true;
|
|
16627
|
+
continue;
|
|
16628
|
+
}
|
|
15837
16629
|
invalid = true;
|
|
15838
16630
|
break;
|
|
15839
16631
|
}
|
|
15840
16632
|
if (!invalid && name.trim().length > 0 && cwd.trim().length > 0) {
|
|
15841
|
-
return { kind: "workspace.new", name, cwd };
|
|
16633
|
+
return { kind: "workspace.new", name, cwd, ...raw ? { raw: true } : {} };
|
|
15842
16634
|
}
|
|
15843
16635
|
}
|
|
15844
16636
|
if (command === "/workspace" && parts[1] === "rm" && parts[2]) {
|
|
@@ -16298,26 +17090,26 @@ var init_permission_handler = __esm(() => {
|
|
|
16298
17090
|
|
|
16299
17091
|
// src/commands/handlers/config-handler.ts
|
|
16300
17092
|
function handleConfigShow(context) {
|
|
16301
|
-
const lines = ["支持修改的配置字段:", ...SUPPORTED_CONFIG_PATHS.map((
|
|
16302
|
-
lines.push("", "兼容旧配置:", ...LEGACY_CONFIG_PATHS.map((
|
|
17093
|
+
const lines = ["支持修改的配置字段:", ...SUPPORTED_CONFIG_PATHS.map((path14) => `- ${path14}`)];
|
|
17094
|
+
lines.push("", "兼容旧配置:", ...LEGACY_CONFIG_PATHS.map((path14) => `- ${path14}`));
|
|
16303
17095
|
if (context.config) {
|
|
16304
17096
|
lines.push("", "示例:", "- /config set channel.replyMode final", "- /config set logging.level debug");
|
|
16305
17097
|
}
|
|
16306
17098
|
return { text: lines.join(`
|
|
16307
17099
|
`) };
|
|
16308
17100
|
}
|
|
16309
|
-
async function handleConfigSet(context,
|
|
17101
|
+
async function handleConfigSet(context, path14, rawValue) {
|
|
16310
17102
|
if (!context.config || !context.configStore) {
|
|
16311
17103
|
return { text: "当前没有加载可写入的配置。" };
|
|
16312
17104
|
}
|
|
16313
17105
|
const previous = cloneAppConfig(context.config);
|
|
16314
17106
|
const updated = cloneAppConfig(context.config);
|
|
16315
|
-
const result = applySupportedConfigUpdate(updated,
|
|
17107
|
+
const result = applySupportedConfigUpdate(updated, path14, rawValue);
|
|
16316
17108
|
if ("error" in result) {
|
|
16317
17109
|
return { text: result.error };
|
|
16318
17110
|
}
|
|
16319
17111
|
await context.configStore.save(updated);
|
|
16320
|
-
if (
|
|
17112
|
+
if (path14 === "transport.permissionMode" || path14 === "transport.nonInteractivePermissions" || path14 === "transport.permissionPolicy") {
|
|
16321
17113
|
try {
|
|
16322
17114
|
await context.transport.updatePermissionPolicy?.(updated.transport);
|
|
16323
17115
|
} catch (error2) {
|
|
@@ -16327,10 +17119,10 @@ async function handleConfigSet(context, path13, rawValue) {
|
|
|
16327
17119
|
}
|
|
16328
17120
|
}
|
|
16329
17121
|
context.replaceConfig(updated);
|
|
16330
|
-
return { text: `配置已更新:${
|
|
17122
|
+
return { text: `配置已更新:${path14} = ${result.renderedValue}` };
|
|
16331
17123
|
}
|
|
16332
|
-
function applySupportedConfigUpdate(config2,
|
|
16333
|
-
switch (
|
|
17124
|
+
function applySupportedConfigUpdate(config2, path14, rawValue) {
|
|
17125
|
+
switch (path14) {
|
|
16334
17126
|
case "transport.type": {
|
|
16335
17127
|
const parsed = parseEnum(rawValue, ["acpx-cli", "acpx-bridge"]);
|
|
16336
17128
|
if (!parsed)
|
|
@@ -16418,18 +17210,18 @@ function applySupportedConfigUpdate(config2, path13, rawValue) {
|
|
|
16418
17210
|
};
|
|
16419
17211
|
}
|
|
16420
17212
|
}
|
|
16421
|
-
const agentMatch =
|
|
17213
|
+
const agentMatch = path14.match(/^agents\.([^.]+)\.(driver|command)$/);
|
|
16422
17214
|
if (agentMatch) {
|
|
16423
17215
|
const [, name, field] = agentMatch;
|
|
16424
17216
|
if (!name || !field) {
|
|
16425
|
-
return { error: `不支持修改这个配置路径:${
|
|
17217
|
+
return { error: `不支持修改这个配置路径:${path14}` };
|
|
16426
17218
|
}
|
|
16427
17219
|
const agent = config2.agents[name];
|
|
16428
17220
|
if (!agent) {
|
|
16429
17221
|
return { error: `Agent「${name}」不存在,请先创建。` };
|
|
16430
17222
|
}
|
|
16431
17223
|
if (!rawValue.trim()) {
|
|
16432
|
-
return { error: `${
|
|
17224
|
+
return { error: `${path14} 不能为空。` };
|
|
16433
17225
|
}
|
|
16434
17226
|
if (field === "driver") {
|
|
16435
17227
|
agent.driver = rawValue;
|
|
@@ -16438,18 +17230,18 @@ function applySupportedConfigUpdate(config2, path13, rawValue) {
|
|
|
16438
17230
|
}
|
|
16439
17231
|
return { renderedValue: rawValue };
|
|
16440
17232
|
}
|
|
16441
|
-
const workspaceMatch =
|
|
17233
|
+
const workspaceMatch = path14.match(/^workspaces\.([^.]+)\.(cwd|description)$/);
|
|
16442
17234
|
if (workspaceMatch) {
|
|
16443
17235
|
const [, name, field] = workspaceMatch;
|
|
16444
17236
|
if (!name || !field) {
|
|
16445
|
-
return { error: `不支持修改这个配置路径:${
|
|
17237
|
+
return { error: `不支持修改这个配置路径:${path14}` };
|
|
16446
17238
|
}
|
|
16447
17239
|
const workspace = config2.workspaces[name];
|
|
16448
17240
|
if (!workspace) {
|
|
16449
17241
|
return { error: `工作区「${name}」不存在,请先创建。` };
|
|
16450
17242
|
}
|
|
16451
17243
|
if (!rawValue.trim()) {
|
|
16452
|
-
return { error: `${
|
|
17244
|
+
return { error: `${path14} 不能为空。` };
|
|
16453
17245
|
}
|
|
16454
17246
|
if (field === "cwd") {
|
|
16455
17247
|
workspace.cwd = rawValue;
|
|
@@ -16458,15 +17250,15 @@ function applySupportedConfigUpdate(config2, path13, rawValue) {
|
|
|
16458
17250
|
}
|
|
16459
17251
|
return { renderedValue: rawValue };
|
|
16460
17252
|
}
|
|
16461
|
-
return { error: `不支持修改这个配置路径:${
|
|
17253
|
+
return { error: `不支持修改这个配置路径:${path14}` };
|
|
16462
17254
|
}
|
|
16463
17255
|
function parseEnum(value, allowed) {
|
|
16464
17256
|
return allowed.includes(value) ? value : null;
|
|
16465
17257
|
}
|
|
16466
|
-
function parsePositiveNumber(rawValue,
|
|
17258
|
+
function parsePositiveNumber(rawValue, path14) {
|
|
16467
17259
|
const value = Number(rawValue);
|
|
16468
17260
|
if (!Number.isFinite(value) || value <= 0) {
|
|
16469
|
-
return { error: `${
|
|
17261
|
+
return { error: `${path14} 必须是正数。` };
|
|
16470
17262
|
}
|
|
16471
17263
|
return { value };
|
|
16472
17264
|
}
|
|
@@ -16901,7 +17693,7 @@ async function handleSessionAttach(context, chatKey, alias, agent, workspace, tr
|
|
|
16901
17693
|
return {
|
|
16902
17694
|
text: [
|
|
16903
17695
|
"没有找到可绑定的已有会话。",
|
|
16904
|
-
`请确认会话名是否正确,然后重新执行:/session attach ${alias} --agent ${agent} --ws ${workspace} --name <会话名>`
|
|
17696
|
+
`请确认会话名是否正确,然后重新执行:/session attach ${alias} --agent ${agent} --ws ${quoteWorkspaceNameIfNeeded(workspace)} --name <会话名>`
|
|
16905
17697
|
].join(`
|
|
16906
17698
|
`)
|
|
16907
17699
|
};
|
|
@@ -17238,6 +18030,7 @@ var NO_CURRENT_SESSION_TEXT = "当前还没有选中的会话。请先执行 /se
|
|
|
17238
18030
|
var init_session_handler = __esm(() => {
|
|
17239
18031
|
init_build_coordinator_prompt();
|
|
17240
18032
|
init_channel_scope();
|
|
18033
|
+
init_workspace_name();
|
|
17241
18034
|
sessionHelp = {
|
|
17242
18035
|
topic: "session",
|
|
17243
18036
|
aliases: ["ss", "sessions"],
|
|
@@ -17885,7 +18678,7 @@ var init_agent_handler = __esm(() => {
|
|
|
17885
18678
|
function handleWorkspaces(context) {
|
|
17886
18679
|
return { text: context.config ? renderWorkspaces(context.config) : "No config loaded." };
|
|
17887
18680
|
}
|
|
17888
|
-
async function handleWorkspaceCreate(context, workspaceName, cwd) {
|
|
18681
|
+
async function handleWorkspaceCreate(context, workspaceName, cwd, options = {}) {
|
|
17889
18682
|
if (!context.config || !context.configStore) {
|
|
17890
18683
|
return { text: "当前没有加载可写入的配置。" };
|
|
17891
18684
|
}
|
|
@@ -17893,9 +18686,18 @@ async function handleWorkspaceCreate(context, workspaceName, cwd) {
|
|
|
17893
18686
|
if (!await pathExists(normalizedCwd)) {
|
|
17894
18687
|
return { text: `工作区路径不存在:${cwd}` };
|
|
17895
18688
|
}
|
|
17896
|
-
|
|
18689
|
+
let name = workspaceName;
|
|
18690
|
+
let notice;
|
|
18691
|
+
if (!options.raw && !isWorkspaceNameValid(workspaceName)) {
|
|
18692
|
+
const base = sanitizeWorkspaceName(workspaceName);
|
|
18693
|
+
name = allocateWorkspaceName(base, context.config.workspaces);
|
|
18694
|
+
notice = `名称 ${JSON.stringify(workspaceName)} 含有特殊字符,已保存为「${name}」。如需保留原名请加 --raw。`;
|
|
18695
|
+
}
|
|
18696
|
+
const updated = await context.configStore.upsertWorkspace(name, normalizedCwd);
|
|
17897
18697
|
context.replaceConfig(updated);
|
|
17898
|
-
|
|
18698
|
+
const savedLine = `工作区「${name}」已保存`;
|
|
18699
|
+
return { text: notice ? `${notice}
|
|
18700
|
+
${savedLine}` : savedLine };
|
|
17899
18701
|
}
|
|
17900
18702
|
async function handleWorkspaceRemove(context, workspaceName) {
|
|
17901
18703
|
if (!context.config || !context.configStore) {
|
|
@@ -17907,6 +18709,7 @@ async function handleWorkspaceRemove(context, workspaceName) {
|
|
|
17907
18709
|
}
|
|
17908
18710
|
var workspaceHelp;
|
|
17909
18711
|
var init_workspace_handler = __esm(() => {
|
|
18712
|
+
init_workspace_name();
|
|
17910
18713
|
init_workspace_path();
|
|
17911
18714
|
workspaceHelp = {
|
|
17912
18715
|
topic: "workspace",
|
|
@@ -17915,7 +18718,7 @@ var init_workspace_handler = __esm(() => {
|
|
|
17915
18718
|
commands: [
|
|
17916
18719
|
{ usage: "/workspaces", description: "查看当前已注册的工作区" },
|
|
17917
18720
|
{ usage: "/workspace 或 /ws", description: "查看工作区列表" },
|
|
17918
|
-
{ usage: "/ws new <name> -d <path>", description: "
|
|
18721
|
+
{ usage: "/ws new <name> -d <path> [--raw]", description: "添加工作区;含特殊字符的名称会被自动规范化,--raw 保留原名" },
|
|
17919
18722
|
{ usage: "/workspace rm <name>", description: "删除工作区" }
|
|
17920
18723
|
],
|
|
17921
18724
|
examples: ['/ws new backend -d "/tmp/backend"', "/workspace rm backend"]
|
|
@@ -18152,7 +18955,7 @@ async function resolveShortcutWorkspace(context, target) {
|
|
|
18152
18955
|
reused: true
|
|
18153
18956
|
};
|
|
18154
18957
|
}
|
|
18155
|
-
const workspaceName = allocateWorkspaceName(
|
|
18958
|
+
const workspaceName = allocateWorkspaceName(sanitizeWorkspaceName(basenameForWorkspacePath(cwd)), context.config?.workspaces ?? {});
|
|
18156
18959
|
const updated = await context.configStore.upsertWorkspace(workspaceName, cwd);
|
|
18157
18960
|
context.replaceConfig(updated);
|
|
18158
18961
|
return {
|
|
@@ -18161,16 +18964,6 @@ async function resolveShortcutWorkspace(context, target) {
|
|
|
18161
18964
|
reused: false
|
|
18162
18965
|
};
|
|
18163
18966
|
}
|
|
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
18967
|
async function allocateUniqueSessionAlias(context, baseAlias, chatKey) {
|
|
18175
18968
|
if (!await hasLogicalSession(context, baseAlias, chatKey)) {
|
|
18176
18969
|
return baseAlias;
|
|
@@ -18196,6 +18989,7 @@ function renderShortcutSessionCreationError(workspace, alias) {
|
|
|
18196
18989
|
};
|
|
18197
18990
|
}
|
|
18198
18991
|
var init_session_shortcut_handler = __esm(() => {
|
|
18992
|
+
init_workspace_name();
|
|
18199
18993
|
init_workspace_path();
|
|
18200
18994
|
init_errors();
|
|
18201
18995
|
init_channel_scope();
|
|
@@ -18208,8 +19002,8 @@ function renderTransportError(session, error2) {
|
|
|
18208
19002
|
return {
|
|
18209
19003
|
text: [
|
|
18210
19004
|
`当前会话「${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 <会话名>`
|
|
19005
|
+
`请先在微信里重新执行:/session new ${session.alias} --agent ${session.agent} --ws ${quoteWorkspaceNameIfNeeded(session.workspace)}`,
|
|
19006
|
+
`如果你要绑定一个已有会话,再执行:/session attach ${session.alias} --agent ${session.agent} --ws ${quoteWorkspaceNameIfNeeded(session.workspace)} --name <会话名>`
|
|
18213
19007
|
].join(`
|
|
18214
19008
|
`)
|
|
18215
19009
|
};
|
|
@@ -18271,7 +19065,7 @@ function renderSessionCreationFailure(session, detail) {
|
|
|
18271
19065
|
text: [
|
|
18272
19066
|
"会话创建失败。",
|
|
18273
19067
|
`错误信息:${summarizeTransportError(detail)}`,
|
|
18274
|
-
`如果你要先绑定一个已有会话,可以执行:/session attach ${session.alias} --agent ${session.agent} --ws ${session.workspace} --name <会话名>`
|
|
19068
|
+
`如果你要先绑定一个已有会话,可以执行:/session attach ${session.alias} --agent ${session.agent} --ws ${quoteWorkspaceNameIfNeeded(session.workspace)} --name <会话名>`
|
|
18275
19069
|
].join(`
|
|
18276
19070
|
`)
|
|
18277
19071
|
};
|
|
@@ -18290,6 +19084,7 @@ async function tryRecoverMissingSession(ops, session, error2) {
|
|
|
18290
19084
|
}
|
|
18291
19085
|
var init_session_recovery_handler = __esm(() => {
|
|
18292
19086
|
init_errors();
|
|
19087
|
+
init_workspace_name();
|
|
18293
19088
|
});
|
|
18294
19089
|
|
|
18295
19090
|
// src/recovery/auto-install-optional-dep.ts
|
|
@@ -18415,10 +19210,10 @@ ${err.message}`, reason: "spawn" });
|
|
|
18415
19210
|
const dir = join10(homedir6(), ".weacpx", "logs");
|
|
18416
19211
|
await mkdir10(dir, { recursive: true });
|
|
18417
19212
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "").replace(/-/g, "");
|
|
18418
|
-
const
|
|
18419
|
-
const stream = createWriteStream(
|
|
19213
|
+
const path14 = join10(dir, `auto-install-${timestamp}.log`);
|
|
19214
|
+
const stream = createWriteStream(path14, { flags: "a" });
|
|
18420
19215
|
return {
|
|
18421
|
-
path:
|
|
19216
|
+
path: path14,
|
|
18422
19217
|
append: async (chunk) => {
|
|
18423
19218
|
await new Promise((resolve3, reject) => stream.write(chunk, (err) => err ? reject(err) : resolve3()));
|
|
18424
19219
|
},
|
|
@@ -18501,9 +19296,9 @@ function isUnder(child, parent) {
|
|
|
18501
19296
|
const p = parent.replace(/[\\/]+$/, "");
|
|
18502
19297
|
return c === p || c.startsWith(p + "/") || c.startsWith(p + "\\");
|
|
18503
19298
|
}
|
|
18504
|
-
async function defaultFsExists(
|
|
19299
|
+
async function defaultFsExists(path14) {
|
|
18505
19300
|
try {
|
|
18506
|
-
await access3(
|
|
19301
|
+
await access3(path14);
|
|
18507
19302
|
return true;
|
|
18508
19303
|
} catch {
|
|
18509
19304
|
return false;
|
|
@@ -18731,7 +19526,7 @@ class CommandRouter {
|
|
|
18731
19526
|
case "workspaces":
|
|
18732
19527
|
return handleWorkspaces(this.createHandlerContext());
|
|
18733
19528
|
case "workspace.new":
|
|
18734
|
-
return await handleWorkspaceCreate(this.createHandlerContext(), command.name, command.cwd);
|
|
19529
|
+
return await handleWorkspaceCreate(this.createHandlerContext(), command.name, command.cwd, command.raw ? { raw: true } : {});
|
|
18735
19530
|
case "workspace.rm":
|
|
18736
19531
|
return await handleWorkspaceRemove(this.createHandlerContext(), command.name);
|
|
18737
19532
|
case "sessions":
|
|
@@ -19193,7 +19988,7 @@ function resolveAcpxCommandMetadata(options = {}) {
|
|
|
19193
19988
|
}
|
|
19194
19989
|
const platform = options.platform ?? process.platform;
|
|
19195
19990
|
const resolvePackageJson = options.resolvePackageJson ?? ((id) => require3.resolve(id));
|
|
19196
|
-
const readPackageJson = options.readPackageJson ?? ((
|
|
19991
|
+
const readPackageJson = options.readPackageJson ?? ((path14) => JSON.parse(readFileSync(path14, "utf8")));
|
|
19197
19992
|
try {
|
|
19198
19993
|
const packageJsonPath = resolvePackageJson("acpx/package.json");
|
|
19199
19994
|
const pkg = readPackageJson(packageJsonPath);
|
|
@@ -19570,8 +20365,8 @@ class OrchestrationServer {
|
|
|
19570
20365
|
if (this.endpoint.kind !== "unix") {
|
|
19571
20366
|
return;
|
|
19572
20367
|
}
|
|
19573
|
-
const removeFile = this.deps.removeFile ?? (async (
|
|
19574
|
-
await rm7(
|
|
20368
|
+
const removeFile = this.deps.removeFile ?? (async (path14) => {
|
|
20369
|
+
await rm7(path14, { force: true });
|
|
19575
20370
|
});
|
|
19576
20371
|
await removeFile(this.endpoint.path);
|
|
19577
20372
|
}
|
|
@@ -19732,9 +20527,9 @@ function requireTaskQuestions(params, key) {
|
|
|
19732
20527
|
};
|
|
19733
20528
|
});
|
|
19734
20529
|
}
|
|
19735
|
-
async function canConnectToEndpoint(
|
|
20530
|
+
async function canConnectToEndpoint(path14) {
|
|
19736
20531
|
return await new Promise((resolve3) => {
|
|
19737
|
-
const socket = createConnection2(
|
|
20532
|
+
const socket = createConnection2(path14);
|
|
19738
20533
|
let settled = false;
|
|
19739
20534
|
const finish = (result) => {
|
|
19740
20535
|
if (settled) {
|
|
@@ -19755,7 +20550,7 @@ async function canConnectToEndpoint(path13) {
|
|
|
19755
20550
|
});
|
|
19756
20551
|
});
|
|
19757
20552
|
}
|
|
19758
|
-
async function listen(server,
|
|
20553
|
+
async function listen(server, path14) {
|
|
19759
20554
|
await new Promise((resolve3, reject) => {
|
|
19760
20555
|
const onError = (error2) => {
|
|
19761
20556
|
server.off("listening", onListening);
|
|
@@ -19767,7 +20562,7 @@ async function listen(server, path13) {
|
|
|
19767
20562
|
};
|
|
19768
20563
|
server.once("error", onError);
|
|
19769
20564
|
server.once("listening", onListening);
|
|
19770
|
-
server.listen(
|
|
20565
|
+
server.listen(path14);
|
|
19771
20566
|
});
|
|
19772
20567
|
}
|
|
19773
20568
|
function isServerNotRunningError(error2) {
|
|
@@ -19891,6 +20686,7 @@ class OrchestrationService {
|
|
|
19891
20686
|
stateMutex;
|
|
19892
20687
|
pendingWorkerSessions = new Map;
|
|
19893
20688
|
pendingLogicalTransportSessions = new Map;
|
|
20689
|
+
pendingParallelStarts = new Map;
|
|
19894
20690
|
constructor(deps) {
|
|
19895
20691
|
this.deps = deps;
|
|
19896
20692
|
this.stateMutex = deps.stateMutex ?? new AsyncMutex;
|
|
@@ -20055,84 +20851,139 @@ class OrchestrationService {
|
|
|
20055
20851
|
const normalizedGroupId = this.normalizeGroupId(input.groupId);
|
|
20056
20852
|
const taskId = this.deps.createId();
|
|
20057
20853
|
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 () => {
|
|
20854
|
+
if (input.parallel) {
|
|
20855
|
+
const queuedResult = await this.mutate(async () => {
|
|
20073
20856
|
const state = await this.deps.loadState();
|
|
20074
|
-
|
|
20075
|
-
|
|
20076
|
-
|
|
20857
|
+
if (this.canStartParallelTask(state, input.targetAgent)) {
|
|
20858
|
+
this.pendingParallelStarts.set(input.targetAgent, (this.pendingParallelStarts.get(input.targetAgent) ?? 0) + 1);
|
|
20859
|
+
return null;
|
|
20077
20860
|
}
|
|
20078
|
-
const
|
|
20861
|
+
const now = this.deps.now().toISOString();
|
|
20862
|
+
const queuedTask = {
|
|
20079
20863
|
taskId,
|
|
20080
20864
|
sourceHandle: input.sourceHandle,
|
|
20081
20865
|
sourceKind: input.sourceKind,
|
|
20082
20866
|
coordinatorSession: input.coordinatorSession,
|
|
20083
|
-
workerSession
|
|
20867
|
+
workerSession,
|
|
20084
20868
|
workspace: input.workspace,
|
|
20085
20869
|
...input.cwd ? { cwd: input.cwd } : {},
|
|
20086
20870
|
targetAgent: input.targetAgent,
|
|
20087
20871
|
...role ? { role } : {},
|
|
20088
20872
|
...normalizedGroupId ? { groupId: normalizedGroupId } : {},
|
|
20089
20873
|
task: input.task,
|
|
20090
|
-
status: "
|
|
20874
|
+
status: "queued",
|
|
20875
|
+
ephemeralWorkerSession: true,
|
|
20091
20876
|
summary: "",
|
|
20092
20877
|
resultText: "",
|
|
20093
20878
|
createdAt: now,
|
|
20094
20879
|
updatedAt: now,
|
|
20095
20880
|
eventSeq: 1,
|
|
20096
|
-
events: [{ seq: 1, at: now, type: "created", status: "
|
|
20881
|
+
events: [{ seq: 1, at: now, type: "created", status: "queued", message: "Task queued at parallel capacity" }],
|
|
20097
20882
|
...input.chatKey ? { chatKey: input.chatKey } : {},
|
|
20098
20883
|
...input.replyContextToken ? { replyContextToken: input.replyContextToken } : {},
|
|
20099
20884
|
...input.accountId ? { accountId: input.accountId } : {}
|
|
20100
20885
|
};
|
|
20101
|
-
|
|
20102
|
-
|
|
20103
|
-
|
|
20104
|
-
|
|
20105
|
-
|
|
20106
|
-
|
|
20107
|
-
|
|
20108
|
-
|
|
20109
|
-
|
|
20110
|
-
|
|
20111
|
-
|
|
20112
|
-
|
|
20113
|
-
this.
|
|
20114
|
-
|
|
20115
|
-
|
|
20116
|
-
|
|
20886
|
+
state.orchestration.tasks[taskId] = queuedTask;
|
|
20887
|
+
await this.deps.saveState(state);
|
|
20888
|
+
return { taskId, status: "queued", workerSession };
|
|
20889
|
+
});
|
|
20890
|
+
if (queuedResult) {
|
|
20891
|
+
this.logEvent("orchestration.task.queued", "parallel task queued at capacity", { taskId, targetAgent: input.targetAgent });
|
|
20892
|
+
return queuedResult;
|
|
20893
|
+
}
|
|
20894
|
+
}
|
|
20895
|
+
const releasePendingParallelStart = input.parallel ? () => {
|
|
20896
|
+
const count = this.pendingParallelStarts.get(input.targetAgent) ?? 0;
|
|
20897
|
+
if (count <= 1) {
|
|
20898
|
+
this.pendingParallelStarts.delete(input.targetAgent);
|
|
20899
|
+
} else {
|
|
20900
|
+
this.pendingParallelStarts.set(input.targetAgent, count - 1);
|
|
20901
|
+
}
|
|
20902
|
+
} : undefined;
|
|
20903
|
+
let ensuredWorkerSession = workerSession;
|
|
20904
|
+
let prepared;
|
|
20905
|
+
const releaseWorkerReservation = await this.reserveProposedWorkerSession(workerSession);
|
|
20906
|
+
try {
|
|
20907
|
+
try {
|
|
20908
|
+
ensuredWorkerSession = await this.ensureReservedWorkerSession({
|
|
20909
|
+
workerSession,
|
|
20910
|
+
sourceHandle: input.sourceHandle,
|
|
20911
|
+
sourceKind: input.sourceKind,
|
|
20117
20912
|
coordinatorSession: input.coordinatorSession,
|
|
20118
20913
|
workspace: input.workspace,
|
|
20119
20914
|
...input.cwd ? { cwd: input.cwd } : {},
|
|
20120
20915
|
targetAgent: input.targetAgent,
|
|
20121
20916
|
role
|
|
20122
|
-
};
|
|
20123
|
-
await this.
|
|
20124
|
-
|
|
20125
|
-
|
|
20126
|
-
|
|
20127
|
-
|
|
20128
|
-
|
|
20129
|
-
|
|
20130
|
-
|
|
20131
|
-
|
|
20917
|
+
});
|
|
20918
|
+
prepared = await this.mutate(async () => {
|
|
20919
|
+
const state = await this.deps.loadState();
|
|
20920
|
+
const now = this.deps.now().toISOString();
|
|
20921
|
+
if (normalizedGroupId) {
|
|
20922
|
+
this.assertGroupOwnership(this.ensureGroups(state)[normalizedGroupId], normalizedGroupId, input.coordinatorSession);
|
|
20923
|
+
}
|
|
20924
|
+
const task = {
|
|
20925
|
+
taskId,
|
|
20926
|
+
sourceHandle: input.sourceHandle,
|
|
20927
|
+
sourceKind: input.sourceKind,
|
|
20928
|
+
coordinatorSession: input.coordinatorSession,
|
|
20929
|
+
workerSession: ensuredWorkerSession,
|
|
20930
|
+
workspace: input.workspace,
|
|
20931
|
+
...input.cwd ? { cwd: input.cwd } : {},
|
|
20932
|
+
targetAgent: input.targetAgent,
|
|
20933
|
+
...role ? { role } : {},
|
|
20934
|
+
...normalizedGroupId ? { groupId: normalizedGroupId } : {},
|
|
20935
|
+
task: input.task,
|
|
20936
|
+
status: "running",
|
|
20937
|
+
summary: "",
|
|
20938
|
+
resultText: "",
|
|
20939
|
+
createdAt: now,
|
|
20940
|
+
updatedAt: now,
|
|
20941
|
+
eventSeq: 1,
|
|
20942
|
+
events: [{ seq: 1, at: now, type: "created", status: "running", message: "Task created" }],
|
|
20943
|
+
...input.chatKey ? { chatKey: input.chatKey } : {},
|
|
20944
|
+
...input.replyContextToken ? { replyContextToken: input.replyContextToken } : {},
|
|
20945
|
+
...input.accountId ? { accountId: input.accountId } : {},
|
|
20946
|
+
...input.parallel ? { ephemeralWorkerSession: true } : {}
|
|
20947
|
+
};
|
|
20948
|
+
let previousGroup;
|
|
20949
|
+
if (normalizedGroupId) {
|
|
20950
|
+
const group = this.ensureGroups(state)[normalizedGroupId];
|
|
20951
|
+
previousGroup = { ...group };
|
|
20952
|
+
group.updatedAt = now;
|
|
20953
|
+
group.coordinatorInjectedAt = undefined;
|
|
20954
|
+
group.injectionPending = undefined;
|
|
20955
|
+
group.injectionAppliedAt = undefined;
|
|
20956
|
+
group.lastInjectionError = undefined;
|
|
20957
|
+
}
|
|
20958
|
+
const previousBinding = state.orchestration.workerBindings[ensuredWorkerSession];
|
|
20959
|
+
this.assertWorkerSessionDoesNotConflictExternalCoordinator(state, ensuredWorkerSession);
|
|
20960
|
+
this.assertWorkerSessionAvailable(state, ensuredWorkerSession, undefined, { allowCurrentReservation: true });
|
|
20961
|
+
state.orchestration.tasks[taskId] = task;
|
|
20962
|
+
state.orchestration.workerBindings[ensuredWorkerSession] = {
|
|
20963
|
+
sourceHandle: ensuredWorkerSession,
|
|
20964
|
+
coordinatorSession: input.coordinatorSession,
|
|
20965
|
+
workspace: input.workspace,
|
|
20966
|
+
...input.cwd ? { cwd: input.cwd } : {},
|
|
20967
|
+
targetAgent: input.targetAgent,
|
|
20968
|
+
role,
|
|
20969
|
+
...input.parallel ? { ephemeral: true } : {}
|
|
20970
|
+
};
|
|
20971
|
+
await this.deps.saveState(state);
|
|
20972
|
+
return {
|
|
20973
|
+
task: { ...task },
|
|
20974
|
+
previousBinding,
|
|
20975
|
+
previousGroup,
|
|
20976
|
+
normalizedGroupId
|
|
20977
|
+
};
|
|
20978
|
+
});
|
|
20979
|
+
} catch (error2) {
|
|
20980
|
+
await releaseWorkerReservation();
|
|
20981
|
+
throw error2;
|
|
20982
|
+
}
|
|
20132
20983
|
await releaseWorkerReservation();
|
|
20133
|
-
|
|
20984
|
+
} finally {
|
|
20985
|
+
releasePendingParallelStart?.();
|
|
20134
20986
|
}
|
|
20135
|
-
await releaseWorkerReservation();
|
|
20136
20987
|
try {
|
|
20137
20988
|
await this.deps.dispatchWorkerTask({
|
|
20138
20989
|
taskId,
|
|
@@ -20182,6 +21033,7 @@ class OrchestrationService {
|
|
|
20182
21033
|
return { sourceContext, targetLocation, role, normalizedGroupId };
|
|
20183
21034
|
});
|
|
20184
21035
|
const autoRun = preflight.sourceContext.sourceKind === "coordinator";
|
|
21036
|
+
const taskId = this.deps.createId();
|
|
20185
21037
|
const workerSessionName = await this.resolveWorkerSession({
|
|
20186
21038
|
sourceHandle: input.sourceHandle,
|
|
20187
21039
|
sourceKind: preflight.sourceContext.sourceKind,
|
|
@@ -20190,18 +21042,18 @@ class OrchestrationService {
|
|
|
20190
21042
|
...preflight.targetLocation.cwd ? { cwd: preflight.targetLocation.cwd } : {},
|
|
20191
21043
|
targetAgent: input.targetAgent,
|
|
20192
21044
|
task: input.task,
|
|
20193
|
-
...preflight.role ? { role: preflight.role } : {}
|
|
21045
|
+
...preflight.role ? { role: preflight.role } : {},
|
|
21046
|
+
...input.parallel ? { parallel: true } : {}
|
|
20194
21047
|
});
|
|
20195
|
-
|
|
20196
|
-
|
|
20197
|
-
try {
|
|
20198
|
-
prepared = await this.mutate(async () => {
|
|
21048
|
+
if (input.parallel && autoRun) {
|
|
21049
|
+
const queuedResult = await this.mutate(async () => {
|
|
20199
21050
|
const state = await this.deps.loadState();
|
|
20200
|
-
this.
|
|
21051
|
+
if (this.canStartParallelTask(state, input.targetAgent)) {
|
|
21052
|
+
this.pendingParallelStarts.set(input.targetAgent, (this.pendingParallelStarts.get(input.targetAgent) ?? 0) + 1);
|
|
21053
|
+
return null;
|
|
21054
|
+
}
|
|
20201
21055
|
const now = this.deps.now().toISOString();
|
|
20202
|
-
const
|
|
20203
|
-
const status = autoRun ? "running" : "needs_confirmation";
|
|
20204
|
-
const task = {
|
|
21056
|
+
const queuedTask = {
|
|
20205
21057
|
taskId,
|
|
20206
21058
|
sourceHandle: input.sourceHandle,
|
|
20207
21059
|
sourceKind: preflight.sourceContext.sourceKind,
|
|
@@ -20213,49 +21065,101 @@ class OrchestrationService {
|
|
|
20213
21065
|
...preflight.role ? { role: preflight.role } : {},
|
|
20214
21066
|
...preflight.normalizedGroupId ? { groupId: preflight.normalizedGroupId } : {},
|
|
20215
21067
|
task: input.task,
|
|
20216
|
-
status,
|
|
21068
|
+
status: "queued",
|
|
21069
|
+
ephemeralWorkerSession: true,
|
|
20217
21070
|
summary: "",
|
|
20218
21071
|
resultText: "",
|
|
20219
21072
|
createdAt: now,
|
|
20220
21073
|
updatedAt: now,
|
|
20221
21074
|
eventSeq: 1,
|
|
20222
|
-
events: [{ seq: 1, at: now, type: "created", status, message: "Task
|
|
21075
|
+
events: [{ seq: 1, at: now, type: "created", status: "queued", message: "Task queued at parallel capacity" }]
|
|
20223
21076
|
};
|
|
20224
|
-
|
|
20225
|
-
|
|
20226
|
-
|
|
20227
|
-
|
|
20228
|
-
|
|
20229
|
-
|
|
20230
|
-
|
|
20231
|
-
|
|
20232
|
-
|
|
20233
|
-
|
|
20234
|
-
|
|
20235
|
-
|
|
20236
|
-
|
|
20237
|
-
|
|
20238
|
-
|
|
20239
|
-
|
|
21077
|
+
state.orchestration.tasks[taskId] = queuedTask;
|
|
21078
|
+
await this.deps.saveState(state);
|
|
21079
|
+
return { taskId, status: "queued", workerSession: workerSessionName };
|
|
21080
|
+
});
|
|
21081
|
+
if (queuedResult) {
|
|
21082
|
+
this.logEvent("orchestration.task.queued", "parallel task queued at capacity", { taskId, targetAgent: input.targetAgent });
|
|
21083
|
+
return queuedResult;
|
|
21084
|
+
}
|
|
21085
|
+
}
|
|
21086
|
+
const releasePendingParallelStart = input.parallel && autoRun ? () => {
|
|
21087
|
+
const count = this.pendingParallelStarts.get(input.targetAgent) ?? 0;
|
|
21088
|
+
if (count <= 1) {
|
|
21089
|
+
this.pendingParallelStarts.delete(input.targetAgent);
|
|
21090
|
+
} else {
|
|
21091
|
+
this.pendingParallelStarts.set(input.targetAgent, count - 1);
|
|
21092
|
+
}
|
|
21093
|
+
} : undefined;
|
|
21094
|
+
let prepared;
|
|
21095
|
+
const releaseWorkerReservation = await this.reserveProposedWorkerSession(workerSessionName);
|
|
21096
|
+
try {
|
|
21097
|
+
try {
|
|
21098
|
+
prepared = await this.mutate(async () => {
|
|
21099
|
+
const state = await this.deps.loadState();
|
|
21100
|
+
this.assertRpcRequestAllowed(state, preflight.sourceContext.sourceKind, preflight.sourceContext.coordinatorSession, input.targetAgent, preflight.role);
|
|
21101
|
+
const now = this.deps.now().toISOString();
|
|
21102
|
+
const status = autoRun ? "running" : "needs_confirmation";
|
|
21103
|
+
const task = {
|
|
21104
|
+
taskId,
|
|
21105
|
+
sourceHandle: input.sourceHandle,
|
|
21106
|
+
sourceKind: preflight.sourceContext.sourceKind,
|
|
20240
21107
|
coordinatorSession: preflight.sourceContext.coordinatorSession,
|
|
21108
|
+
workerSession: workerSessionName,
|
|
20241
21109
|
workspace: preflight.targetLocation.workspace,
|
|
20242
21110
|
...preflight.targetLocation.cwd ? { cwd: preflight.targetLocation.cwd } : {},
|
|
20243
21111
|
targetAgent: input.targetAgent,
|
|
20244
|
-
role: preflight.role
|
|
21112
|
+
...preflight.role ? { role: preflight.role } : {},
|
|
21113
|
+
...preflight.normalizedGroupId ? { groupId: preflight.normalizedGroupId } : {},
|
|
21114
|
+
task: input.task,
|
|
21115
|
+
status,
|
|
21116
|
+
summary: "",
|
|
21117
|
+
resultText: "",
|
|
21118
|
+
createdAt: now,
|
|
21119
|
+
updatedAt: now,
|
|
21120
|
+
eventSeq: 1,
|
|
21121
|
+
events: [{ seq: 1, at: now, type: "created", status, message: "Task created" }],
|
|
21122
|
+
...input.parallel ? { ephemeralWorkerSession: true } : {}
|
|
20245
21123
|
};
|
|
20246
|
-
|
|
20247
|
-
|
|
20248
|
-
|
|
20249
|
-
|
|
20250
|
-
|
|
20251
|
-
|
|
20252
|
-
|
|
20253
|
-
|
|
20254
|
-
|
|
21124
|
+
if (preflight.normalizedGroupId) {
|
|
21125
|
+
const group = this.ensureGroups(state)[preflight.normalizedGroupId];
|
|
21126
|
+
group.updatedAt = now;
|
|
21127
|
+
group.coordinatorInjectedAt = undefined;
|
|
21128
|
+
group.injectionPending = undefined;
|
|
21129
|
+
group.injectionAppliedAt = undefined;
|
|
21130
|
+
group.lastInjectionError = undefined;
|
|
21131
|
+
}
|
|
21132
|
+
let previousBinding;
|
|
21133
|
+
if (autoRun) {
|
|
21134
|
+
previousBinding = state.orchestration.workerBindings[workerSessionName];
|
|
21135
|
+
this.assertWorkerSessionDoesNotConflictExternalCoordinator(state, workerSessionName);
|
|
21136
|
+
this.assertWorkerSessionAvailable(state, workerSessionName, undefined, { allowCurrentReservation: true });
|
|
21137
|
+
state.orchestration.tasks[taskId] = task;
|
|
21138
|
+
state.orchestration.workerBindings[workerSessionName] = {
|
|
21139
|
+
sourceHandle: workerSessionName,
|
|
21140
|
+
coordinatorSession: preflight.sourceContext.coordinatorSession,
|
|
21141
|
+
workspace: preflight.targetLocation.workspace,
|
|
21142
|
+
...preflight.targetLocation.cwd ? { cwd: preflight.targetLocation.cwd } : {},
|
|
21143
|
+
targetAgent: input.targetAgent,
|
|
21144
|
+
role: preflight.role,
|
|
21145
|
+
...input.parallel ? { ephemeral: true } : {}
|
|
21146
|
+
};
|
|
21147
|
+
} else {
|
|
21148
|
+
this.assertWorkerSessionDoesNotConflictExternalCoordinator(state, workerSessionName);
|
|
21149
|
+
this.assertWorkerSessionAvailable(state, workerSessionName, undefined, { allowCurrentReservation: true });
|
|
21150
|
+
state.orchestration.tasks[taskId] = task;
|
|
21151
|
+
}
|
|
21152
|
+
await this.deps.saveState(state);
|
|
21153
|
+
return { task: { ...task }, status, previousBinding, normalizedGroupId: preflight.normalizedGroupId };
|
|
21154
|
+
});
|
|
21155
|
+
} catch (error2) {
|
|
21156
|
+
await releaseWorkerReservation();
|
|
21157
|
+
throw error2;
|
|
21158
|
+
}
|
|
20255
21159
|
await releaseWorkerReservation();
|
|
20256
|
-
|
|
21160
|
+
} finally {
|
|
21161
|
+
releasePendingParallelStart?.();
|
|
20257
21162
|
}
|
|
20258
|
-
await releaseWorkerReservation();
|
|
20259
21163
|
if (autoRun) {
|
|
20260
21164
|
this.runAutoRunRpcWorkerTask({
|
|
20261
21165
|
task: prepared.task,
|
|
@@ -21224,6 +22128,16 @@ class OrchestrationService {
|
|
|
21224
22128
|
await this.recordOpenQuestionWakeError(prepared.task.taskId, prepared.replacementQuestionId, error2 instanceof Error ? error2.message : String(error2));
|
|
21225
22129
|
}
|
|
21226
22130
|
}
|
|
22131
|
+
if (input.decision === "accept") {
|
|
22132
|
+
try {
|
|
22133
|
+
await this.reconcileParallelSlots();
|
|
22134
|
+
} catch (error2) {
|
|
22135
|
+
this.logEvent("orchestration.parallel.reconcile_failed", "reconcile failed after contested result accepted", {
|
|
22136
|
+
taskId: prepared.task.taskId,
|
|
22137
|
+
message: error2 instanceof Error ? error2.message : String(error2)
|
|
22138
|
+
});
|
|
22139
|
+
}
|
|
22140
|
+
}
|
|
21227
22141
|
return prepared.task;
|
|
21228
22142
|
}
|
|
21229
22143
|
async listTasks(filter) {
|
|
@@ -21690,6 +22604,16 @@ class OrchestrationService {
|
|
|
21690
22604
|
if (prepared.closedPackageId) {
|
|
21691
22605
|
await this.handoffQueuedQuestions(prepared.task.coordinatorSession, prepared.closedPackageId);
|
|
21692
22606
|
}
|
|
22607
|
+
if (!prepared.shouldPropagate && this.isTerminalStatus(prepared.task.status)) {
|
|
22608
|
+
try {
|
|
22609
|
+
await this.reconcileParallelSlots();
|
|
22610
|
+
} catch (error2) {
|
|
22611
|
+
this.logEvent("orchestration.parallel.reconcile_failed", "reconcile failed after non-running cancel", {
|
|
22612
|
+
taskId: prepared.task.taskId,
|
|
22613
|
+
message: error2 instanceof Error ? error2.message : String(error2)
|
|
22614
|
+
});
|
|
22615
|
+
}
|
|
22616
|
+
}
|
|
21693
22617
|
return prepared.task;
|
|
21694
22618
|
}
|
|
21695
22619
|
async completeTaskCancellation(taskId) {
|
|
@@ -21752,6 +22676,14 @@ class OrchestrationService {
|
|
|
21752
22676
|
return prepared.task;
|
|
21753
22677
|
}
|
|
21754
22678
|
this.logEvent("orchestration.task.cancel_completed", "task cancellation completed", this.taskContext(prepared.task));
|
|
22679
|
+
try {
|
|
22680
|
+
await this.reconcileParallelSlots();
|
|
22681
|
+
} catch (error2) {
|
|
22682
|
+
this.logEvent("orchestration.parallel.reconcile_failed", "reconcile failed after cancel completion", {
|
|
22683
|
+
taskId: prepared.task.taskId,
|
|
22684
|
+
message: error2 instanceof Error ? error2.message : String(error2)
|
|
22685
|
+
});
|
|
22686
|
+
}
|
|
21755
22687
|
return prepared.task;
|
|
21756
22688
|
}
|
|
21757
22689
|
async failTaskCancellation(taskId, errorMessage) {
|
|
@@ -21796,6 +22728,34 @@ class OrchestrationService {
|
|
|
21796
22728
|
task: currentTask.task,
|
|
21797
22729
|
...currentTask.role ? { role: currentTask.role } : {}
|
|
21798
22730
|
});
|
|
22731
|
+
if (currentTask.ephemeralWorkerSession === true) {
|
|
22732
|
+
const queuedResult = await this.mutate(async () => {
|
|
22733
|
+
const state = await this.deps.loadState();
|
|
22734
|
+
const task = state.orchestration.tasks[input.taskId];
|
|
22735
|
+
if (!task) {
|
|
22736
|
+
throw new Error(`task "${input.taskId}" does not exist`);
|
|
22737
|
+
}
|
|
22738
|
+
this.assertCoordinatorOwnership(task, input.coordinatorSession);
|
|
22739
|
+
this.assertNeedsConfirmation(task);
|
|
22740
|
+
if (this.canStartParallelTask(state, task.targetAgent)) {
|
|
22741
|
+
return null;
|
|
22742
|
+
}
|
|
22743
|
+
const now = this.deps.now().toISOString();
|
|
22744
|
+
task.workerSession = workerSession;
|
|
22745
|
+
task.status = "queued";
|
|
22746
|
+
task.updatedAt = now;
|
|
22747
|
+
this.appendTaskEvent(task, now, "status_changed", {
|
|
22748
|
+
status: "queued",
|
|
22749
|
+
message: "Task queued at parallel capacity"
|
|
22750
|
+
});
|
|
22751
|
+
await this.deps.saveState(state);
|
|
22752
|
+
return { ...task };
|
|
22753
|
+
});
|
|
22754
|
+
if (queuedResult) {
|
|
22755
|
+
this.logEvent("orchestration.task.queued", "parallel task queued at capacity on approve", { taskId: input.taskId, targetAgent: currentTask.targetAgent });
|
|
22756
|
+
return queuedResult;
|
|
22757
|
+
}
|
|
22758
|
+
}
|
|
21799
22759
|
const releaseWorkerReservation = await this.reserveProposedWorkerSession(workerSession, input.taskId);
|
|
21800
22760
|
let ensuredWorkerSession = workerSession;
|
|
21801
22761
|
let prepared;
|
|
@@ -21837,7 +22797,8 @@ class OrchestrationService {
|
|
|
21837
22797
|
workspace: task.workspace,
|
|
21838
22798
|
...task.cwd ? { cwd: task.cwd } : {},
|
|
21839
22799
|
targetAgent: task.targetAgent,
|
|
21840
|
-
role: task.role
|
|
22800
|
+
role: task.role,
|
|
22801
|
+
...task.ephemeralWorkerSession ? { ephemeral: true } : {}
|
|
21841
22802
|
};
|
|
21842
22803
|
await this.deps.saveState(state);
|
|
21843
22804
|
return {
|
|
@@ -21891,6 +22852,10 @@ class OrchestrationService {
|
|
|
21891
22852
|
}
|
|
21892
22853
|
async resolveWorkerSession(input) {
|
|
21893
22854
|
const role = this.normalizeRole(input.role);
|
|
22855
|
+
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(":");
|
|
22856
|
+
if (input.parallel) {
|
|
22857
|
+
return `${baseName}:p-${this.deps.createId()}`;
|
|
22858
|
+
}
|
|
21894
22859
|
const reusable = await this.deps.findReusableWorkerSession?.({
|
|
21895
22860
|
sourceHandle: input.sourceHandle,
|
|
21896
22861
|
sourceKind: input.sourceKind,
|
|
@@ -21903,7 +22868,7 @@ class OrchestrationService {
|
|
|
21903
22868
|
if (reusable && reusable.trim().length > 0) {
|
|
21904
22869
|
return reusable.trim();
|
|
21905
22870
|
}
|
|
21906
|
-
return
|
|
22871
|
+
return baseName;
|
|
21907
22872
|
}
|
|
21908
22873
|
async reserveProposedWorkerSession(workerSession, excludingTaskId) {
|
|
21909
22874
|
await this.mutate(async () => {
|
|
@@ -22061,7 +23026,7 @@ class OrchestrationService {
|
|
|
22061
23026
|
if (role && policy.allowedAgentRequestRoles.length > 0 && !policy.allowedAgentRequestRoles.includes(role)) {
|
|
22062
23027
|
throw new Error(`role "${role}" is not allowed for agent-requested delegation`);
|
|
22063
23028
|
}
|
|
22064
|
-
const outstandingRequests = Object.values(state.orchestration.tasks).filter((task) => task.coordinatorSession === coordinatorSession && task.sourceKind !== "human" && (task.status === "needs_confirmation" || task.status === "running"));
|
|
23029
|
+
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
23030
|
if (outstandingRequests.length >= policy.maxPendingAgentRequestsPerCoordinator) {
|
|
22066
23031
|
throw new Error("agent-requested delegation quota exceeded for this coordinator");
|
|
22067
23032
|
}
|
|
@@ -22226,6 +23191,137 @@ class OrchestrationService {
|
|
|
22226
23191
|
hasActiveTaskWorkerSession(state, workerSession, excludingTaskId) {
|
|
22227
23192
|
return Object.values(state.orchestration.tasks).some((task) => task.taskId !== excludingTaskId && task.workerSession === workerSession && (!this.isTerminalStatus(task.status) || task.reviewPending !== undefined));
|
|
22228
23193
|
}
|
|
23194
|
+
countActiveParallelSlots(state, targetAgent) {
|
|
23195
|
+
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;
|
|
23196
|
+
const pending = this.pendingParallelStarts.get(targetAgent) ?? 0;
|
|
23197
|
+
return persisted + pending;
|
|
23198
|
+
}
|
|
23199
|
+
canStartParallelTask(state, targetAgent) {
|
|
23200
|
+
const cap = this.deps.config.orchestration.maxParallelTasksPerAgent;
|
|
23201
|
+
return this.countActiveParallelSlots(state, targetAgent) < cap;
|
|
23202
|
+
}
|
|
23203
|
+
async reconcileParallelSlots() {
|
|
23204
|
+
const toClose = await this.mutate(async () => {
|
|
23205
|
+
const state = await this.deps.loadState();
|
|
23206
|
+
const collected = [];
|
|
23207
|
+
for (const task of Object.values(state.orchestration.tasks)) {
|
|
23208
|
+
if (task.ephemeralWorkerSession === true && task.ephemeralWorkerSessionClosed !== true && task.workerSession && task.reviewPending === undefined && this.isTerminalStatus(task.status)) {
|
|
23209
|
+
task.ephemeralWorkerSessionClosed = true;
|
|
23210
|
+
if (state.orchestration.workerBindings[task.workerSession] !== undefined) {
|
|
23211
|
+
delete state.orchestration.workerBindings[task.workerSession];
|
|
23212
|
+
collected.push({
|
|
23213
|
+
workerSession: task.workerSession,
|
|
23214
|
+
coordinatorSession: task.coordinatorSession,
|
|
23215
|
+
workspace: task.workspace,
|
|
23216
|
+
...task.cwd ? { cwd: task.cwd } : {},
|
|
23217
|
+
targetAgent: task.targetAgent,
|
|
23218
|
+
...task.role ? { role: task.role } : {}
|
|
23219
|
+
});
|
|
23220
|
+
}
|
|
23221
|
+
}
|
|
23222
|
+
}
|
|
23223
|
+
if (collected.length > 0) {
|
|
23224
|
+
await this.deps.saveState(state);
|
|
23225
|
+
}
|
|
23226
|
+
return collected;
|
|
23227
|
+
});
|
|
23228
|
+
for (const req of toClose) {
|
|
23229
|
+
try {
|
|
23230
|
+
await this.deps.closeWorkerSession?.(req);
|
|
23231
|
+
} catch (error2) {
|
|
23232
|
+
this.logEvent("orchestration.parallel.close_failed", "failed to close ephemeral worker session", {
|
|
23233
|
+
workerSession: req.workerSession,
|
|
23234
|
+
message: error2 instanceof Error ? error2.message : String(error2)
|
|
23235
|
+
});
|
|
23236
|
+
}
|
|
23237
|
+
}
|
|
23238
|
+
for (;; ) {
|
|
23239
|
+
const next = await this.mutate(async () => {
|
|
23240
|
+
const state = await this.deps.loadState();
|
|
23241
|
+
const queued = Object.values(state.orchestration.tasks).filter((t) => t.status === "queued" && t.ephemeralWorkerSession === true).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
23242
|
+
for (const task of queued) {
|
|
23243
|
+
if (!this.canStartParallelTask(state, task.targetAgent)) {
|
|
23244
|
+
continue;
|
|
23245
|
+
}
|
|
23246
|
+
task.status = "running";
|
|
23247
|
+
task.updatedAt = this.deps.now().toISOString();
|
|
23248
|
+
state.orchestration.workerBindings[task.workerSession] = {
|
|
23249
|
+
sourceHandle: task.workerSession,
|
|
23250
|
+
coordinatorSession: task.coordinatorSession,
|
|
23251
|
+
workspace: task.workspace,
|
|
23252
|
+
...task.cwd ? { cwd: task.cwd } : {},
|
|
23253
|
+
targetAgent: task.targetAgent,
|
|
23254
|
+
...task.role ? { role: task.role } : {},
|
|
23255
|
+
ephemeral: true
|
|
23256
|
+
};
|
|
23257
|
+
await this.deps.saveState(state);
|
|
23258
|
+
return { ...task };
|
|
23259
|
+
}
|
|
23260
|
+
return null;
|
|
23261
|
+
});
|
|
23262
|
+
if (!next) {
|
|
23263
|
+
break;
|
|
23264
|
+
}
|
|
23265
|
+
try {
|
|
23266
|
+
await this.ensureReservedWorkerSession({
|
|
23267
|
+
workerSession: next.workerSession,
|
|
23268
|
+
sourceHandle: next.sourceHandle,
|
|
23269
|
+
sourceKind: next.sourceKind,
|
|
23270
|
+
coordinatorSession: next.coordinatorSession,
|
|
23271
|
+
workspace: next.workspace,
|
|
23272
|
+
...next.cwd ? { cwd: next.cwd } : {},
|
|
23273
|
+
targetAgent: next.targetAgent,
|
|
23274
|
+
...next.role ? { role: next.role } : {}
|
|
23275
|
+
});
|
|
23276
|
+
await this.deps.dispatchWorkerTask({
|
|
23277
|
+
taskId: next.taskId,
|
|
23278
|
+
workerSession: next.workerSession,
|
|
23279
|
+
coordinatorSession: next.coordinatorSession,
|
|
23280
|
+
workspace: next.workspace,
|
|
23281
|
+
...next.cwd ? { cwd: next.cwd } : {},
|
|
23282
|
+
targetAgent: next.targetAgent,
|
|
23283
|
+
...next.role ? { role: next.role } : {},
|
|
23284
|
+
task: next.task
|
|
23285
|
+
});
|
|
23286
|
+
} catch (error2) {
|
|
23287
|
+
await this.mutate(async () => {
|
|
23288
|
+
const state = await this.deps.loadState();
|
|
23289
|
+
const task = state.orchestration.tasks[next.taskId];
|
|
23290
|
+
if (task && task.status === "running") {
|
|
23291
|
+
task.status = "queued";
|
|
23292
|
+
task.updatedAt = this.deps.now().toISOString();
|
|
23293
|
+
delete state.orchestration.workerBindings[next.workerSession];
|
|
23294
|
+
this.appendTaskEvent(task, task.updatedAt, "status_changed", {
|
|
23295
|
+
status: "queued",
|
|
23296
|
+
message: "Task re-queued after drain failure"
|
|
23297
|
+
});
|
|
23298
|
+
await this.deps.saveState(state);
|
|
23299
|
+
}
|
|
23300
|
+
});
|
|
23301
|
+
this.logEvent("orchestration.parallel.drain_failed", "failed to drain queued parallel task", {
|
|
23302
|
+
taskId: next.taskId,
|
|
23303
|
+
workerSession: next.workerSession,
|
|
23304
|
+
message: error2 instanceof Error ? error2.message : String(error2)
|
|
23305
|
+
});
|
|
23306
|
+
break;
|
|
23307
|
+
}
|
|
23308
|
+
await this.mutate(async () => {
|
|
23309
|
+
const state = await this.deps.loadState();
|
|
23310
|
+
const task = state.orchestration.tasks[next.taskId];
|
|
23311
|
+
if (task && task.status === "running") {
|
|
23312
|
+
this.appendTaskEvent(task, task.updatedAt, "status_changed", {
|
|
23313
|
+
status: "running",
|
|
23314
|
+
message: "Task drained from parallel queue"
|
|
23315
|
+
});
|
|
23316
|
+
await this.deps.saveState(state);
|
|
23317
|
+
}
|
|
23318
|
+
});
|
|
23319
|
+
this.logEvent("orchestration.task.drained", "parallel task drained from queue", {
|
|
23320
|
+
taskId: next.taskId,
|
|
23321
|
+
targetAgent: next.targetAgent
|
|
23322
|
+
});
|
|
23323
|
+
}
|
|
23324
|
+
}
|
|
22229
23325
|
async assertProposedWorkerSessionDoesNotConflictExternalCoordinator(workerSession) {
|
|
22230
23326
|
const state = await this.deps.loadState();
|
|
22231
23327
|
this.assertWorkerSessionDoesNotConflictExternalCoordinator(state, workerSession);
|
|
@@ -22884,6 +23980,20 @@ class SessionService {
|
|
|
22884
23980
|
async createSession(alias, agent, workspace) {
|
|
22885
23981
|
return await this.createLogicalSession(alias, agent, workspace, `${workspace}:${alias}`);
|
|
22886
23982
|
}
|
|
23983
|
+
listAllResolvedSessions() {
|
|
23984
|
+
const seen = new Set;
|
|
23985
|
+
const resolved = [];
|
|
23986
|
+
for (const session of Object.values(this.state.sessions)) {
|
|
23987
|
+
if (seen.has(session.transport_session)) {
|
|
23988
|
+
continue;
|
|
23989
|
+
}
|
|
23990
|
+
seen.add(session.transport_session);
|
|
23991
|
+
try {
|
|
23992
|
+
resolved.push(this.toResolvedSession(session));
|
|
23993
|
+
} catch {}
|
|
23994
|
+
}
|
|
23995
|
+
return resolved;
|
|
23996
|
+
}
|
|
22887
23997
|
resolveSession(alias, agent, workspace, transportSession) {
|
|
22888
23998
|
this.validateSession(alias, agent, workspace);
|
|
22889
23999
|
return this.toResolvedSession({
|
|
@@ -23236,6 +24346,13 @@ async function runConsole(paths, deps) {
|
|
|
23236
24346
|
trigger: "startup"
|
|
23237
24347
|
});
|
|
23238
24348
|
} catch {}
|
|
24349
|
+
try {
|
|
24350
|
+
await runtime.orchestration.service.reconcileParallelSlots();
|
|
24351
|
+
} catch (reconcileError) {
|
|
24352
|
+
await runtime.logger.error("orchestration.parallel.reconcile_failed", "failed to reconcile parallel slots at startup", {
|
|
24353
|
+
message: reconcileError instanceof Error ? reconcileError.message : String(reconcileError)
|
|
24354
|
+
});
|
|
24355
|
+
}
|
|
23239
24356
|
consumerLock = deps.consumerLock ?? deps.consumerLockFactory?.(runtime);
|
|
23240
24357
|
if (consumerLock) {
|
|
23241
24358
|
const lockMeta = {
|
|
@@ -23564,7 +24681,8 @@ async function spawnAcpxBridgeClient(options = {}) {
|
|
|
23564
24681
|
...process.env,
|
|
23565
24682
|
WEACPX_BRIDGE_ACPX_COMMAND: options.acpxCommand ?? "acpx",
|
|
23566
24683
|
WEACPX_BRIDGE_PERMISSION_MODE: options.permissionMode ?? "approve-all",
|
|
23567
|
-
WEACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS: options.nonInteractivePermissions ?? "deny"
|
|
24684
|
+
WEACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS: options.nonInteractivePermissions ?? "deny",
|
|
24685
|
+
...typeof options.queueOwnerTtlSeconds === "number" && Number.isFinite(options.queueOwnerTtlSeconds) ? { WEACPX_BRIDGE_QUEUE_OWNER_TTL_SECONDS: String(options.queueOwnerTtlSeconds) } : {}
|
|
23568
24686
|
},
|
|
23569
24687
|
stdio: ["pipe", "pipe", "inherit"]
|
|
23570
24688
|
});
|
|
@@ -23992,9 +25110,9 @@ var init_spawn_command = __esm(() => {
|
|
|
23992
25110
|
});
|
|
23993
25111
|
|
|
23994
25112
|
// src/transport/prompt-media.ts
|
|
23995
|
-
import { mkdtemp, open as
|
|
25113
|
+
import { mkdtemp, open as open4, rm as rm8, writeFile as writeFile7 } from "node:fs/promises";
|
|
23996
25114
|
import { tmpdir as defaultTmpdir } from "node:os";
|
|
23997
|
-
import
|
|
25115
|
+
import path14 from "node:path";
|
|
23998
25116
|
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
23999
25117
|
async function createStructuredPromptFile(text, media, deps = defaultStructuredPromptFileDeps) {
|
|
24000
25118
|
const mediaList = normalizePromptMedia(media);
|
|
@@ -24028,7 +25146,7 @@ async function createStructuredPromptFile(text, media, deps = defaultStructuredP
|
|
|
24028
25146
|
type: "resource",
|
|
24029
25147
|
resource: {
|
|
24030
25148
|
uri: pathToFileURL2(item.filePath).toString(),
|
|
24031
|
-
text: `${item.fileName ??
|
|
25149
|
+
text: `${item.fileName ?? path14.basename(item.filePath)} ${item.mimeType} ${item.type}`
|
|
24032
25150
|
}
|
|
24033
25151
|
});
|
|
24034
25152
|
}
|
|
@@ -24042,7 +25160,7 @@ function normalizePromptMedia(media) {
|
|
|
24042
25160
|
function buildAttachmentSummary(items) {
|
|
24043
25161
|
const lines = ["Attachments available as local files:"];
|
|
24044
25162
|
for (const [index, item] of items.entries()) {
|
|
24045
|
-
lines.push(`${index + 1}. ${item.type} ${item.fileName ??
|
|
25163
|
+
lines.push(`${index + 1}. ${item.type} ${item.fileName ?? path14.basename(item.filePath)} ${item.mimeType} ${item.filePath}`);
|
|
24046
25164
|
}
|
|
24047
25165
|
return lines.join(`
|
|
24048
25166
|
`);
|
|
@@ -24050,8 +25168,8 @@ function buildAttachmentSummary(items) {
|
|
|
24050
25168
|
async function writeStructuredPromptBlocks(blocks, deps) {
|
|
24051
25169
|
let dir = "";
|
|
24052
25170
|
try {
|
|
24053
|
-
dir = await deps.mkdtemp(
|
|
24054
|
-
const filePath =
|
|
25171
|
+
dir = await deps.mkdtemp(path14.join(deps.tmpdir(), "weacpx-acp-prompt-"));
|
|
25172
|
+
const filePath = path14.join(dir, "prompt.json");
|
|
24055
25173
|
await deps.writeFile(filePath, JSON.stringify(blocks), "utf8");
|
|
24056
25174
|
return { filePath, cleanup: async () => deps.rm(dir, { recursive: true, force: true }) };
|
|
24057
25175
|
} catch (error2) {
|
|
@@ -24061,7 +25179,7 @@ async function writeStructuredPromptBlocks(blocks, deps) {
|
|
|
24061
25179
|
}
|
|
24062
25180
|
}
|
|
24063
25181
|
async function readImageFileBounded(filePath, maxBytes) {
|
|
24064
|
-
const handle = await
|
|
25182
|
+
const handle = await open4(filePath, "r");
|
|
24065
25183
|
try {
|
|
24066
25184
|
const imageStats = await handle.stat();
|
|
24067
25185
|
if (!imageStats.isFile()) {
|
|
@@ -24116,7 +25234,7 @@ var init_prompt_media = __esm(() => {
|
|
|
24116
25234
|
defaultStructuredPromptFileDeps = {
|
|
24117
25235
|
readImageFile: readImageFileBounded,
|
|
24118
25236
|
mkdtemp,
|
|
24119
|
-
writeFile:
|
|
25237
|
+
writeFile: writeFile7,
|
|
24120
25238
|
rm: rm8,
|
|
24121
25239
|
tmpdir: defaultTmpdir
|
|
24122
25240
|
};
|
|
@@ -24423,12 +25541,12 @@ function resolveNodePtyHelperPath(packageJsonPath, platform, arch) {
|
|
|
24423
25541
|
}
|
|
24424
25542
|
return join12(dirname12(packageJsonPath), "prebuilds", `${platform}-${arch}`, "spawn-helper");
|
|
24425
25543
|
}
|
|
24426
|
-
async function ensureNodePtyHelperExecutable(helperPath,
|
|
25544
|
+
async function ensureNodePtyHelperExecutable(helperPath, chmod3 = chmodFs) {
|
|
24427
25545
|
if (!helperPath) {
|
|
24428
25546
|
return;
|
|
24429
25547
|
}
|
|
24430
25548
|
try {
|
|
24431
|
-
await
|
|
25549
|
+
await chmod3(helperPath, 493);
|
|
24432
25550
|
} catch (error2) {
|
|
24433
25551
|
if (error2.code === "ENOENT") {
|
|
24434
25552
|
return;
|
|
@@ -24721,6 +25839,7 @@ class AcpxCliTransport {
|
|
|
24721
25839
|
permissionMode;
|
|
24722
25840
|
nonInteractivePermissions;
|
|
24723
25841
|
permissionPolicy;
|
|
25842
|
+
queueOwnerTtlSeconds;
|
|
24724
25843
|
runCommand;
|
|
24725
25844
|
runPtyCommand;
|
|
24726
25845
|
queueOwnerLauncher;
|
|
@@ -24731,10 +25850,12 @@ class AcpxCliTransport {
|
|
|
24731
25850
|
this.permissionMode = options.permissionMode ?? "approve-all";
|
|
24732
25851
|
this.nonInteractivePermissions = options.nonInteractivePermissions ?? "deny";
|
|
24733
25852
|
this.permissionPolicy = options.permissionPolicy;
|
|
25853
|
+
this.queueOwnerTtlSeconds = options.queueOwnerTtlSeconds;
|
|
24734
25854
|
this.runCommand = runCommand;
|
|
24735
25855
|
this.runPtyCommand = runPtyCommand;
|
|
24736
25856
|
this.queueOwnerLauncher = queueOwnerLauncher ?? new AcpxQueueOwnerLauncher({
|
|
24737
|
-
acpxCommand: this.command
|
|
25857
|
+
acpxCommand: this.command,
|
|
25858
|
+
...typeof this.queueOwnerTtlSeconds === "number" && Number.isFinite(this.queueOwnerTtlSeconds) ? { ttlMs: this.queueOwnerTtlSeconds * 1000 } : {}
|
|
24738
25859
|
});
|
|
24739
25860
|
this.streamingHooks = streamingHooks;
|
|
24740
25861
|
}
|
|
@@ -25058,7 +26179,8 @@ ${baseText}` : "" };
|
|
|
25058
26179
|
"--json-strict",
|
|
25059
26180
|
"--cwd",
|
|
25060
26181
|
session.cwd,
|
|
25061
|
-
...this.buildPermissionArgs()
|
|
26182
|
+
...this.buildPermissionArgs(),
|
|
26183
|
+
...this.buildQueueOwnerTtlArgs()
|
|
25062
26184
|
];
|
|
25063
26185
|
const tail2 = promptFile ? ["prompt", "-s", session.transportSession, "--file", promptFile] : ["prompt", "-s", session.transportSession, text];
|
|
25064
26186
|
if (session.agentCommand) {
|
|
@@ -25066,6 +26188,12 @@ ${baseText}` : "" };
|
|
|
25066
26188
|
}
|
|
25067
26189
|
return [...prefix, session.agent, ...tail2];
|
|
25068
26190
|
}
|
|
26191
|
+
buildQueueOwnerTtlArgs() {
|
|
26192
|
+
if (typeof this.queueOwnerTtlSeconds !== "number" || !Number.isFinite(this.queueOwnerTtlSeconds)) {
|
|
26193
|
+
return [];
|
|
26194
|
+
}
|
|
26195
|
+
return ["--ttl", String(this.queueOwnerTtlSeconds)];
|
|
26196
|
+
}
|
|
25069
26197
|
buildPermissionArgs() {
|
|
25070
26198
|
const modeFlag = permissionModeToFlag(this.permissionMode);
|
|
25071
26199
|
const args = [modeFlag, "--non-interactive-permissions", this.nonInteractivePermissions];
|
|
@@ -25112,6 +26240,146 @@ var init_acpx_cli_transport = __esm(() => {
|
|
|
25112
26240
|
require4 = createRequire5(import.meta.url);
|
|
25113
26241
|
});
|
|
25114
26242
|
|
|
26243
|
+
// src/transport/queue-owner-reaper.ts
|
|
26244
|
+
import { spawn as spawn10 } from "node:child_process";
|
|
26245
|
+
async function reapQueueOwners(acpxCommand, targets, deps = {}) {
|
|
26246
|
+
const resolveRecordId = deps.resolveRecordId ?? defaultResolveRecordId;
|
|
26247
|
+
const terminate = deps.terminate ?? terminateAcpxQueueOwner;
|
|
26248
|
+
const timeoutMs = deps.timeoutMs ?? 5000;
|
|
26249
|
+
const seen = new Set;
|
|
26250
|
+
const unique = targets.filter((target) => {
|
|
26251
|
+
if (seen.has(target.transportSession)) {
|
|
26252
|
+
return false;
|
|
26253
|
+
}
|
|
26254
|
+
seen.add(target.transportSession);
|
|
26255
|
+
return true;
|
|
26256
|
+
});
|
|
26257
|
+
let terminated = 0;
|
|
26258
|
+
const reapOne = async (target) => {
|
|
26259
|
+
try {
|
|
26260
|
+
const recordId = await resolveRecordId(acpxCommand, target);
|
|
26261
|
+
if (!recordId) {
|
|
26262
|
+
return;
|
|
26263
|
+
}
|
|
26264
|
+
await terminate(recordId);
|
|
26265
|
+
terminated += 1;
|
|
26266
|
+
} catch (error2) {
|
|
26267
|
+
deps.onError?.(target, error2);
|
|
26268
|
+
}
|
|
26269
|
+
};
|
|
26270
|
+
await settleWithinTimeout(Promise.all(unique.map(reapOne)), timeoutMs);
|
|
26271
|
+
return { terminated, attempted: unique.length };
|
|
26272
|
+
}
|
|
26273
|
+
function settleWithinTimeout(work, timeoutMs) {
|
|
26274
|
+
return new Promise((resolve3) => {
|
|
26275
|
+
let settled = false;
|
|
26276
|
+
const finish = () => {
|
|
26277
|
+
if (!settled) {
|
|
26278
|
+
settled = true;
|
|
26279
|
+
resolve3();
|
|
26280
|
+
}
|
|
26281
|
+
};
|
|
26282
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
26283
|
+
if (typeof timer.unref === "function") {
|
|
26284
|
+
timer.unref();
|
|
26285
|
+
}
|
|
26286
|
+
work.then(() => {
|
|
26287
|
+
clearTimeout(timer);
|
|
26288
|
+
finish();
|
|
26289
|
+
}, () => {
|
|
26290
|
+
clearTimeout(timer);
|
|
26291
|
+
finish();
|
|
26292
|
+
});
|
|
26293
|
+
});
|
|
26294
|
+
}
|
|
26295
|
+
async function defaultResolveRecordId(acpxCommand, target) {
|
|
26296
|
+
const args = [
|
|
26297
|
+
"--format",
|
|
26298
|
+
"quiet",
|
|
26299
|
+
"--cwd",
|
|
26300
|
+
target.cwd,
|
|
26301
|
+
...target.agentCommand ? ["--agent", target.agentCommand] : [target.agent],
|
|
26302
|
+
"sessions",
|
|
26303
|
+
"show",
|
|
26304
|
+
target.transportSession
|
|
26305
|
+
];
|
|
26306
|
+
const spawnSpec = resolveSpawnCommand(acpxCommand, args);
|
|
26307
|
+
const result = await runCapture2(spawnSpec.command, spawnSpec.args, 4000);
|
|
26308
|
+
if (result.code !== 0) {
|
|
26309
|
+
return null;
|
|
26310
|
+
}
|
|
26311
|
+
return parseRecordId(result.stdout);
|
|
26312
|
+
}
|
|
26313
|
+
function parseRecordId(stdout2) {
|
|
26314
|
+
try {
|
|
26315
|
+
const parsed = JSON.parse(stdout2);
|
|
26316
|
+
if (typeof parsed.acpxRecordId === "string") {
|
|
26317
|
+
return parsed.acpxRecordId;
|
|
26318
|
+
}
|
|
26319
|
+
if (typeof parsed.id === "string") {
|
|
26320
|
+
return parsed.id;
|
|
26321
|
+
}
|
|
26322
|
+
} catch {
|
|
26323
|
+
const firstLine = stdout2.trim().split(/\r?\n/, 1)[0];
|
|
26324
|
+
if (firstLine && /^[\w.:-]+$/.test(firstLine) && firstLine.length >= 8) {
|
|
26325
|
+
return firstLine;
|
|
26326
|
+
}
|
|
26327
|
+
}
|
|
26328
|
+
return null;
|
|
26329
|
+
}
|
|
26330
|
+
function runCapture2(command, args, timeoutMs) {
|
|
26331
|
+
return new Promise((resolve3) => {
|
|
26332
|
+
const child = spawn10(command, args, { stdio: ["ignore", "pipe", "ignore"] });
|
|
26333
|
+
let stdout2 = "";
|
|
26334
|
+
let done = false;
|
|
26335
|
+
const finish = (code) => {
|
|
26336
|
+
if (done) {
|
|
26337
|
+
return;
|
|
26338
|
+
}
|
|
26339
|
+
done = true;
|
|
26340
|
+
clearTimeout(timer);
|
|
26341
|
+
resolve3({ code, stdout: stdout2 });
|
|
26342
|
+
};
|
|
26343
|
+
const timer = setTimeout(() => {
|
|
26344
|
+
child.kill("SIGKILL");
|
|
26345
|
+
finish(1);
|
|
26346
|
+
}, timeoutMs);
|
|
26347
|
+
child.stdout?.on("data", (chunk) => {
|
|
26348
|
+
stdout2 += String(chunk);
|
|
26349
|
+
});
|
|
26350
|
+
child.once("error", () => finish(1));
|
|
26351
|
+
child.once("close", (code) => finish(code ?? 1));
|
|
26352
|
+
});
|
|
26353
|
+
}
|
|
26354
|
+
var init_queue_owner_reaper = __esm(() => {
|
|
26355
|
+
init_spawn_command();
|
|
26356
|
+
init_acpx_queue_owner_launcher();
|
|
26357
|
+
});
|
|
26358
|
+
|
|
26359
|
+
// src/transport/collect-reap-targets.ts
|
|
26360
|
+
function workerBindingReapTargets(orchestration, config2) {
|
|
26361
|
+
const targets = [];
|
|
26362
|
+
for (const [workerSession, binding] of Object.entries(orchestration.workerBindings)) {
|
|
26363
|
+
const agentConfig = config2.agents[binding.targetAgent];
|
|
26364
|
+
if (!agentConfig) {
|
|
26365
|
+
continue;
|
|
26366
|
+
}
|
|
26367
|
+
const cwd = binding.cwd ?? config2.workspaces[binding.workspace]?.cwd;
|
|
26368
|
+
if (!cwd) {
|
|
26369
|
+
continue;
|
|
26370
|
+
}
|
|
26371
|
+
const agentCommand = resolveAgentCommand(agentConfig.driver, agentConfig.command);
|
|
26372
|
+
targets.push({
|
|
26373
|
+
agent: binding.targetAgent,
|
|
26374
|
+
...agentCommand ? { agentCommand } : {},
|
|
26375
|
+
cwd,
|
|
26376
|
+
transportSession: workerSession
|
|
26377
|
+
});
|
|
26378
|
+
}
|
|
26379
|
+
return targets;
|
|
26380
|
+
}
|
|
26381
|
+
var init_collect_reap_targets = () => {};
|
|
26382
|
+
|
|
25115
26383
|
// src/channels/channel-registry.ts
|
|
25116
26384
|
var exports_channel_registry = {};
|
|
25117
26385
|
__export(exports_channel_registry, {
|
|
@@ -25307,7 +26575,11 @@ function startProgressHeartbeat(orchestration, config2, logger2, channel) {
|
|
|
25307
26575
|
if (thresholdSeconds <= 0) {
|
|
25308
26576
|
return;
|
|
25309
26577
|
}
|
|
26578
|
+
let ticking = false;
|
|
25310
26579
|
return setInterval(async () => {
|
|
26580
|
+
if (ticking)
|
|
26581
|
+
return;
|
|
26582
|
+
ticking = true;
|
|
25311
26583
|
try {
|
|
25312
26584
|
const tasks = await orchestration.listHeartbeatTasks(thresholdSeconds);
|
|
25313
26585
|
for (const task of tasks) {
|
|
@@ -25328,6 +26600,8 @@ function startProgressHeartbeat(orchestration, config2, logger2, channel) {
|
|
|
25328
26600
|
await logger2.error("orchestration.heartbeat.check_failed", "heartbeat check failed", {
|
|
25329
26601
|
message: error2 instanceof Error ? error2.message : String(error2)
|
|
25330
26602
|
});
|
|
26603
|
+
} finally {
|
|
26604
|
+
ticking = false;
|
|
25331
26605
|
}
|
|
25332
26606
|
}, 60000);
|
|
25333
26607
|
}
|
|
@@ -25380,7 +26654,8 @@ async function buildApp(paths, deps = {}) {
|
|
|
25380
26654
|
acpxCommand,
|
|
25381
26655
|
bridgeEntryPath: resolveBridgeEntryPath(),
|
|
25382
26656
|
permissionMode: config2.transport.permissionMode,
|
|
25383
|
-
nonInteractivePermissions: config2.transport.nonInteractivePermissions
|
|
26657
|
+
nonInteractivePermissions: config2.transport.nonInteractivePermissions,
|
|
26658
|
+
...typeof config2.transport.queueOwnerTtlSeconds === "number" ? { queueOwnerTtlSeconds: config2.transport.queueOwnerTtlSeconds } : {}
|
|
25384
26659
|
})))) : deps.createCliTransport?.(acpxCommand) ?? new AcpxCliTransport({ ...config2.transport, command: acpxCommand });
|
|
25385
26660
|
const quota = new QuotaManager({
|
|
25386
26661
|
onInbound: (chatKey) => {
|
|
@@ -25595,6 +26870,14 @@ async function buildApp(paths, deps = {}) {
|
|
|
25595
26870
|
resultText: ""
|
|
25596
26871
|
});
|
|
25597
26872
|
}
|
|
26873
|
+
try {
|
|
26874
|
+
await orchestration.reconcileParallelSlots();
|
|
26875
|
+
} catch (reconcileError) {
|
|
26876
|
+
await logger2.error("orchestration.parallel.reconcile_failed", "failed to reconcile parallel slots after worker turn", {
|
|
26877
|
+
taskId: input.taskId,
|
|
26878
|
+
message: reconcileError instanceof Error ? reconcileError.message : String(reconcileError)
|
|
26879
|
+
});
|
|
26880
|
+
}
|
|
25598
26881
|
if (taskRecord && shouldNotifyTaskCompletion(taskRecord)) {
|
|
25599
26882
|
try {
|
|
25600
26883
|
await sendCompletionNotice(taskRecord);
|
|
@@ -25662,6 +26945,13 @@ async function buildApp(paths, deps = {}) {
|
|
|
25662
26945
|
throw new Error(result.message || "worker task cancel was not acknowledged");
|
|
25663
26946
|
}
|
|
25664
26947
|
},
|
|
26948
|
+
closeWorkerSession: async ({ workerSession, targetAgent, workspace, cwd }) => {
|
|
26949
|
+
if (!transport.removeSession) {
|
|
26950
|
+
return;
|
|
26951
|
+
}
|
|
26952
|
+
const session = resolveWorkerRuntimeSession({ workerSession, targetAgent, workspace, ...cwd ? { cwd } : {} });
|
|
26953
|
+
await transport.removeSession(session);
|
|
26954
|
+
},
|
|
25665
26955
|
resumeWorkerTask: async ({ taskId, workerSession, coordinatorSession, targetAgent, workspace, cwd, answer }) => {
|
|
25666
26956
|
launchWorkerTurn({
|
|
25667
26957
|
taskId,
|
|
@@ -25687,7 +26977,7 @@ async function buildApp(paths, deps = {}) {
|
|
|
25687
26977
|
}
|
|
25688
26978
|
},
|
|
25689
26979
|
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);
|
|
26980
|
+
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
26981
|
return binding?.[0] ?? null;
|
|
25692
26982
|
},
|
|
25693
26983
|
logger: logger2
|
|
@@ -25740,6 +27030,35 @@ async function buildApp(paths, deps = {}) {
|
|
|
25740
27030
|
clearInterval(progressHeartbeatInterval);
|
|
25741
27031
|
}
|
|
25742
27032
|
await Promise.allSettled([...pendingWorkerDispatches]);
|
|
27033
|
+
try {
|
|
27034
|
+
const targets = [
|
|
27035
|
+
...sessions.listAllResolvedSessions().map((session) => ({
|
|
27036
|
+
agent: session.agent,
|
|
27037
|
+
...session.agentCommand ? { agentCommand: session.agentCommand } : {},
|
|
27038
|
+
cwd: session.cwd,
|
|
27039
|
+
transportSession: session.transportSession
|
|
27040
|
+
})),
|
|
27041
|
+
...workerBindingReapTargets(state.orchestration, config2)
|
|
27042
|
+
];
|
|
27043
|
+
if (targets.length > 0) {
|
|
27044
|
+
const { terminated, attempted } = await reapQueueOwners(acpxCommand, targets, {
|
|
27045
|
+
onError: (target, error2) => {
|
|
27046
|
+
logger2.info("transport.queue_owner_reap.failed", "failed to reap queue owner on shutdown", {
|
|
27047
|
+
transport_session: target.transportSession,
|
|
27048
|
+
error: error2 instanceof Error ? error2.message : String(error2)
|
|
27049
|
+
}).catch(() => {});
|
|
27050
|
+
}
|
|
27051
|
+
});
|
|
27052
|
+
await logger2.info("transport.queue_owner_reap.completed", "reaped warm queue owners on shutdown", {
|
|
27053
|
+
terminated,
|
|
27054
|
+
attempted
|
|
27055
|
+
}).catch(() => {});
|
|
27056
|
+
}
|
|
27057
|
+
} catch (err) {
|
|
27058
|
+
await logger2.error("transport.queue_owner_reap.error", "queue owner reap failed during shutdown", {
|
|
27059
|
+
error: err instanceof Error ? err.message : String(err)
|
|
27060
|
+
}).catch(() => {});
|
|
27061
|
+
}
|
|
25743
27062
|
await debouncedStateStore.dispose();
|
|
25744
27063
|
if ("dispose" in transport && typeof transport.dispose === "function") {
|
|
25745
27064
|
await transport.dispose();
|
|
@@ -25770,7 +27089,12 @@ async function main() {
|
|
|
25770
27089
|
await ensureConfigExists(paths.configPath);
|
|
25771
27090
|
const startupConfig = await loadConfig(paths.configPath);
|
|
25772
27091
|
const { loadConfiguredPlugins: loadConfiguredPlugins2 } = await Promise.resolve().then(() => (init_plugin_loader(), exports_plugin_loader));
|
|
25773
|
-
await loadConfiguredPlugins2({
|
|
27092
|
+
await loadConfiguredPlugins2({
|
|
27093
|
+
plugins: startupConfig.plugins,
|
|
27094
|
+
onPluginError: ({ name, error: error2 }) => {
|
|
27095
|
+
console.error(`[weacpx] skipping plugin ${name}: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
27096
|
+
}
|
|
27097
|
+
});
|
|
25774
27098
|
const { channelDeps } = await prepareChannelMedia(paths.configPath, startupConfig);
|
|
25775
27099
|
const channelRegistry = new MessageChannelRegistry(createMessageChannels2(startupConfig.channels, channelDeps));
|
|
25776
27100
|
await runConsole(paths, {
|
|
@@ -25854,6 +27178,8 @@ var init_main = __esm(async () => {
|
|
|
25854
27178
|
init_acpx_bridge_client();
|
|
25855
27179
|
init_acpx_bridge_transport();
|
|
25856
27180
|
init_acpx_cli_transport();
|
|
27181
|
+
init_queue_owner_reaper();
|
|
27182
|
+
init_collect_reap_targets();
|
|
25857
27183
|
init_channel_registry();
|
|
25858
27184
|
init_media_store();
|
|
25859
27185
|
init_quota_errors();
|
|
@@ -25864,7 +27190,7 @@ var init_main = __esm(async () => {
|
|
|
25864
27190
|
});
|
|
25865
27191
|
|
|
25866
27192
|
// src/doctor/checks/acpx-check.ts
|
|
25867
|
-
import { spawn as
|
|
27193
|
+
import { spawn as spawn11 } from "node:child_process";
|
|
25868
27194
|
async function checkAcpx(options = {}) {
|
|
25869
27195
|
const runtimePaths = (options.resolveRuntimePaths ?? resolveRuntimePaths)();
|
|
25870
27196
|
try {
|
|
@@ -25911,7 +27237,7 @@ function buildDetails(metadata, version2, verbose) {
|
|
|
25911
27237
|
async function defaultRunVersion(command) {
|
|
25912
27238
|
const spawnSpec = resolveSpawnCommand(command, ["--version"]);
|
|
25913
27239
|
return await new Promise((resolve3, reject) => {
|
|
25914
|
-
const child =
|
|
27240
|
+
const child = spawn11(spawnSpec.command, spawnSpec.args, {
|
|
25915
27241
|
stdio: ["ignore", "pipe", "pipe"]
|
|
25916
27242
|
});
|
|
25917
27243
|
let stdout2 = "";
|
|
@@ -26247,107 +27573,107 @@ async function checkRuntime(options = {}) {
|
|
|
26247
27573
|
}
|
|
26248
27574
|
function createRuntimeFsProbe() {
|
|
26249
27575
|
return {
|
|
26250
|
-
stat: async (
|
|
26251
|
-
access: async (
|
|
27576
|
+
stat: async (path15) => await stat3(path15),
|
|
27577
|
+
access: async (path15, mode) => await access4(path15, mode)
|
|
26252
27578
|
};
|
|
26253
27579
|
}
|
|
26254
|
-
async function checkDirectoryCreatable(label,
|
|
27580
|
+
async function checkDirectoryCreatable(label, path15, probe, platform) {
|
|
26255
27581
|
try {
|
|
26256
|
-
const stats = await probe.stat(
|
|
27582
|
+
const stats = await probe.stat(path15);
|
|
26257
27583
|
if (!stats.isDirectory()) {
|
|
26258
27584
|
return {
|
|
26259
27585
|
ok: false,
|
|
26260
|
-
detail: `${label}: ${
|
|
27586
|
+
detail: `${label}: ${path15} (exists but is not a directory)`
|
|
26261
27587
|
};
|
|
26262
27588
|
}
|
|
26263
|
-
await probe.access(
|
|
27589
|
+
await probe.access(path15, directoryAccessMode(platform));
|
|
26264
27590
|
return {
|
|
26265
27591
|
ok: true,
|
|
26266
|
-
detail: `${label}: ${
|
|
27592
|
+
detail: `${label}: ${path15} (writable)`
|
|
26267
27593
|
};
|
|
26268
27594
|
} catch (error2) {
|
|
26269
27595
|
if (!isMissingPathError(error2)) {
|
|
26270
27596
|
return {
|
|
26271
27597
|
ok: false,
|
|
26272
|
-
detail: `${label}: ${
|
|
27598
|
+
detail: `${label}: ${path15} (unusable: ${formatError6(error2)})`
|
|
26273
27599
|
};
|
|
26274
27600
|
}
|
|
26275
|
-
const parentCheck = await checkCreatableAncestorDirectory(
|
|
27601
|
+
const parentCheck = await checkCreatableAncestorDirectory(path15, probe, platform);
|
|
26276
27602
|
if (!parentCheck.ok) {
|
|
26277
27603
|
return {
|
|
26278
27604
|
ok: false,
|
|
26279
|
-
detail: `${label}: ${
|
|
27605
|
+
detail: `${label}: ${path15} (parent not writable: ${parentCheck.blockingPath})`
|
|
26280
27606
|
};
|
|
26281
27607
|
}
|
|
26282
27608
|
return {
|
|
26283
27609
|
ok: true,
|
|
26284
|
-
detail: `${label}: ${
|
|
27610
|
+
detail: `${label}: ${path15} (creatable via ${parentCheck.creatableFrom})`
|
|
26285
27611
|
};
|
|
26286
27612
|
}
|
|
26287
27613
|
}
|
|
26288
|
-
async function checkFileCreatable(label,
|
|
27614
|
+
async function checkFileCreatable(label, path15, probe, platform) {
|
|
26289
27615
|
try {
|
|
26290
|
-
const stats = await probe.stat(
|
|
27616
|
+
const stats = await probe.stat(path15);
|
|
26291
27617
|
if (stats.isDirectory()) {
|
|
26292
27618
|
return {
|
|
26293
27619
|
ok: false,
|
|
26294
|
-
detail: `${label}: ${
|
|
27620
|
+
detail: `${label}: ${path15} (exists but is a directory)`
|
|
26295
27621
|
};
|
|
26296
27622
|
}
|
|
26297
|
-
await probe.access(
|
|
27623
|
+
await probe.access(path15, constants.W_OK);
|
|
26298
27624
|
return {
|
|
26299
27625
|
ok: true,
|
|
26300
|
-
detail: `${label}: ${
|
|
27626
|
+
detail: `${label}: ${path15} (writable)`
|
|
26301
27627
|
};
|
|
26302
27628
|
} catch (error2) {
|
|
26303
27629
|
if (!isMissingPathError(error2)) {
|
|
26304
27630
|
return {
|
|
26305
27631
|
ok: false,
|
|
26306
|
-
detail: `${label}: ${
|
|
27632
|
+
detail: `${label}: ${path15} (unusable: ${formatError6(error2)})`
|
|
26307
27633
|
};
|
|
26308
27634
|
}
|
|
26309
|
-
const parentCheck = await checkCreatableAncestorDirectory(dirname14(
|
|
27635
|
+
const parentCheck = await checkCreatableAncestorDirectory(dirname14(path15), probe, platform);
|
|
26310
27636
|
if (!parentCheck.ok) {
|
|
26311
27637
|
return {
|
|
26312
27638
|
ok: false,
|
|
26313
|
-
detail: `${label}: ${
|
|
27639
|
+
detail: `${label}: ${path15} (parent not writable: ${parentCheck.blockingPath})`
|
|
26314
27640
|
};
|
|
26315
27641
|
}
|
|
26316
27642
|
return {
|
|
26317
27643
|
ok: true,
|
|
26318
|
-
detail: `${label}: ${
|
|
27644
|
+
detail: `${label}: ${path15} (creatable via ${parentCheck.creatableFrom})`
|
|
26319
27645
|
};
|
|
26320
27646
|
}
|
|
26321
27647
|
}
|
|
26322
|
-
async function checkCreatableAncestorDirectory(
|
|
27648
|
+
async function checkCreatableAncestorDirectory(path15, probe, platform) {
|
|
26323
27649
|
try {
|
|
26324
|
-
const stats = await probe.stat(
|
|
27650
|
+
const stats = await probe.stat(path15);
|
|
26325
27651
|
if (!stats.isDirectory()) {
|
|
26326
27652
|
return {
|
|
26327
27653
|
ok: false,
|
|
26328
|
-
creatableFrom:
|
|
26329
|
-
blockingPath:
|
|
27654
|
+
creatableFrom: path15,
|
|
27655
|
+
blockingPath: path15
|
|
26330
27656
|
};
|
|
26331
27657
|
}
|
|
26332
|
-
await probe.access(
|
|
27658
|
+
await probe.access(path15, directoryAccessMode(platform));
|
|
26333
27659
|
return {
|
|
26334
27660
|
ok: true,
|
|
26335
|
-
creatableFrom:
|
|
27661
|
+
creatableFrom: path15
|
|
26336
27662
|
};
|
|
26337
27663
|
} catch (error2) {
|
|
26338
27664
|
if (!isMissingPathError(error2)) {
|
|
26339
27665
|
return {
|
|
26340
27666
|
ok: false,
|
|
26341
|
-
creatableFrom:
|
|
26342
|
-
blockingPath:
|
|
27667
|
+
creatableFrom: path15,
|
|
27668
|
+
blockingPath: path15
|
|
26343
27669
|
};
|
|
26344
27670
|
}
|
|
26345
|
-
const parent = dirname14(
|
|
26346
|
-
if (parent ===
|
|
27671
|
+
const parent = dirname14(path15);
|
|
27672
|
+
if (parent === path15) {
|
|
26347
27673
|
return {
|
|
26348
27674
|
ok: false,
|
|
26349
|
-
creatableFrom:
|
|
26350
|
-
blockingPath:
|
|
27675
|
+
creatableFrom: path15,
|
|
27676
|
+
blockingPath: path15
|
|
26351
27677
|
};
|
|
26352
27678
|
}
|
|
26353
27679
|
const parentCheck = await checkCreatableAncestorDirectory(parent, probe, platform);
|
|
@@ -26932,7 +28258,7 @@ import { fileURLToPath as fileURLToPath6 } from "node:url";
|
|
|
26932
28258
|
|
|
26933
28259
|
// src/daemon/daemon-runtime.ts
|
|
26934
28260
|
init_daemon_status();
|
|
26935
|
-
import { mkdir as mkdir5, rm as rm3, writeFile as
|
|
28261
|
+
import { mkdir as mkdir5, rm as rm3, writeFile as writeFile3 } from "node:fs/promises";
|
|
26936
28262
|
import { dirname as dirname5 } from "node:path";
|
|
26937
28263
|
|
|
26938
28264
|
class DaemonRuntime {
|
|
@@ -26960,7 +28286,7 @@ class DaemonRuntime {
|
|
|
26960
28286
|
stderr_log: this.paths.stderrLog
|
|
26961
28287
|
};
|
|
26962
28288
|
await mkdir5(dirname5(this.paths.pidFile), { recursive: true });
|
|
26963
|
-
await
|
|
28289
|
+
await writeFile3(this.paths.pidFile, `${this.options.pid}
|
|
26964
28290
|
`);
|
|
26965
28291
|
await this.statusStore.save(this.currentStatus);
|
|
26966
28292
|
}
|
|
@@ -39393,7 +40719,8 @@ function buildWeacpxMcpToolRegistry(input) {
|
|
|
39393
40719
|
task: exports_external.string().min(1),
|
|
39394
40720
|
workingDirectory: exports_external.string().min(1).optional(),
|
|
39395
40721
|
role: exports_external.string().min(1).optional(),
|
|
39396
|
-
groupId: exports_external.string().min(1).optional()
|
|
40722
|
+
groupId: exports_external.string().min(1).optional(),
|
|
40723
|
+
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
40724
|
}).strict(),
|
|
39398
40725
|
handler: async (args) => await asToolResult(async () => {
|
|
39399
40726
|
const input2 = args;
|
|
@@ -39414,7 +40741,8 @@ function buildWeacpxMcpToolRegistry(input) {
|
|
|
39414
40741
|
targetAgent: exports_external.string().min(1),
|
|
39415
40742
|
task: exports_external.string().min(1),
|
|
39416
40743
|
workingDirectory: exports_external.string().min(1).optional(),
|
|
39417
|
-
role: exports_external.string().min(1).optional()
|
|
40744
|
+
role: exports_external.string().min(1).optional(),
|
|
40745
|
+
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
40746
|
}).strict()).min(1)
|
|
39419
40747
|
}).strict(),
|
|
39420
40748
|
handler: async (args) => await asToolResult(async () => {
|
|
@@ -39433,7 +40761,8 @@ function buildWeacpxMcpToolRegistry(input) {
|
|
|
39433
40761
|
task: entry.task,
|
|
39434
40762
|
...entry.workingDirectory ? { workingDirectory: entry.workingDirectory } : {},
|
|
39435
40763
|
...entry.role ? { role: entry.role } : {},
|
|
39436
|
-
...groupId ? { groupId } : {}
|
|
40764
|
+
...groupId ? { groupId } : {},
|
|
40765
|
+
...entry.parallel !== undefined ? { parallel: entry.parallel } : {}
|
|
39437
40766
|
});
|
|
39438
40767
|
results.push({ index, taskId: result.taskId, status: result.status });
|
|
39439
40768
|
} catch (error2) {
|
|
@@ -39657,7 +40986,7 @@ function createErrorResult(message) {
|
|
|
39657
40986
|
};
|
|
39658
40987
|
}
|
|
39659
40988
|
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.`;
|
|
40989
|
+
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
40990
|
return [`Delegation task "${result.taskId}" created.`, `- Status: ${result.status}`, next].join(`
|
|
39662
40991
|
`);
|
|
39663
40992
|
}
|
|
@@ -39928,7 +41257,8 @@ function createOrchestrationTransport(endpoint, deps = {}) {
|
|
|
39928
41257
|
task: input.task,
|
|
39929
41258
|
...input.workingDirectory !== undefined ? { cwd: input.workingDirectory } : {},
|
|
39930
41259
|
...input.role !== undefined ? { role: input.role } : {},
|
|
39931
|
-
...input.groupId !== undefined ? { groupId: input.groupId } : {}
|
|
41260
|
+
...input.groupId !== undefined ? { groupId: input.groupId } : {},
|
|
41261
|
+
...input.parallel !== undefined ? { parallel: input.parallel } : {}
|
|
39932
41262
|
}),
|
|
39933
41263
|
createGroup: async (input) => await client.createGroup(input),
|
|
39934
41264
|
getTask: async (input) => await client.getTaskForCoordinator(input),
|
|
@@ -40641,10 +41971,12 @@ function parseSourceHandle(args, env = process.env) {
|
|
|
40641
41971
|
|
|
40642
41972
|
// src/cli.ts
|
|
40643
41973
|
init_workspace_path();
|
|
41974
|
+
init_workspace_name();
|
|
40644
41975
|
init_state_store();
|
|
40645
41976
|
|
|
40646
41977
|
// src/onboarding.ts
|
|
40647
41978
|
init_workspace_path();
|
|
41979
|
+
init_workspace_name();
|
|
40648
41980
|
init_agent_templates();
|
|
40649
41981
|
function isFirstUse(config2, state) {
|
|
40650
41982
|
return Object.keys(state.sessions ?? {}).length === 0 && Object.keys(config2.workspaces ?? {}).length === 0 && (config2.plugins ?? []).length === 0;
|
|
@@ -40655,7 +41987,7 @@ async function maybeRunFirstUseOnboarding(input) {
|
|
|
40655
41987
|
if (!input.deps.isInteractive())
|
|
40656
41988
|
return { created: false };
|
|
40657
41989
|
const cwd = normalizeWorkspacePath(input.deps.cwd());
|
|
40658
|
-
const workspaceName =
|
|
41990
|
+
const workspaceName = allocateWorkspaceName(sanitizeWorkspaceName(basenameForWorkspacePath(cwd)), input.config.workspaces);
|
|
40659
41991
|
const yes = (await input.deps.promptText(`检测到首次使用 weacpx。是否将当前目录创建为工作区「${workspaceName}」?[Y/n] `)).trim().toLowerCase();
|
|
40660
41992
|
if (yes === "n" || yes === "no")
|
|
40661
41993
|
return { created: false };
|
|
@@ -40696,18 +42028,6 @@ function resolveTemplateChoice(answer, names) {
|
|
|
40696
42028
|
return names[index - 1];
|
|
40697
42029
|
return names.includes(answer) ? answer : null;
|
|
40698
42030
|
}
|
|
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
42031
|
|
|
40712
42032
|
// src/cli-update.ts
|
|
40713
42033
|
init_plugin_home();
|
|
@@ -42371,7 +43691,7 @@ var HELP_LINES = [
|
|
|
42371
43691
|
"weacpx doctor - 运行诊断",
|
|
42372
43692
|
"weacpx version - 查看版本",
|
|
42373
43693
|
"weacpx agent|agents list|add|rm|templates - 管理本机 Agent",
|
|
42374
|
-
"weacpx workspace list|add|rm - 管理本机工作区(别名:ws)",
|
|
43694
|
+
"weacpx workspace list|add [name] [--raw]|rm <name> - 管理本机工作区(别名:ws)",
|
|
42375
43695
|
"weacpx mcp-stdio [--coordinator-session <session>] [--source-handle <handle>] [--workspace <name>] - 启动 MCP stdio 服务"
|
|
42376
43696
|
];
|
|
42377
43697
|
function getUsageText() {
|
|
@@ -42635,10 +43955,23 @@ async function handleWorkspaceCli(args, deps) {
|
|
|
42635
43955
|
if (args.length !== 1)
|
|
42636
43956
|
return null;
|
|
42637
43957
|
return await workspaceList(deps.print);
|
|
42638
|
-
case "add":
|
|
42639
|
-
|
|
42640
|
-
|
|
42641
|
-
|
|
43958
|
+
case "add": {
|
|
43959
|
+
const rest = args.slice(1);
|
|
43960
|
+
let rawFlag = false;
|
|
43961
|
+
let explicit;
|
|
43962
|
+
for (const token of rest) {
|
|
43963
|
+
if (token === "--raw") {
|
|
43964
|
+
if (rawFlag)
|
|
43965
|
+
return null;
|
|
43966
|
+
rawFlag = true;
|
|
43967
|
+
continue;
|
|
43968
|
+
}
|
|
43969
|
+
if (explicit !== undefined)
|
|
43970
|
+
return null;
|
|
43971
|
+
explicit = token;
|
|
43972
|
+
}
|
|
43973
|
+
return await workspaceAdd(explicit, { ...deps, raw: rawFlag });
|
|
43974
|
+
}
|
|
42642
43975
|
case "rm":
|
|
42643
43976
|
if (args.length !== 2 || !args[1])
|
|
42644
43977
|
return null;
|
|
@@ -42663,13 +43996,20 @@ async function workspaceList(print) {
|
|
|
42663
43996
|
}
|
|
42664
43997
|
async function workspaceAdd(rawName, deps) {
|
|
42665
43998
|
const cwd = normalizeWorkspacePath(deps.cwd());
|
|
42666
|
-
const
|
|
42667
|
-
if (
|
|
43999
|
+
const input = rawName === undefined ? basenameForWorkspacePath(cwd) : rawName.trim();
|
|
44000
|
+
if (input.length === 0) {
|
|
42668
44001
|
deps.print("工作区名称不能为空。");
|
|
42669
44002
|
return 1;
|
|
42670
44003
|
}
|
|
42671
44004
|
const store = await createCliConfigStore();
|
|
42672
44005
|
const config2 = await store.load();
|
|
44006
|
+
let name = input;
|
|
44007
|
+
if (!deps.raw && !isWorkspaceNameValid(input)) {
|
|
44008
|
+
const base = sanitizeWorkspaceName(input);
|
|
44009
|
+
name = allocateWorkspaceName(base, config2.workspaces);
|
|
44010
|
+
const sourceLabel = rawName === undefined ? "目录名" : "名称";
|
|
44011
|
+
deps.print(`${sourceLabel} ${JSON.stringify(input)} 含有特殊字符,已保存为「${name}」。如需保留原名请加 --raw。`);
|
|
44012
|
+
}
|
|
42673
44013
|
const existing = config2.workspaces[name];
|
|
42674
44014
|
if (existing) {
|
|
42675
44015
|
if (sameWorkspacePath(existing.cwd, cwd)) {
|
|
@@ -42677,7 +44017,7 @@ async function workspaceAdd(rawName, deps) {
|
|
|
42677
44017
|
return 0;
|
|
42678
44018
|
}
|
|
42679
44019
|
deps.print(`工作区「${name}」已存在,但路径不同:${existing.cwd}`);
|
|
42680
|
-
deps.print(`请换一个名称,或先执行:weacpx workspace rm ${name}`);
|
|
44020
|
+
deps.print(`请换一个名称,或先执行:weacpx workspace rm ${quoteWorkspaceNameIfNeeded(name)}`);
|
|
42681
44021
|
return 1;
|
|
42682
44022
|
}
|
|
42683
44023
|
await store.upsertWorkspace(name, cwd);
|
|
@@ -42830,7 +44170,12 @@ async function defaultRun(options = {}) {
|
|
|
42830
44170
|
await ensureConfigExists(runtimePaths.configPath);
|
|
42831
44171
|
const config2 = await loadConfig(runtimePaths.configPath);
|
|
42832
44172
|
const { loadConfiguredPlugins: loadConfiguredPlugins2 } = await Promise.resolve().then(() => (init_plugin_loader(), exports_plugin_loader));
|
|
42833
|
-
await loadConfiguredPlugins2({
|
|
44173
|
+
await loadConfiguredPlugins2({
|
|
44174
|
+
plugins: config2.plugins,
|
|
44175
|
+
onPluginError: ({ name, error: error2 }) => {
|
|
44176
|
+
console.error(`[weacpx] skipping plugin ${name}: ${error2 instanceof Error ? error2.message : String(error2)}`);
|
|
44177
|
+
}
|
|
44178
|
+
});
|
|
42834
44179
|
const { createMessageChannels: createMessageChannels2 } = await Promise.resolve().then(() => (init_create_channel(), exports_create_channel));
|
|
42835
44180
|
const { MessageChannelRegistry: MessageChannelRegistry2 } = await Promise.resolve().then(() => (init_channel_registry(), exports_channel_registry));
|
|
42836
44181
|
const daemonPaths = resolveDaemonPathsForCurrentConfig();
|