chatccc 0.2.203 → 0.2.205
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 +14 -10
- package/config.sample.json +8 -5
- package/package.json +1 -1
- package/src/__tests__/agent-activity.test.ts +76 -0
- package/src/__tests__/codex-adapter.test.ts +7 -7
- package/src/__tests__/config-reload.test.ts +19 -6
- package/src/__tests__/config-sample.test.ts +10 -1
- package/src/__tests__/cursor-adapter.test.ts +5 -5
- package/src/__tests__/session.test.ts +97 -17
- package/src/__tests__/startup-lifecycle.test.ts +98 -0
- package/src/__tests__/web-ui.test.ts +59 -0
- package/src/adapters/codex-adapter.ts +1 -0
- package/src/adapters/cursor-adapter.ts +1 -0
- package/src/agent-activity.ts +170 -0
- package/src/config.ts +19 -11
- package/src/index.ts +59 -19
- package/src/orchestrator.ts +11 -4
- package/src/session-chat-binding.ts +6 -4
- package/src/session.ts +89 -60
- package/src/startup-lifecycle.ts +96 -0
- package/src/stream-state.ts +13 -7
- package/src/web-ui.ts +185 -127
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import type { UnifiedBlock } from "./adapters/adapter-interface.ts";
|
|
2
|
+
|
|
3
|
+
export type AgentActivityKind =
|
|
4
|
+
| "starting"
|
|
5
|
+
| "thinking"
|
|
6
|
+
| "tool"
|
|
7
|
+
| "processing"
|
|
8
|
+
| "responding"
|
|
9
|
+
| "searching"
|
|
10
|
+
| "compacting";
|
|
11
|
+
|
|
12
|
+
/** The user-visible activity of a running Agent turn. */
|
|
13
|
+
export interface AgentActivity {
|
|
14
|
+
kind: AgentActivityKind;
|
|
15
|
+
/** Time when the current activity began, used for truthful elapsed time. */
|
|
16
|
+
startedAt: number;
|
|
17
|
+
toolName?: string;
|
|
18
|
+
toolCount?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ActiveTool {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
startedAt: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AgentActivityTracker {
|
|
28
|
+
activity: AgentActivity;
|
|
29
|
+
activeTools: Map<string, ActiveTool>;
|
|
30
|
+
nextAnonymousToolId: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createAgentActivityTracker(now = Date.now()): AgentActivityTracker {
|
|
34
|
+
return {
|
|
35
|
+
activity: { kind: "starting", startedAt: now },
|
|
36
|
+
activeTools: new Map(),
|
|
37
|
+
nextAnonymousToolId: 1,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function sameVisibleActivity(left: AgentActivity, right: AgentActivity): boolean {
|
|
42
|
+
return left.kind === right.kind
|
|
43
|
+
&& left.toolName === right.toolName
|
|
44
|
+
&& left.toolCount === right.toolCount;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function setActivity(tracker: AgentActivityTracker, next: AgentActivity): boolean {
|
|
48
|
+
if (sameVisibleActivity(tracker.activity, next)) return false;
|
|
49
|
+
tracker.activity = next;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function refreshToolActivity(tracker: AgentActivityTracker): boolean {
|
|
54
|
+
const tools = [...tracker.activeTools.values()];
|
|
55
|
+
const first = tools[0];
|
|
56
|
+
if (!first) return false;
|
|
57
|
+
return setActivity(tracker, {
|
|
58
|
+
kind: "tool",
|
|
59
|
+
startedAt: first.startedAt,
|
|
60
|
+
toolName: first.name,
|
|
61
|
+
toolCount: tools.length,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function removeCompletedTool(tracker: AgentActivityTracker, toolUseId: string): void {
|
|
66
|
+
if (toolUseId && tracker.activeTools.delete(toolUseId)) return;
|
|
67
|
+
|
|
68
|
+
// Older adapters did not always include a tool ID. Prefer an anonymous entry;
|
|
69
|
+
// if there is only one active call, it is still safe to match that result.
|
|
70
|
+
const anonymousId = [...tracker.activeTools.keys()].find((id) => id.startsWith("anonymous:"));
|
|
71
|
+
if (anonymousId) {
|
|
72
|
+
tracker.activeTools.delete(anonymousId);
|
|
73
|
+
} else if (tracker.activeTools.size === 1) {
|
|
74
|
+
const onlyId = tracker.activeTools.keys().next().value as string | undefined;
|
|
75
|
+
if (onlyId) tracker.activeTools.delete(onlyId);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Applies one normalized Agent event and returns whether the persisted activity
|
|
81
|
+
* changed. Tool activity takes precedence while a tool call is still active.
|
|
82
|
+
*/
|
|
83
|
+
export function updateAgentActivity(
|
|
84
|
+
tracker: AgentActivityTracker,
|
|
85
|
+
block: UnifiedBlock,
|
|
86
|
+
now = Date.now(),
|
|
87
|
+
): boolean {
|
|
88
|
+
if (block.type === "tool_use") {
|
|
89
|
+
const id = block.id || `anonymous:${tracker.nextAnonymousToolId++}`;
|
|
90
|
+
const existing = tracker.activeTools.get(id);
|
|
91
|
+
tracker.activeTools.set(id, {
|
|
92
|
+
id,
|
|
93
|
+
name: block.name || "未知工具",
|
|
94
|
+
startedAt: existing?.startedAt ?? now,
|
|
95
|
+
});
|
|
96
|
+
return refreshToolActivity(tracker);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (block.type === "tool_result") {
|
|
100
|
+
removeCompletedTool(tracker, block.tool_use_id);
|
|
101
|
+
if (tracker.activeTools.size > 0) return refreshToolActivity(tracker);
|
|
102
|
+
return setActivity(tracker, { kind: "processing", startedAt: now });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (tracker.activeTools.size > 0) return false;
|
|
106
|
+
|
|
107
|
+
switch (block.type) {
|
|
108
|
+
case "thinking":
|
|
109
|
+
case "redacted_thinking":
|
|
110
|
+
return setActivity(tracker, { kind: "thinking", startedAt: now });
|
|
111
|
+
case "text":
|
|
112
|
+
case "text_final":
|
|
113
|
+
return setActivity(tracker, { kind: "responding", startedAt: now });
|
|
114
|
+
case "search_result":
|
|
115
|
+
return setActivity(tracker, { kind: "searching", startedAt: now });
|
|
116
|
+
case "compact_boundary":
|
|
117
|
+
return setActivity(tracker, { kind: "compacting", startedAt: now });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function formatElapsed(startedAt: number, now: number): string {
|
|
122
|
+
const totalSeconds = Math.max(0, Math.floor((now - startedAt) / 1000));
|
|
123
|
+
if (totalSeconds < 60) return `${totalSeconds}秒`;
|
|
124
|
+
const totalMinutes = Math.floor(totalSeconds / 60);
|
|
125
|
+
const seconds = totalSeconds % 60;
|
|
126
|
+
if (totalMinutes < 60) return `${totalMinutes}分${seconds}秒`;
|
|
127
|
+
const hours = Math.floor(totalMinutes / 60);
|
|
128
|
+
const minutes = totalMinutes % 60;
|
|
129
|
+
return `${hours}小时${minutes}分`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function displayToolName(name: string | undefined): string {
|
|
133
|
+
const normalized = (name || "未知工具").replace(/\s+/g, " ").trim();
|
|
134
|
+
return normalized.length > 24 ? `${normalized.slice(0, 23)}…` : normalized;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function formatAgentActivityTitle(
|
|
138
|
+
activity: AgentActivity | undefined,
|
|
139
|
+
now = Date.now(),
|
|
140
|
+
): string {
|
|
141
|
+
if (!activity) return "正在处理";
|
|
142
|
+
|
|
143
|
+
let label: string;
|
|
144
|
+
switch (activity.kind) {
|
|
145
|
+
case "starting":
|
|
146
|
+
label = "正在启动 Agent";
|
|
147
|
+
break;
|
|
148
|
+
case "thinking":
|
|
149
|
+
label = "思考中";
|
|
150
|
+
break;
|
|
151
|
+
case "tool": {
|
|
152
|
+
const count = Math.max(1, activity.toolCount ?? 1);
|
|
153
|
+
label = `正在执行 ${displayToolName(activity.toolName)}${count > 1 ? ` 等 ${count} 项` : ""}`;
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
case "processing":
|
|
157
|
+
label = "正在处理工具结果";
|
|
158
|
+
break;
|
|
159
|
+
case "responding":
|
|
160
|
+
label = "正在生成回复";
|
|
161
|
+
break;
|
|
162
|
+
case "searching":
|
|
163
|
+
label = "正在处理搜索结果";
|
|
164
|
+
break;
|
|
165
|
+
case "compacting":
|
|
166
|
+
label = "正在整理上下文";
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
return `${label} · ${formatElapsed(activity.startedAt, now)}`;
|
|
170
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -150,10 +150,11 @@ export interface RawStreamLogsConfig {
|
|
|
150
150
|
ccc: RawStreamAgentLogConfig;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
export interface AppConfig {
|
|
154
|
-
feishu: FeishuConfig;
|
|
155
|
-
platforms: PlatformsConfig;
|
|
156
|
-
|
|
153
|
+
export interface AppConfig {
|
|
154
|
+
feishu: FeishuConfig;
|
|
155
|
+
platforms: PlatformsConfig;
|
|
156
|
+
webUi: { openOnStart: boolean };
|
|
157
|
+
chromeDevtools: ChromeDevtoolsConfig;
|
|
157
158
|
port: number;
|
|
158
159
|
gitTimeoutSeconds: number;
|
|
159
160
|
/** 若为 false,AI 生成过程中用户发送消息不会打断,须先点「停止」再发送新消息 */
|
|
@@ -418,9 +419,10 @@ function normalizeRawStreamAgentLogConfig(raw: unknown): RawStreamAgentLogConfig
|
|
|
418
419
|
|
|
419
420
|
function loadConfig(): AppConfig {
|
|
420
421
|
const defaults: AppConfig = {
|
|
421
|
-
feishu: { appId: "", appSecret: "" },
|
|
422
|
-
platforms: { feishu: { enabled: true }, ilink: { enabled: true } },
|
|
423
|
-
|
|
422
|
+
feishu: { appId: "", appSecret: "" },
|
|
423
|
+
platforms: { feishu: { enabled: true }, ilink: { enabled: true } },
|
|
424
|
+
webUi: { openOnStart: true },
|
|
425
|
+
chromeDevtools: { enabled: false, port: 15166, chromePath: "" },
|
|
424
426
|
port: 18080,
|
|
425
427
|
gitTimeoutSeconds: 180,
|
|
426
428
|
allowInterrupt: false,
|
|
@@ -495,6 +497,7 @@ function loadConfig(): AppConfig {
|
|
|
495
497
|
};
|
|
496
498
|
codex?: { enabled?: unknown; defaultAgent?: unknown; path?: unknown; command?: unknown; model?: unknown; alternativeModel?: unknown; effort?: unknown };
|
|
497
499
|
ccc?: { DEEPSEEK_API_KEY?: unknown; DEEPSEEK_BASE_URL?: unknown; model?: unknown };
|
|
500
|
+
webUi?: { openOnStart?: unknown };
|
|
498
501
|
chromeDevtools?: { enabled?: unknown; port?: unknown; chromePath?: unknown };
|
|
499
502
|
rawStreamLogs?: unknown;
|
|
500
503
|
};
|
|
@@ -510,6 +513,7 @@ function loadConfig(): AppConfig {
|
|
|
510
513
|
const cursorRaw = (parsed.cursor ?? {}) as NonNullable<typeof parsed.cursor>;
|
|
511
514
|
const codexRaw = (parsed.codex ?? {}) as NonNullable<typeof parsed.codex>;
|
|
512
515
|
const cccRaw = (parsed.ccc ?? {}) as NonNullable<typeof parsed.ccc>;
|
|
516
|
+
const webUiRaw = (parsed.webUi ?? {}) as NonNullable<typeof parsed.webUi>;
|
|
513
517
|
const chromeDevtoolsRaw = (parsed.chromeDevtools ?? {}) as NonNullable<typeof parsed.chromeDevtools>;
|
|
514
518
|
const rawStreamLogsRaw = typeof parsed.rawStreamLogs === "object" && parsed.rawStreamLogs !== null
|
|
515
519
|
? parsed.rawStreamLogs as unknown as Record<string, unknown>
|
|
@@ -577,7 +581,7 @@ function loadConfig(): AppConfig {
|
|
|
577
581
|
appId: feishu.appId ?? "",
|
|
578
582
|
appSecret: feishu.appSecret ?? "",
|
|
579
583
|
},
|
|
580
|
-
platforms: {
|
|
584
|
+
platforms: {
|
|
581
585
|
feishu: {
|
|
582
586
|
enabled: typeof (parsed.platforms as unknown as Record<string, unknown> | undefined)?.feishu === "object"
|
|
583
587
|
? Boolean(((parsed.platforms as unknown as Record<string, unknown>).feishu as Record<string, unknown>).enabled ?? true)
|
|
@@ -595,9 +599,13 @@ function loadConfig(): AppConfig {
|
|
|
595
599
|
reuseTokenOnStart: typeof (parsed.platforms as unknown as Record<string, unknown> | undefined)?.ilink === "object"
|
|
596
600
|
? Boolean(((parsed.platforms as unknown as Record<string, unknown>).ilink as Record<string, unknown>).reuseTokenOnStart ?? true)
|
|
597
601
|
: true,
|
|
598
|
-
},
|
|
599
|
-
},
|
|
600
|
-
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
webUi: {
|
|
605
|
+
// 兼容升级前没有 webUi 字段的 config.json:缺省仍按“自动打开”处理。
|
|
606
|
+
openOnStart: typeof webUiRaw.openOnStart === "boolean" ? webUiRaw.openOnStart : true,
|
|
607
|
+
},
|
|
608
|
+
chromeDevtools: {
|
|
601
609
|
enabled: typeof chromeDevtoolsRaw.enabled === "boolean" ? chromeDevtoolsRaw.enabled : false,
|
|
602
610
|
port: Number.isInteger(chromeDevtoolsPort) && chromeDevtoolsPort >= 1 && chromeDevtoolsPort <= 65535
|
|
603
611
|
? chromeDevtoolsPort
|
package/src/index.ts
CHANGED
|
@@ -28,12 +28,18 @@ import { WSClient, EventDispatcher, Domain } from "@larksuiteoapi/node-sdk";
|
|
|
28
28
|
import WebSocket from "ws";
|
|
29
29
|
|
|
30
30
|
import { appendStartupTrace, attachRelayWebSocket, ensureSingleInstance, freeRelayListenPort, installCrashLogging, waitForPortFree } from "./shared.ts";
|
|
31
|
-
import { createUiRouter, setExtraApiHandler, setReloadConfigHook, startSetupMode } from "./web-ui.ts";
|
|
31
|
+
import { createUiRouter, setExtraApiHandler, setReloadConfigHook, startSetupMode } from "./web-ui.ts";
|
|
32
|
+
import {
|
|
33
|
+
buildWebUiUrl,
|
|
34
|
+
openWebUiInDefaultBrowser,
|
|
35
|
+
shouldAutoOpenWebUi,
|
|
36
|
+
} from "./startup-lifecycle.ts";
|
|
32
37
|
import { buildPlatformStartupPlan } from "./platform-startup.ts";
|
|
33
38
|
import { makeTraceId, logTrace } from "./trace.ts";
|
|
34
39
|
import {
|
|
35
|
-
CHATCCC_PORT,
|
|
36
|
-
|
|
40
|
+
CHATCCC_PORT,
|
|
41
|
+
config,
|
|
42
|
+
APP_ID,
|
|
37
43
|
APP_SECRET,
|
|
38
44
|
FEISHU_ENABLED,
|
|
39
45
|
FEISHU_PLATFORM_TYPE,
|
|
@@ -720,11 +726,17 @@ async function startConfiguredPlatforms(
|
|
|
720
726
|
// Main
|
|
721
727
|
// ---------------------------------------------------------------------------
|
|
722
728
|
|
|
723
|
-
async function main(): Promise<void> {
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
729
|
+
async function main(): Promise<void> {
|
|
730
|
+
// 用户直接运行 chatccc 时打开系统默认浏览器;由 `/restart`、`/update`
|
|
731
|
+
// 或 Web UI 拉起的替代进程会携带内部标记,不重复打扰用户。
|
|
732
|
+
const autoOpenWebUi = shouldAutoOpenWebUi({
|
|
733
|
+
openOnStart: config.webUi.openOnStart,
|
|
734
|
+
});
|
|
735
|
+
appendStartupTrace("main: entered", {
|
|
736
|
+
argv: process.argv.join(" ").slice(0, 400),
|
|
737
|
+
CHATCCC_PORT,
|
|
738
|
+
PROJECT_ROOT,
|
|
739
|
+
autoOpenWebUi,
|
|
728
740
|
});
|
|
729
741
|
|
|
730
742
|
// 黑匣子:所有未捕获异常 / 信号 / beforeExit 都同步写入 startup-trace.log(appendFileSync)。
|
|
@@ -780,9 +792,18 @@ async function main(): Promise<void> {
|
|
|
780
792
|
console.log(`${"=".repeat(60)}`);
|
|
781
793
|
console.log(` 发送消息: POST http://127.0.0.1:${SIM_PORT}/api/sim/inject-message`);
|
|
782
794
|
console.log(` 消息日志: ~/.chatccc/sim/messages.jsonl`);
|
|
783
|
-
console.log(`${"=".repeat(60)}\n`);
|
|
784
|
-
|
|
785
|
-
|
|
795
|
+
console.log(`${"=".repeat(60)}\n`);
|
|
796
|
+
|
|
797
|
+
if (autoOpenWebUi) {
|
|
798
|
+
const url = buildWebUiUrl(SIM_PORT);
|
|
799
|
+
const opened = openWebUiInDefaultBrowser(SIM_PORT);
|
|
800
|
+
appendStartupTrace(
|
|
801
|
+
opened ? "web-ui: opening simulate browser" : "web-ui: simulate browser unavailable",
|
|
802
|
+
{ url },
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
installShutdownHandlers(simServer);
|
|
786
807
|
return;
|
|
787
808
|
}
|
|
788
809
|
|
|
@@ -830,8 +851,9 @@ async function main(): Promise<void> {
|
|
|
830
851
|
if (!APP_ID.trim() || !APP_SECRET.trim()) {
|
|
831
852
|
// 凭证不全:进 setup 向导。注入 onActivate 回调让用户点"保存并启动"
|
|
832
853
|
// 时,原地(同进程)调用 startBotService,复用 setup HTTP server。
|
|
833
|
-
startSetupMode(CHATCCC_PORT, {
|
|
834
|
-
|
|
854
|
+
startSetupMode(CHATCCC_PORT, {
|
|
855
|
+
openBrowser: autoOpenWebUi,
|
|
856
|
+
onActivate: async (httpServer: Server) => {
|
|
835
857
|
reloadRuntimeConfig("setup-activate");
|
|
836
858
|
appendStartupTrace("setup-activate: reloaded config from disk", {
|
|
837
859
|
appIdMaskAfterReload: maskAppId(APP_ID),
|
|
@@ -867,17 +889,35 @@ async function main(): Promise<void> {
|
|
|
867
889
|
await waitForPortFree(CHATCCC_PORT);
|
|
868
890
|
appendStartupTrace("main: port free confirmed", { CHATCCC_PORT });
|
|
869
891
|
}
|
|
870
|
-
const httpServer = createServer(createUiRouter());
|
|
871
|
-
await listenWithRetry(httpServer, CHATCCC_PORT, "127.0.0.1").catch((err: NodeJS.ErrnoException) => {
|
|
892
|
+
const httpServer = createServer(createUiRouter());
|
|
893
|
+
await listenWithRetry(httpServer, CHATCCC_PORT, "127.0.0.1").catch((err: NodeJS.ErrnoException) => {
|
|
872
894
|
console.error(`\n[启动] 本地中继 WebSocket 监听失败:端口 ${CHATCCC_PORT}(${err.code ?? "?"} — ${err.message})`);
|
|
873
895
|
console.error(
|
|
874
896
|
" 处理建议: 关闭占用该端口的其它程序,或在 config.json 的 port 字段里改成其它未占用端口(如 18081)。"
|
|
875
897
|
);
|
|
876
898
|
printServiceDidNotStart(`本地中继端口 ${CHATCCC_PORT} 无法监听(${err.code ?? "?"} — ${err.message})`);
|
|
877
|
-
process.exit(1);
|
|
878
|
-
});
|
|
879
|
-
|
|
880
|
-
|
|
899
|
+
process.exit(1);
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
// 必须等 HTTP server 真正监听后再发起打开请求,避免浏览器先到一步看到
|
|
903
|
+
// ERR_CONNECTION_REFUSED。Chrome CDP 守护仍保持自己原有的独立行为。
|
|
904
|
+
if (autoOpenWebUi) {
|
|
905
|
+
const url = buildWebUiUrl(CHATCCC_PORT);
|
|
906
|
+
const opened = openWebUiInDefaultBrowser(CHATCCC_PORT);
|
|
907
|
+
if (opened) {
|
|
908
|
+
console.log(`[WEB-UI] 已请求系统默认浏览器打开: ${url}`);
|
|
909
|
+
appendStartupTrace("web-ui: opening default browser", { url });
|
|
910
|
+
} else {
|
|
911
|
+
appendStartupTrace("web-ui: default browser unavailable", { url });
|
|
912
|
+
}
|
|
913
|
+
} else {
|
|
914
|
+
appendStartupTrace("web-ui: default browser skipped by lifecycle or preference", {
|
|
915
|
+
url: buildWebUiUrl(CHATCCC_PORT),
|
|
916
|
+
openOnStart: config.webUi.openOnStart,
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
await startConfiguredPlatforms(httpServer, { failOnFeishuError: false });
|
|
881
921
|
|
|
882
922
|
installShutdownHandlers(httpServer);
|
|
883
923
|
}
|
package/src/orchestrator.ts
CHANGED
|
@@ -91,6 +91,7 @@ import { applySharedPrefix } from "./shared-prefix.ts";
|
|
|
91
91
|
import { cwdDisplayName, sessionChatName } from "./session-name.ts";
|
|
92
92
|
import { reloadRuntimeConfig } from "./runtime-reload.ts";
|
|
93
93
|
import { acquireUpdateCommandGuard } from "./update-command-guard.ts";
|
|
94
|
+
import { createInternalRestartEnv } from "./startup-lifecycle.ts";
|
|
94
95
|
export { type PlatformAdapter } from "./platform-adapter.ts";
|
|
95
96
|
import type { ChatAvatarUsageHints, PlatformAdapter } from "./platform-adapter.ts";
|
|
96
97
|
import type { CodexUsageSummary } from "./feishu-api.ts";
|
|
@@ -480,7 +481,12 @@ function syncUpdateAndRestart(): void {
|
|
|
480
481
|
|
|
481
482
|
// 3. spawn new chatccc
|
|
482
483
|
try {
|
|
483
|
-
const child = spawn(binPath, [], {
|
|
484
|
+
const child = spawn(binPath, [], {
|
|
485
|
+
detached: true,
|
|
486
|
+
stdio: "ignore",
|
|
487
|
+
shell: true,
|
|
488
|
+
env: createInternalRestartEnv(),
|
|
489
|
+
});
|
|
484
490
|
child.unref();
|
|
485
491
|
updLog(`spawn new chatccc OK, childPid=${child.pid}, bin=${binPath}`);
|
|
486
492
|
appendStartupTrace("update: spawn OK", { childPid: child.pid, binPath });
|
|
@@ -545,9 +551,10 @@ export async function handleCommand(
|
|
|
545
551
|
appendStartupTrace("restart: spawn begin", { fromPid: process.pid });
|
|
546
552
|
const child = spawn("npx", ["tsx", "src/index.ts"], {
|
|
547
553
|
cwd: PROJECT_ROOT,
|
|
548
|
-
detached: true,
|
|
549
|
-
stdio: "ignore",
|
|
550
|
-
shell: true,
|
|
554
|
+
detached: true,
|
|
555
|
+
stdio: "ignore",
|
|
556
|
+
shell: true,
|
|
557
|
+
env: createInternalRestartEnv(),
|
|
551
558
|
});
|
|
552
559
|
|
|
553
560
|
child.on("error", (err) => {
|
|
@@ -139,8 +139,10 @@ export interface DisplayCardState {
|
|
|
139
139
|
cardId: string;
|
|
140
140
|
sequence: number;
|
|
141
141
|
cardBusy: boolean;
|
|
142
|
-
cardCreatedAt: number;
|
|
143
|
-
lastSentContent: string;
|
|
142
|
+
cardCreatedAt: number;
|
|
143
|
+
lastSentContent: string;
|
|
144
|
+
/** Last rendered activity header; elapsed time can change without body output. */
|
|
145
|
+
lastSentHeaderTitle?: string;
|
|
144
146
|
streamErrorNotified: boolean;
|
|
145
147
|
/** 所属 session */
|
|
146
148
|
sessionId: string;
|
|
@@ -153,8 +155,8 @@ export interface DisplayCardState {
|
|
|
153
155
|
lastSentAccLen?: number;
|
|
154
156
|
/** WeChat delta: 上次发送时的 finalReply */
|
|
155
157
|
lastSentFinalReply?: string;
|
|
156
|
-
/**
|
|
157
|
-
dotCount: number;
|
|
158
|
+
/** Liveness animation counter; the explicit activity header conveys Agent state. */
|
|
159
|
+
dotCount: number;
|
|
158
160
|
}
|
|
159
161
|
|
|
160
162
|
export const displayCards = new Map<string, DisplayCardState>();
|