chatccc 0.2.287 → 0.2.288
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 +50 -46
- package/deepccc-agent/README.md +153 -151
- package/deepccc-agent/package.json +1 -1
- package/dist/src/adapters/codex-adapter.js +184 -179
- package/dist/src/adapters/codex-app-server.js +280 -0
- package/dist/src/config-utils.js +36 -0
- package/dist/src/config.js +34 -1
- package/dist/src/index.js +2 -1
- package/dist/src/orchestrator.js +6 -5
- package/dist/src/session-chat-binding.js +10 -4
- package/dist/src/session.js +15 -5
- package/package.json +1 -1
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// codex-app-server.ts — Codex CLI app-server 进程管理 + JSON-RPC 客户端
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// Codex CLI 的 app-server 模式:一个常驻进程托管多个 thread(会话),每个
|
|
5
|
+
// thread 内可有一个活跃 turn。相比 `codex exec` 一次性子进程,app-server 支持:
|
|
6
|
+
// - turn/steer : 运行中向当前 turn 注入新消息(协作式让位,对齐 ccc)
|
|
7
|
+
// - turn/interrupt : 打断当前 turn(不影响同进程内其他 thread)
|
|
8
|
+
// - thread/resume : 按 thread_id 从磁盘恢复持久化线程
|
|
9
|
+
//
|
|
10
|
+
// 本文件提供:
|
|
11
|
+
// - JsonRpcClient : WebSocket JSON-RPC 客户端
|
|
12
|
+
// - CodexAppServerManager : 单例常驻进程生命周期(懒启动 + 跨平台收尸)
|
|
13
|
+
//
|
|
14
|
+
// 协议字段名以 `codex app-server generate-ts` 导出的官方绑定为准:
|
|
15
|
+
// - thread/start 用 `sandbox: "danger-full-access"`(字符串枚举)
|
|
16
|
+
// - turn/start 用 `sandboxPolicy: { type: "dangerFullAccess" }`(对象)
|
|
17
|
+
// =============================================================================
|
|
18
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
19
|
+
import { createServer } from "node:net";
|
|
20
|
+
import WebSocket from "ws";
|
|
21
|
+
import { killProcessTree } from "./proc-tree-kill.js";
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// AsyncQueue — 把 WebSocket 通知流转换成 async iterable
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
export class AsyncQueue {
|
|
26
|
+
items = [];
|
|
27
|
+
waiters = [];
|
|
28
|
+
ended = false;
|
|
29
|
+
push(item) {
|
|
30
|
+
if (this.ended)
|
|
31
|
+
return;
|
|
32
|
+
const waiter = this.waiters.shift();
|
|
33
|
+
if (waiter)
|
|
34
|
+
waiter(item);
|
|
35
|
+
else
|
|
36
|
+
this.items.push(item);
|
|
37
|
+
}
|
|
38
|
+
end() {
|
|
39
|
+
if (this.ended)
|
|
40
|
+
return;
|
|
41
|
+
this.ended = true;
|
|
42
|
+
for (const waiter of this.waiters.splice(0))
|
|
43
|
+
waiter(null);
|
|
44
|
+
}
|
|
45
|
+
[Symbol.asyncIterator]() {
|
|
46
|
+
const queue = this;
|
|
47
|
+
return {
|
|
48
|
+
async next() {
|
|
49
|
+
if (queue.items.length > 0) {
|
|
50
|
+
return { value: queue.items.shift(), done: false };
|
|
51
|
+
}
|
|
52
|
+
if (queue.ended) {
|
|
53
|
+
return { value: undefined, done: true };
|
|
54
|
+
}
|
|
55
|
+
const value = await new Promise((resolve) => {
|
|
56
|
+
queue.waiters.push(resolve);
|
|
57
|
+
});
|
|
58
|
+
if (value === null) {
|
|
59
|
+
return { value: undefined, done: true };
|
|
60
|
+
}
|
|
61
|
+
return { value, done: false };
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export class JsonRpcClient {
|
|
67
|
+
ws;
|
|
68
|
+
nextId = 1;
|
|
69
|
+
pending = new Map();
|
|
70
|
+
onNotification = null;
|
|
71
|
+
onServerRequest = null;
|
|
72
|
+
onClose = null;
|
|
73
|
+
constructor(ws) {
|
|
74
|
+
this.ws = ws;
|
|
75
|
+
ws.on("message", (data) => this.handleMessage(data.toString()));
|
|
76
|
+
ws.on("close", () => this.handleClose());
|
|
77
|
+
ws.on("error", () => {
|
|
78
|
+
/* 关闭路径统一走 close 事件 */
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
static async connect(url) {
|
|
82
|
+
const ws = new WebSocket(url);
|
|
83
|
+
await new Promise((resolve, reject) => {
|
|
84
|
+
ws.once("open", () => resolve());
|
|
85
|
+
ws.once("error", (err) => reject(new Error(`codex app-server 连接失败: ${err.message}`)));
|
|
86
|
+
});
|
|
87
|
+
return new JsonRpcClient(ws);
|
|
88
|
+
}
|
|
89
|
+
handleMessage(raw) {
|
|
90
|
+
let msg;
|
|
91
|
+
try {
|
|
92
|
+
msg = JSON.parse(raw);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// 响应(有 id、无 method)→ resolve 对应 pending 请求
|
|
98
|
+
if (msg.id !== undefined && msg.method === undefined) {
|
|
99
|
+
const pendingReq = this.pending.get(msg.id);
|
|
100
|
+
if (pendingReq) {
|
|
101
|
+
this.pending.delete(msg.id);
|
|
102
|
+
clearTimeout(pendingReq.timer);
|
|
103
|
+
if (msg.error !== undefined && msg.error !== null) {
|
|
104
|
+
pendingReq.reject(new Error(JSON.stringify(msg.error)));
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
pendingReq.resolve(msg.result);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// 服务端请求(有 method + id)→ 需要客户端 respond
|
|
113
|
+
if (msg.method !== undefined && msg.id !== undefined) {
|
|
114
|
+
this.onServerRequest?.(msg.method, msg.params ?? {}, msg.id);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// 服务端通知(有 method、无 id)
|
|
118
|
+
if (msg.method !== undefined) {
|
|
119
|
+
this.onNotification?.(msg.method, msg.params ?? {});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
handleClose() {
|
|
123
|
+
const err = new Error("codex app-server 连接已关闭");
|
|
124
|
+
for (const [id, p] of this.pending) {
|
|
125
|
+
this.pending.delete(id);
|
|
126
|
+
clearTimeout(p.timer);
|
|
127
|
+
p.reject(err);
|
|
128
|
+
}
|
|
129
|
+
this.onClose?.();
|
|
130
|
+
}
|
|
131
|
+
request(method, params, timeoutMs = 120000) {
|
|
132
|
+
const id = this.nextId++;
|
|
133
|
+
this.ws.send(JSON.stringify({ method, id, params }));
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
const timer = setTimeout(() => {
|
|
136
|
+
this.pending.delete(id);
|
|
137
|
+
reject(new Error(`codex app-server 请求超时: ${method}`));
|
|
138
|
+
}, timeoutMs);
|
|
139
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
respond(id, result) {
|
|
143
|
+
this.ws.send(JSON.stringify({ id, result }));
|
|
144
|
+
}
|
|
145
|
+
respondError(id, code, message) {
|
|
146
|
+
this.ws.send(JSON.stringify({ id, error: { code, message } }));
|
|
147
|
+
}
|
|
148
|
+
close() {
|
|
149
|
+
try {
|
|
150
|
+
this.ws.close();
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
/* ignore */
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// 端口选择 / 就绪等待(跨平台)
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
async function pickFreePort() {
|
|
161
|
+
const server = createServer();
|
|
162
|
+
await new Promise((resolve, reject) => {
|
|
163
|
+
server.once("error", reject);
|
|
164
|
+
server.listen(0, "127.0.0.1", () => resolve());
|
|
165
|
+
});
|
|
166
|
+
const address = server.address();
|
|
167
|
+
const port = typeof address === "object" && address !== null ? address.port : 0;
|
|
168
|
+
await new Promise((resolve, reject) => server.close((e) => (e ? reject(e) : resolve())));
|
|
169
|
+
return port;
|
|
170
|
+
}
|
|
171
|
+
async function waitForReady(port, timeoutMs = 20000) {
|
|
172
|
+
const deadline = Date.now() + timeoutMs;
|
|
173
|
+
while (Date.now() < deadline) {
|
|
174
|
+
try {
|
|
175
|
+
const r = await fetch(`http://127.0.0.1:${port}/readyz`);
|
|
176
|
+
if (r.ok)
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
/* not ready yet */
|
|
181
|
+
}
|
|
182
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
183
|
+
}
|
|
184
|
+
throw new Error("codex app-server 启动超时(readyz 未就绪)");
|
|
185
|
+
}
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// CodexAppServerManager — 单例常驻进程生命周期
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
export class CodexAppServerManager {
|
|
190
|
+
static instance = null;
|
|
191
|
+
proc = null;
|
|
192
|
+
port = null;
|
|
193
|
+
starting = null;
|
|
194
|
+
cleanupRegistered = false;
|
|
195
|
+
static get() {
|
|
196
|
+
if (!CodexAppServerManager.instance) {
|
|
197
|
+
CodexAppServerManager.instance = new CodexAppServerManager();
|
|
198
|
+
}
|
|
199
|
+
return CodexAppServerManager.instance;
|
|
200
|
+
}
|
|
201
|
+
ensureStarted(command) {
|
|
202
|
+
if (this.port !== null)
|
|
203
|
+
return Promise.resolve(this.port);
|
|
204
|
+
if (this.starting)
|
|
205
|
+
return this.starting;
|
|
206
|
+
this.starting = this.start(command);
|
|
207
|
+
return this.starting;
|
|
208
|
+
}
|
|
209
|
+
async start(command) {
|
|
210
|
+
const port = await pickFreePort();
|
|
211
|
+
const proc = spawn(command, ["app-server", "--listen", `ws://127.0.0.1:${port}`], {
|
|
212
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
213
|
+
windowsHide: true,
|
|
214
|
+
// Windows 下 npm 全局安装的 codex 是 .cmd shim,需经 shell 解析;
|
|
215
|
+
// Unix 下直接 exec 二进制,避免 shell:true 的转义风险与 deprecation 警告。
|
|
216
|
+
shell: process.platform === "win32",
|
|
217
|
+
});
|
|
218
|
+
let stderr = "";
|
|
219
|
+
proc.stderr?.on("data", (chunk) => {
|
|
220
|
+
stderr += chunk.toString();
|
|
221
|
+
});
|
|
222
|
+
proc.once("error", (err) => {
|
|
223
|
+
console.error(`[Codex app-server] spawn 失败: ${err.message}`);
|
|
224
|
+
});
|
|
225
|
+
proc.on("close", (code) => {
|
|
226
|
+
if (this.proc === proc) {
|
|
227
|
+
this.proc = null;
|
|
228
|
+
this.port = null;
|
|
229
|
+
}
|
|
230
|
+
if (code !== 0 && stderr.trim()) {
|
|
231
|
+
console.error(`[Codex app-server] 退出码=${code}: ${stderr.trim().slice(0, 2000)}`);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
try {
|
|
235
|
+
await waitForReady(port);
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
void killProcessTree(proc.pid);
|
|
239
|
+
throw err;
|
|
240
|
+
}
|
|
241
|
+
this.proc = proc;
|
|
242
|
+
this.port = port;
|
|
243
|
+
this.registerCleanup();
|
|
244
|
+
return port;
|
|
245
|
+
}
|
|
246
|
+
/** 主进程退出时尽力收尸(Windows taskkill / Unix SIGTERM),避免孤儿常驻进程。 */
|
|
247
|
+
registerCleanup() {
|
|
248
|
+
if (this.cleanupRegistered)
|
|
249
|
+
return;
|
|
250
|
+
this.cleanupRegistered = true;
|
|
251
|
+
const cleanup = () => {
|
|
252
|
+
const pid = this.proc?.pid;
|
|
253
|
+
if (pid === undefined)
|
|
254
|
+
return;
|
|
255
|
+
try {
|
|
256
|
+
if (process.platform === "win32") {
|
|
257
|
+
spawnSync("taskkill", ["/F", "/T", "/PID", String(pid)], { stdio: "ignore" });
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
this.proc?.kill("SIGTERM");
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
/* best effort */
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
process.once("exit", cleanup);
|
|
268
|
+
process.once("SIGINT", cleanup);
|
|
269
|
+
process.once("SIGTERM", cleanup);
|
|
270
|
+
}
|
|
271
|
+
async shutdown() {
|
|
272
|
+
this.starting = null;
|
|
273
|
+
const proc = this.proc;
|
|
274
|
+
this.proc = null;
|
|
275
|
+
this.port = null;
|
|
276
|
+
if (proc) {
|
|
277
|
+
await killProcessTree(proc.pid);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
package/dist/src/config-utils.js
CHANGED
|
@@ -147,3 +147,39 @@ export function readToolCliPath(raw, options) {
|
|
|
147
147
|
}
|
|
148
148
|
return "";
|
|
149
149
|
}
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Codex CLI 版本检查(app-server 模式要求的最低版本)
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
/**
|
|
154
|
+
* app-server 模式(thread/start + turn 注入)官方推荐的最低 Codex CLI 版本。
|
|
155
|
+
* 版本不足时仅警告、不阻断启动——当前默认仍是 `codex exec` 模式,exec 不依赖
|
|
156
|
+
* 该最低版本。
|
|
157
|
+
*/
|
|
158
|
+
export const CODEX_MIN_APP_SERVER_VERSION = "0.60.0";
|
|
159
|
+
/**
|
|
160
|
+
* 从 `codex --version` 的输出中解析三段式版本号。
|
|
161
|
+
* 实际输出形如 `codex-cli 0.153.4`(可能带换行或附加后缀)。
|
|
162
|
+
* 找不到形如 x.y.z 的版本号时返回 null。
|
|
163
|
+
*/
|
|
164
|
+
export function parseCodexVersion(output) {
|
|
165
|
+
const match = output.match(/\d+\.\d+\.\d+/);
|
|
166
|
+
return match ? match[0] : null;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* 轻量 semver 比较(按数值逐段比较,缺段按 0 补齐,忽略 prerelease/build)。
|
|
170
|
+
* 返回 -1(a < b)、0(a == b)、1(a > b)。
|
|
171
|
+
*/
|
|
172
|
+
export function compareSemver(a, b) {
|
|
173
|
+
const pa = a.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
174
|
+
const pb = b.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
175
|
+
const len = Math.max(pa.length, pb.length);
|
|
176
|
+
for (let i = 0; i < len; i += 1) {
|
|
177
|
+
const x = pa[i] ?? 0;
|
|
178
|
+
const y = pb[i] ?? 0;
|
|
179
|
+
if (x < y)
|
|
180
|
+
return -1;
|
|
181
|
+
if (x > y)
|
|
182
|
+
return 1;
|
|
183
|
+
}
|
|
184
|
+
return 0;
|
|
185
|
+
}
|
package/dist/src/config.js
CHANGED
|
@@ -8,7 +8,7 @@ import { AGENT_TOOLS } from "./agent-tool.js";
|
|
|
8
8
|
import { CHATCCC_PACKAGE_ROOT } from "./package-root.js";
|
|
9
9
|
import { printServiceDidNotStart } from "./exit-banner.js";
|
|
10
10
|
import { appendStartupTrace, setupFileLogging } from "./shared.js";
|
|
11
|
-
import { anthropicConfigDisplay, autoDetectCodexPath, autoDetectCursorPath, normalizeCccProviderOverride, normalizeOptionalConfigField, readToolCliPath, resolveCccEnabled, } from "./config-utils.js";
|
|
11
|
+
import { CODEX_MIN_APP_SERVER_VERSION, anthropicConfigDisplay, autoDetectCodexPath, autoDetectCursorPath, compareSemver, normalizeCccProviderOverride, normalizeOptionalConfigField, parseCodexVersion, readToolCliPath, resolveCccEnabled, } from "./config-utils.js";
|
|
12
12
|
export { AGENT_TOOLS } from "./agent-tool.js";
|
|
13
13
|
// 重新导出 config-utils 中的纯函数/常量,保持对外 API 不变
|
|
14
14
|
// (历史上这些符号都从 ./config.ts 导入;新代码可直接从 ./config-utils.ts 导入以避免触发本文件的副作用)
|
|
@@ -795,6 +795,39 @@ export function reportEnvironmentVariableReadout() {
|
|
|
795
795
|
console.log(` /git 命令超时: ${GIT_TIMEOUT_SECONDS}s`);
|
|
796
796
|
console.log(" ------------------------------------------------------------------");
|
|
797
797
|
}
|
|
798
|
+
/**
|
|
799
|
+
* 启动时检查 Codex CLI 版本是否满足 app-server 模式的最低要求。
|
|
800
|
+
*
|
|
801
|
+
* 版本不足 / 无法探测时仅打印 warning,不阻断启动:当前默认仍是 `codex exec`
|
|
802
|
+
* 模式,exec 并不依赖该最低版本。待未来全面切到 app-server 时,再把这里升级为
|
|
803
|
+
* spawn 前的硬校验。
|
|
804
|
+
*/
|
|
805
|
+
export function reportCodexVersionCheck() {
|
|
806
|
+
const codexPath = config.codex.path.trim() || "codex";
|
|
807
|
+
let version = null;
|
|
808
|
+
try {
|
|
809
|
+
const out = execFileSync(codexPath, ["--version"], {
|
|
810
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
811
|
+
windowsHide: true,
|
|
812
|
+
timeout: 5000,
|
|
813
|
+
}).toString();
|
|
814
|
+
version = parseCodexVersion(out);
|
|
815
|
+
}
|
|
816
|
+
catch {
|
|
817
|
+
version = null;
|
|
818
|
+
}
|
|
819
|
+
if (!version) {
|
|
820
|
+
console.warn(` [警告] [可选] codex 版本:无法探测 Codex CLI 版本(命令: ${codexPath})。` +
|
|
821
|
+
`切换 app-server 模式前需 Codex CLI >= ${CODEX_MIN_APP_SERVER_VERSION}。`);
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
if (compareSemver(version, CODEX_MIN_APP_SERVER_VERSION) < 0) {
|
|
825
|
+
console.warn(` [警告] [可选] codex 版本:当前 ${version} 低于 app-server 要求的最低 ${CODEX_MIN_APP_SERVER_VERSION}。` +
|
|
826
|
+
`现有 exec 模式不受影响;切换 app-server 模式前请升级 Codex CLI。`);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
console.log(` [成功] [可选] codex 版本:${version}(满足 app-server 最低 ${CODEX_MIN_APP_SERVER_VERSION})`);
|
|
830
|
+
}
|
|
798
831
|
/** 飞书凭证缺失时打印可操作的说明并退出 */
|
|
799
832
|
export function explainMissingFeishuCredentialsAndExit() {
|
|
800
833
|
appendStartupTrace("explainMissingFeishuCredentialsAndExit: exiting", {
|
package/dist/src/index.js
CHANGED
|
@@ -31,7 +31,7 @@ import { configureAgentTeamMainAgent } from "./agent-team/main-agent-bootstrap.j
|
|
|
31
31
|
import { buildWebUiUrl, createServiceLifecycleGuard, announceInternalRestartReady, INTERNAL_RESTART_ENV_VAR, openWebUiInDefaultBrowser, shouldAutoOpenWebUi, } from "./startup-lifecycle.js";
|
|
32
32
|
import { buildPlatformStartupPlan } from "./platform-startup.js";
|
|
33
33
|
import { makeTraceId, logTrace } from "./trace.js";
|
|
34
|
-
import { CHATCCC_PORT, config, APP_ID, APP_SECRET, FEISHU_ENABLED, FEISHU_PLATFORM_TYPE, ILINK_ENABLED, ILINK_REUSE_TOKEN_ON_START, BASE_URL, LOCAL_RELAY_URL, PID_FILE, PROJECT_ROOT, USE_LOCAL, USE_SIMULATE, appendChatLog, fileLog, reportEnvironmentVariableReadout, maskAppId, resolveDefaultAgentTool, toolDisplayName, ts, } from "./config.js";
|
|
34
|
+
import { CHATCCC_PORT, config, APP_ID, APP_SECRET, FEISHU_ENABLED, FEISHU_PLATFORM_TYPE, ILINK_ENABLED, ILINK_REUSE_TOKEN_ON_START, BASE_URL, LOCAL_RELAY_URL, PID_FILE, PROJECT_ROOT, USE_LOCAL, USE_SIMULATE, appendChatLog, fileLog, reportEnvironmentVariableReadout, reportCodexVersionCheck, maskAppId, resolveDefaultAgentTool, toolDisplayName, ts, } from "./config.js";
|
|
35
35
|
import { printServiceDidNotStart, printServiceRunningHint } from "./exit-banner.js";
|
|
36
36
|
import { addReaction, createGroupChat, extractSessionInfo, formatDelayNotice, getChatInfo, getTenantAccessToken, recallMessage, sendCardReply, sendRawCard, sendTextReply, setChatAvatar, updateCardMessage, updateChatInfo, disbandChat, sendRestartCard, verifyAllPermissions, reportPermissionResults, setPlatform, consumeCodexRateLimitResetCredit, } from "./feishu-platform.js";
|
|
37
37
|
import { SimulatedPlatform, SIM_DEFAULT_CHAT_ID } from "./sim-platform.js";
|
|
@@ -665,6 +665,7 @@ async function main() {
|
|
|
665
665
|
});
|
|
666
666
|
console.log(`[启动 2/7] 环境与凭证检查`);
|
|
667
667
|
reportEnvironmentVariableReadout();
|
|
668
|
+
reportCodexVersionCheck();
|
|
668
669
|
console.log(` 工作目录: ${process.cwd()}`);
|
|
669
670
|
console.log(` 包根目录: ${PROJECT_ROOT}`);
|
|
670
671
|
if (FEISHU_ENABLED) {
|
package/dist/src/orchestrator.js
CHANGED
|
@@ -2239,15 +2239,16 @@ async function handleCommandInternal(platform, text, chatId, openId, msgTimestam
|
|
|
2239
2239
|
}
|
|
2240
2240
|
// 并发检查:同一 session 只能有一个活跃 prompt,多余消息进入队列
|
|
2241
2241
|
if (isSessionRunning(sessionId)) {
|
|
2242
|
-
// ccc
|
|
2243
|
-
// model step 边界逐条吸收进当前 turn(不新开 turn
|
|
2244
|
-
|
|
2242
|
+
// ccc 内核与 codex app-server 支持协作式让位:运行期新消息进入注入队列,
|
|
2243
|
+
// 由 drainInput 在每个 model step 边界逐条吸收进当前 turn(不新开 turn)。
|
|
2244
|
+
// 其他 agent 保持整轮队列。
|
|
2245
|
+
if (descriptionTool === "ccc" || descriptionTool === "codex") {
|
|
2245
2246
|
const injected = pushInjection(sessionId, {
|
|
2246
2247
|
text: promptText, chatId, openId, msgTimestamp, chatType, traceId: tid,
|
|
2247
2248
|
});
|
|
2248
2249
|
if (injected) {
|
|
2249
2250
|
logTrace(tid, "INJECT_QUEUED", { sessionId });
|
|
2250
|
-
console.log(`[${ts()}] [INJECT_QUEUED] Session ${sessionId} (
|
|
2251
|
+
console.log(`[${ts()}] [INJECT_QUEUED] Session ${sessionId} (${descriptionTool}) busy, message from chat ${chatId} queued for step-boundary injection`);
|
|
2251
2252
|
if (platform.kind === "wechat") {
|
|
2252
2253
|
await platform.sendText(chatId, "当前会话正在生成中,你的消息会在当前步骤结束后注入本轮处理。").catch(() => { });
|
|
2253
2254
|
}
|
|
@@ -2257,7 +2258,7 @@ async function handleCommandInternal(platform, text, chatId, openId, msgTimestam
|
|
|
2257
2258
|
}
|
|
2258
2259
|
else {
|
|
2259
2260
|
logTrace(tid, "INJECT_QUEUE_FULL", { sessionId });
|
|
2260
|
-
console.log(`[${ts()}] [INJECT_QUEUE_FULL] Session ${sessionId} (
|
|
2261
|
+
console.log(`[${ts()}] [INJECT_QUEUE_FULL] Session ${sessionId} (${descriptionTool}) injection queue full, rejecting message from chat ${chatId}`);
|
|
2261
2262
|
if (platform.kind === "wechat") {
|
|
2262
2263
|
await platform.sendText(chatId, "当前待注入消息过多,请等待或发送 /stop(停止生成)或 /cancel(清空注入)。").catch(() => { });
|
|
2263
2264
|
}
|
|
@@ -163,10 +163,10 @@ export function hasQueuedMessage(sessionId) {
|
|
|
163
163
|
return queuedMessages.has(sessionId);
|
|
164
164
|
}
|
|
165
165
|
// ---------------------------------------------------------------------------
|
|
166
|
-
// pendingInjections: sessionId →
|
|
167
|
-
// 与上面的 queuedMessages(整轮队列,深度 1)分离:ccc
|
|
168
|
-
//
|
|
169
|
-
// 继续走整轮队列。turn 结束后剩余未注入的消息转回普通队列消费。
|
|
166
|
+
// pendingInjections: sessionId → 运行中待注入消息(ccc 内核与 codex app-server)
|
|
167
|
+
// 与上面的 queuedMessages(整轮队列,深度 1)分离:ccc/codex 运行期的新消息
|
|
168
|
+
// 进入这里,由 drainInput 在每个 model step 边界逐条吸收进当前 turn;其他
|
|
169
|
+
// agent 继续走整轮队列。turn 结束后剩余未注入的消息转回普通队列消费。
|
|
170
170
|
// ---------------------------------------------------------------------------
|
|
171
171
|
export const MAX_PENDING_INJECTIONS = 50;
|
|
172
172
|
export const pendingInjections = new Map();
|
|
@@ -187,6 +187,12 @@ export function shiftInjection(sessionId) {
|
|
|
187
187
|
pendingInjections.delete(sessionId);
|
|
188
188
|
return msg;
|
|
189
189
|
}
|
|
190
|
+
/** 注入未能被运行中 turn 吸收时,把消息退回队列头部。 */
|
|
191
|
+
export function unshiftInjection(sessionId, msg) {
|
|
192
|
+
const list = pendingInjections.get(sessionId) ?? [];
|
|
193
|
+
list.unshift(msg);
|
|
194
|
+
pendingInjections.set(sessionId, list);
|
|
195
|
+
}
|
|
190
196
|
export function drainRemainingInjections(sessionId) {
|
|
191
197
|
const list = pendingInjections.get(sessionId);
|
|
192
198
|
pendingInjections.delete(sessionId);
|
package/dist/src/session.js
CHANGED
|
@@ -29,7 +29,7 @@ function compressWechatDisplayText(text) {
|
|
|
29
29
|
}
|
|
30
30
|
import { readStreamState, writeStreamState, createEmptyStreamState, isFinalReplySentForTurn, markFinalReplySent, } from "./stream-state.js";
|
|
31
31
|
import { addCardToTurn, finalizeTurnCards, markCardDone } from "./turn-cards.js";
|
|
32
|
-
import { bindChatToSession, unbindChatFromSession, getChatsForSession, activePrompts, displayCards, unifiedDisplayLoopHandle, setUnifiedDisplayLoopHandle, rebuildSessionChatsFromRegistry, recordLastActiveChat, getLastActiveChat, pickDisplayChat, dequeueMessage, consumeQueuedMessage, cancelQueuedMessage, shiftInjection, drainRemainingInjections, clearInjections, setQueuePreservedChat, consumeQueuePreservedChat, markSessionFinalizing, clearSessionFinalizing, reserveAutoRecovery, consumeAutoRecoveryReservation, cancelAutoRecoveryReservation, hasAutoRecoveryReservation, } from "./session-chat-binding.js";
|
|
32
|
+
import { bindChatToSession, unbindChatFromSession, getChatsForSession, activePrompts, displayCards, unifiedDisplayLoopHandle, setUnifiedDisplayLoopHandle, rebuildSessionChatsFromRegistry, recordLastActiveChat, getLastActiveChat, pickDisplayChat, dequeueMessage, consumeQueuedMessage, cancelQueuedMessage, shiftInjection, unshiftInjection, drainRemainingInjections, clearInjections, setQueuePreservedChat, consumeQueuePreservedChat, markSessionFinalizing, clearSessionFinalizing, reserveAutoRecovery, consumeAutoRecoveryReservation, cancelAutoRecoveryReservation, hasAutoRecoveryReservation, } from "./session-chat-binding.js";
|
|
33
33
|
async function sendFinalReplyTextOnce(platform, chatId, sessionId, turnCount, text) {
|
|
34
34
|
const sent = await platform.sendText(chatId, text).then((ok) => ok !== false).catch(() => false);
|
|
35
35
|
if (sent)
|
|
@@ -1287,6 +1287,8 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
|
|
|
1287
1287
|
}
|
|
1288
1288
|
}
|
|
1289
1289
|
try {
|
|
1290
|
+
// 供 onInjectionRejected 退回的“最后一条已取出但未成功注入”的消息
|
|
1291
|
+
let lastShiftedInjection = null;
|
|
1290
1292
|
for await (const unifiedMsg of adapter.prompt(sessionId, userTextWithCapabilities, cwd, controller.signal, {
|
|
1291
1293
|
onProcessStart: (processInfo) => {
|
|
1292
1294
|
startPromptProcessMonitor(sessionId, processInfo);
|
|
@@ -1303,16 +1305,24 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
|
|
|
1303
1305
|
if (prompt)
|
|
1304
1306
|
prompt.closeSession = closeSession;
|
|
1305
1307
|
},
|
|
1306
|
-
// ccc
|
|
1307
|
-
// model step
|
|
1308
|
-
drainInput: tool === "ccc" ? () => {
|
|
1308
|
+
// ccc 内核与 codex app-server 支持协作式让位:同步从注入队列取队首文本,
|
|
1309
|
+
// 供适配器在每个 model step 边界吸收。其他 adapter 忽略该字段。
|
|
1310
|
+
drainInput: tool === "ccc" || tool === "codex" ? () => {
|
|
1309
1311
|
const injected = shiftInjection(sessionId);
|
|
1310
1312
|
if (!injected)
|
|
1311
1313
|
return undefined;
|
|
1314
|
+
lastShiftedInjection = injected;
|
|
1312
1315
|
// 与首次消息保持一致的结构化包装;imSkillsPrompt 已在首次消息中,
|
|
1313
1316
|
// 此处不重复注入,避免多轮注入导致上下文膨胀。
|
|
1314
1317
|
return `[User message]\n${injected.text}\n[/User message]`;
|
|
1315
1318
|
} : undefined,
|
|
1319
|
+
// 注入未被当前 turn 吸收(如 turn 恰好已结束)时,退回队列头部,避免丢消息。
|
|
1320
|
+
onInjectionRejected: tool === "ccc" || tool === "codex" ? () => {
|
|
1321
|
+
if (lastShiftedInjection) {
|
|
1322
|
+
unshiftInjection(sessionId, lastShiftedInjection);
|
|
1323
|
+
lastShiftedInjection = null;
|
|
1324
|
+
}
|
|
1325
|
+
} : undefined,
|
|
1316
1326
|
})) {
|
|
1317
1327
|
if (unifiedMsg.isFinalResponse) {
|
|
1318
1328
|
const prompt = activePrompts.get(sessionId);
|
|
@@ -1653,7 +1663,7 @@ export async function runAgentSession(sessionId, userText, platform, _chatId, ms
|
|
|
1653
1663
|
// 第一次 response-stall 后保留普通缓存;恢复轮结束后由恢复轮的
|
|
1654
1664
|
// finally 再消费,顺序固定为“自动恢复 → 用户缓存”。
|
|
1655
1665
|
queuedForConsumption = dequeueMessage(sessionId);
|
|
1656
|
-
// ccc 注入队列剩余(turn 期间未注入完,如 non-streaming 或收尾窗口到达)
|
|
1666
|
+
// ccc/codex 注入队列剩余(turn 期间未注入完,如 non-streaming 或收尾窗口到达)
|
|
1657
1667
|
// 取第一条转普通消费,其余留待下一轮结束后继续消费。
|
|
1658
1668
|
if (!queuedForConsumption) {
|
|
1659
1669
|
queuedForConsumption = shiftInjection(sessionId);
|