weacpx 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -39
- package/config.example.json +4 -1
- package/dist/bridge/bridge-main.js +264 -30
- package/dist/cli.js +1767 -718
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -47,6 +47,129 @@ var __export = (target, all) => {
|
|
|
47
47
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
48
48
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
49
49
|
|
|
50
|
+
// src/weixin/monitor/consumer-lock.ts
|
|
51
|
+
import { mkdir as mkdir5, open as open2, readFile as readFile3, rm as rm4 } from "node:fs/promises";
|
|
52
|
+
import { dirname as dirname4, join as join2 } from "node:path";
|
|
53
|
+
import { homedir } from "node:os";
|
|
54
|
+
function createWeixinConsumerLock(options = {}) {
|
|
55
|
+
const lockFilePath = options.lockFilePath ?? join2(homedir(), ".weacpx", "runtime", "weixin-consumer.lock.json");
|
|
56
|
+
const isProcessRunning = options.isProcessRunning ?? defaultIsProcessRunning2;
|
|
57
|
+
const onDiagnostic = options.onDiagnostic;
|
|
58
|
+
return {
|
|
59
|
+
async acquire(meta) {
|
|
60
|
+
await mkdir5(dirname4(lockFilePath), { recursive: true });
|
|
61
|
+
while (true) {
|
|
62
|
+
try {
|
|
63
|
+
const handle = await open2(lockFilePath, "wx");
|
|
64
|
+
try {
|
|
65
|
+
await handle.writeFile(`${JSON.stringify(meta, null, 2)}
|
|
66
|
+
`, "utf8");
|
|
67
|
+
} finally {
|
|
68
|
+
await handle.close();
|
|
69
|
+
}
|
|
70
|
+
await onDiagnostic?.("lock_acquired", {
|
|
71
|
+
lockFilePath,
|
|
72
|
+
pid: meta.pid,
|
|
73
|
+
mode: meta.mode,
|
|
74
|
+
configPath: meta.configPath,
|
|
75
|
+
statePath: meta.statePath,
|
|
76
|
+
hostname: meta.hostname
|
|
77
|
+
});
|
|
78
|
+
return;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
const code = error.code;
|
|
81
|
+
if (code !== "EEXIST") {
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
await onDiagnostic?.("lock_exists", {
|
|
85
|
+
lockFilePath,
|
|
86
|
+
pid: meta.pid,
|
|
87
|
+
mode: meta.mode
|
|
88
|
+
});
|
|
89
|
+
const existing = await loadLockMetadata(lockFilePath);
|
|
90
|
+
if (!existing) {
|
|
91
|
+
await rm4(lockFilePath, { force: true });
|
|
92
|
+
await onDiagnostic?.("lock_invalid_removed", {
|
|
93
|
+
lockFilePath,
|
|
94
|
+
reason: "invalid_or_unreadable_metadata"
|
|
95
|
+
});
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (!isProcessRunning(existing.pid)) {
|
|
99
|
+
await rm4(lockFilePath, { force: true });
|
|
100
|
+
await onDiagnostic?.("lock_stale_removed", {
|
|
101
|
+
lockFilePath,
|
|
102
|
+
stalePid: existing.pid,
|
|
103
|
+
staleMode: existing.mode,
|
|
104
|
+
staleConfigPath: existing.configPath,
|
|
105
|
+
staleStatePath: existing.statePath,
|
|
106
|
+
reason: "owner_process_not_running"
|
|
107
|
+
});
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
await onDiagnostic?.("lock_active_conflict", {
|
|
111
|
+
lockFilePath,
|
|
112
|
+
activePid: existing.pid,
|
|
113
|
+
activeMode: existing.mode,
|
|
114
|
+
activeConfigPath: existing.configPath,
|
|
115
|
+
activeStatePath: existing.statePath,
|
|
116
|
+
requestedPid: meta.pid,
|
|
117
|
+
requestedMode: meta.mode
|
|
118
|
+
});
|
|
119
|
+
throw new ActiveWeixinConsumerLockError(lockFilePath, existing);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
async release() {
|
|
124
|
+
await rm4(lockFilePath, { force: true });
|
|
125
|
+
await onDiagnostic?.("lock_released", {
|
|
126
|
+
lockFilePath
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
async function loadLockMetadata(path) {
|
|
132
|
+
try {
|
|
133
|
+
const raw = await readFile3(path, "utf8");
|
|
134
|
+
const parsed = JSON.parse(raw);
|
|
135
|
+
if (!parsed || typeof parsed.pid !== "number" || !parsed.mode || !parsed.configPath || !parsed.statePath) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
return parsed;
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function defaultIsProcessRunning2(pid) {
|
|
144
|
+
try {
|
|
145
|
+
process.kill(pid, 0);
|
|
146
|
+
return true;
|
|
147
|
+
} catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
var ActiveWeixinConsumerLockError;
|
|
152
|
+
var init_consumer_lock = __esm(() => {
|
|
153
|
+
ActiveWeixinConsumerLockError = class ActiveWeixinConsumerLockError extends Error {
|
|
154
|
+
existing;
|
|
155
|
+
lockFilePath;
|
|
156
|
+
constructor(lockFilePath, existing) {
|
|
157
|
+
super([
|
|
158
|
+
"weacpx Weixin consumer is already running.",
|
|
159
|
+
`pid: ${existing.pid}`,
|
|
160
|
+
`mode: ${existing.mode}`,
|
|
161
|
+
`config: ${existing.configPath}`,
|
|
162
|
+
`state: ${existing.statePath}`,
|
|
163
|
+
"Try stopping the existing instance or close the foreground `weacpx run` process before starting a new one."
|
|
164
|
+
].join(`
|
|
165
|
+
`));
|
|
166
|
+
this.name = "ActiveWeixinConsumerLockError";
|
|
167
|
+
this.lockFilePath = lockFilePath;
|
|
168
|
+
this.existing = existing;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
|
|
50
173
|
// node_modules/qrcode-terminal/vendor/QRCode/QRMode.js
|
|
51
174
|
var require_QRMode = __commonJS((exports, module) => {
|
|
52
175
|
module.exports = {
|
|
@@ -2428,6 +2551,25 @@ var init_media_download = __esm(() => {
|
|
|
2428
2551
|
WEIXIN_MEDIA_MAX_BYTES = 100 * 1024 * 1024;
|
|
2429
2552
|
});
|
|
2430
2553
|
|
|
2554
|
+
// src/weixin/messaging/execute-chat-turn.ts
|
|
2555
|
+
async function executeChatTurn(params) {
|
|
2556
|
+
let usedReply = false;
|
|
2557
|
+
const response = await params.agent.chat({
|
|
2558
|
+
...params.request,
|
|
2559
|
+
reply: async (text) => {
|
|
2560
|
+
const delivered = await params.onReplySegment?.(text);
|
|
2561
|
+
if (delivered !== false) {
|
|
2562
|
+
usedReply = true;
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
});
|
|
2566
|
+
return {
|
|
2567
|
+
text: usedReply ? undefined : response.text,
|
|
2568
|
+
media: response.media,
|
|
2569
|
+
usedReply
|
|
2570
|
+
};
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2431
2573
|
// src/weixin/messaging/inbound.ts
|
|
2432
2574
|
function contextTokenKey(accountId, userId) {
|
|
2433
2575
|
return `${accountId}:${userId}`;
|
|
@@ -2826,23 +2968,29 @@ var init_slash_commands = __esm(() => {
|
|
|
2826
2968
|
init_send();
|
|
2827
2969
|
});
|
|
2828
2970
|
|
|
2829
|
-
// src/weixin/messaging/
|
|
2971
|
+
// src/weixin/messaging/handle-weixin-message-turn.ts
|
|
2830
2972
|
import crypto4 from "node:crypto";
|
|
2831
2973
|
import fs6 from "node:fs/promises";
|
|
2974
|
+
import { tmpdir } from "node:os";
|
|
2832
2975
|
import path9 from "node:path";
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
ext =
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2976
|
+
function resolveMediaTempDir(customRoot) {
|
|
2977
|
+
return customRoot ?? path9.join(tmpdir(), "weacpx", "media");
|
|
2978
|
+
}
|
|
2979
|
+
function createSaveMediaBuffer(mediaTempDir) {
|
|
2980
|
+
return async function saveMediaBuffer(buffer, contentType, subdir, _maxBytes, originalFilename) {
|
|
2981
|
+
const dir = path9.join(resolveMediaTempDir(mediaTempDir), subdir ?? "");
|
|
2982
|
+
await fs6.mkdir(dir, { recursive: true });
|
|
2983
|
+
let ext = ".bin";
|
|
2984
|
+
if (originalFilename) {
|
|
2985
|
+
ext = path9.extname(originalFilename) || ".bin";
|
|
2986
|
+
} else if (contentType) {
|
|
2987
|
+
ext = getExtensionFromMime(contentType);
|
|
2988
|
+
}
|
|
2989
|
+
const name = `${Date.now()}-${crypto4.randomBytes(4).toString("hex")}${ext}`;
|
|
2990
|
+
const filePath = path9.join(dir, name);
|
|
2991
|
+
await fs6.writeFile(filePath, buffer);
|
|
2992
|
+
return { path: filePath };
|
|
2993
|
+
};
|
|
2846
2994
|
}
|
|
2847
2995
|
function extractTextBody(itemList) {
|
|
2848
2996
|
if (!itemList?.length)
|
|
@@ -2857,13 +3005,13 @@ function extractTextBody(itemList) {
|
|
|
2857
3005
|
function findMediaItem(itemList) {
|
|
2858
3006
|
if (!itemList?.length)
|
|
2859
3007
|
return;
|
|
2860
|
-
const direct = itemList.find((
|
|
3008
|
+
const direct = itemList.find((item) => item.type === MessageItemType.IMAGE && hasDownloadableMedia(item.image_item?.media)) ?? itemList.find((item) => item.type === MessageItemType.VIDEO && hasDownloadableMedia(item.video_item?.media)) ?? itemList.find((item) => item.type === MessageItemType.FILE && hasDownloadableMedia(item.file_item?.media)) ?? itemList.find((item) => item.type === MessageItemType.VOICE && hasDownloadableMedia(item.voice_item?.media) && !item.voice_item?.text);
|
|
2861
3009
|
if (direct)
|
|
2862
3010
|
return direct;
|
|
2863
|
-
const refItem = itemList.find((
|
|
3011
|
+
const refItem = itemList.find((item) => item.type === MessageItemType.TEXT && item.ref_msg?.message_item && isMediaItem(item.ref_msg.message_item));
|
|
2864
3012
|
return refItem?.ref_msg?.message_item ?? undefined;
|
|
2865
3013
|
}
|
|
2866
|
-
async function
|
|
3014
|
+
async function handleWeixinMessageTurn(full, deps) {
|
|
2867
3015
|
const receivedAt = Date.now();
|
|
2868
3016
|
const textBody = extractTextBody(full.item_list);
|
|
2869
3017
|
if (textBody.startsWith("/")) {
|
|
@@ -2891,7 +3039,7 @@ async function processOneMessage(full, deps) {
|
|
|
2891
3039
|
try {
|
|
2892
3040
|
const downloaded = await downloadMediaFromItem(mediaItem, {
|
|
2893
3041
|
cdnBaseUrl: deps.cdnBaseUrl,
|
|
2894
|
-
saveMedia:
|
|
3042
|
+
saveMedia: createSaveMediaBuffer(deps.mediaTempDir),
|
|
2895
3043
|
log: deps.log,
|
|
2896
3044
|
errLog: deps.errLog,
|
|
2897
3045
|
label: "inbound"
|
|
@@ -2914,26 +3062,31 @@ async function processOneMessage(full, deps) {
|
|
|
2914
3062
|
};
|
|
2915
3063
|
}
|
|
2916
3064
|
} catch (err) {
|
|
2917
|
-
|
|
3065
|
+
deps.errLog(`media download failed: ${String(err)}`);
|
|
2918
3066
|
}
|
|
2919
3067
|
}
|
|
2920
3068
|
const to = full.from_user_id ?? "";
|
|
2921
|
-
const
|
|
3069
|
+
const sendReplySegment = async (text) => {
|
|
3070
|
+
const plainText = markdownToPlainText(text).trim();
|
|
3071
|
+
if (plainText.length === 0) {
|
|
3072
|
+
return false;
|
|
3073
|
+
}
|
|
2922
3074
|
try {
|
|
2923
3075
|
await sendMessageWeixin({
|
|
2924
3076
|
to,
|
|
2925
|
-
text:
|
|
3077
|
+
text: plainText,
|
|
2926
3078
|
opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken }
|
|
2927
3079
|
});
|
|
3080
|
+
return true;
|
|
2928
3081
|
} catch (err) {
|
|
2929
|
-
|
|
3082
|
+
deps.errLog(`intermediate reply failed: ${String(err)}`);
|
|
3083
|
+
return false;
|
|
2930
3084
|
}
|
|
2931
3085
|
};
|
|
2932
3086
|
const request = {
|
|
2933
3087
|
conversationId: full.from_user_id ?? "",
|
|
2934
3088
|
text: bodyFromItemList(full.item_list),
|
|
2935
|
-
media
|
|
2936
|
-
reply
|
|
3089
|
+
media
|
|
2937
3090
|
};
|
|
2938
3091
|
let typingTimer;
|
|
2939
3092
|
const startTyping = () => {
|
|
@@ -2954,31 +3107,40 @@ async function processOneMessage(full, deps) {
|
|
|
2954
3107
|
typingTimer = setInterval(startTyping, 1e4);
|
|
2955
3108
|
}
|
|
2956
3109
|
try {
|
|
2957
|
-
const
|
|
2958
|
-
|
|
3110
|
+
const turn = await executeChatTurn({
|
|
3111
|
+
agent: deps.agent,
|
|
3112
|
+
request,
|
|
3113
|
+
onReplySegment: sendReplySegment
|
|
3114
|
+
});
|
|
3115
|
+
if (turn.media) {
|
|
2959
3116
|
let filePath;
|
|
2960
|
-
const mediaUrl =
|
|
3117
|
+
const mediaUrl = turn.media.url;
|
|
2961
3118
|
if (mediaUrl.startsWith("http://") || mediaUrl.startsWith("https://")) {
|
|
2962
|
-
filePath = await downloadRemoteImageToTemp(mediaUrl, path9.join(
|
|
3119
|
+
filePath = await downloadRemoteImageToTemp(mediaUrl, path9.join(resolveMediaTempDir(deps.mediaTempDir), "outbound"));
|
|
2963
3120
|
} else {
|
|
2964
3121
|
filePath = path9.isAbsolute(mediaUrl) ? mediaUrl : path9.resolve(mediaUrl);
|
|
2965
3122
|
}
|
|
2966
3123
|
await sendWeixinMediaFile({
|
|
2967
3124
|
filePath,
|
|
2968
3125
|
to,
|
|
2969
|
-
text:
|
|
3126
|
+
text: turn.text ? markdownToPlainText(turn.text) : "",
|
|
2970
3127
|
opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken },
|
|
2971
3128
|
cdnBaseUrl: deps.cdnBaseUrl
|
|
2972
3129
|
});
|
|
2973
|
-
} else if (
|
|
3130
|
+
} else if (turn.text) {
|
|
3131
|
+
const finalText = markdownToPlainText(turn.text).trim();
|
|
3132
|
+
if (finalText.length === 0) {
|
|
3133
|
+
return;
|
|
3134
|
+
}
|
|
2974
3135
|
await sendMessageWeixin({
|
|
2975
3136
|
to,
|
|
2976
|
-
text:
|
|
3137
|
+
text: finalText,
|
|
2977
3138
|
opts: { baseUrl: deps.baseUrl, token: deps.token, contextToken }
|
|
2978
3139
|
});
|
|
2979
3140
|
}
|
|
2980
3141
|
} catch (err) {
|
|
2981
|
-
|
|
3142
|
+
const errorText = err instanceof Error ? err.stack ?? err.message : JSON.stringify(err);
|
|
3143
|
+
deps.errLog(`handleWeixinMessageTurn: agent or send failed: ${errorText}`);
|
|
2982
3144
|
sendWeixinErrorNotice({
|
|
2983
3145
|
to,
|
|
2984
3146
|
contextToken,
|
|
@@ -3003,14 +3165,13 @@ async function processOneMessage(full, deps) {
|
|
|
3003
3165
|
}
|
|
3004
3166
|
}
|
|
3005
3167
|
}
|
|
3006
|
-
var
|
|
3007
|
-
var
|
|
3168
|
+
var hasDownloadableMedia = (media) => media?.encrypt_query_param || media?.full_url;
|
|
3169
|
+
var init_handle_weixin_message_turn = __esm(() => {
|
|
3008
3170
|
init_api();
|
|
3009
3171
|
init_types();
|
|
3010
3172
|
init_upload();
|
|
3011
3173
|
init_media_download();
|
|
3012
3174
|
init_mime();
|
|
3013
|
-
init_logger();
|
|
3014
3175
|
init_inbound();
|
|
3015
3176
|
init_error_notice();
|
|
3016
3177
|
init_send_media();
|
|
@@ -3138,7 +3299,7 @@ async function monitorWeixinProvider(opts) {
|
|
|
3138
3299
|
aLog.info(`inbound: from=${full.from_user_id} types=${full.item_list?.map((i) => i.type).join(",") ?? "none"}`);
|
|
3139
3300
|
const fromUserId = full.from_user_id ?? "";
|
|
3140
3301
|
const cachedConfig = await configManager.getForUser(fromUserId, full.context_token);
|
|
3141
|
-
await
|
|
3302
|
+
await handleWeixinMessageTurn(full, {
|
|
3142
3303
|
accountId,
|
|
3143
3304
|
agent,
|
|
3144
3305
|
baseUrl,
|
|
@@ -3180,7 +3341,7 @@ var init_monitor = __esm(() => {
|
|
|
3180
3341
|
init_api();
|
|
3181
3342
|
init_config_cache();
|
|
3182
3343
|
init_session_guard();
|
|
3183
|
-
|
|
3344
|
+
init_handle_weixin_message_turn();
|
|
3184
3345
|
init_sync_buf();
|
|
3185
3346
|
init_logger();
|
|
3186
3347
|
});
|
|
@@ -3327,93 +3488,9 @@ var init_weixin_sdk = __esm(() => {
|
|
|
3327
3488
|
init_weixin();
|
|
3328
3489
|
});
|
|
3329
3490
|
|
|
3330
|
-
// src/config/agent-templates.ts
|
|
3331
|
-
function getAgentTemplate(name) {
|
|
3332
|
-
const template = TEMPLATES[name];
|
|
3333
|
-
if (!template) {
|
|
3334
|
-
return null;
|
|
3335
|
-
}
|
|
3336
|
-
return {
|
|
3337
|
-
...template
|
|
3338
|
-
};
|
|
3339
|
-
}
|
|
3340
|
-
function listAgentTemplates() {
|
|
3341
|
-
return Object.keys(TEMPLATES);
|
|
3342
|
-
}
|
|
3343
|
-
var TEMPLATES;
|
|
3344
|
-
var init_agent_templates = __esm(() => {
|
|
3345
|
-
TEMPLATES = {
|
|
3346
|
-
codex: {
|
|
3347
|
-
driver: "codex"
|
|
3348
|
-
},
|
|
3349
|
-
claude: {
|
|
3350
|
-
driver: "claude"
|
|
3351
|
-
}
|
|
3352
|
-
};
|
|
3353
|
-
});
|
|
3354
|
-
|
|
3355
|
-
// src/formatting/render-text.ts
|
|
3356
|
-
function renderHelpText() {
|
|
3357
|
-
return [
|
|
3358
|
-
"可用命令:",
|
|
3359
|
-
"",
|
|
3360
|
-
"先看这 3 个:",
|
|
3361
|
-
"/ss new <agent> -d <path> - 新建会话",
|
|
3362
|
-
"/use <alias> - 切会话",
|
|
3363
|
-
"/status - 看状态",
|
|
3364
|
-
"",
|
|
3365
|
-
"Agent:",
|
|
3366
|
-
"/agents - 看 Agent",
|
|
3367
|
-
"/agent add <codex|claude> - 加 Agent",
|
|
3368
|
-
"/agent rm <name> - 删 Agent",
|
|
3369
|
-
"",
|
|
3370
|
-
"工作区:",
|
|
3371
|
-
"/workspaces - 看工作区",
|
|
3372
|
-
"/workspace 或 /ws - 工作区命令",
|
|
3373
|
-
"/ws new <name> -d <path> - 加工作区",
|
|
3374
|
-
"/workspace rm <name> - 删工作区",
|
|
3375
|
-
"",
|
|
3376
|
-
"会话:",
|
|
3377
|
-
"/sessions - 看会话",
|
|
3378
|
-
"/session 或 /ss - 会话命令",
|
|
3379
|
-
"/ss <agent> -d <path> - 快速新建",
|
|
3380
|
-
"/ss new <agent> -d <path> - 新建会话",
|
|
3381
|
-
"/ss new <alias> -a <name> --ws <name> - 指定配置新建",
|
|
3382
|
-
"/ss attach <alias> -a <name> --ws <name> --name <transport-session> - 挂已有会话",
|
|
3383
|
-
"/use <alias> - 切会话",
|
|
3384
|
-
"/session reset 或 /clear - 清上下文",
|
|
3385
|
-
"",
|
|
3386
|
-
"权限:",
|
|
3387
|
-
"/pm 或 /permission - 权限设置",
|
|
3388
|
-
"/pm set <allow|read|deny> - 设审批级别",
|
|
3389
|
-
"/pm auto [allow|deny|fail] - 设自动处理",
|
|
3390
|
-
"",
|
|
3391
|
-
"常用:",
|
|
3392
|
-
"/status - 看状态",
|
|
3393
|
-
"/cancel 或 /stop - 停当前任务"
|
|
3394
|
-
].join(`
|
|
3395
|
-
`);
|
|
3396
|
-
}
|
|
3397
|
-
function renderAgents(config) {
|
|
3398
|
-
const names = Object.keys(config.agents);
|
|
3399
|
-
if (names.length === 0) {
|
|
3400
|
-
return "还没有注册任何 Agent。";
|
|
3401
|
-
}
|
|
3402
|
-
return ["已注册的 Agent:", ...names.map((name) => `- ${name}`)].join(`
|
|
3403
|
-
`);
|
|
3404
|
-
}
|
|
3405
|
-
function renderWorkspaces(config) {
|
|
3406
|
-
const names = Object.entries(config.workspaces);
|
|
3407
|
-
if (names.length === 0) {
|
|
3408
|
-
return "还没有注册任何工作区。";
|
|
3409
|
-
}
|
|
3410
|
-
return ["已注册的工作区:", ...names.map(([name, workspace]) => `- ${name}: ${workspace.cwd}`)].join(`
|
|
3411
|
-
`);
|
|
3412
|
-
}
|
|
3413
|
-
|
|
3414
3491
|
// src/logging/app-logger.ts
|
|
3415
|
-
import { appendFile, mkdir as
|
|
3416
|
-
import { basename, dirname as
|
|
3492
|
+
import { appendFile, mkdir as mkdir6, readdir, rename, rm as rm5, stat } from "node:fs/promises";
|
|
3493
|
+
import { basename, dirname as dirname5, join as join3 } from "node:path";
|
|
3417
3494
|
function createNoopAppLogger() {
|
|
3418
3495
|
return {
|
|
3419
3496
|
debug: async () => {},
|
|
@@ -3443,7 +3520,7 @@ function createAppLogger(options) {
|
|
|
3443
3520
|
return;
|
|
3444
3521
|
}
|
|
3445
3522
|
const line = formatLogLine(now(), level, event, message, context);
|
|
3446
|
-
await
|
|
3523
|
+
await mkdir6(dirname5(options.filePath), { recursive: true });
|
|
3447
3524
|
await rotateIfNeeded(options.filePath, Buffer.byteLength(line), options.maxSizeBytes, options.maxFiles);
|
|
3448
3525
|
await appendFile(options.filePath, line, "utf8");
|
|
3449
3526
|
}
|
|
@@ -3464,10 +3541,10 @@ async function rotateIfNeeded(filePath, incomingSize, maxSizeBytes, maxFiles) {
|
|
|
3464
3541
|
return;
|
|
3465
3542
|
}
|
|
3466
3543
|
if (maxFiles <= 0) {
|
|
3467
|
-
await
|
|
3544
|
+
await rm5(filePath, { force: true });
|
|
3468
3545
|
return;
|
|
3469
3546
|
}
|
|
3470
|
-
await
|
|
3547
|
+
await rm5(`${filePath}.${maxFiles}`, { force: true });
|
|
3471
3548
|
for (let index = maxFiles - 1;index >= 1; index -= 1) {
|
|
3472
3549
|
const source = `${filePath}.${index}`;
|
|
3473
3550
|
try {
|
|
@@ -3481,7 +3558,7 @@ async function rotateIfNeeded(filePath, incomingSize, maxSizeBytes, maxFiles) {
|
|
|
3481
3558
|
await rename(filePath, `${filePath}.1`);
|
|
3482
3559
|
}
|
|
3483
3560
|
async function cleanupExpiredRotatedLogs(filePath, retentionDays, now) {
|
|
3484
|
-
const parentDir =
|
|
3561
|
+
const parentDir = dirname5(filePath);
|
|
3485
3562
|
const prefix = `${basename(filePath)}.`;
|
|
3486
3563
|
const cutoff = now().getTime() - retentionDays * 24 * 60 * 60 * 1000;
|
|
3487
3564
|
let files = [];
|
|
@@ -3497,10 +3574,10 @@ async function cleanupExpiredRotatedLogs(filePath, retentionDays, now) {
|
|
|
3497
3574
|
if (!file.startsWith(prefix) || !/^\d+$/.test(file.slice(prefix.length))) {
|
|
3498
3575
|
continue;
|
|
3499
3576
|
}
|
|
3500
|
-
const candidate =
|
|
3577
|
+
const candidate = join3(parentDir, file);
|
|
3501
3578
|
const details = await stat(candidate);
|
|
3502
3579
|
if (details.mtime.getTime() < cutoff) {
|
|
3503
|
-
await
|
|
3580
|
+
await rm5(candidate, { force: true });
|
|
3504
3581
|
}
|
|
3505
3582
|
}
|
|
3506
3583
|
}
|
|
@@ -3532,16 +3609,16 @@ var init_app_logger = __esm(() => {
|
|
|
3532
3609
|
});
|
|
3533
3610
|
|
|
3534
3611
|
// src/transport/acpx-session-index.ts
|
|
3535
|
-
import { readFile as
|
|
3536
|
-
import { homedir } from "node:os";
|
|
3612
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
3613
|
+
import { homedir as homedir2 } from "node:os";
|
|
3537
3614
|
import { resolve } from "node:path";
|
|
3538
3615
|
async function resolveSessionAgentCommandFromIndex(session) {
|
|
3539
|
-
const home = process.env.HOME ??
|
|
3616
|
+
const home = process.env.HOME ?? homedir2();
|
|
3540
3617
|
if (!home) {
|
|
3541
3618
|
return;
|
|
3542
3619
|
}
|
|
3543
3620
|
try {
|
|
3544
|
-
const raw = await
|
|
3621
|
+
const raw = await readFile4(resolve(home, ".acpx", "sessions", "index.json"), "utf8");
|
|
3545
3622
|
const parsed = JSON.parse(raw);
|
|
3546
3623
|
const targetCwd = resolve(session.cwd);
|
|
3547
3624
|
const match = parsed.entries?.find((entry) => entry.name === session.transportSession && entry.cwd === targetCwd && typeof entry.agentCommand === "string" && entry.agentCommand.trim().length > 0);
|
|
@@ -3692,8 +3769,10 @@ function parseCommand(input) {
|
|
|
3692
3769
|
}
|
|
3693
3770
|
const parts = tokenizeCommand(trimmed);
|
|
3694
3771
|
const command = normalizeCommand(parts[0] ?? "");
|
|
3695
|
-
if (command === "/help")
|
|
3772
|
+
if (command === "/help" && parts.length === 1)
|
|
3696
3773
|
return { kind: "help" };
|
|
3774
|
+
if (command === "/help" && parts.length === 2)
|
|
3775
|
+
return { kind: "help", topic: parts[1] };
|
|
3697
3776
|
if (command === "/agents")
|
|
3698
3777
|
return { kind: "agents" };
|
|
3699
3778
|
if (command === "/workspaces")
|
|
@@ -3708,6 +3787,10 @@ function parseCommand(input) {
|
|
|
3708
3787
|
return { kind: "session.reset" };
|
|
3709
3788
|
if (command === "/mode" && parts.length === 1)
|
|
3710
3789
|
return { kind: "mode.show" };
|
|
3790
|
+
if (command === "/replymode" && parts.length === 1)
|
|
3791
|
+
return { kind: "replymode.show" };
|
|
3792
|
+
if (command === "/config" && parts.length === 1)
|
|
3793
|
+
return { kind: "config.show" };
|
|
3711
3794
|
if (command === "/permission" && parts.length === 1)
|
|
3712
3795
|
return { kind: "permission.status" };
|
|
3713
3796
|
if (command === "/session" && parts.length === 1)
|
|
@@ -3731,12 +3814,21 @@ function parseCommand(input) {
|
|
|
3731
3814
|
return { kind: "permission.auto.set", policy };
|
|
3732
3815
|
}
|
|
3733
3816
|
}
|
|
3817
|
+
if (command === "/config" && parts[1] === "set" && parts.length === 4) {
|
|
3818
|
+
return { kind: "config.set", path: parts[2] ?? "", value: parts[3] ?? "" };
|
|
3819
|
+
}
|
|
3734
3820
|
if (command === "/use" && parts[1]) {
|
|
3735
3821
|
return { kind: "session.use", alias: parts[1] };
|
|
3736
3822
|
}
|
|
3737
3823
|
if (command === "/mode" && parts[1]) {
|
|
3738
3824
|
return { kind: "mode.set", modeId: parts[1] };
|
|
3739
3825
|
}
|
|
3826
|
+
if (command === "/replymode" && parts[1] === "reset" && parts.length === 2) {
|
|
3827
|
+
return { kind: "replymode.reset" };
|
|
3828
|
+
}
|
|
3829
|
+
if (command === "/replymode" && (parts[1] === "stream" || parts[1] === "final") && parts.length === 2) {
|
|
3830
|
+
return { kind: "replymode.set", replyMode: parts[1] };
|
|
3831
|
+
}
|
|
3740
3832
|
if (command === "/agent" && parts[1] === "add" && parts[2]) {
|
|
3741
3833
|
return { kind: "agent.add", template: parts[2] };
|
|
3742
3834
|
}
|
|
@@ -3746,13 +3838,23 @@ function parseCommand(input) {
|
|
|
3746
3838
|
if (command === "/workspace" && parts[1] === "new" && parts[2]) {
|
|
3747
3839
|
const name = parts[2];
|
|
3748
3840
|
let cwd = "";
|
|
3841
|
+
let invalid = false;
|
|
3749
3842
|
for (let index = 3;index < parts.length; index += 1) {
|
|
3750
3843
|
if (parts[index] === "--cwd" || parts[index] === "-d") {
|
|
3844
|
+
if (index + 1 >= parts.length) {
|
|
3845
|
+
invalid = true;
|
|
3846
|
+
break;
|
|
3847
|
+
}
|
|
3751
3848
|
cwd = parts[index + 1] ?? "";
|
|
3752
3849
|
index += 1;
|
|
3850
|
+
continue;
|
|
3753
3851
|
}
|
|
3852
|
+
invalid = true;
|
|
3853
|
+
break;
|
|
3854
|
+
}
|
|
3855
|
+
if (!invalid && name.trim().length > 0 && cwd.trim().length > 0) {
|
|
3856
|
+
return { kind: "workspace.new", name, cwd };
|
|
3754
3857
|
}
|
|
3755
|
-
return { kind: "workspace.new", name, cwd };
|
|
3756
3858
|
}
|
|
3757
3859
|
if (command === "/workspace" && parts[1] === "rm" && parts[2]) {
|
|
3758
3860
|
return { kind: "workspace.rm", name: parts[2] };
|
|
@@ -3762,26 +3864,41 @@ function parseCommand(input) {
|
|
|
3762
3864
|
const alias = parts[2];
|
|
3763
3865
|
let agent = "";
|
|
3764
3866
|
let workspace = "";
|
|
3867
|
+
let invalid = false;
|
|
3765
3868
|
for (let index = 3;index < parts.length; index += 1) {
|
|
3766
3869
|
if (parts[index] === "--agent" || parts[index] === "-a") {
|
|
3870
|
+
if (index + 1 >= parts.length) {
|
|
3871
|
+
invalid = true;
|
|
3872
|
+
break;
|
|
3873
|
+
}
|
|
3767
3874
|
agent = parts[index + 1] ?? "";
|
|
3768
3875
|
index += 1;
|
|
3876
|
+
continue;
|
|
3769
3877
|
} else if (parts[index] === "--ws" || parts[index] === "-ws") {
|
|
3878
|
+
if (index + 1 >= parts.length) {
|
|
3879
|
+
invalid = true;
|
|
3880
|
+
break;
|
|
3881
|
+
}
|
|
3770
3882
|
workspace = parts[index + 1] ?? "";
|
|
3771
3883
|
index += 1;
|
|
3884
|
+
continue;
|
|
3772
3885
|
}
|
|
3886
|
+
invalid = true;
|
|
3887
|
+
break;
|
|
3888
|
+
}
|
|
3889
|
+
if (!invalid && alias.trim().length > 0 && agent.trim().length > 0 && workspace.trim().length > 0) {
|
|
3890
|
+
return { kind: "session.new", alias, agent, workspace };
|
|
3773
3891
|
}
|
|
3774
|
-
return { kind: "session.new", alias, agent, workspace };
|
|
3775
3892
|
}
|
|
3776
|
-
const
|
|
3777
|
-
if (
|
|
3778
|
-
return { kind: "session.shortcut.new", agent: parts[2],
|
|
3893
|
+
const shortcutTarget = readSessionShortcutTarget(parts, 3);
|
|
3894
|
+
if (shortcutTarget) {
|
|
3895
|
+
return { kind: "session.shortcut.new", agent: parts[2], ...shortcutTarget };
|
|
3779
3896
|
}
|
|
3780
3897
|
}
|
|
3781
3898
|
if (command === "/session" && parts[1] && parts[1] !== "new" && parts[1] !== "attach" && parts[1] !== "reset") {
|
|
3782
|
-
const
|
|
3783
|
-
if (
|
|
3784
|
-
return { kind: "session.shortcut", agent: parts[1],
|
|
3899
|
+
const shortcutTarget = readSessionShortcutTarget(parts, 2);
|
|
3900
|
+
if (shortcutTarget) {
|
|
3901
|
+
return { kind: "session.shortcut", agent: parts[1], ...shortcutTarget };
|
|
3785
3902
|
}
|
|
3786
3903
|
}
|
|
3787
3904
|
if (command === "/session" && parts[1] === "attach" && parts[2]) {
|
|
@@ -3789,19 +3906,39 @@ function parseCommand(input) {
|
|
|
3789
3906
|
let agent = "";
|
|
3790
3907
|
let workspace = "";
|
|
3791
3908
|
let transportSession = "";
|
|
3909
|
+
let invalid = false;
|
|
3792
3910
|
for (let index = 3;index < parts.length; index += 1) {
|
|
3793
3911
|
if (parts[index] === "--agent" || parts[index] === "-a") {
|
|
3912
|
+
if (index + 1 >= parts.length) {
|
|
3913
|
+
invalid = true;
|
|
3914
|
+
break;
|
|
3915
|
+
}
|
|
3794
3916
|
agent = parts[index + 1] ?? "";
|
|
3795
3917
|
index += 1;
|
|
3918
|
+
continue;
|
|
3796
3919
|
} else if (parts[index] === "--ws" || parts[index] === "-ws") {
|
|
3920
|
+
if (index + 1 >= parts.length) {
|
|
3921
|
+
invalid = true;
|
|
3922
|
+
break;
|
|
3923
|
+
}
|
|
3797
3924
|
workspace = parts[index + 1] ?? "";
|
|
3798
3925
|
index += 1;
|
|
3926
|
+
continue;
|
|
3799
3927
|
} else if (parts[index] === "--name") {
|
|
3928
|
+
if (index + 1 >= parts.length) {
|
|
3929
|
+
invalid = true;
|
|
3930
|
+
break;
|
|
3931
|
+
}
|
|
3800
3932
|
transportSession = parts[index + 1] ?? "";
|
|
3801
3933
|
index += 1;
|
|
3934
|
+
continue;
|
|
3802
3935
|
}
|
|
3936
|
+
invalid = true;
|
|
3937
|
+
break;
|
|
3938
|
+
}
|
|
3939
|
+
if (!invalid && alias.trim().length > 0 && agent.trim().length > 0 && workspace.trim().length > 0 && transportSession.trim().length > 0) {
|
|
3940
|
+
return { kind: "session.attach", alias, agent, workspace, transportSession };
|
|
3803
3941
|
}
|
|
3804
|
-
return { kind: "session.attach", alias, agent, workspace, transportSession };
|
|
3805
3942
|
}
|
|
3806
3943
|
if (command.startsWith("/") && isRecognizedCommand(command)) {
|
|
3807
3944
|
return { kind: "invalid", text: trimmed, recognizedCommand: command };
|
|
@@ -3811,13 +3948,42 @@ function parseCommand(input) {
|
|
|
3811
3948
|
function hasAnyFlag(parts, flags) {
|
|
3812
3949
|
return parts.some((part) => flags.includes(part));
|
|
3813
3950
|
}
|
|
3814
|
-
function
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3951
|
+
function readSessionShortcutTarget(parts, startIndex) {
|
|
3952
|
+
let cwd = "";
|
|
3953
|
+
let workspace = "";
|
|
3954
|
+
let invalid = false;
|
|
3955
|
+
for (let index = startIndex;index < parts.length; index += 1) {
|
|
3956
|
+
if (parts[index] === "--cwd" || parts[index] === "-d") {
|
|
3957
|
+
if (index + 1 >= parts.length || workspace) {
|
|
3958
|
+
invalid = true;
|
|
3959
|
+
break;
|
|
3960
|
+
}
|
|
3961
|
+
cwd = parts[index + 1] ?? "";
|
|
3962
|
+
index += 1;
|
|
3963
|
+
continue;
|
|
3964
|
+
}
|
|
3965
|
+
if (parts[index] === "--ws" || parts[index] === "-ws") {
|
|
3966
|
+
if (index + 1 >= parts.length || cwd) {
|
|
3967
|
+
invalid = true;
|
|
3968
|
+
break;
|
|
3969
|
+
}
|
|
3970
|
+
workspace = parts[index + 1] ?? "";
|
|
3971
|
+
index += 1;
|
|
3972
|
+
continue;
|
|
3818
3973
|
}
|
|
3974
|
+
invalid = true;
|
|
3975
|
+
break;
|
|
3819
3976
|
}
|
|
3820
|
-
|
|
3977
|
+
if (invalid) {
|
|
3978
|
+
return null;
|
|
3979
|
+
}
|
|
3980
|
+
if (cwd.trim().length > 0) {
|
|
3981
|
+
return { cwd };
|
|
3982
|
+
}
|
|
3983
|
+
if (workspace.trim().length > 0) {
|
|
3984
|
+
return { workspace };
|
|
3985
|
+
}
|
|
3986
|
+
return null;
|
|
3821
3987
|
}
|
|
3822
3988
|
function normalizeCommand(command) {
|
|
3823
3989
|
if (command === "/ss")
|
|
@@ -3843,7 +4009,7 @@ function toPermissionMode(value) {
|
|
|
3843
4009
|
return null;
|
|
3844
4010
|
}
|
|
3845
4011
|
function toNonInteractivePermission(value) {
|
|
3846
|
-
if (value === "
|
|
4012
|
+
if (value === "deny" || value === "fail") {
|
|
3847
4013
|
return value;
|
|
3848
4014
|
}
|
|
3849
4015
|
return null;
|
|
@@ -3890,6 +4056,8 @@ var init_parse_command = __esm(() => {
|
|
|
3890
4056
|
"/cancel",
|
|
3891
4057
|
"/clear",
|
|
3892
4058
|
"/mode",
|
|
4059
|
+
"/replymode",
|
|
4060
|
+
"/config",
|
|
3893
4061
|
"/permission",
|
|
3894
4062
|
"/session",
|
|
3895
4063
|
"/workspace",
|
|
@@ -3898,431 +4066,1228 @@ var init_parse_command = __esm(() => {
|
|
|
3898
4066
|
]);
|
|
3899
4067
|
});
|
|
3900
4068
|
|
|
3901
|
-
// src/commands/
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
4069
|
+
// src/commands/config-clone.ts
|
|
4070
|
+
function cloneAppConfig(config) {
|
|
4071
|
+
return {
|
|
4072
|
+
transport: { ...config.transport },
|
|
4073
|
+
logging: { ...config.logging },
|
|
4074
|
+
wechat: { ...config.wechat },
|
|
4075
|
+
agents: Object.fromEntries(Object.entries(config.agents).map(([name, agent]) => [name, { ...agent }])),
|
|
4076
|
+
workspaces: Object.fromEntries(Object.entries(config.workspaces).map(([name, workspace]) => [name, { ...workspace }]))
|
|
4077
|
+
};
|
|
4078
|
+
}
|
|
3905
4079
|
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
constructor(sessions, transport, config, configStore, logger2, resolveSessionAgentCommand = resolveSessionAgentCommandFromIndex) {
|
|
3914
|
-
this.sessions = sessions;
|
|
3915
|
-
this.transport = transport;
|
|
3916
|
-
this.config = config;
|
|
3917
|
-
this.configStore = configStore;
|
|
3918
|
-
this.resolveSessionAgentCommand = resolveSessionAgentCommand;
|
|
3919
|
-
this.logger = logger2 ?? createNoopAppLogger();
|
|
4080
|
+
// src/commands/handlers/permission-handler.ts
|
|
4081
|
+
function handlePermissionStatus(context, title) {
|
|
4082
|
+
return { text: renderPermissionStatus(context.config, title) };
|
|
4083
|
+
}
|
|
4084
|
+
async function handlePermissionModeSet(context, mode) {
|
|
4085
|
+
if (!context.config || !context.configStore) {
|
|
4086
|
+
return { text: "当前没有加载可写入的配置。" };
|
|
3920
4087
|
}
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
|
|
3939
|
-
|
|
3940
|
-
|
|
4088
|
+
const previous = cloneAppConfig(context.config);
|
|
4089
|
+
const updated = await context.configStore.updateTransport({
|
|
4090
|
+
permissionMode: mode
|
|
4091
|
+
});
|
|
4092
|
+
try {
|
|
4093
|
+
await context.transport.updatePermissionPolicy?.(updated.transport);
|
|
4094
|
+
} catch (error) {
|
|
4095
|
+
await context.configStore.save(previous);
|
|
4096
|
+
context.replaceConfig(previous);
|
|
4097
|
+
throw error;
|
|
4098
|
+
}
|
|
4099
|
+
context.replaceConfig(updated);
|
|
4100
|
+
return { text: renderPermissionStatus(context.config, "权限模式已更新:") };
|
|
4101
|
+
}
|
|
4102
|
+
function handlePermissionAutoStatus(context, title) {
|
|
4103
|
+
return { text: renderPermissionStatus(context.config, title) };
|
|
4104
|
+
}
|
|
4105
|
+
async function handlePermissionAutoSet(context, policy) {
|
|
4106
|
+
if (!context.config || !context.configStore) {
|
|
4107
|
+
return { text: "当前没有加载可写入的配置。" };
|
|
4108
|
+
}
|
|
4109
|
+
const previous = cloneAppConfig(context.config);
|
|
4110
|
+
const updated = await context.configStore.updateTransport({
|
|
4111
|
+
nonInteractivePermissions: policy
|
|
4112
|
+
});
|
|
4113
|
+
try {
|
|
4114
|
+
await context.transport.updatePermissionPolicy?.(updated.transport);
|
|
4115
|
+
} catch (error) {
|
|
4116
|
+
await context.configStore.save(previous);
|
|
4117
|
+
context.replaceConfig(previous);
|
|
4118
|
+
throw error;
|
|
4119
|
+
}
|
|
4120
|
+
context.replaceConfig(updated);
|
|
4121
|
+
return { text: renderPermissionStatus(context.config, "非交互策略已更新:") };
|
|
4122
|
+
}
|
|
4123
|
+
function renderPermissionStatus(config, title) {
|
|
4124
|
+
const permissionMode = config?.transport.permissionMode ?? "approve-all";
|
|
4125
|
+
const nonInteractivePermissions = config?.transport.nonInteractivePermissions ?? "deny";
|
|
4126
|
+
return [title, `- mode: ${permissionMode}`, `- auto: ${nonInteractivePermissions}`].join(`
|
|
4127
|
+
`);
|
|
4128
|
+
}
|
|
4129
|
+
var permissionHelp;
|
|
4130
|
+
var init_permission_handler = __esm(() => {
|
|
4131
|
+
permissionHelp = {
|
|
4132
|
+
topic: "permission",
|
|
4133
|
+
aliases: ["pm"],
|
|
4134
|
+
summary: "查看和修改 transport 权限策略。",
|
|
4135
|
+
commands: [
|
|
4136
|
+
{ usage: "/pm 或 /permission", description: "查看当前权限模式" },
|
|
4137
|
+
{ usage: "/pm set <allow|read|deny>", description: "设置审批级别" },
|
|
4138
|
+
{ usage: "/pm auto", description: "查看当前非交互策略" },
|
|
4139
|
+
{ usage: "/pm auto <deny|fail>", description: "设置非交互策略" }
|
|
4140
|
+
],
|
|
4141
|
+
examples: ["/pm set read", "/pm auto deny"]
|
|
4142
|
+
};
|
|
4143
|
+
});
|
|
4144
|
+
|
|
4145
|
+
// src/commands/handlers/config-handler.ts
|
|
4146
|
+
function handleConfigShow(context) {
|
|
4147
|
+
const lines = ["支持修改的配置字段:", ...SUPPORTED_CONFIG_PATHS.map((path11) => `- ${path11}`)];
|
|
4148
|
+
if (context.config) {
|
|
4149
|
+
lines.push("", "示例:", "- /config set wechat.replyMode final", "- /config set logging.level debug");
|
|
4150
|
+
}
|
|
4151
|
+
return { text: lines.join(`
|
|
4152
|
+
`) };
|
|
4153
|
+
}
|
|
4154
|
+
async function handleConfigSet(context, path11, rawValue) {
|
|
4155
|
+
if (!context.config || !context.configStore) {
|
|
4156
|
+
return { text: "当前没有加载可写入的配置。" };
|
|
4157
|
+
}
|
|
4158
|
+
const previous = cloneAppConfig(context.config);
|
|
4159
|
+
const updated = cloneAppConfig(context.config);
|
|
4160
|
+
const result = applySupportedConfigUpdate(updated, path11, rawValue);
|
|
4161
|
+
if ("error" in result) {
|
|
4162
|
+
return { text: result.error };
|
|
4163
|
+
}
|
|
4164
|
+
await context.configStore.save(updated);
|
|
4165
|
+
if (path11 === "transport.permissionMode" || path11 === "transport.nonInteractivePermissions") {
|
|
4166
|
+
try {
|
|
4167
|
+
await context.transport.updatePermissionPolicy?.(updated.transport);
|
|
4168
|
+
} catch (error) {
|
|
4169
|
+
await context.configStore.save(previous);
|
|
4170
|
+
context.replaceConfig(previous);
|
|
4171
|
+
throw error;
|
|
4172
|
+
}
|
|
4173
|
+
}
|
|
4174
|
+
context.replaceConfig(updated);
|
|
4175
|
+
return { text: `配置已更新:${path11} = ${result.renderedValue}` };
|
|
4176
|
+
}
|
|
4177
|
+
function applySupportedConfigUpdate(config, path11, rawValue) {
|
|
4178
|
+
switch (path11) {
|
|
4179
|
+
case "transport.type": {
|
|
4180
|
+
const parsed = parseEnum(rawValue, ["acpx-cli", "acpx-bridge"]);
|
|
4181
|
+
if (!parsed)
|
|
4182
|
+
return { error: "transport.type 只支持:acpx-cli、acpx-bridge" };
|
|
4183
|
+
config.transport.type = parsed;
|
|
4184
|
+
return { renderedValue: parsed };
|
|
4185
|
+
}
|
|
4186
|
+
case "transport.command":
|
|
4187
|
+
if (!rawValue.trim())
|
|
4188
|
+
return { error: "transport.command 不能为空。" };
|
|
4189
|
+
config.transport.command = rawValue;
|
|
4190
|
+
return { renderedValue: rawValue };
|
|
4191
|
+
case "transport.sessionInitTimeoutMs": {
|
|
4192
|
+
const parsed = parsePositiveNumber(rawValue, "transport.sessionInitTimeoutMs");
|
|
4193
|
+
if ("error" in parsed)
|
|
4194
|
+
return parsed;
|
|
4195
|
+
config.transport.sessionInitTimeoutMs = parsed.value;
|
|
4196
|
+
return { renderedValue: String(parsed.value) };
|
|
4197
|
+
}
|
|
4198
|
+
case "transport.permissionMode": {
|
|
4199
|
+
const parsed = parseEnum(rawValue, ["approve-all", "approve-reads", "deny-all"]);
|
|
4200
|
+
if (!parsed)
|
|
4201
|
+
return { error: "transport.permissionMode 只支持:approve-all、approve-reads、deny-all" };
|
|
4202
|
+
config.transport.permissionMode = parsed;
|
|
4203
|
+
return { renderedValue: parsed };
|
|
4204
|
+
}
|
|
4205
|
+
case "transport.nonInteractivePermissions": {
|
|
4206
|
+
const parsed = parseEnum(rawValue, ["deny", "fail"]);
|
|
4207
|
+
if (!parsed)
|
|
4208
|
+
return { error: "transport.nonInteractivePermissions 只支持:deny、fail" };
|
|
4209
|
+
config.transport.nonInteractivePermissions = parsed;
|
|
4210
|
+
return { renderedValue: parsed };
|
|
4211
|
+
}
|
|
4212
|
+
case "logging.level": {
|
|
4213
|
+
const parsed = parseEnum(rawValue, ["error", "info", "debug"]);
|
|
4214
|
+
if (!parsed)
|
|
4215
|
+
return { error: "logging.level 只支持:error、info、debug" };
|
|
4216
|
+
config.logging.level = parsed;
|
|
4217
|
+
return { renderedValue: parsed };
|
|
4218
|
+
}
|
|
4219
|
+
case "logging.maxSizeBytes": {
|
|
4220
|
+
const parsed = parsePositiveNumber(rawValue, "logging.maxSizeBytes");
|
|
4221
|
+
if ("error" in parsed)
|
|
4222
|
+
return parsed;
|
|
4223
|
+
config.logging.maxSizeBytes = parsed.value;
|
|
4224
|
+
return { renderedValue: String(parsed.value) };
|
|
4225
|
+
}
|
|
4226
|
+
case "logging.maxFiles": {
|
|
4227
|
+
const parsed = parsePositiveNumber(rawValue, "logging.maxFiles");
|
|
4228
|
+
if ("error" in parsed)
|
|
4229
|
+
return parsed;
|
|
4230
|
+
config.logging.maxFiles = parsed.value;
|
|
4231
|
+
return { renderedValue: String(parsed.value) };
|
|
4232
|
+
}
|
|
4233
|
+
case "logging.retentionDays": {
|
|
4234
|
+
const parsed = parsePositiveNumber(rawValue, "logging.retentionDays");
|
|
4235
|
+
if ("error" in parsed)
|
|
4236
|
+
return parsed;
|
|
4237
|
+
config.logging.retentionDays = parsed.value;
|
|
4238
|
+
return { renderedValue: String(parsed.value) };
|
|
4239
|
+
}
|
|
4240
|
+
case "wechat.replyMode": {
|
|
4241
|
+
const parsed = parseEnum(rawValue, ["stream", "final"]);
|
|
4242
|
+
if (!parsed)
|
|
4243
|
+
return { error: "wechat.replyMode 只支持:stream、final" };
|
|
4244
|
+
config.wechat.replyMode = parsed;
|
|
4245
|
+
return { renderedValue: parsed };
|
|
4246
|
+
}
|
|
4247
|
+
}
|
|
4248
|
+
const agentMatch = path11.match(/^agents\.([^.]+)\.(driver|command)$/);
|
|
4249
|
+
if (agentMatch) {
|
|
4250
|
+
const [, name, field] = agentMatch;
|
|
4251
|
+
if (!name || !field) {
|
|
4252
|
+
return { error: `不支持修改这个配置路径:${path11}` };
|
|
4253
|
+
}
|
|
4254
|
+
const agent = config.agents[name];
|
|
4255
|
+
if (!agent) {
|
|
4256
|
+
return { error: `Agent「${name}」不存在,请先创建。` };
|
|
4257
|
+
}
|
|
4258
|
+
if (!rawValue.trim()) {
|
|
4259
|
+
return { error: `${path11} 不能为空。` };
|
|
4260
|
+
}
|
|
4261
|
+
if (field === "driver") {
|
|
4262
|
+
agent.driver = rawValue;
|
|
4263
|
+
} else {
|
|
4264
|
+
agent.command = rawValue;
|
|
4265
|
+
}
|
|
4266
|
+
return { renderedValue: rawValue };
|
|
4267
|
+
}
|
|
4268
|
+
const workspaceMatch = path11.match(/^workspaces\.([^.]+)\.(cwd|description)$/);
|
|
4269
|
+
if (workspaceMatch) {
|
|
4270
|
+
const [, name, field] = workspaceMatch;
|
|
4271
|
+
if (!name || !field) {
|
|
4272
|
+
return { error: `不支持修改这个配置路径:${path11}` };
|
|
4273
|
+
}
|
|
4274
|
+
const workspace = config.workspaces[name];
|
|
4275
|
+
if (!workspace) {
|
|
4276
|
+
return { error: `工作区「${name}」不存在,请先创建。` };
|
|
4277
|
+
}
|
|
4278
|
+
if (!rawValue.trim()) {
|
|
4279
|
+
return { error: `${path11} 不能为空。` };
|
|
4280
|
+
}
|
|
4281
|
+
if (field === "cwd") {
|
|
4282
|
+
workspace.cwd = rawValue;
|
|
4283
|
+
} else {
|
|
4284
|
+
workspace.description = rawValue;
|
|
4285
|
+
}
|
|
4286
|
+
return { renderedValue: rawValue };
|
|
4287
|
+
}
|
|
4288
|
+
return { error: `不支持修改这个配置路径:${path11}` };
|
|
4289
|
+
}
|
|
4290
|
+
function parseEnum(value, allowed) {
|
|
4291
|
+
return allowed.includes(value) ? value : null;
|
|
4292
|
+
}
|
|
4293
|
+
function parsePositiveNumber(rawValue, path11) {
|
|
4294
|
+
const value = Number(rawValue);
|
|
4295
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
4296
|
+
return { error: `${path11} 必须是正数。` };
|
|
4297
|
+
}
|
|
4298
|
+
return { value };
|
|
4299
|
+
}
|
|
4300
|
+
var SUPPORTED_CONFIG_PATHS, configHelp;
|
|
4301
|
+
var init_config_handler = __esm(() => {
|
|
4302
|
+
SUPPORTED_CONFIG_PATHS = [
|
|
4303
|
+
"transport.type",
|
|
4304
|
+
"transport.command",
|
|
4305
|
+
"transport.sessionInitTimeoutMs",
|
|
4306
|
+
"transport.permissionMode",
|
|
4307
|
+
"transport.nonInteractivePermissions",
|
|
4308
|
+
"logging.level",
|
|
4309
|
+
"logging.maxSizeBytes",
|
|
4310
|
+
"logging.maxFiles",
|
|
4311
|
+
"logging.retentionDays",
|
|
4312
|
+
"wechat.replyMode",
|
|
4313
|
+
"agents.<name>.driver",
|
|
4314
|
+
"agents.<name>.command",
|
|
4315
|
+
"workspaces.<name>.cwd",
|
|
4316
|
+
"workspaces.<name>.description"
|
|
4317
|
+
];
|
|
4318
|
+
configHelp = {
|
|
4319
|
+
topic: "config",
|
|
4320
|
+
aliases: [],
|
|
4321
|
+
summary: "查看和修改受支持的配置字段。",
|
|
4322
|
+
commands: [
|
|
4323
|
+
{ usage: "/config", description: "查看当前支持修改的配置路径" },
|
|
4324
|
+
{ usage: "/config set <path> <value>", description: "修改一个受支持的配置值" }
|
|
4325
|
+
],
|
|
4326
|
+
examples: ["/config set wechat.replyMode final", "/config set logging.level debug"]
|
|
4327
|
+
};
|
|
4328
|
+
});
|
|
4329
|
+
|
|
4330
|
+
// src/commands/handlers/session-handler.ts
|
|
4331
|
+
async function handleSessions(context, chatKey) {
|
|
4332
|
+
const sessions = await context.sessions.listSessions(chatKey);
|
|
4333
|
+
if (sessions.length === 0) {
|
|
4334
|
+
return { text: "还没有会话。请先执行 /session new <alias> --agent <name> --ws <name>。" };
|
|
4335
|
+
}
|
|
4336
|
+
return {
|
|
4337
|
+
text: [
|
|
4338
|
+
"会话列表:",
|
|
4339
|
+
...sessions.map((session) => `- ${session.alias} (${session.agent} @ ${session.workspace})${session.isCurrent ? " [当前]" : ""}`)
|
|
4340
|
+
].join(`
|
|
3941
4341
|
`)
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
permissionMode: command.mode
|
|
3978
|
-
});
|
|
3979
|
-
this.replaceConfig(updated);
|
|
3980
|
-
return { text: this.renderPermissionStatus("权限模式已更新:") };
|
|
3981
|
-
}
|
|
3982
|
-
case "permission.auto.status":
|
|
3983
|
-
return { text: this.renderPermissionStatus("当前非交互策略:") };
|
|
3984
|
-
case "permission.auto.set": {
|
|
3985
|
-
if (!this.config || !this.configStore) {
|
|
3986
|
-
return { text: "当前没有加载可写入的配置。" };
|
|
3987
|
-
}
|
|
3988
|
-
const updated = await this.configStore.updateTransport({
|
|
3989
|
-
nonInteractivePermissions: command.policy
|
|
3990
|
-
});
|
|
3991
|
-
this.replaceConfig(updated);
|
|
3992
|
-
return { text: this.renderPermissionStatus("非交互策略已更新:") };
|
|
3993
|
-
}
|
|
3994
|
-
case "workspaces":
|
|
3995
|
-
return { text: this.config ? renderWorkspaces(this.config) : "No config loaded." };
|
|
3996
|
-
case "workspace.new": {
|
|
3997
|
-
if (!this.config || !this.configStore) {
|
|
3998
|
-
return { text: "当前没有加载可写入的配置。" };
|
|
3999
|
-
}
|
|
4000
|
-
const wsCwd = normalizePathForWorkspace(command.cwd);
|
|
4001
|
-
if (!await pathExists(wsCwd)) {
|
|
4002
|
-
return { text: `工作区路径不存在:${command.cwd}` };
|
|
4003
|
-
}
|
|
4004
|
-
const updated = await this.configStore.upsertWorkspace(command.name, wsCwd);
|
|
4005
|
-
this.replaceConfig(updated);
|
|
4006
|
-
return { text: `工作区「${command.name}」已保存` };
|
|
4007
|
-
}
|
|
4008
|
-
case "workspace.rm": {
|
|
4009
|
-
if (!this.config || !this.configStore) {
|
|
4010
|
-
return { text: "当前没有加载可写入的配置。" };
|
|
4011
|
-
}
|
|
4012
|
-
const updated = await this.configStore.removeWorkspace(command.name);
|
|
4013
|
-
this.replaceConfig(updated);
|
|
4014
|
-
return { text: `工作区「${command.name}」已删除` };
|
|
4015
|
-
}
|
|
4016
|
-
case "sessions": {
|
|
4017
|
-
const sessions = await this.sessions.listSessions(chatKey);
|
|
4018
|
-
if (sessions.length === 0) {
|
|
4019
|
-
return { text: "还没有会话。请先执行 /session new <alias> --agent <name> --ws <name>。" };
|
|
4020
|
-
}
|
|
4021
|
-
return {
|
|
4022
|
-
text: [
|
|
4023
|
-
"会话列表:",
|
|
4024
|
-
...sessions.map((session) => `- ${session.alias} (${session.agent} @ ${session.workspace})${session.isCurrent ? " [当前]" : ""}`)
|
|
4025
|
-
].join(`
|
|
4342
|
+
};
|
|
4343
|
+
}
|
|
4344
|
+
async function handleSessionNew(context, chatKey, alias, agent, workspace) {
|
|
4345
|
+
const session = context.lifecycle.resolveSession(alias, agent, workspace, `${workspace}:${alias}`);
|
|
4346
|
+
try {
|
|
4347
|
+
await context.lifecycle.ensureTransportSession(session);
|
|
4348
|
+
const exists = await context.lifecycle.checkTransportSession(session);
|
|
4349
|
+
if (!exists) {
|
|
4350
|
+
return context.recovery.renderSessionCreationVerificationError(session);
|
|
4351
|
+
}
|
|
4352
|
+
} catch (error) {
|
|
4353
|
+
return context.recovery.renderSessionCreationError(session, error);
|
|
4354
|
+
}
|
|
4355
|
+
await context.sessions.attachSession(alias, agent, workspace, session.transportSession);
|
|
4356
|
+
await context.lifecycle.refreshSessionTransportAgentCommand(alias);
|
|
4357
|
+
await context.sessions.useSession(chatKey, alias);
|
|
4358
|
+
await context.logger.info("session.created", "created and selected logical session", {
|
|
4359
|
+
alias,
|
|
4360
|
+
agent,
|
|
4361
|
+
workspace
|
|
4362
|
+
});
|
|
4363
|
+
return { text: `会话「${alias}」已创建并切换` };
|
|
4364
|
+
}
|
|
4365
|
+
async function handleSessionShortcut(context, chatKey, agent, target, createNew) {
|
|
4366
|
+
return await context.lifecycle.handleSessionShortcut(chatKey, agent, target, createNew);
|
|
4367
|
+
}
|
|
4368
|
+
async function handleSessionAttach(context, chatKey, alias, agent, workspace, transportSession) {
|
|
4369
|
+
const attached = context.lifecycle.resolveSession(alias, agent, workspace, transportSession);
|
|
4370
|
+
const exists = await context.lifecycle.checkTransportSession(attached);
|
|
4371
|
+
if (!exists) {
|
|
4372
|
+
return {
|
|
4373
|
+
text: [
|
|
4374
|
+
"没有找到可绑定的已有会话。",
|
|
4375
|
+
`请确认会话名是否正确,然后重新执行:/session attach ${alias} --agent ${agent} --ws ${workspace} --name <会话名>`
|
|
4376
|
+
].join(`
|
|
4026
4377
|
`)
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
text: [
|
|
4060
|
-
"没有找到可绑定的已有会话。",
|
|
4061
|
-
`请确认会话名是否正确,然后重新执行:/session attach ${command.alias} --agent ${command.agent} --ws ${command.workspace} --name <会话名>`
|
|
4062
|
-
].join(`
|
|
4378
|
+
};
|
|
4379
|
+
}
|
|
4380
|
+
await context.sessions.attachSession(alias, agent, workspace, transportSession);
|
|
4381
|
+
await context.lifecycle.refreshSessionTransportAgentCommand(alias);
|
|
4382
|
+
await context.sessions.useSession(chatKey, alias);
|
|
4383
|
+
await context.logger.info("session.attached", "attached existing transport session", {
|
|
4384
|
+
alias,
|
|
4385
|
+
agent,
|
|
4386
|
+
workspace,
|
|
4387
|
+
transportSession
|
|
4388
|
+
});
|
|
4389
|
+
return { text: `会话「${alias}」已绑定并切换` };
|
|
4390
|
+
}
|
|
4391
|
+
async function handleSessionUse(context, chatKey, alias) {
|
|
4392
|
+
await context.sessions.useSession(chatKey, alias);
|
|
4393
|
+
await context.logger.info("session.selected", "selected logical session", {
|
|
4394
|
+
alias,
|
|
4395
|
+
chatKey
|
|
4396
|
+
});
|
|
4397
|
+
return { text: `已切换到会话「${alias}」` };
|
|
4398
|
+
}
|
|
4399
|
+
async function handleModeShow(context, chatKey) {
|
|
4400
|
+
const session = await context.sessions.getCurrentSession(chatKey);
|
|
4401
|
+
if (!session) {
|
|
4402
|
+
return { text: NO_CURRENT_SESSION_TEXT };
|
|
4403
|
+
}
|
|
4404
|
+
return {
|
|
4405
|
+
text: [
|
|
4406
|
+
"当前 mode:",
|
|
4407
|
+
`- 会话:${session.alias}`,
|
|
4408
|
+
`- mode:${session.modeId ?? "未设置"}`
|
|
4409
|
+
].join(`
|
|
4063
4410
|
`)
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
"当前 mode:",
|
|
4092
|
-
`- 会话:${session.alias}`,
|
|
4093
|
-
`- mode:${session.modeId ?? "未设置"}`
|
|
4094
|
-
].join(`
|
|
4411
|
+
};
|
|
4412
|
+
}
|
|
4413
|
+
async function handleModeSet(context, chatKey, modeId) {
|
|
4414
|
+
const session = await context.sessions.getCurrentSession(chatKey);
|
|
4415
|
+
if (!session) {
|
|
4416
|
+
return { text: NO_CURRENT_SESSION_TEXT };
|
|
4417
|
+
}
|
|
4418
|
+
await context.interaction.setModeTransportSession(session, modeId);
|
|
4419
|
+
await context.sessions.setCurrentSessionMode(chatKey, modeId);
|
|
4420
|
+
return { text: `已设置当前会话 mode:${modeId}` };
|
|
4421
|
+
}
|
|
4422
|
+
async function handleReplyModeShow(context, chatKey) {
|
|
4423
|
+
const session = await context.sessions.getCurrentSession(chatKey);
|
|
4424
|
+
if (!session) {
|
|
4425
|
+
return { text: NO_CURRENT_SESSION_TEXT };
|
|
4426
|
+
}
|
|
4427
|
+
const globalDefault = context.config?.wechat.replyMode ?? "stream";
|
|
4428
|
+
const sessionOverride = session.replyMode;
|
|
4429
|
+
const effective = sessionOverride ?? globalDefault;
|
|
4430
|
+
return {
|
|
4431
|
+
text: [
|
|
4432
|
+
"当前 reply mode:",
|
|
4433
|
+
`- 会话:${session.alias}`,
|
|
4434
|
+
`- 全局默认:${globalDefault}`,
|
|
4435
|
+
`- 当前会话覆盖:${sessionOverride ?? "未设置"}`,
|
|
4436
|
+
`- 当前生效:${effective}`
|
|
4437
|
+
].join(`
|
|
4095
4438
|
`)
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4102
|
-
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
|
|
4439
|
+
};
|
|
4440
|
+
}
|
|
4441
|
+
async function handleReplyModeSet(context, chatKey, replyMode) {
|
|
4442
|
+
const session = await context.sessions.getCurrentSession(chatKey);
|
|
4443
|
+
if (!session) {
|
|
4444
|
+
return { text: NO_CURRENT_SESSION_TEXT };
|
|
4445
|
+
}
|
|
4446
|
+
await context.sessions.setCurrentSessionReplyMode(chatKey, replyMode);
|
|
4447
|
+
return { text: `已设置当前会话 reply mode:${replyMode}` };
|
|
4448
|
+
}
|
|
4449
|
+
async function handleReplyModeReset(context, chatKey) {
|
|
4450
|
+
const session = await context.sessions.getCurrentSession(chatKey);
|
|
4451
|
+
if (!session) {
|
|
4452
|
+
return { text: NO_CURRENT_SESSION_TEXT };
|
|
4453
|
+
}
|
|
4454
|
+
await context.sessions.setCurrentSessionReplyMode(chatKey, undefined);
|
|
4455
|
+
const globalDefault = context.config?.wechat.replyMode ?? "stream";
|
|
4456
|
+
return { text: `已重置当前会话 reply mode,当前回退到全局默认:${globalDefault}` };
|
|
4457
|
+
}
|
|
4458
|
+
async function handleStatus(context, chatKey) {
|
|
4459
|
+
const session = await context.sessions.getCurrentSession(chatKey);
|
|
4460
|
+
if (!session) {
|
|
4461
|
+
return { text: NO_CURRENT_SESSION_TEXT };
|
|
4462
|
+
}
|
|
4463
|
+
return {
|
|
4464
|
+
text: [
|
|
4465
|
+
"当前会话:",
|
|
4466
|
+
`- 名称:${session.alias}`,
|
|
4467
|
+
`- Agent:${session.agent}`,
|
|
4468
|
+
`- 工作区:${session.workspace}`
|
|
4469
|
+
].join(`
|
|
4119
4470
|
`)
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4471
|
+
};
|
|
4472
|
+
}
|
|
4473
|
+
async function handleCancel(context, chatKey) {
|
|
4474
|
+
const session = await context.sessions.getCurrentSession(chatKey);
|
|
4475
|
+
if (!session) {
|
|
4476
|
+
return { text: NO_CURRENT_SESSION_TEXT };
|
|
4477
|
+
}
|
|
4478
|
+
try {
|
|
4479
|
+
const result = await context.interaction.cancelTransportSession(session);
|
|
4480
|
+
return { text: result.message || "cancelled" };
|
|
4481
|
+
} catch (error) {
|
|
4482
|
+
return context.recovery.renderTransportError(session, error);
|
|
4483
|
+
}
|
|
4484
|
+
}
|
|
4485
|
+
async function handleSessionReset(context, chatKey) {
|
|
4486
|
+
return await context.lifecycle.resetCurrentSession(chatKey);
|
|
4487
|
+
}
|
|
4488
|
+
async function handlePrompt(context, chatKey, text, reply) {
|
|
4489
|
+
const session = await context.sessions.getCurrentSession(chatKey);
|
|
4490
|
+
if (!session) {
|
|
4491
|
+
return { text: NO_CURRENT_SESSION_TEXT };
|
|
4492
|
+
}
|
|
4493
|
+
try {
|
|
4494
|
+
const effectiveReplyMode = session.replyMode ?? context.config?.wechat.replyMode ?? "stream";
|
|
4495
|
+
const transportReply = effectiveReplyMode === "stream" ? reply : undefined;
|
|
4496
|
+
const result = await context.interaction.promptTransportSession(session, text, transportReply);
|
|
4497
|
+
return { text: result.text };
|
|
4498
|
+
} catch (error) {
|
|
4499
|
+
const recovered = await context.recovery.tryRecoverMissingSession(session, error);
|
|
4500
|
+
if (recovered) {
|
|
4501
|
+
const effectiveReplyMode = recovered.replyMode ?? context.config?.wechat.replyMode ?? "stream";
|
|
4502
|
+
const transportReply = effectiveReplyMode === "stream" ? reply : undefined;
|
|
4503
|
+
const result = await context.interaction.promptTransportSession(recovered, text, transportReply);
|
|
4504
|
+
return { text: result.text };
|
|
4505
|
+
}
|
|
4506
|
+
return context.recovery.renderTransportError(session, error);
|
|
4507
|
+
}
|
|
4508
|
+
}
|
|
4509
|
+
var NO_CURRENT_SESSION_TEXT = "当前还没有选中的会话。请先执行 /session new ... 或 /use <alias>。", sessionHelp, modeHelp, replyModeHelp, statusHelp, cancelHelp;
|
|
4510
|
+
var init_session_handler = __esm(() => {
|
|
4511
|
+
sessionHelp = {
|
|
4512
|
+
topic: "session",
|
|
4513
|
+
aliases: ["ss", "sessions"],
|
|
4514
|
+
summary: "创建、恢复、切换和重置逻辑会话。",
|
|
4515
|
+
commands: [
|
|
4516
|
+
{ usage: "/sessions", description: "查看当前会话列表" },
|
|
4517
|
+
{ usage: "/session 或 /ss", description: "查看会话列表" },
|
|
4518
|
+
{ usage: "/ss <agent> (-d <path> | --ws <name>)", description: "快速新建或复用一个会话" },
|
|
4519
|
+
{ usage: "/ss new <agent> (-d <path> | --ws <name>)", description: "强制新建会话" },
|
|
4520
|
+
{ usage: "/ss new <alias> -a <name> --ws <name>", description: "按指定配置新建会话" },
|
|
4521
|
+
{ usage: "/ss attach <alias> -a <name> --ws <name> --name <transport-session>", description: "绑定已有会话" },
|
|
4522
|
+
{ usage: "/use <alias>", description: "切换当前会话" },
|
|
4523
|
+
{ usage: "/session reset 或 /clear", description: "重置当前会话上下文" }
|
|
4524
|
+
],
|
|
4525
|
+
examples: ["/ss codex -d /absolute/path/to/repo", "/use backend-fix", "/session reset"]
|
|
4526
|
+
};
|
|
4527
|
+
modeHelp = {
|
|
4528
|
+
topic: "mode",
|
|
4529
|
+
aliases: [],
|
|
4530
|
+
summary: "查看或设置当前会话 mode。",
|
|
4531
|
+
commands: [
|
|
4532
|
+
{ usage: "/mode", description: "查看当前会话已保存的 mode" },
|
|
4533
|
+
{ usage: "/mode <id>", description: "设置当前会话 mode" }
|
|
4534
|
+
],
|
|
4535
|
+
examples: ["/mode", "/mode plan"]
|
|
4536
|
+
};
|
|
4537
|
+
replyModeHelp = {
|
|
4538
|
+
topic: "replymode",
|
|
4539
|
+
aliases: [],
|
|
4540
|
+
summary: "查看或设置当前逻辑会话的回复输出模式。",
|
|
4541
|
+
commands: [
|
|
4542
|
+
{ usage: "/replymode", description: "查看全局默认、当前覆盖和实际生效值" },
|
|
4543
|
+
{ usage: "/replymode stream", description: "当前会话使用流式回复" },
|
|
4544
|
+
{ usage: "/replymode final", description: "当前会话只发送最终文本" },
|
|
4545
|
+
{ usage: "/replymode reset", description: "清除当前会话覆盖并回退到全局默认" }
|
|
4546
|
+
],
|
|
4547
|
+
examples: ["/replymode", "/replymode final"]
|
|
4548
|
+
};
|
|
4549
|
+
statusHelp = {
|
|
4550
|
+
topic: "status",
|
|
4551
|
+
aliases: [],
|
|
4552
|
+
summary: "查看当前选中会话的状态。",
|
|
4553
|
+
commands: [{ usage: "/status", description: "查看当前会话状态" }],
|
|
4554
|
+
examples: ["/status"]
|
|
4555
|
+
};
|
|
4556
|
+
cancelHelp = {
|
|
4557
|
+
topic: "cancel",
|
|
4558
|
+
aliases: ["stop"],
|
|
4559
|
+
summary: "取消当前会话里正在执行的任务。",
|
|
4560
|
+
commands: [
|
|
4561
|
+
{ usage: "/cancel", description: "取消当前任务" },
|
|
4562
|
+
{ usage: "/stop", description: "取消当前任务(/cancel 别名)" }
|
|
4563
|
+
],
|
|
4564
|
+
examples: ["/cancel"]
|
|
4565
|
+
};
|
|
4566
|
+
});
|
|
4567
|
+
|
|
4568
|
+
// src/commands/transport-diagnostics.ts
|
|
4569
|
+
function summarizeTransportError(message) {
|
|
4570
|
+
return message.replace(/\s+/g, " ").trim().slice(0, 200);
|
|
4571
|
+
}
|
|
4572
|
+
function summarizeTransportDiagnostic(output) {
|
|
4573
|
+
const trimmed = output.replace(/\s+/g, " ").trim();
|
|
4574
|
+
if (trimmed.length === 0) {
|
|
4575
|
+
return;
|
|
4576
|
+
}
|
|
4577
|
+
return trimmed.slice(0, 200);
|
|
4578
|
+
}
|
|
4579
|
+
function summarizeTransportDiagnosticTail(output) {
|
|
4580
|
+
const trimmed = output.replace(/\s+/g, " ").trim();
|
|
4581
|
+
if (trimmed.length === 0) {
|
|
4582
|
+
return;
|
|
4583
|
+
}
|
|
4584
|
+
return trimmed.slice(-200);
|
|
4585
|
+
}
|
|
4586
|
+
function summarizeTransportNdjson(output, prefix) {
|
|
4587
|
+
const lines = output.split(`
|
|
4588
|
+
`).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
4589
|
+
if (lines.length === 0) {
|
|
4590
|
+
return {};
|
|
4591
|
+
}
|
|
4592
|
+
const methods = new Set;
|
|
4593
|
+
let agentMessageChunkCount = 0;
|
|
4594
|
+
let stopReason;
|
|
4595
|
+
for (const line of lines) {
|
|
4596
|
+
try {
|
|
4597
|
+
const payload = JSON.parse(line);
|
|
4598
|
+
if (typeof payload.method === "string" && payload.method.length > 0) {
|
|
4599
|
+
methods.add(payload.method);
|
|
4153
4600
|
}
|
|
4154
|
-
|
|
4601
|
+
if (payload.params?.update?.sessionUpdate === "agent_message_chunk") {
|
|
4602
|
+
agentMessageChunkCount += 1;
|
|
4603
|
+
}
|
|
4604
|
+
if (typeof payload.result?.stopReason === "string" && payload.result.stopReason.length > 0) {
|
|
4605
|
+
stopReason = payload.result.stopReason;
|
|
4606
|
+
}
|
|
4607
|
+
} catch {
|
|
4608
|
+
continue;
|
|
4609
|
+
}
|
|
4155
4610
|
}
|
|
4156
|
-
|
|
4157
|
-
|
|
4611
|
+
const summary = {
|
|
4612
|
+
[`${prefix}LineCount`]: lines.length
|
|
4613
|
+
};
|
|
4614
|
+
if (methods.size > 0) {
|
|
4615
|
+
summary[`${prefix}Methods`] = [...methods].join(",");
|
|
4158
4616
|
}
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4617
|
+
if (agentMessageChunkCount > 0) {
|
|
4618
|
+
summary[`${prefix}AgentMessageChunkCount`] = agentMessageChunkCount;
|
|
4619
|
+
}
|
|
4620
|
+
if (stopReason) {
|
|
4621
|
+
summary[`${prefix}StopReason`] = stopReason;
|
|
4622
|
+
}
|
|
4623
|
+
return summary;
|
|
4624
|
+
}
|
|
4625
|
+
function isPartialPromptOutputError(message) {
|
|
4626
|
+
return message.includes("未收到最终回复");
|
|
4627
|
+
}
|
|
4628
|
+
|
|
4629
|
+
// src/formatting/render-text.ts
|
|
4630
|
+
function renderAgents(config) {
|
|
4631
|
+
const names = Object.keys(config.agents);
|
|
4632
|
+
if (names.length === 0) {
|
|
4633
|
+
return "还没有注册任何 Agent。";
|
|
4634
|
+
}
|
|
4635
|
+
return ["已注册的 Agent:", ...names.map((name) => `- ${name}`)].join(`
|
|
4636
|
+
`);
|
|
4637
|
+
}
|
|
4638
|
+
function renderWorkspaces(config) {
|
|
4639
|
+
const names = Object.entries(config.workspaces);
|
|
4640
|
+
if (names.length === 0) {
|
|
4641
|
+
return "还没有注册任何工作区。";
|
|
4642
|
+
}
|
|
4643
|
+
return ["已注册的工作区:", ...names.map(([name, workspace]) => `- ${name}: ${workspace.cwd}`)].join(`
|
|
4644
|
+
`);
|
|
4645
|
+
}
|
|
4646
|
+
|
|
4647
|
+
// src/config/agent-templates.ts
|
|
4648
|
+
function getAgentTemplate(name) {
|
|
4649
|
+
const template = TEMPLATES[name];
|
|
4650
|
+
if (!template) {
|
|
4651
|
+
return null;
|
|
4652
|
+
}
|
|
4653
|
+
return {
|
|
4654
|
+
...template
|
|
4655
|
+
};
|
|
4656
|
+
}
|
|
4657
|
+
function listAgentTemplates() {
|
|
4658
|
+
return Object.keys(TEMPLATES);
|
|
4659
|
+
}
|
|
4660
|
+
var TEMPLATES;
|
|
4661
|
+
var init_agent_templates = __esm(() => {
|
|
4662
|
+
TEMPLATES = {
|
|
4663
|
+
codex: {
|
|
4664
|
+
driver: "codex"
|
|
4665
|
+
},
|
|
4666
|
+
claude: {
|
|
4667
|
+
driver: "claude"
|
|
4162
4668
|
}
|
|
4163
|
-
|
|
4164
|
-
|
|
4165
|
-
|
|
4669
|
+
};
|
|
4670
|
+
});
|
|
4671
|
+
|
|
4672
|
+
// src/commands/handlers/agent-handler.ts
|
|
4673
|
+
function handleAgents(context) {
|
|
4674
|
+
return { text: context.config ? renderAgents(context.config) : "No config loaded." };
|
|
4675
|
+
}
|
|
4676
|
+
async function handleAgentAdd(context, templateName) {
|
|
4677
|
+
if (!context.config || !context.configStore) {
|
|
4678
|
+
return { text: "当前没有加载可写入的配置。" };
|
|
4679
|
+
}
|
|
4680
|
+
const template = getAgentTemplate(templateName);
|
|
4681
|
+
if (!template) {
|
|
4682
|
+
return { text: `暂不支持这个 Agent 模板。当前可用:${listAgentTemplates().join("、")}` };
|
|
4683
|
+
}
|
|
4684
|
+
const updated = await context.configStore.upsertAgent(templateName, template);
|
|
4685
|
+
context.replaceConfig(updated);
|
|
4686
|
+
return { text: `Agent「${templateName}」已保存` };
|
|
4687
|
+
}
|
|
4688
|
+
async function handleAgentRemove(context, agentName) {
|
|
4689
|
+
if (!context.config || !context.configStore) {
|
|
4690
|
+
return { text: "当前没有加载可写入的配置。" };
|
|
4691
|
+
}
|
|
4692
|
+
if (!context.config.agents[agentName]) {
|
|
4693
|
+
return { text: "没有找到这个 Agent。" };
|
|
4694
|
+
}
|
|
4695
|
+
const updated = await context.configStore.removeAgent(agentName);
|
|
4696
|
+
context.replaceConfig(updated);
|
|
4697
|
+
return { text: `Agent「${agentName}」已删除` };
|
|
4698
|
+
}
|
|
4699
|
+
var agentHelp;
|
|
4700
|
+
var init_agent_handler = __esm(() => {
|
|
4701
|
+
init_agent_templates();
|
|
4702
|
+
agentHelp = {
|
|
4703
|
+
topic: "agent",
|
|
4704
|
+
aliases: ["agents"],
|
|
4705
|
+
summary: "管理已注册的 Agent。",
|
|
4706
|
+
commands: [
|
|
4707
|
+
{ usage: "/agents", description: "查看当前已注册的 Agent" },
|
|
4708
|
+
{ usage: "/agent add <codex|claude>", description: "添加内置 Agent 模板" },
|
|
4709
|
+
{ usage: "/agent rm <name>", description: "删除一个 Agent" }
|
|
4710
|
+
],
|
|
4711
|
+
examples: ["/agent add claude", "/agent rm codex"]
|
|
4712
|
+
};
|
|
4713
|
+
});
|
|
4714
|
+
|
|
4715
|
+
// src/commands/handlers/workspace-handler.ts
|
|
4716
|
+
import { access } from "node:fs/promises";
|
|
4717
|
+
import { homedir as homedir3 } from "node:os";
|
|
4718
|
+
import { normalize } from "node:path";
|
|
4719
|
+
function handleWorkspaces(context) {
|
|
4720
|
+
return { text: context.config ? renderWorkspaces(context.config) : "No config loaded." };
|
|
4721
|
+
}
|
|
4722
|
+
async function handleWorkspaceCreate(context, workspaceName, cwd) {
|
|
4723
|
+
if (!context.config || !context.configStore) {
|
|
4724
|
+
return { text: "当前没有加载可写入的配置。" };
|
|
4725
|
+
}
|
|
4726
|
+
const normalizedCwd = normalizePathForWorkspace(cwd);
|
|
4727
|
+
if (!await pathExists(normalizedCwd)) {
|
|
4728
|
+
return { text: `工作区路径不存在:${cwd}` };
|
|
4729
|
+
}
|
|
4730
|
+
const updated = await context.configStore.upsertWorkspace(workspaceName, normalizedCwd);
|
|
4731
|
+
context.replaceConfig(updated);
|
|
4732
|
+
return { text: `工作区「${workspaceName}」已保存` };
|
|
4733
|
+
}
|
|
4734
|
+
async function handleWorkspaceRemove(context, workspaceName) {
|
|
4735
|
+
if (!context.config || !context.configStore) {
|
|
4736
|
+
return { text: "当前没有加载可写入的配置。" };
|
|
4737
|
+
}
|
|
4738
|
+
const updated = await context.configStore.removeWorkspace(workspaceName);
|
|
4739
|
+
context.replaceConfig(updated);
|
|
4740
|
+
return { text: `工作区「${workspaceName}」已删除` };
|
|
4741
|
+
}
|
|
4742
|
+
async function pathExists(path11) {
|
|
4743
|
+
try {
|
|
4744
|
+
await access(path11);
|
|
4745
|
+
return true;
|
|
4746
|
+
} catch {
|
|
4747
|
+
return false;
|
|
4748
|
+
}
|
|
4749
|
+
}
|
|
4750
|
+
function normalizePathForWorkspace(path11) {
|
|
4751
|
+
const expanded = path11.startsWith("~") ? homedir3() + path11.slice(1) : path11;
|
|
4752
|
+
return normalize(expanded);
|
|
4753
|
+
}
|
|
4754
|
+
var workspaceHelp;
|
|
4755
|
+
var init_workspace_handler = __esm(() => {
|
|
4756
|
+
workspaceHelp = {
|
|
4757
|
+
topic: "workspace",
|
|
4758
|
+
aliases: ["ws", "workspaces"],
|
|
4759
|
+
summary: "管理已注册的工作区。",
|
|
4760
|
+
commands: [
|
|
4761
|
+
{ usage: "/workspaces", description: "查看当前已注册的工作区" },
|
|
4762
|
+
{ usage: "/workspace 或 /ws", description: "查看工作区列表" },
|
|
4763
|
+
{ usage: "/ws new <name> -d <path>", description: "添加工作区" },
|
|
4764
|
+
{ usage: "/workspace rm <name>", description: "删除工作区" }
|
|
4765
|
+
],
|
|
4766
|
+
examples: ['/ws new backend -d "/tmp/backend"', "/workspace rm backend"]
|
|
4767
|
+
};
|
|
4768
|
+
});
|
|
4769
|
+
|
|
4770
|
+
// src/commands/help/help-registry.ts
|
|
4771
|
+
function getHelpTopic(topic) {
|
|
4772
|
+
return HELP_TOPIC_MAP.get(topic) ?? null;
|
|
4773
|
+
}
|
|
4774
|
+
function listHelpTopics() {
|
|
4775
|
+
return HELP_TOPICS;
|
|
4776
|
+
}
|
|
4777
|
+
var HELP_TOPICS, HELP_TOPIC_MAP;
|
|
4778
|
+
var init_help_registry = __esm(() => {
|
|
4779
|
+
init_agent_handler();
|
|
4780
|
+
init_config_handler();
|
|
4781
|
+
init_permission_handler();
|
|
4782
|
+
init_session_handler();
|
|
4783
|
+
init_workspace_handler();
|
|
4784
|
+
HELP_TOPICS = [
|
|
4785
|
+
sessionHelp,
|
|
4786
|
+
workspaceHelp,
|
|
4787
|
+
agentHelp,
|
|
4788
|
+
permissionHelp,
|
|
4789
|
+
configHelp,
|
|
4790
|
+
modeHelp,
|
|
4791
|
+
replyModeHelp,
|
|
4792
|
+
statusHelp,
|
|
4793
|
+
cancelHelp
|
|
4794
|
+
];
|
|
4795
|
+
HELP_TOPIC_MAP = new Map;
|
|
4796
|
+
for (const topic of HELP_TOPICS) {
|
|
4797
|
+
HELP_TOPIC_MAP.set(topic.topic, topic);
|
|
4798
|
+
for (const alias of topic.aliases) {
|
|
4799
|
+
HELP_TOPIC_MAP.set(alias, topic);
|
|
4166
4800
|
}
|
|
4167
|
-
|
|
4168
|
-
|
|
4801
|
+
}
|
|
4802
|
+
});
|
|
4803
|
+
|
|
4804
|
+
// src/commands/handlers/help-handler.ts
|
|
4805
|
+
function handleHelp(topic) {
|
|
4806
|
+
if (!topic) {
|
|
4807
|
+
return { text: renderHelpIndex() };
|
|
4808
|
+
}
|
|
4809
|
+
const entry = getHelpTopic(topic);
|
|
4810
|
+
if (!entry) {
|
|
4811
|
+
return { text: renderUnknownHelpTopic(topic) };
|
|
4812
|
+
}
|
|
4813
|
+
return { text: renderHelpTopic(entry) };
|
|
4814
|
+
}
|
|
4815
|
+
function renderHelpIndex() {
|
|
4816
|
+
const topics = listHelpTopics();
|
|
4817
|
+
return [
|
|
4818
|
+
"常用入口:",
|
|
4819
|
+
"- /ss <agent> (-d <path> | --ws <name>) - 快速新建或切到会话",
|
|
4820
|
+
"- /use <alias> - 切换当前会话",
|
|
4821
|
+
"- /status - 查看当前会话状态",
|
|
4822
|
+
"",
|
|
4823
|
+
"顶级命令:",
|
|
4824
|
+
...topics.map((topic) => `- ${topic.topic} - ${topic.summary}`),
|
|
4825
|
+
"",
|
|
4826
|
+
"查看专题说明:",
|
|
4827
|
+
"- /help <topic>",
|
|
4828
|
+
"- 例如:/help ss、/help ws、/help pm"
|
|
4829
|
+
].join(`
|
|
4830
|
+
`);
|
|
4831
|
+
}
|
|
4832
|
+
function renderHelpTopic(topic) {
|
|
4833
|
+
return [
|
|
4834
|
+
`帮助主题:${topic.topic}`,
|
|
4835
|
+
`说明:${topic.summary}`,
|
|
4836
|
+
...topic.aliases.length > 0 ? [`别名:${topic.aliases.join("、")}`] : [],
|
|
4837
|
+
"",
|
|
4838
|
+
"命令:",
|
|
4839
|
+
...topic.commands.map((command) => `- ${command.usage} - ${command.description}`),
|
|
4840
|
+
...topic.examples && topic.examples.length > 0 ? ["", "示例:", ...topic.examples.map((example) => `- ${example}`)] : []
|
|
4841
|
+
].join(`
|
|
4842
|
+
`);
|
|
4843
|
+
}
|
|
4844
|
+
function renderUnknownHelpTopic(topic) {
|
|
4845
|
+
return [
|
|
4846
|
+
`未知帮助主题:${topic}`,
|
|
4847
|
+
"",
|
|
4848
|
+
"可用主题:",
|
|
4849
|
+
...listHelpTopics().map((entry) => `- ${entry.topic}`)
|
|
4850
|
+
].join(`
|
|
4851
|
+
`);
|
|
4852
|
+
}
|
|
4853
|
+
var init_help_handler = __esm(() => {
|
|
4854
|
+
init_help_registry();
|
|
4855
|
+
});
|
|
4856
|
+
|
|
4857
|
+
// src/commands/handlers/session-shortcut-handler.ts
|
|
4858
|
+
import { access as access2 } from "node:fs/promises";
|
|
4859
|
+
import { basename as basename2, normalize as normalize2 } from "node:path";
|
|
4860
|
+
import { homedir as homedir4 } from "node:os";
|
|
4861
|
+
async function handleSessionShortcutCommand(context, ops, chatKey, agent, target, createNew) {
|
|
4862
|
+
if (!context.config || !context.configStore) {
|
|
4863
|
+
return { text: "当前没有加载可写入的配置。" };
|
|
4864
|
+
}
|
|
4865
|
+
if (!context.config.agents[agent]) {
|
|
4866
|
+
return { text: `agent "${agent}" is not registered` };
|
|
4867
|
+
}
|
|
4868
|
+
const workspace = await resolveShortcutWorkspace(context, target);
|
|
4869
|
+
if ("error" in workspace) {
|
|
4870
|
+
return { text: workspace.error };
|
|
4871
|
+
}
|
|
4872
|
+
await context.logger.info("session.shortcut.workspace", "resolved shortcut workspace", {
|
|
4873
|
+
workspace: workspace.name,
|
|
4874
|
+
cwd: workspace.cwd,
|
|
4875
|
+
reused: workspace.reused
|
|
4876
|
+
});
|
|
4877
|
+
const baseAlias = `${workspace.name}:${agent}`;
|
|
4878
|
+
const alias = createNew ? await allocateUniqueSessionAlias(context, baseAlias, chatKey) : baseAlias;
|
|
4879
|
+
if (!createNew && await hasLogicalSession(context, alias, chatKey)) {
|
|
4880
|
+
await context.sessions.useSession(chatKey, alias);
|
|
4881
|
+
await context.logger.info("session.shortcut.reused", "reused existing logical session", {
|
|
4882
|
+
alias,
|
|
4169
4883
|
workspace: workspace.name,
|
|
4170
|
-
|
|
4171
|
-
reused: workspace.reused
|
|
4884
|
+
agent
|
|
4172
4885
|
});
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4886
|
+
return {
|
|
4887
|
+
text: [
|
|
4888
|
+
`已切换到会话「${alias}」`,
|
|
4889
|
+
`- 复用工作区:${workspace.name}`,
|
|
4890
|
+
`- 复用会话:${alias}`
|
|
4891
|
+
].join(`
|
|
4892
|
+
`)
|
|
4893
|
+
};
|
|
4894
|
+
}
|
|
4895
|
+
const session = ops.resolveSession(alias, agent, workspace.name, `${workspace.name}:${alias}`);
|
|
4896
|
+
try {
|
|
4897
|
+
await ops.ensureTransportSession(session);
|
|
4898
|
+
const exists = await ops.checkTransportSession(session);
|
|
4899
|
+
if (!exists) {
|
|
4900
|
+
return renderShortcutSessionCreationError(workspace, alias);
|
|
4901
|
+
}
|
|
4902
|
+
} catch {
|
|
4903
|
+
return renderShortcutSessionCreationError(workspace, alias);
|
|
4904
|
+
}
|
|
4905
|
+
await context.sessions.attachSession(alias, agent, workspace.name, session.transportSession);
|
|
4906
|
+
await ops.refreshSessionTransportAgentCommand(alias);
|
|
4907
|
+
await context.sessions.useSession(chatKey, alias);
|
|
4908
|
+
await context.logger.info("session.shortcut.created", "created new logical session from shortcut", {
|
|
4909
|
+
alias,
|
|
4910
|
+
workspace: workspace.name,
|
|
4911
|
+
agent,
|
|
4912
|
+
workspaceReused: workspace.reused
|
|
4913
|
+
});
|
|
4914
|
+
return {
|
|
4915
|
+
text: [
|
|
4916
|
+
`已创建并切换到会话「${alias}」`,
|
|
4917
|
+
workspace.reused ? `- 复用工作区:${workspace.name}` : `- 新增工作区:${workspace.name} -> ${workspace.cwd}`,
|
|
4918
|
+
`- 新增会话:${alias}`
|
|
4919
|
+
].join(`
|
|
4920
|
+
`)
|
|
4921
|
+
};
|
|
4922
|
+
}
|
|
4923
|
+
async function resolveShortcutWorkspace(context, target) {
|
|
4924
|
+
if (target.workspace) {
|
|
4925
|
+
const workspace = context.config?.workspaces[target.workspace];
|
|
4926
|
+
if (!workspace) {
|
|
4927
|
+
return { error: `workspace "${target.workspace}" is not registered` };
|
|
4928
|
+
}
|
|
4929
|
+
return {
|
|
4930
|
+
name: target.workspace,
|
|
4931
|
+
cwd: workspace.cwd,
|
|
4932
|
+
reused: true
|
|
4933
|
+
};
|
|
4934
|
+
}
|
|
4935
|
+
const cwdInput = target.cwd ?? "";
|
|
4936
|
+
const cwd = normalizePathForWorkspace2(cwdInput);
|
|
4937
|
+
if (!await pathExists2(cwd)) {
|
|
4938
|
+
return { error: `工作区路径不存在:${cwdInput}` };
|
|
4939
|
+
}
|
|
4940
|
+
const existingByPath = Object.entries(context.config?.workspaces ?? {}).find(([, workspace]) => sameWorkspacePath(workspace.cwd, cwd));
|
|
4941
|
+
if (existingByPath) {
|
|
4942
|
+
return {
|
|
4943
|
+
name: existingByPath[0],
|
|
4944
|
+
cwd: existingByPath[1].cwd,
|
|
4945
|
+
reused: true
|
|
4946
|
+
};
|
|
4947
|
+
}
|
|
4948
|
+
const workspaceName = allocateWorkspaceName(context, basename2(cwd));
|
|
4949
|
+
const updated = await context.configStore.upsertWorkspace(workspaceName, cwd);
|
|
4950
|
+
context.replaceConfig(updated);
|
|
4951
|
+
return {
|
|
4952
|
+
name: workspaceName,
|
|
4953
|
+
cwd,
|
|
4954
|
+
reused: false
|
|
4955
|
+
};
|
|
4956
|
+
}
|
|
4957
|
+
function allocateWorkspaceName(context, baseName) {
|
|
4958
|
+
if (!context.config?.workspaces[baseName]) {
|
|
4959
|
+
return baseName;
|
|
4960
|
+
}
|
|
4961
|
+
let suffix = 2;
|
|
4962
|
+
while (context.config.workspaces[`${baseName}-${suffix}`]) {
|
|
4963
|
+
suffix += 1;
|
|
4964
|
+
}
|
|
4965
|
+
return `${baseName}-${suffix}`;
|
|
4966
|
+
}
|
|
4967
|
+
async function allocateUniqueSessionAlias(context, baseAlias, chatKey) {
|
|
4968
|
+
if (!await hasLogicalSession(context, baseAlias, chatKey)) {
|
|
4969
|
+
return baseAlias;
|
|
4970
|
+
}
|
|
4971
|
+
let suffix = 2;
|
|
4972
|
+
while (await hasLogicalSession(context, `${baseAlias}-${suffix}`, chatKey)) {
|
|
4973
|
+
suffix += 1;
|
|
4974
|
+
}
|
|
4975
|
+
return `${baseAlias}-${suffix}`;
|
|
4976
|
+
}
|
|
4977
|
+
async function hasLogicalSession(context, alias, chatKey) {
|
|
4978
|
+
const sessions = await context.sessions.listSessions(chatKey);
|
|
4979
|
+
return sessions.some((session) => session.alias === alias);
|
|
4980
|
+
}
|
|
4981
|
+
function renderShortcutSessionCreationError(workspace, alias) {
|
|
4982
|
+
return {
|
|
4983
|
+
text: [
|
|
4984
|
+
`会话「${alias}」创建失败。`,
|
|
4985
|
+
workspace.reused ? `- 复用工作区:${workspace.name}` : `- 已新增工作区:${workspace.name} -> ${workspace.cwd}`,
|
|
4986
|
+
"- 会话未创建,请重试。"
|
|
4987
|
+
].join(`
|
|
4988
|
+
`)
|
|
4989
|
+
};
|
|
4990
|
+
}
|
|
4991
|
+
async function pathExists2(path11) {
|
|
4992
|
+
try {
|
|
4993
|
+
await access2(path11);
|
|
4994
|
+
return true;
|
|
4995
|
+
} catch {
|
|
4996
|
+
return false;
|
|
4997
|
+
}
|
|
4998
|
+
}
|
|
4999
|
+
function normalizePathForWorkspace2(path11) {
|
|
5000
|
+
const expanded = path11.startsWith("~") ? homedir4() + path11.slice(1) : path11;
|
|
5001
|
+
return normalize2(expanded);
|
|
5002
|
+
}
|
|
5003
|
+
function sameWorkspacePath(left, right) {
|
|
5004
|
+
const normalizedLeft = normalizePathForWorkspace2(left);
|
|
5005
|
+
const normalizedRight = normalizePathForWorkspace2(right);
|
|
5006
|
+
if (process.platform === "win32") {
|
|
5007
|
+
return normalizedLeft.toLowerCase() === normalizedRight.toLowerCase();
|
|
5008
|
+
}
|
|
5009
|
+
return normalizedLeft === normalizedRight;
|
|
5010
|
+
}
|
|
5011
|
+
var init_session_shortcut_handler = () => {};
|
|
5012
|
+
|
|
5013
|
+
// src/commands/handlers/session-recovery-handler.ts
|
|
5014
|
+
function renderTransportError(session, error) {
|
|
5015
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5016
|
+
if (message.includes("No acpx session found")) {
|
|
5017
|
+
return {
|
|
5018
|
+
text: [
|
|
5019
|
+
`当前会话「${session.alias}」暂时不可用。`,
|
|
5020
|
+
`请先在微信里重新执行:/session new ${session.alias} --agent ${session.agent} --ws ${session.workspace}`,
|
|
5021
|
+
`如果你要绑定一个已有会话,再执行:/session attach ${session.alias} --agent ${session.agent} --ws ${session.workspace} --name <会话名>`
|
|
5022
|
+
].join(`
|
|
5023
|
+
`)
|
|
5024
|
+
};
|
|
5025
|
+
}
|
|
5026
|
+
if (!isPartialPromptOutputError(message)) {
|
|
5027
|
+
throw error;
|
|
5028
|
+
}
|
|
5029
|
+
return {
|
|
5030
|
+
text: [
|
|
5031
|
+
`当前会话「${session.alias}」执行中断,未收到最终回复。`,
|
|
5032
|
+
"请直接重试;如果长时间无响应,可先发送 /cancel 后再重试。",
|
|
5033
|
+
`错误信息:${summarizeTransportError(message)}`
|
|
5034
|
+
].join(`
|
|
5035
|
+
`)
|
|
5036
|
+
};
|
|
5037
|
+
}
|
|
5038
|
+
function renderSessionCreationError(session, error) {
|
|
5039
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5040
|
+
if (message.includes("timed out") && message.includes("sessions new")) {
|
|
5041
|
+
return renderSessionCreationFailure(session, message);
|
|
5042
|
+
}
|
|
5043
|
+
throw error;
|
|
5044
|
+
}
|
|
5045
|
+
function renderSessionCreationVerificationError(session) {
|
|
5046
|
+
return renderSessionCreationFailure(session, "未检测到可用的后端会话。");
|
|
5047
|
+
}
|
|
5048
|
+
function renderSessionCreationFailure(session, detail) {
|
|
5049
|
+
return {
|
|
5050
|
+
text: [
|
|
5051
|
+
"会话创建失败。",
|
|
5052
|
+
`错误信息:${summarizeTransportError(detail)}`,
|
|
5053
|
+
`如果你要先绑定一个已有会话,可以执行:/session attach ${session.alias} --agent ${session.agent} --ws ${session.workspace} --name <会话名>`
|
|
5054
|
+
].join(`
|
|
5055
|
+
`)
|
|
5056
|
+
};
|
|
5057
|
+
}
|
|
5058
|
+
async function tryRecoverMissingSession(ops, session, error) {
|
|
5059
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
5060
|
+
if (!message.includes("No acpx session found")) {
|
|
5061
|
+
return null;
|
|
5062
|
+
}
|
|
5063
|
+
const transportAgentCommand = await ops.resolveSessionAgentCommand(session);
|
|
5064
|
+
if (!transportAgentCommand || transportAgentCommand === session.agentCommand) {
|
|
5065
|
+
return null;
|
|
5066
|
+
}
|
|
5067
|
+
await ops.setSessionTransportAgentCommand(session.alias, transportAgentCommand);
|
|
5068
|
+
return await ops.getSession(session.alias);
|
|
5069
|
+
}
|
|
5070
|
+
var init_session_recovery_handler = () => {};
|
|
5071
|
+
|
|
5072
|
+
// src/commands/handlers/session-reset-handler.ts
|
|
5073
|
+
async function handleSessionResetCommand(context, ops, chatKey) {
|
|
5074
|
+
const session = await context.sessions.getCurrentSession(chatKey);
|
|
5075
|
+
if (!session) {
|
|
5076
|
+
return { text: NO_CURRENT_SESSION_TEXT2 };
|
|
5077
|
+
}
|
|
5078
|
+
const resetSession = ops.resolveSession(session.alias, session.agent, session.workspace, buildResetTransportSessionName(session, ops.now()));
|
|
5079
|
+
try {
|
|
5080
|
+
await ops.ensureTransportSession(resetSession);
|
|
5081
|
+
const exists = await ops.checkTransportSession(resetSession);
|
|
5082
|
+
if (!exists) {
|
|
4182
5083
|
return {
|
|
4183
5084
|
text: [
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
`- 复用会话:${alias}`
|
|
5085
|
+
`会话「${session.alias}」重置失败。`,
|
|
5086
|
+
"新的后端会话未创建成功,请稍后重试。"
|
|
4187
5087
|
].join(`
|
|
4188
5088
|
`)
|
|
4189
5089
|
};
|
|
4190
5090
|
}
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
5091
|
+
} catch (error) {
|
|
5092
|
+
return renderTransportError(resetSession, error);
|
|
5093
|
+
}
|
|
5094
|
+
await context.sessions.attachSession(resetSession.alias, resetSession.agent, resetSession.workspace, resetSession.transportSession);
|
|
5095
|
+
await ops.refreshSessionTransportAgentCommand(resetSession.alias);
|
|
5096
|
+
await context.sessions.useSession(chatKey, resetSession.alias);
|
|
5097
|
+
await context.logger.info("session.reset", "reset current logical session", {
|
|
5098
|
+
alias: resetSession.alias,
|
|
5099
|
+
agent: resetSession.agent,
|
|
5100
|
+
workspace: resetSession.workspace,
|
|
5101
|
+
transportSession: resetSession.transportSession,
|
|
5102
|
+
chatKey
|
|
5103
|
+
});
|
|
5104
|
+
return { text: `会话「${resetSession.alias}」已重置` };
|
|
5105
|
+
}
|
|
5106
|
+
function buildResetTransportSessionName(session, now) {
|
|
5107
|
+
return `${session.workspace}:${session.alias}:reset-${now}`;
|
|
5108
|
+
}
|
|
5109
|
+
var NO_CURRENT_SESSION_TEXT2 = "当前还没有选中的会话。请先执行 /session new ... 或 /use <alias>。";
|
|
5110
|
+
var init_session_reset_handler = __esm(() => {
|
|
5111
|
+
init_session_recovery_handler();
|
|
5112
|
+
});
|
|
5113
|
+
|
|
5114
|
+
// src/commands/command-router.ts
|
|
5115
|
+
class CommandRouter {
|
|
5116
|
+
sessions;
|
|
5117
|
+
transport;
|
|
5118
|
+
config;
|
|
5119
|
+
configStore;
|
|
5120
|
+
resolveSessionAgentCommand;
|
|
5121
|
+
logger;
|
|
5122
|
+
constructor(sessions, transport, config, configStore, logger2, resolveSessionAgentCommand = resolveSessionAgentCommandFromIndex) {
|
|
5123
|
+
this.sessions = sessions;
|
|
5124
|
+
this.transport = transport;
|
|
5125
|
+
this.config = config;
|
|
5126
|
+
this.configStore = configStore;
|
|
5127
|
+
this.resolveSessionAgentCommand = resolveSessionAgentCommand;
|
|
5128
|
+
this.logger = logger2 ?? createNoopAppLogger();
|
|
5129
|
+
}
|
|
5130
|
+
async handle(chatKey, input, reply) {
|
|
5131
|
+
const startedAt = Date.now();
|
|
5132
|
+
const command = parseCommand(input);
|
|
5133
|
+
await this.logger.debug("command.parsed", "parsed inbound command", {
|
|
5134
|
+
chatKey,
|
|
5135
|
+
kind: command.kind
|
|
5136
|
+
});
|
|
5137
|
+
return await this.executeCommand(chatKey, command.kind, startedAt, async () => {
|
|
5138
|
+
switch (command.kind) {
|
|
5139
|
+
case "invalid":
|
|
5140
|
+
return {
|
|
5141
|
+
text: [
|
|
5142
|
+
"无法识别的命令格式。",
|
|
5143
|
+
"",
|
|
5144
|
+
"正确的会话创建格式:",
|
|
5145
|
+
"/session new <别名> --agent <Agent名> --ws <工作区名>",
|
|
5146
|
+
"",
|
|
5147
|
+
"例如:",
|
|
5148
|
+
"/session new demo --agent claude --ws weacpx"
|
|
5149
|
+
].join(`
|
|
5150
|
+
`)
|
|
5151
|
+
};
|
|
5152
|
+
case "help":
|
|
5153
|
+
return handleHelp(command.topic);
|
|
5154
|
+
case "agents":
|
|
5155
|
+
return handleAgents(this.createHandlerContext());
|
|
5156
|
+
case "agent.add":
|
|
5157
|
+
return await handleAgentAdd(this.createHandlerContext(), command.template);
|
|
5158
|
+
case "agent.rm":
|
|
5159
|
+
return await handleAgentRemove(this.createHandlerContext(), command.name);
|
|
5160
|
+
case "permission.status":
|
|
5161
|
+
return handlePermissionStatus(this.createHandlerContext(), "当前权限模式:");
|
|
5162
|
+
case "permission.mode.set":
|
|
5163
|
+
return await handlePermissionModeSet(this.createHandlerContext(), command.mode);
|
|
5164
|
+
case "permission.auto.status":
|
|
5165
|
+
return handlePermissionAutoStatus(this.createHandlerContext(), "当前非交互策略:");
|
|
5166
|
+
case "permission.auto.set":
|
|
5167
|
+
return await handlePermissionAutoSet(this.createHandlerContext(), command.policy);
|
|
5168
|
+
case "config.show":
|
|
5169
|
+
return handleConfigShow(this.createHandlerContext());
|
|
5170
|
+
case "config.set":
|
|
5171
|
+
return await handleConfigSet(this.createHandlerContext(), command.path, command.value);
|
|
5172
|
+
case "workspaces":
|
|
5173
|
+
return handleWorkspaces(this.createHandlerContext());
|
|
5174
|
+
case "workspace.new":
|
|
5175
|
+
return await handleWorkspaceCreate(this.createHandlerContext(), command.name, command.cwd);
|
|
5176
|
+
case "workspace.rm":
|
|
5177
|
+
return await handleWorkspaceRemove(this.createHandlerContext(), command.name);
|
|
5178
|
+
case "sessions":
|
|
5179
|
+
return await handleSessions(this.createSessionHandlerContext(), chatKey);
|
|
5180
|
+
case "session.new":
|
|
5181
|
+
return await handleSessionNew(this.createSessionHandlerContext(), chatKey, command.alias, command.agent, command.workspace);
|
|
5182
|
+
case "session.shortcut":
|
|
5183
|
+
return await handleSessionShortcut(this.createSessionHandlerContext(), chatKey, command.agent, command, false);
|
|
5184
|
+
case "session.shortcut.new":
|
|
5185
|
+
return await handleSessionShortcut(this.createSessionHandlerContext(), chatKey, command.agent, command, true);
|
|
5186
|
+
case "session.attach":
|
|
5187
|
+
return await handleSessionAttach(this.createSessionHandlerContext(), chatKey, command.alias, command.agent, command.workspace, command.transportSession);
|
|
5188
|
+
case "session.use":
|
|
5189
|
+
return await handleSessionUse(this.createSessionHandlerContext(), chatKey, command.alias);
|
|
5190
|
+
case "mode.show":
|
|
5191
|
+
return await handleModeShow(this.createSessionHandlerContext(), chatKey);
|
|
5192
|
+
case "mode.set":
|
|
5193
|
+
return await handleModeSet(this.createSessionHandlerContext(), chatKey, command.modeId);
|
|
5194
|
+
case "replymode.show":
|
|
5195
|
+
return await handleReplyModeShow(this.createSessionHandlerContext(), chatKey);
|
|
5196
|
+
case "replymode.set":
|
|
5197
|
+
return await handleReplyModeSet(this.createSessionHandlerContext(), chatKey, command.replyMode);
|
|
5198
|
+
case "replymode.reset":
|
|
5199
|
+
return await handleReplyModeReset(this.createSessionHandlerContext(), chatKey);
|
|
5200
|
+
case "status":
|
|
5201
|
+
return await handleStatus(this.createSessionHandlerContext(), chatKey);
|
|
5202
|
+
case "cancel":
|
|
5203
|
+
return await handleCancel(this.createSessionHandlerContext(), chatKey);
|
|
5204
|
+
case "session.reset":
|
|
5205
|
+
return await handleSessionReset(this.createSessionHandlerContext(), chatKey);
|
|
5206
|
+
case "prompt":
|
|
5207
|
+
return await handlePrompt(this.createSessionHandlerContext(), chatKey, command.text, reply);
|
|
4197
5208
|
}
|
|
4198
|
-
} catch {
|
|
4199
|
-
return this.renderShortcutSessionCreationError(workspace, alias);
|
|
4200
|
-
}
|
|
4201
|
-
await this.sessions.attachSession(alias, agent, workspace.name, session.transportSession);
|
|
4202
|
-
await this.refreshSessionTransportAgentCommand(alias);
|
|
4203
|
-
await this.sessions.useSession(chatKey, alias);
|
|
4204
|
-
await this.logger.info("session.shortcut.created", "created new logical session from shortcut", {
|
|
4205
|
-
alias,
|
|
4206
|
-
workspace: workspace.name,
|
|
4207
|
-
agent,
|
|
4208
|
-
workspaceReused: workspace.reused
|
|
4209
5209
|
});
|
|
4210
|
-
return {
|
|
4211
|
-
text: [
|
|
4212
|
-
`已创建并切换到会话「${alias}」`,
|
|
4213
|
-
workspace.reused ? `- 复用工作区:${workspace.name}` : `- 新增工作区:${workspace.name} -> ${workspace.cwd}`,
|
|
4214
|
-
`- 新增会话:${alias}`
|
|
4215
|
-
].join(`
|
|
4216
|
-
`)
|
|
4217
|
-
};
|
|
4218
5210
|
}
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
return;
|
|
4222
|
-
}
|
|
4223
|
-
this.config.transport = { ...updated.transport };
|
|
4224
|
-
this.config.agents = { ...updated.agents };
|
|
4225
|
-
this.config.workspaces = { ...updated.workspaces };
|
|
4226
|
-
}
|
|
4227
|
-
renderPermissionStatus(title) {
|
|
4228
|
-
const permissionMode = this.config?.transport.permissionMode ?? "approve-all";
|
|
4229
|
-
const nonInteractivePermissions = this.config?.transport.nonInteractivePermissions ?? "fail";
|
|
4230
|
-
return [title, `- mode: ${permissionMode}`, `- auto: ${nonInteractivePermissions}`].join(`
|
|
4231
|
-
`);
|
|
5211
|
+
async clearSession(chatKey) {
|
|
5212
|
+
await handleSessionResetCommand(this.createHandlerContext(), this.createSessionResetOps(), chatKey);
|
|
4232
5213
|
}
|
|
4233
|
-
|
|
4234
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
4235
|
-
if (message.includes("No acpx session found")) {
|
|
4236
|
-
return {
|
|
4237
|
-
text: [
|
|
4238
|
-
`当前会话「${session.alias}」暂时不可用。`,
|
|
4239
|
-
`请先在微信里重新执行:/session new ${session.alias} --agent ${session.agent} --ws ${session.workspace}`,
|
|
4240
|
-
`如果你要绑定一个已有会话,再执行:/session attach ${session.alias} --agent ${session.agent} --ws ${session.workspace} --name <会话名>`
|
|
4241
|
-
].join(`
|
|
4242
|
-
`)
|
|
4243
|
-
};
|
|
4244
|
-
}
|
|
4245
|
-
if (!isPartialPromptOutputError(message)) {
|
|
4246
|
-
throw error;
|
|
4247
|
-
}
|
|
5214
|
+
createHandlerContext() {
|
|
4248
5215
|
return {
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
5216
|
+
sessions: this.sessions,
|
|
5217
|
+
transport: this.transport,
|
|
5218
|
+
config: this.config,
|
|
5219
|
+
configStore: this.configStore,
|
|
5220
|
+
logger: this.logger,
|
|
5221
|
+
replaceConfig: (updated) => this.replaceConfig(updated)
|
|
4255
5222
|
};
|
|
4256
5223
|
}
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
5224
|
+
createSessionHandlerContext() {
|
|
5225
|
+
return {
|
|
5226
|
+
...this.createHandlerContext(),
|
|
5227
|
+
lifecycle: this.createSessionLifecycleOps(),
|
|
5228
|
+
interaction: this.createSessionInteractionOps(),
|
|
5229
|
+
recovery: this.createSessionRenderRecoveryOps()
|
|
5230
|
+
};
|
|
4263
5231
|
}
|
|
4264
|
-
|
|
5232
|
+
createSessionLifecycleOps() {
|
|
4265
5233
|
return {
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
5234
|
+
resolveSession: (alias, agent, workspace, transportSession) => this.sessions.resolveSession(alias, agent, workspace, transportSession),
|
|
5235
|
+
ensureTransportSession: (session) => this.ensureTransportSession(session),
|
|
5236
|
+
checkTransportSession: (session) => this.checkTransportSession(session),
|
|
5237
|
+
handleSessionShortcut: (chatKey, agent, target, createNew) => handleSessionShortcutCommand(this.createHandlerContext(), this.createSessionShortcutOps(), chatKey, agent, target, createNew),
|
|
5238
|
+
resetCurrentSession: (chatKey) => handleSessionResetCommand(this.createHandlerContext(), this.createSessionResetOps(), chatKey),
|
|
5239
|
+
refreshSessionTransportAgentCommand: (alias) => this.refreshSessionTransportAgentCommand(alias)
|
|
4271
5240
|
};
|
|
4272
5241
|
}
|
|
4273
|
-
|
|
4274
|
-
const existingByPath = Object.entries(this.config?.workspaces ?? {}).find(([, workspace]) => sameWorkspacePath(workspace.cwd, cwd));
|
|
4275
|
-
if (existingByPath) {
|
|
4276
|
-
return {
|
|
4277
|
-
name: existingByPath[0],
|
|
4278
|
-
cwd: existingByPath[1].cwd,
|
|
4279
|
-
reused: true
|
|
4280
|
-
};
|
|
4281
|
-
}
|
|
4282
|
-
const baseName = basename2(cwd);
|
|
4283
|
-
const workspaceName = this.allocateWorkspaceName(baseName, cwd);
|
|
4284
|
-
const updated = await this.configStore.upsertWorkspace(workspaceName, cwd);
|
|
4285
|
-
this.replaceConfig(updated);
|
|
5242
|
+
createSessionInteractionOps() {
|
|
4286
5243
|
return {
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
5244
|
+
setModeTransportSession: (session, modeId) => this.setModeTransportSession(session, modeId),
|
|
5245
|
+
cancelTransportSession: (session) => this.cancelTransportSession(session),
|
|
5246
|
+
promptTransportSession: (session, text, reply) => this.promptTransportSession(session, text, reply)
|
|
4290
5247
|
};
|
|
4291
5248
|
}
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
}
|
|
4300
|
-
return `${baseName}-${suffix}`;
|
|
5249
|
+
createSessionRenderRecoveryOps() {
|
|
5250
|
+
return {
|
|
5251
|
+
renderSessionCreationError: (session, error) => renderSessionCreationError(session, error),
|
|
5252
|
+
renderSessionCreationVerificationError: (session) => renderSessionCreationVerificationError(session),
|
|
5253
|
+
tryRecoverMissingSession: (session, error) => tryRecoverMissingSession(this.createSessionRecoveryOps(), session, error),
|
|
5254
|
+
renderTransportError: (session, error) => renderTransportError(session, error)
|
|
5255
|
+
};
|
|
4301
5256
|
}
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
}
|
|
4310
|
-
return `${baseAlias}-${suffix}`;
|
|
5257
|
+
createSessionResetOps() {
|
|
5258
|
+
return {
|
|
5259
|
+
ensureTransportSession: (session) => this.ensureTransportSession(session),
|
|
5260
|
+
checkTransportSession: (session) => this.checkTransportSession(session),
|
|
5261
|
+
resolveSession: (alias, agent, workspace, transportSession) => this.sessions.resolveSession(alias, agent, workspace, transportSession),
|
|
5262
|
+
refreshSessionTransportAgentCommand: (alias) => this.refreshSessionTransportAgentCommand(alias),
|
|
5263
|
+
now: () => Date.now()
|
|
5264
|
+
};
|
|
4311
5265
|
}
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
5266
|
+
createSessionRecoveryOps() {
|
|
5267
|
+
return {
|
|
5268
|
+
resolveSessionAgentCommand: (session) => this.resolveSessionAgentCommand(session),
|
|
5269
|
+
setSessionTransportAgentCommand: (alias, command) => this.sessions.setSessionTransportAgentCommand(alias, command),
|
|
5270
|
+
getSession: (alias) => this.sessions.getSession(alias)
|
|
5271
|
+
};
|
|
4315
5272
|
}
|
|
4316
|
-
|
|
5273
|
+
createSessionShortcutOps() {
|
|
4317
5274
|
return {
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
].join(`
|
|
4323
|
-
`)
|
|
5275
|
+
resolveSession: (alias, agent, workspace, transportSession) => this.sessions.resolveSession(alias, agent, workspace, transportSession),
|
|
5276
|
+
ensureTransportSession: (session) => this.ensureTransportSession(session),
|
|
5277
|
+
checkTransportSession: (session) => this.checkTransportSession(session),
|
|
5278
|
+
refreshSessionTransportAgentCommand: (alias) => this.refreshSessionTransportAgentCommand(alias)
|
|
4324
5279
|
};
|
|
4325
5280
|
}
|
|
5281
|
+
replaceConfig(updated) {
|
|
5282
|
+
if (!this.config) {
|
|
5283
|
+
return;
|
|
5284
|
+
}
|
|
5285
|
+
this.config.transport = { ...updated.transport };
|
|
5286
|
+
this.config.logging = { ...updated.logging };
|
|
5287
|
+
this.config.wechat = { ...updated.wechat };
|
|
5288
|
+
this.config.agents = { ...updated.agents };
|
|
5289
|
+
this.config.workspaces = { ...updated.workspaces };
|
|
5290
|
+
}
|
|
4326
5291
|
async executeCommand(chatKey, kind, startedAt, operation) {
|
|
4327
5292
|
try {
|
|
4328
5293
|
const response = await operation();
|
|
@@ -4345,42 +5310,6 @@ class CommandRouter {
|
|
|
4345
5310
|
async ensureTransportSession(session) {
|
|
4346
5311
|
await this.measureTransportCall("ensure_session", session, () => this.transport.ensureSession(session));
|
|
4347
5312
|
}
|
|
4348
|
-
async resetCurrentSession(chatKey) {
|
|
4349
|
-
const session = await this.sessions.getCurrentSession(chatKey);
|
|
4350
|
-
if (!session) {
|
|
4351
|
-
return { text: "当前还没有选中的会话。请先执行 /session new ... 或 /use <alias>。" };
|
|
4352
|
-
}
|
|
4353
|
-
const resetSession = this.sessions.resolveSession(session.alias, session.agent, session.workspace, this.buildResetTransportSessionName(session));
|
|
4354
|
-
try {
|
|
4355
|
-
await this.ensureTransportSession(resetSession);
|
|
4356
|
-
const exists = await this.checkTransportSession(resetSession);
|
|
4357
|
-
if (!exists) {
|
|
4358
|
-
return {
|
|
4359
|
-
text: [
|
|
4360
|
-
`会话「${session.alias}」重置失败。`,
|
|
4361
|
-
"新的后端会话未创建成功,请稍后重试。"
|
|
4362
|
-
].join(`
|
|
4363
|
-
`)
|
|
4364
|
-
};
|
|
4365
|
-
}
|
|
4366
|
-
} catch (error) {
|
|
4367
|
-
return this.renderTransportError(resetSession, error);
|
|
4368
|
-
}
|
|
4369
|
-
await this.sessions.attachSession(resetSession.alias, resetSession.agent, resetSession.workspace, resetSession.transportSession);
|
|
4370
|
-
await this.refreshSessionTransportAgentCommand(resetSession.alias);
|
|
4371
|
-
await this.sessions.useSession(chatKey, resetSession.alias);
|
|
4372
|
-
await this.logger.info("session.reset", "reset current logical session", {
|
|
4373
|
-
alias: resetSession.alias,
|
|
4374
|
-
agent: resetSession.agent,
|
|
4375
|
-
workspace: resetSession.workspace,
|
|
4376
|
-
transportSession: resetSession.transportSession,
|
|
4377
|
-
chatKey
|
|
4378
|
-
});
|
|
4379
|
-
return { text: `会话「${resetSession.alias}」已重置` };
|
|
4380
|
-
}
|
|
4381
|
-
buildResetTransportSessionName(session) {
|
|
4382
|
-
return `${session.workspace}:${session.alias}:reset-${Date.now()}`;
|
|
4383
|
-
}
|
|
4384
5313
|
async checkTransportSession(session) {
|
|
4385
5314
|
return await this.measureTransportCall("has_session", session, () => this.transport.hasSession(session));
|
|
4386
5315
|
}
|
|
@@ -4404,18 +5333,6 @@ class CommandRouter {
|
|
|
4404
5333
|
}
|
|
4405
5334
|
await this.sessions.setSessionTransportAgentCommand(alias, transportAgentCommand);
|
|
4406
5335
|
}
|
|
4407
|
-
async tryRecoverMissingSession(session, error) {
|
|
4408
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
4409
|
-
if (!message.includes("No acpx session found")) {
|
|
4410
|
-
return null;
|
|
4411
|
-
}
|
|
4412
|
-
const transportAgentCommand = await this.resolveSessionAgentCommand(session);
|
|
4413
|
-
if (!transportAgentCommand || transportAgentCommand === session.agentCommand) {
|
|
4414
|
-
return null;
|
|
4415
|
-
}
|
|
4416
|
-
await this.sessions.setSessionTransportAgentCommand(session.alias, transportAgentCommand);
|
|
4417
|
-
return await this.sessions.getSession(session.alias);
|
|
4418
|
-
}
|
|
4419
5336
|
async measureTransportCall(operation, session, callback) {
|
|
4420
5337
|
const startedAt = Date.now();
|
|
4421
5338
|
try {
|
|
@@ -4453,91 +5370,20 @@ class CommandRouter {
|
|
|
4453
5370
|
}
|
|
4454
5371
|
}
|
|
4455
5372
|
}
|
|
4456
|
-
async function pathExists(path11) {
|
|
4457
|
-
try {
|
|
4458
|
-
await access(path11);
|
|
4459
|
-
return true;
|
|
4460
|
-
} catch {
|
|
4461
|
-
return false;
|
|
4462
|
-
}
|
|
4463
|
-
}
|
|
4464
|
-
function normalizePathForWorkspace(path11) {
|
|
4465
|
-
const expanded = path11.startsWith("~") ? homedir2() + path11.slice(1) : path11;
|
|
4466
|
-
return normalize(expanded);
|
|
4467
|
-
}
|
|
4468
|
-
function sameWorkspacePath(left, right) {
|
|
4469
|
-
const normalizedLeft = normalizePathForWorkspace(left);
|
|
4470
|
-
const normalizedRight = normalizePathForWorkspace(right);
|
|
4471
|
-
if (process.platform === "win32") {
|
|
4472
|
-
return normalizedLeft.toLowerCase() === normalizedRight.toLowerCase();
|
|
4473
|
-
}
|
|
4474
|
-
return normalizedLeft === normalizedRight;
|
|
4475
|
-
}
|
|
4476
|
-
function summarizeTransportError(message) {
|
|
4477
|
-
return message.replace(/\s+/g, " ").trim().slice(0, 200);
|
|
4478
|
-
}
|
|
4479
|
-
function summarizeTransportDiagnostic(output) {
|
|
4480
|
-
const trimmed = output.replace(/\s+/g, " ").trim();
|
|
4481
|
-
if (trimmed.length === 0) {
|
|
4482
|
-
return;
|
|
4483
|
-
}
|
|
4484
|
-
return trimmed.slice(0, 200);
|
|
4485
|
-
}
|
|
4486
|
-
function summarizeTransportDiagnosticTail(output) {
|
|
4487
|
-
const trimmed = output.replace(/\s+/g, " ").trim();
|
|
4488
|
-
if (trimmed.length === 0) {
|
|
4489
|
-
return;
|
|
4490
|
-
}
|
|
4491
|
-
return trimmed.slice(-200);
|
|
4492
|
-
}
|
|
4493
|
-
function summarizeTransportNdjson(output, prefix) {
|
|
4494
|
-
const lines = output.split(`
|
|
4495
|
-
`).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
4496
|
-
if (lines.length === 0) {
|
|
4497
|
-
return {};
|
|
4498
|
-
}
|
|
4499
|
-
const methods = new Set;
|
|
4500
|
-
let agentMessageChunkCount = 0;
|
|
4501
|
-
let stopReason;
|
|
4502
|
-
for (const line of lines) {
|
|
4503
|
-
try {
|
|
4504
|
-
const payload = JSON.parse(line);
|
|
4505
|
-
if (typeof payload.method === "string" && payload.method.length > 0) {
|
|
4506
|
-
methods.add(payload.method);
|
|
4507
|
-
}
|
|
4508
|
-
if (payload.params?.update?.sessionUpdate === "agent_message_chunk") {
|
|
4509
|
-
agentMessageChunkCount += 1;
|
|
4510
|
-
}
|
|
4511
|
-
if (typeof payload.result?.stopReason === "string" && payload.result.stopReason.length > 0) {
|
|
4512
|
-
stopReason = payload.result.stopReason;
|
|
4513
|
-
}
|
|
4514
|
-
} catch {
|
|
4515
|
-
continue;
|
|
4516
|
-
}
|
|
4517
|
-
}
|
|
4518
|
-
const summary = {
|
|
4519
|
-
[`${prefix}LineCount`]: lines.length
|
|
4520
|
-
};
|
|
4521
|
-
if (methods.size > 0) {
|
|
4522
|
-
summary[`${prefix}Methods`] = [...methods].join(",");
|
|
4523
|
-
}
|
|
4524
|
-
if (agentMessageChunkCount > 0) {
|
|
4525
|
-
summary[`${prefix}AgentMessageChunkCount`] = agentMessageChunkCount;
|
|
4526
|
-
}
|
|
4527
|
-
if (stopReason) {
|
|
4528
|
-
summary[`${prefix}StopReason`] = stopReason;
|
|
4529
|
-
}
|
|
4530
|
-
return summary;
|
|
4531
|
-
}
|
|
4532
|
-
function isPartialPromptOutputError(message) {
|
|
4533
|
-
return message.includes("未收到最终回复");
|
|
4534
|
-
}
|
|
4535
5373
|
var init_command_router = __esm(() => {
|
|
4536
|
-
init_agent_templates();
|
|
4537
5374
|
init_app_logger();
|
|
4538
5375
|
init_acpx_session_index();
|
|
4539
5376
|
init_prompt_output();
|
|
4540
5377
|
init_parse_command();
|
|
5378
|
+
init_permission_handler();
|
|
5379
|
+
init_config_handler();
|
|
5380
|
+
init_session_handler();
|
|
5381
|
+
init_help_handler();
|
|
5382
|
+
init_agent_handler();
|
|
5383
|
+
init_workspace_handler();
|
|
5384
|
+
init_session_shortcut_handler();
|
|
5385
|
+
init_session_recovery_handler();
|
|
5386
|
+
init_session_reset_handler();
|
|
4541
5387
|
});
|
|
4542
5388
|
|
|
4543
5389
|
// src/config/resolve-agent-command.ts
|
|
@@ -4556,12 +5402,12 @@ function isLegacyCodexCommand(command) {
|
|
|
4556
5402
|
}
|
|
4557
5403
|
|
|
4558
5404
|
// src/config/load-config.ts
|
|
4559
|
-
import { readFile as
|
|
5405
|
+
import { readFile as readFile5 } from "node:fs/promises";
|
|
4560
5406
|
function isRecord(value) {
|
|
4561
5407
|
return typeof value === "object" && value !== null;
|
|
4562
5408
|
}
|
|
4563
5409
|
async function loadConfig(path11, options = {}) {
|
|
4564
|
-
const raw = JSON.parse(await
|
|
5410
|
+
const raw = JSON.parse(await readFile5(path11, "utf8"));
|
|
4565
5411
|
return parseConfig(raw, options);
|
|
4566
5412
|
}
|
|
4567
5413
|
function parseConfig(raw, options = {}) {
|
|
@@ -4581,8 +5427,8 @@ function parseConfig(raw, options = {}) {
|
|
|
4581
5427
|
if ("permissionMode" in transport && transport.permissionMode !== "approve-all" && transport.permissionMode !== "approve-reads" && transport.permissionMode !== "deny-all") {
|
|
4582
5428
|
throw new Error("transport.permissionMode must be approve-all, approve-reads, or deny-all");
|
|
4583
5429
|
}
|
|
4584
|
-
if ("nonInteractivePermissions" in transport && transport.nonInteractivePermissions !== "
|
|
4585
|
-
throw new Error("transport.nonInteractivePermissions must be
|
|
5430
|
+
if ("nonInteractivePermissions" in transport && transport.nonInteractivePermissions !== "deny" && transport.nonInteractivePermissions !== "fail") {
|
|
5431
|
+
throw new Error("transport.nonInteractivePermissions must be deny or fail");
|
|
4586
5432
|
}
|
|
4587
5433
|
if (!isRecord(raw.agents)) {
|
|
4588
5434
|
throw new Error("agents must be an object");
|
|
@@ -4591,9 +5437,13 @@ function parseConfig(raw, options = {}) {
|
|
|
4591
5437
|
throw new Error("workspaces must be an object");
|
|
4592
5438
|
}
|
|
4593
5439
|
const logging = raw.logging;
|
|
5440
|
+
const wechat = raw.wechat;
|
|
4594
5441
|
if (logging !== undefined && !isRecord(logging)) {
|
|
4595
5442
|
throw new Error("logging must be an object");
|
|
4596
5443
|
}
|
|
5444
|
+
if (wechat !== undefined && !isRecord(wechat)) {
|
|
5445
|
+
throw new Error("wechat must be an object");
|
|
5446
|
+
}
|
|
4597
5447
|
if (isRecord(logging) && "level" in logging && logging.level !== "error" && logging.level !== "info" && logging.level !== "debug") {
|
|
4598
5448
|
throw new Error("logging.level must be error, info, or debug");
|
|
4599
5449
|
}
|
|
@@ -4602,6 +5452,9 @@ function parseConfig(raw, options = {}) {
|
|
|
4602
5452
|
throw new Error(`logging.${field} must be a positive number`);
|
|
4603
5453
|
}
|
|
4604
5454
|
}
|
|
5455
|
+
if (isRecord(wechat) && "replyMode" in wechat && wechat.replyMode !== "stream" && wechat.replyMode !== "final") {
|
|
5456
|
+
throw new Error("wechat.replyMode must be stream or final");
|
|
5457
|
+
}
|
|
4605
5458
|
for (const [name, agent] of Object.entries(raw.agents)) {
|
|
4606
5459
|
if (!isRecord(agent) || typeof agent.driver !== "string" || agent.driver.length === 0) {
|
|
4607
5460
|
throw new Error(`agent "${name}" must define a non-empty driver`);
|
|
@@ -4638,9 +5491,10 @@ function parseConfig(raw, options = {}) {
|
|
|
4638
5491
|
}
|
|
4639
5492
|
const transportType = transport.type === "acpx-cli" || transport.type === "acpx-bridge" ? transport.type : "acpx-bridge";
|
|
4640
5493
|
const permissionMode = transport.permissionMode === "approve-all" || transport.permissionMode === "approve-reads" || transport.permissionMode === "deny-all" ? transport.permissionMode : DEFAULT_PERMISSION_MODE;
|
|
4641
|
-
const nonInteractivePermissions = transport.nonInteractivePermissions === "
|
|
5494
|
+
const nonInteractivePermissions = transport.nonInteractivePermissions === "deny" || transport.nonInteractivePermissions === "fail" ? transport.nonInteractivePermissions : DEFAULT_NON_INTERACTIVE_PERMISSIONS;
|
|
4642
5495
|
const loggingLevel = logging?.level;
|
|
4643
5496
|
const resolvedLoggingLevel = loggingLevel === "error" || loggingLevel === "info" || loggingLevel === "debug" ? loggingLevel : options.defaultLoggingLevel ?? DEFAULT_LOGGING_CONFIG.level;
|
|
5497
|
+
const replyMode = wechat?.replyMode === "stream" || wechat?.replyMode === "final" ? wechat.replyMode : DEFAULT_WECHAT_REPLY_MODE;
|
|
4644
5498
|
return {
|
|
4645
5499
|
transport: {
|
|
4646
5500
|
...typeof transport.command === "string" ? { command: transport.command } : {},
|
|
@@ -4655,11 +5509,14 @@ function parseConfig(raw, options = {}) {
|
|
|
4655
5509
|
maxFiles: typeof logging?.maxFiles === "number" ? logging.maxFiles : DEFAULT_LOGGING_CONFIG.maxFiles,
|
|
4656
5510
|
retentionDays: typeof logging?.retentionDays === "number" ? logging.retentionDays : DEFAULT_LOGGING_CONFIG.retentionDays
|
|
4657
5511
|
},
|
|
5512
|
+
wechat: {
|
|
5513
|
+
replyMode
|
|
5514
|
+
},
|
|
4658
5515
|
agents,
|
|
4659
5516
|
workspaces
|
|
4660
5517
|
};
|
|
4661
5518
|
}
|
|
4662
|
-
var DEFAULT_LOGGING_CONFIG, DEFAULT_PERMISSION_MODE = "approve-all", DEFAULT_NON_INTERACTIVE_PERMISSIONS = "
|
|
5519
|
+
var DEFAULT_LOGGING_CONFIG, DEFAULT_PERMISSION_MODE = "approve-all", DEFAULT_NON_INTERACTIVE_PERMISSIONS = "deny", DEFAULT_WECHAT_REPLY_MODE = "stream";
|
|
4663
5520
|
var init_load_config = __esm(() => {
|
|
4664
5521
|
DEFAULT_LOGGING_CONFIG = {
|
|
4665
5522
|
level: "info",
|
|
@@ -4670,8 +5527,8 @@ var init_load_config = __esm(() => {
|
|
|
4670
5527
|
});
|
|
4671
5528
|
|
|
4672
5529
|
// src/config/config-store.ts
|
|
4673
|
-
import { mkdir as
|
|
4674
|
-
import { dirname as
|
|
5530
|
+
import { mkdir as mkdir7, writeFile as writeFile5 } from "node:fs/promises";
|
|
5531
|
+
import { dirname as dirname6 } from "node:path";
|
|
4675
5532
|
|
|
4676
5533
|
class ConfigStore {
|
|
4677
5534
|
path;
|
|
@@ -4682,8 +5539,8 @@ class ConfigStore {
|
|
|
4682
5539
|
return await loadConfig(this.path);
|
|
4683
5540
|
}
|
|
4684
5541
|
async save(config) {
|
|
4685
|
-
await
|
|
4686
|
-
await
|
|
5542
|
+
await mkdir7(dirname6(this.path), { recursive: true });
|
|
5543
|
+
await writeFile5(this.path, `${JSON.stringify(config, null, 2)}
|
|
4687
5544
|
`, "utf8");
|
|
4688
5545
|
}
|
|
4689
5546
|
async upsertWorkspace(name, cwd, description) {
|
|
@@ -4723,13 +5580,22 @@ class ConfigStore {
|
|
|
4723
5580
|
await this.save(config);
|
|
4724
5581
|
return config;
|
|
4725
5582
|
}
|
|
5583
|
+
async updateWechat(wechat) {
|
|
5584
|
+
const config = await this.load();
|
|
5585
|
+
config.wechat = {
|
|
5586
|
+
...config.wechat,
|
|
5587
|
+
...wechat
|
|
5588
|
+
};
|
|
5589
|
+
await this.save(config);
|
|
5590
|
+
return config;
|
|
5591
|
+
}
|
|
4726
5592
|
}
|
|
4727
5593
|
var init_config_store = __esm(() => {
|
|
4728
5594
|
init_load_config();
|
|
4729
5595
|
});
|
|
4730
5596
|
|
|
4731
5597
|
// src/config/ensure-config.ts
|
|
4732
|
-
import { readFile as
|
|
5598
|
+
import { readFile as readFile6 } from "node:fs/promises";
|
|
4733
5599
|
async function ensureConfigExists(path11) {
|
|
4734
5600
|
try {
|
|
4735
5601
|
await loadConfig(path11);
|
|
@@ -4743,7 +5609,10 @@ async function ensureConfigExists(path11) {
|
|
|
4743
5609
|
}
|
|
4744
5610
|
async function loadDefaultConfigTemplate() {
|
|
4745
5611
|
const templatePath = new URL("../../config.example.json", import.meta.url);
|
|
4746
|
-
|
|
5612
|
+
return normalizeDefaultConfigTemplate(JSON.parse(await readFile6(templatePath, "utf8")));
|
|
5613
|
+
}
|
|
5614
|
+
function normalizeDefaultConfigTemplate(raw) {
|
|
5615
|
+
const template = parseConfig(raw);
|
|
4747
5616
|
return {
|
|
4748
5617
|
...template,
|
|
4749
5618
|
agents: Object.fromEntries(Object.entries(template.agents).map(([name, agent]) => [
|
|
@@ -4888,6 +5757,23 @@ class SessionService {
|
|
|
4888
5757
|
session.last_used_at = new Date().toISOString();
|
|
4889
5758
|
await this.persist();
|
|
4890
5759
|
}
|
|
5760
|
+
async setCurrentSessionReplyMode(chatKey, replyMode) {
|
|
5761
|
+
const currentAlias = this.state.chat_contexts[chatKey]?.current_session;
|
|
5762
|
+
if (!currentAlias) {
|
|
5763
|
+
throw new Error("no current session selected");
|
|
5764
|
+
}
|
|
5765
|
+
const session = this.state.sessions[currentAlias];
|
|
5766
|
+
if (!session) {
|
|
5767
|
+
throw new Error("no current session selected");
|
|
5768
|
+
}
|
|
5769
|
+
if (replyMode) {
|
|
5770
|
+
session.reply_mode = replyMode;
|
|
5771
|
+
} else {
|
|
5772
|
+
delete session.reply_mode;
|
|
5773
|
+
}
|
|
5774
|
+
session.last_used_at = new Date().toISOString();
|
|
5775
|
+
await this.persist();
|
|
5776
|
+
}
|
|
4891
5777
|
async getCurrentSession(chatKey) {
|
|
4892
5778
|
const currentAlias = this.state.chat_contexts[chatKey]?.current_session;
|
|
4893
5779
|
if (!currentAlias) {
|
|
@@ -4912,6 +5798,13 @@ class SessionService {
|
|
|
4912
5798
|
}
|
|
4913
5799
|
toResolvedSession(session) {
|
|
4914
5800
|
const agentConfig = this.config.agents[session.agent];
|
|
5801
|
+
if (!agentConfig) {
|
|
5802
|
+
throw new Error(`session "${session.alias}" references agent "${session.agent}", but that agent is no longer registered`);
|
|
5803
|
+
}
|
|
5804
|
+
const workspaceConfig = this.config.workspaces[session.workspace];
|
|
5805
|
+
if (!workspaceConfig) {
|
|
5806
|
+
throw new Error(`session "${session.alias}" references workspace "${session.workspace}", but that workspace is no longer registered`);
|
|
5807
|
+
}
|
|
4915
5808
|
return {
|
|
4916
5809
|
alias: session.alias,
|
|
4917
5810
|
agent: session.agent,
|
|
@@ -4919,7 +5812,8 @@ class SessionService {
|
|
|
4919
5812
|
workspace: session.workspace,
|
|
4920
5813
|
transportSession: session.transport_session,
|
|
4921
5814
|
modeId: session.mode_id,
|
|
4922
|
-
|
|
5815
|
+
replyMode: session.reply_mode,
|
|
5816
|
+
cwd: workspaceConfig.cwd
|
|
4923
5817
|
};
|
|
4924
5818
|
}
|
|
4925
5819
|
async setSessionTransportAgentCommand(alias, transportAgentCommand) {
|
|
@@ -4951,6 +5845,7 @@ class SessionService {
|
|
|
4951
5845
|
transport_session: transportSession,
|
|
4952
5846
|
...normalizedTransportAgentCommand ? { transport_agent_command: normalizedTransportAgentCommand } : existingSession?.transport_agent_command ? { transport_agent_command: existingSession.transport_agent_command } : {},
|
|
4953
5847
|
mode_id: existingSession?.mode_id,
|
|
5848
|
+
reply_mode: existingSession?.reply_mode,
|
|
4954
5849
|
created_at: existingSession?.created_at ?? now,
|
|
4955
5850
|
last_used_at: now
|
|
4956
5851
|
};
|
|
@@ -4959,6 +5854,15 @@ class SessionService {
|
|
|
4959
5854
|
return this.toResolvedSession(session);
|
|
4960
5855
|
}
|
|
4961
5856
|
validateSession(alias, agent, workspace) {
|
|
5857
|
+
if (alias.trim().length === 0) {
|
|
5858
|
+
throw new Error("session alias must be a non-empty string");
|
|
5859
|
+
}
|
|
5860
|
+
if (agent.trim().length === 0) {
|
|
5861
|
+
throw new Error("agent must be a non-empty string");
|
|
5862
|
+
}
|
|
5863
|
+
if (workspace.trim().length === 0) {
|
|
5864
|
+
throw new Error("workspace must be a non-empty string");
|
|
5865
|
+
}
|
|
4962
5866
|
if (!this.config.workspaces[workspace]) {
|
|
4963
5867
|
throw new Error(`workspace "${workspace}" is not registered`);
|
|
4964
5868
|
}
|
|
@@ -4978,8 +5882,28 @@ function createEmptyState() {
|
|
|
4978
5882
|
}
|
|
4979
5883
|
|
|
4980
5884
|
// src/state/state-store.ts
|
|
4981
|
-
import { mkdir as
|
|
4982
|
-
import { dirname as
|
|
5885
|
+
import { mkdir as mkdir8, readFile as readFile7, writeFile as writeFile6 } from "node:fs/promises";
|
|
5886
|
+
import { dirname as dirname7 } from "node:path";
|
|
5887
|
+
function isRecord2(value) {
|
|
5888
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5889
|
+
}
|
|
5890
|
+
function parseState(raw, path11) {
|
|
5891
|
+
if (!isRecord2(raw)) {
|
|
5892
|
+
throw new Error(`state file "${path11}" must contain a JSON object`);
|
|
5893
|
+
}
|
|
5894
|
+
const sessions = raw.sessions;
|
|
5895
|
+
if (!isRecord2(sessions)) {
|
|
5896
|
+
throw new Error(`state file "${path11}" must contain an object field "sessions"`);
|
|
5897
|
+
}
|
|
5898
|
+
const chatContexts = raw.chat_contexts;
|
|
5899
|
+
if (!isRecord2(chatContexts)) {
|
|
5900
|
+
throw new Error(`state file "${path11}" must contain an object field "chat_contexts"`);
|
|
5901
|
+
}
|
|
5902
|
+
return {
|
|
5903
|
+
sessions,
|
|
5904
|
+
chat_contexts: chatContexts
|
|
5905
|
+
};
|
|
5906
|
+
}
|
|
4983
5907
|
|
|
4984
5908
|
class StateStore {
|
|
4985
5909
|
path;
|
|
@@ -4988,11 +5912,19 @@ class StateStore {
|
|
|
4988
5912
|
}
|
|
4989
5913
|
async load() {
|
|
4990
5914
|
try {
|
|
4991
|
-
const content = await
|
|
5915
|
+
const content = await readFile7(this.path, "utf8");
|
|
4992
5916
|
if (content.trim() === "") {
|
|
4993
5917
|
return createEmptyState();
|
|
4994
5918
|
}
|
|
4995
|
-
|
|
5919
|
+
let parsed;
|
|
5920
|
+
try {
|
|
5921
|
+
parsed = JSON.parse(content);
|
|
5922
|
+
} catch (error) {
|
|
5923
|
+
throw new Error(`failed to parse state file "${this.path}"`, {
|
|
5924
|
+
cause: error
|
|
5925
|
+
});
|
|
5926
|
+
}
|
|
5927
|
+
return parseState(parsed, this.path);
|
|
4996
5928
|
} catch (error) {
|
|
4997
5929
|
if (error.code === "ENOENT") {
|
|
4998
5930
|
return createEmptyState();
|
|
@@ -5001,8 +5933,8 @@ class StateStore {
|
|
|
5001
5933
|
}
|
|
5002
5934
|
}
|
|
5003
5935
|
async save(state) {
|
|
5004
|
-
await
|
|
5005
|
-
await
|
|
5936
|
+
await mkdir8(dirname7(this.path), { recursive: true });
|
|
5937
|
+
await writeFile6(this.path, JSON.stringify(state, null, 2));
|
|
5006
5938
|
}
|
|
5007
5939
|
}
|
|
5008
5940
|
var init_state_store = () => {};
|
|
@@ -5014,12 +5946,17 @@ __export(exports_run_console, {
|
|
|
5014
5946
|
});
|
|
5015
5947
|
async function runConsole(paths, deps) {
|
|
5016
5948
|
const runtime = await deps.buildApp(paths);
|
|
5949
|
+
const consumerLock = deps.consumerLock ?? deps.consumerLockFactory?.(runtime);
|
|
5017
5950
|
const sdk = await deps.loadWeixinSdk();
|
|
5018
5951
|
const setIntervalFn = deps.setInterval ?? ((fn, delay) => setInterval(fn, delay));
|
|
5019
5952
|
const clearIntervalFn = deps.clearInterval ?? ((timer) => clearInterval(timer));
|
|
5020
5953
|
const addProcessListener = deps.addProcessListener ?? ((signal, handler) => process.on(signal, handler));
|
|
5021
5954
|
const removeProcessListener = deps.removeProcessListener ?? ((signal, handler) => process.off(signal, handler));
|
|
5955
|
+
const processPid = deps.processPid ?? process.pid;
|
|
5956
|
+
const now = deps.now ?? (() => new Date().toISOString());
|
|
5957
|
+
const hostname = deps.hostname ?? (() => "");
|
|
5022
5958
|
let heartbeatTimer = null;
|
|
5959
|
+
let consumerLockAcquired = false;
|
|
5023
5960
|
const shutdownController = new AbortController;
|
|
5024
5961
|
const signalHandler = () => {
|
|
5025
5962
|
shutdownController.abort();
|
|
@@ -5036,6 +5973,53 @@ async function runConsole(paths, deps) {
|
|
|
5036
5973
|
deps.daemonRuntime?.heartbeat().catch(() => {});
|
|
5037
5974
|
}, deps.heartbeatIntervalMs ?? 30000);
|
|
5038
5975
|
}
|
|
5976
|
+
if (consumerLock) {
|
|
5977
|
+
const lockMeta = {
|
|
5978
|
+
pid: processPid,
|
|
5979
|
+
mode: deps.daemonRuntime ? "daemon" : "foreground",
|
|
5980
|
+
startedAt: now(),
|
|
5981
|
+
configPath: paths.configPath,
|
|
5982
|
+
statePath: paths.statePath,
|
|
5983
|
+
hostname: hostname() || undefined
|
|
5984
|
+
};
|
|
5985
|
+
await runtime.logger.info("weixin.consumer_lock.acquire_attempt", "attempting to acquire weixin consumer lock", {
|
|
5986
|
+
pid: lockMeta.pid,
|
|
5987
|
+
mode: lockMeta.mode,
|
|
5988
|
+
configPath: lockMeta.configPath,
|
|
5989
|
+
statePath: lockMeta.statePath,
|
|
5990
|
+
hostname: lockMeta.hostname
|
|
5991
|
+
});
|
|
5992
|
+
try {
|
|
5993
|
+
await consumerLock.acquire(lockMeta);
|
|
5994
|
+
consumerLockAcquired = true;
|
|
5995
|
+
await runtime.logger.info("weixin.consumer_lock.acquired", "acquired weixin consumer lock", {
|
|
5996
|
+
pid: lockMeta.pid,
|
|
5997
|
+
mode: lockMeta.mode,
|
|
5998
|
+
configPath: lockMeta.configPath,
|
|
5999
|
+
statePath: lockMeta.statePath
|
|
6000
|
+
});
|
|
6001
|
+
} catch (error) {
|
|
6002
|
+
if (error instanceof ActiveWeixinConsumerLockError) {
|
|
6003
|
+
await runtime.logger.error("weixin.consumer_lock.acquire_failed", "weixin consumer lock is already held by another process", {
|
|
6004
|
+
conflictType: "active_lock_holder",
|
|
6005
|
+
activePid: error.existing.pid,
|
|
6006
|
+
activeMode: error.existing.mode,
|
|
6007
|
+
activeConfigPath: error.existing.configPath,
|
|
6008
|
+
activeStatePath: error.existing.statePath,
|
|
6009
|
+
requestedPid: lockMeta.pid,
|
|
6010
|
+
requestedMode: lockMeta.mode
|
|
6011
|
+
});
|
|
6012
|
+
} else {
|
|
6013
|
+
await runtime.logger.error("weixin.consumer_lock.acquire_failed", "failed to acquire weixin consumer lock", {
|
|
6014
|
+
conflictType: deps.daemonRuntime ? "daemon_startup_lock_failure" : "foreground_startup_lock_failure",
|
|
6015
|
+
requestedPid: lockMeta.pid,
|
|
6016
|
+
requestedMode: lockMeta.mode,
|
|
6017
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6018
|
+
});
|
|
6019
|
+
}
|
|
6020
|
+
throw error;
|
|
6021
|
+
}
|
|
6022
|
+
}
|
|
5039
6023
|
if (!sdk.isLoggedIn()) {
|
|
5040
6024
|
console.log("[weacpx] 未检测到登录凭证,正在启动扫码登录...");
|
|
5041
6025
|
await sdk.login();
|
|
@@ -5056,17 +6040,30 @@ async function runConsole(paths, deps) {
|
|
|
5056
6040
|
if (deps.daemonRuntime) {
|
|
5057
6041
|
await deps.daemonRuntime.stop();
|
|
5058
6042
|
}
|
|
6043
|
+
if (consumerLockAcquired) {
|
|
6044
|
+
await consumerLock?.release();
|
|
6045
|
+
await runtime.logger.info("weixin.consumer_lock.released", "released weixin consumer lock", {
|
|
6046
|
+
pid: processPid
|
|
6047
|
+
});
|
|
6048
|
+
}
|
|
5059
6049
|
if (disposeError) {
|
|
5060
6050
|
throw disposeError;
|
|
5061
6051
|
}
|
|
5062
6052
|
}
|
|
5063
6053
|
}
|
|
6054
|
+
var init_run_console = __esm(() => {
|
|
6055
|
+
init_consumer_lock();
|
|
6056
|
+
});
|
|
5064
6057
|
|
|
5065
6058
|
// src/transport/acpx-bridge/acpx-bridge-protocol.ts
|
|
5066
6059
|
function encodeBridgeRequest(request) {
|
|
5067
6060
|
return `${JSON.stringify(request)}
|
|
5068
6061
|
`;
|
|
5069
6062
|
}
|
|
6063
|
+
function encodeBridgePromptSegmentEvent(event) {
|
|
6064
|
+
return `${JSON.stringify(event)}
|
|
6065
|
+
`;
|
|
6066
|
+
}
|
|
5070
6067
|
|
|
5071
6068
|
// src/transport/acpx-bridge/acpx-bridge-client.ts
|
|
5072
6069
|
import { spawn as spawn2 } from "node:child_process";
|
|
@@ -5077,30 +6074,59 @@ class AcpxBridgeClient {
|
|
|
5077
6074
|
writeLine;
|
|
5078
6075
|
nextId = 1;
|
|
5079
6076
|
pending = new Map;
|
|
6077
|
+
terminalError = null;
|
|
5080
6078
|
constructor(writeLine) {
|
|
5081
6079
|
this.writeLine = writeLine;
|
|
5082
6080
|
}
|
|
5083
|
-
request(method, params) {
|
|
6081
|
+
request(method, params, onEvent) {
|
|
6082
|
+
if (this.terminalError) {
|
|
6083
|
+
return Promise.reject(this.terminalError);
|
|
6084
|
+
}
|
|
5084
6085
|
const id = String(this.nextId);
|
|
5085
6086
|
this.nextId += 1;
|
|
5086
6087
|
return awaitable((resolve2, reject) => {
|
|
5087
6088
|
this.pending.set(id, {
|
|
5088
6089
|
resolve: (value) => resolve2(value),
|
|
5089
|
-
reject
|
|
6090
|
+
reject,
|
|
6091
|
+
onEvent
|
|
5090
6092
|
});
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
6093
|
+
try {
|
|
6094
|
+
const didWrite = this.writeLine(encodeBridgeRequest({
|
|
6095
|
+
id,
|
|
6096
|
+
method,
|
|
6097
|
+
params
|
|
6098
|
+
}));
|
|
6099
|
+
if (didWrite === false) {
|
|
6100
|
+
this.pending.delete(id);
|
|
6101
|
+
reject(new Error("bridge write buffer is full"));
|
|
6102
|
+
}
|
|
6103
|
+
} catch (error) {
|
|
6104
|
+
this.pending.delete(id);
|
|
6105
|
+
reject(error);
|
|
6106
|
+
}
|
|
5096
6107
|
});
|
|
5097
6108
|
}
|
|
5098
6109
|
handleLine(line) {
|
|
5099
|
-
|
|
5100
|
-
|
|
6110
|
+
let message;
|
|
6111
|
+
try {
|
|
6112
|
+
message = JSON.parse(line);
|
|
6113
|
+
} catch {
|
|
6114
|
+
return;
|
|
6115
|
+
}
|
|
6116
|
+
const pending = this.pending.get(message.id);
|
|
5101
6117
|
if (!pending) {
|
|
5102
6118
|
return;
|
|
5103
6119
|
}
|
|
6120
|
+
if ("event" in message) {
|
|
6121
|
+
if (message.event === "prompt.segment") {
|
|
6122
|
+
pending.onEvent?.({
|
|
6123
|
+
type: "prompt.segment",
|
|
6124
|
+
text: message.text
|
|
6125
|
+
});
|
|
6126
|
+
}
|
|
6127
|
+
return;
|
|
6128
|
+
}
|
|
6129
|
+
const response = message;
|
|
5104
6130
|
this.pending.delete(response.id);
|
|
5105
6131
|
if (response.ok) {
|
|
5106
6132
|
pending.resolve(response.result);
|
|
@@ -5117,6 +6143,7 @@ class AcpxBridgeClient {
|
|
|
5117
6143
|
pending.reject(new Error(response.error.message));
|
|
5118
6144
|
}
|
|
5119
6145
|
handleExit(error) {
|
|
6146
|
+
this.terminalError = error;
|
|
5120
6147
|
const pendingRequests = [...this.pending.values()];
|
|
5121
6148
|
this.pending.clear();
|
|
5122
6149
|
for (const pending of pendingRequests) {
|
|
@@ -5148,13 +6175,11 @@ async function spawnAcpxBridgeClient(options = {}) {
|
|
|
5148
6175
|
...process.env,
|
|
5149
6176
|
WEACPX_BRIDGE_ACPX_COMMAND: options.acpxCommand ?? "acpx",
|
|
5150
6177
|
WEACPX_BRIDGE_PERMISSION_MODE: options.permissionMode ?? "approve-all",
|
|
5151
|
-
WEACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS: options.nonInteractivePermissions ?? "
|
|
6178
|
+
WEACPX_BRIDGE_NON_INTERACTIVE_PERMISSIONS: options.nonInteractivePermissions ?? "deny"
|
|
5152
6179
|
},
|
|
5153
6180
|
stdio: ["pipe", "pipe", "inherit"]
|
|
5154
6181
|
});
|
|
5155
|
-
const client = new AcpxBridgeClient((line) =>
|
|
5156
|
-
child.stdin.write(line);
|
|
5157
|
-
});
|
|
6182
|
+
const client = new AcpxBridgeClient((line) => child.stdin.write(line));
|
|
5158
6183
|
const output = createInterface({
|
|
5159
6184
|
input: child.stdout,
|
|
5160
6185
|
crlfDelay: Infinity
|
|
@@ -5201,10 +6226,14 @@ class AcpxBridgeTransport {
|
|
|
5201
6226
|
async ensureSession(session) {
|
|
5202
6227
|
await this.client.request("ensureSession", this.toParams(session));
|
|
5203
6228
|
}
|
|
5204
|
-
async prompt(session, text,
|
|
6229
|
+
async prompt(session, text, reply) {
|
|
5205
6230
|
return await this.client.request("prompt", {
|
|
5206
6231
|
...this.toParams(session),
|
|
5207
6232
|
text
|
|
6233
|
+
}, (event) => {
|
|
6234
|
+
if (event.type === "prompt.segment") {
|
|
6235
|
+
reply?.(event.text);
|
|
6236
|
+
}
|
|
5208
6237
|
});
|
|
5209
6238
|
}
|
|
5210
6239
|
async setMode(session, modeId) {
|
|
@@ -5220,8 +6249,8 @@ class AcpxBridgeTransport {
|
|
|
5220
6249
|
const result = await this.client.request("hasSession", this.toParams(session));
|
|
5221
6250
|
return result.exists;
|
|
5222
6251
|
}
|
|
5223
|
-
async
|
|
5224
|
-
|
|
6252
|
+
async updatePermissionPolicy(policy) {
|
|
6253
|
+
await this.client.request("updatePermissionPolicy", { ...policy });
|
|
5225
6254
|
}
|
|
5226
6255
|
async dispose() {
|
|
5227
6256
|
await this.client.dispose?.();
|
|
@@ -5311,12 +6340,12 @@ function parseStreamingChunks(state, line) {
|
|
|
5311
6340
|
|
|
5312
6341
|
// src/transport/acpx-cli/node-pty-helper.ts
|
|
5313
6342
|
import { chmod as chmodFs } from "node:fs/promises";
|
|
5314
|
-
import { dirname as
|
|
6343
|
+
import { dirname as dirname8, join as join4 } from "node:path";
|
|
5315
6344
|
function resolveNodePtyHelperPath(packageJsonPath, platform, arch) {
|
|
5316
6345
|
if (platform === "win32") {
|
|
5317
6346
|
return null;
|
|
5318
6347
|
}
|
|
5319
|
-
return
|
|
6348
|
+
return join4(dirname8(packageJsonPath), "prebuilds", `${platform}-${arch}`, "spawn-helper");
|
|
5320
6349
|
}
|
|
5321
6350
|
async function ensureNodePtyHelperExecutable(helperPath, chmod = chmodFs) {
|
|
5322
6351
|
if (!helperPath) {
|
|
@@ -5404,7 +6433,7 @@ class AcpxCliTransport {
|
|
|
5404
6433
|
this.command = options.command ?? "acpx";
|
|
5405
6434
|
this.sessionInitTimeoutMs = options.sessionInitTimeoutMs ?? 120000;
|
|
5406
6435
|
this.permissionMode = options.permissionMode ?? "approve-all";
|
|
5407
|
-
this.nonInteractivePermissions = options.nonInteractivePermissions ?? "
|
|
6436
|
+
this.nonInteractivePermissions = options.nonInteractivePermissions ?? "deny";
|
|
5408
6437
|
this.runCommand = runCommand;
|
|
5409
6438
|
this.runPtyCommand = runPtyCommand;
|
|
5410
6439
|
}
|
|
@@ -5448,6 +6477,10 @@ class AcpxCliTransport {
|
|
|
5448
6477
|
message: output.trim()
|
|
5449
6478
|
};
|
|
5450
6479
|
}
|
|
6480
|
+
async updatePermissionPolicy(policy) {
|
|
6481
|
+
this.permissionMode = policy.permissionMode;
|
|
6482
|
+
this.nonInteractivePermissions = policy.nonInteractivePermissions;
|
|
6483
|
+
}
|
|
5451
6484
|
async hasSession(session) {
|
|
5452
6485
|
const result = await this.runCommand(this.command, this.buildArgs(session, [
|
|
5453
6486
|
"sessions",
|
|
@@ -5456,9 +6489,6 @@ class AcpxCliTransport {
|
|
|
5456
6489
|
]));
|
|
5457
6490
|
return result.code === 0;
|
|
5458
6491
|
}
|
|
5459
|
-
async listSessions() {
|
|
5460
|
-
return [];
|
|
5461
|
-
}
|
|
5462
6492
|
async run(args, options) {
|
|
5463
6493
|
const result = await this.runCommandWithTimeout(this.runCommand, args, options);
|
|
5464
6494
|
if (result.code !== 0) {
|
|
@@ -5607,8 +6637,8 @@ __export(exports_main, {
|
|
|
5607
6637
|
main: () => main2,
|
|
5608
6638
|
buildApp: () => buildApp
|
|
5609
6639
|
});
|
|
5610
|
-
import { homedir as
|
|
5611
|
-
import { dirname as
|
|
6640
|
+
import { homedir as homedir5 } from "node:os";
|
|
6641
|
+
import { dirname as dirname9, join as join5 } from "node:path";
|
|
5612
6642
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
5613
6643
|
async function buildApp(paths, deps = {}) {
|
|
5614
6644
|
await ensureConfigExists(paths.configPath);
|
|
@@ -5670,7 +6700,7 @@ async function main2() {
|
|
|
5670
6700
|
}
|
|
5671
6701
|
}
|
|
5672
6702
|
function resolveRuntimePaths() {
|
|
5673
|
-
const home = process.env.HOME ??
|
|
6703
|
+
const home = process.env.HOME ?? homedir5();
|
|
5674
6704
|
if (!home) {
|
|
5675
6705
|
throw new Error("Unable to resolve the current user home directory");
|
|
5676
6706
|
}
|
|
@@ -5686,9 +6716,9 @@ function resolveBridgeEntryPath() {
|
|
|
5686
6716
|
return fileURLToPath3(new URL("./bridge/bridge-main.ts", import.meta.url));
|
|
5687
6717
|
}
|
|
5688
6718
|
function resolveAppLogPath(configPath) {
|
|
5689
|
-
const rootDir =
|
|
5690
|
-
const runtimeDir =
|
|
5691
|
-
return
|
|
6719
|
+
const rootDir = dirname9(configPath);
|
|
6720
|
+
const runtimeDir = join5(rootDir, "runtime");
|
|
6721
|
+
return join5(runtimeDir, "app.log");
|
|
5692
6722
|
}
|
|
5693
6723
|
var init_main = __esm(async () => {
|
|
5694
6724
|
init_command_router();
|
|
@@ -5700,6 +6730,7 @@ var init_main = __esm(async () => {
|
|
|
5700
6730
|
init_app_logger();
|
|
5701
6731
|
init_session_service();
|
|
5702
6732
|
init_state_store();
|
|
6733
|
+
init_run_console();
|
|
5703
6734
|
init_acpx_bridge_client();
|
|
5704
6735
|
init_acpx_cli_transport();
|
|
5705
6736
|
init_weixin_sdk();
|
|
@@ -5707,7 +6738,7 @@ var init_main = __esm(async () => {
|
|
|
5707
6738
|
});
|
|
5708
6739
|
|
|
5709
6740
|
// src/cli.ts
|
|
5710
|
-
import { homedir as
|
|
6741
|
+
import { homedir as homedir6 } from "node:os";
|
|
5711
6742
|
import { sep } from "node:path";
|
|
5712
6743
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
5713
6744
|
|
|
@@ -5788,7 +6819,7 @@ class DaemonController {
|
|
|
5788
6819
|
return { state: "stopped", stale: true };
|
|
5789
6820
|
}
|
|
5790
6821
|
if (!status) {
|
|
5791
|
-
return { state: "
|
|
6822
|
+
return { state: "indeterminate", pid, reason: "missing-status" };
|
|
5792
6823
|
}
|
|
5793
6824
|
return {
|
|
5794
6825
|
state: "running",
|
|
@@ -5801,6 +6832,9 @@ class DaemonController {
|
|
|
5801
6832
|
if (current.state === "running") {
|
|
5802
6833
|
return { state: "already-running", pid: current.pid };
|
|
5803
6834
|
}
|
|
6835
|
+
if (current.state === "indeterminate") {
|
|
6836
|
+
throw new Error(`weacpx daemon process is already running (pid ${current.pid}) but status metadata is missing`);
|
|
6837
|
+
}
|
|
5804
6838
|
await this.statusStore.clear();
|
|
5805
6839
|
const pid = await this.deps.spawnDetached();
|
|
5806
6840
|
await this.writePid(pid);
|
|
@@ -6008,27 +7042,30 @@ async function spawnWindowsHiddenProcess(request) {
|
|
|
6008
7042
|
async function defaultTerminateProcess(pid) {
|
|
6009
7043
|
await terminateProcessTree(pid);
|
|
6010
7044
|
}
|
|
6011
|
-
async function terminateProcessTree(pid, platform = process.platform, runCommand = defaultRunProcessCommand) {
|
|
7045
|
+
async function terminateProcessTree(pid, platform = process.platform, runCommand = defaultRunProcessCommand, killProcess = (targetPid, signal) => {
|
|
7046
|
+
process.kill(targetPid, signal);
|
|
7047
|
+
}, isProcessRunning = defaultIsProcessRunning) {
|
|
6012
7048
|
if (platform === "win32") {
|
|
6013
7049
|
try {
|
|
6014
7050
|
await runCommand("taskkill", ["/PID", String(pid), "/T", "/F"]);
|
|
6015
7051
|
} catch {}
|
|
6016
7052
|
return;
|
|
6017
7053
|
}
|
|
7054
|
+
const targetPid = pid > 0 ? -pid : pid;
|
|
6018
7055
|
try {
|
|
6019
|
-
|
|
7056
|
+
killProcess(targetPid, "SIGTERM");
|
|
6020
7057
|
} catch {
|
|
6021
7058
|
return;
|
|
6022
7059
|
}
|
|
6023
7060
|
const deadline = Date.now() + 5000;
|
|
6024
7061
|
while (Date.now() < deadline) {
|
|
6025
|
-
if (!
|
|
7062
|
+
if (!isProcessRunning(targetPid)) {
|
|
6026
7063
|
return;
|
|
6027
7064
|
}
|
|
6028
7065
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
6029
7066
|
}
|
|
6030
7067
|
try {
|
|
6031
|
-
|
|
7068
|
+
killProcess(targetPid, "SIGKILL");
|
|
6032
7069
|
} catch {}
|
|
6033
7070
|
}
|
|
6034
7071
|
async function defaultRunProcessCommand(command, args) {
|
|
@@ -6103,6 +7140,7 @@ class DaemonRuntime {
|
|
|
6103
7140
|
}
|
|
6104
7141
|
|
|
6105
7142
|
// src/cli.ts
|
|
7143
|
+
init_consumer_lock();
|
|
6106
7144
|
var HELP_LINES = [
|
|
6107
7145
|
"用法:",
|
|
6108
7146
|
"weacpx login - 微信登录",
|
|
@@ -6139,6 +7177,11 @@ async function runCli(args, deps = {}) {
|
|
|
6139
7177
|
}
|
|
6140
7178
|
case "status": {
|
|
6141
7179
|
const status = await controller.getStatus();
|
|
7180
|
+
if (status.state === "indeterminate") {
|
|
7181
|
+
print("weacpx 进程仍在运行,但状态元数据缺失");
|
|
7182
|
+
print(`PID: ${status.pid}`);
|
|
7183
|
+
return 1;
|
|
7184
|
+
}
|
|
6142
7185
|
if (status.state !== "running") {
|
|
6143
7186
|
print("weacpx 未运行");
|
|
6144
7187
|
return 0;
|
|
@@ -6182,7 +7225,7 @@ async function defaultRun() {
|
|
|
6182
7225
|
const [{ buildApp: buildApp2, resolveRuntimePaths: resolveRuntimePaths2 }, { loadWeixinSdk: loadWeixinSdk2 }, { runConsole: runConsole2 }] = await Promise.all([
|
|
6183
7226
|
init_main().then(() => exports_main),
|
|
6184
7227
|
Promise.resolve().then(() => (init_weixin_sdk(), exports_weixin_sdk)),
|
|
6185
|
-
Promise.resolve().then(() => exports_run_console)
|
|
7228
|
+
Promise.resolve().then(() => (init_run_console(), exports_run_console))
|
|
6186
7229
|
]);
|
|
6187
7230
|
const runtimePaths = resolveRuntimePaths2();
|
|
6188
7231
|
const daemonPaths = resolveDaemonPaths({ home: requireHome() });
|
|
@@ -6192,7 +7235,13 @@ async function defaultRun() {
|
|
|
6192
7235
|
defaultLoggingLevel: resolveCliEntryPath().includes(`${sep}src${sep}`) ? "debug" : "info"
|
|
6193
7236
|
}),
|
|
6194
7237
|
loadWeixinSdk: loadWeixinSdk2,
|
|
6195
|
-
daemonRuntime
|
|
7238
|
+
daemonRuntime,
|
|
7239
|
+
consumerLockFactory: (runtime) => createWeixinConsumerLock({
|
|
7240
|
+
lockFilePath: `${daemonPaths.runtimeDir}${sep}weixin-consumer.lock.json`,
|
|
7241
|
+
onDiagnostic: async (event, context) => {
|
|
7242
|
+
await runtime.logger.info(`weixin.consumer_lock.${event}`, "weixin consumer lock diagnostic", context);
|
|
7243
|
+
}
|
|
7244
|
+
})
|
|
6196
7245
|
});
|
|
6197
7246
|
}
|
|
6198
7247
|
function createDefaultController() {
|
|
@@ -6205,7 +7254,7 @@ function createDefaultController() {
|
|
|
6205
7254
|
});
|
|
6206
7255
|
}
|
|
6207
7256
|
function requireHome() {
|
|
6208
|
-
const home = process.env.HOME ??
|
|
7257
|
+
const home = process.env.HOME ?? homedir6();
|
|
6209
7258
|
if (!home) {
|
|
6210
7259
|
throw new Error("Unable to resolve the current user home directory");
|
|
6211
7260
|
}
|