@keyou007/agent-bot 0.1.13-alpha.1 → 0.1.13-alpha.3
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/CHANGELOG.md +11 -0
- package/README.md +106 -103
- package/README.zh.md +76 -73
- package/dist/acp/AcpProcessManager.js +8 -3
- package/dist/acp/AcpProcessManager.js.map +1 -1
- package/dist/acp/AcpSessionManager.js +15 -0
- package/dist/acp/AcpSessionManager.js.map +1 -1
- package/dist/cli/InitializationWelcome.js +116 -0
- package/dist/cli/InitializationWelcome.js.map +1 -0
- package/dist/cli/help.js +4 -2
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/serverStatus.js +27 -0
- package/dist/cli/serverStatus.js.map +1 -1
- package/dist/cli/taskListOutput.js +72 -0
- package/dist/cli/taskListOutput.js.map +1 -0
- package/dist/cli.js +103 -56
- package/dist/cli.js.map +1 -1
- package/dist/codex/CodexProcessManager.js +34 -4
- package/dist/codex/CodexProcessManager.js.map +1 -1
- package/dist/codex/CodexRuntime.js +3 -0
- package/dist/codex/CodexRuntime.js.map +1 -1
- package/dist/feishu/CardRenderer.js +164 -2
- package/dist/feishu/CardRenderer.js.map +1 -1
- package/dist/feishu/FeishuConnector.js +16 -5
- package/dist/feishu/FeishuConnector.js.map +1 -1
- package/dist/index.js +23 -4
- package/dist/index.js.map +1 -1
- package/dist/proxy/ProxySessionController.js +157 -59
- package/dist/proxy/ProxySessionController.js.map +1 -1
- package/dist/runtime/AcpRuntimeAdapter.js +3 -0
- package/dist/runtime/AcpRuntimeAdapter.js.map +1 -1
- package/dist/runtime/agentEnvironment.js +24 -4
- package/dist/runtime/agentEnvironment.js.map +1 -1
- package/dist/runtime/createAgentRuntimeRegistry.js +7 -3
- package/dist/runtime/createAgentRuntimeRegistry.js.map +1 -1
- package/dist/state/StateStore.js +12 -9
- package/dist/state/StateStore.js.map +1 -1
- package/dist/supervision/SafeRestartNotifier.js +1 -9
- package/dist/supervision/SafeRestartNotifier.js.map +1 -1
- package/docs/technical-reference.md +12 -4
- package/docs/technical-reference.zh.md +12 -4
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/skills/agent-bot/SKILL.md +53 -22
- package/src/acp/AcpProcessManager.ts +16 -3
- package/src/acp/AcpSessionManager.ts +14 -0
- package/src/cli/InitializationWelcome.ts +160 -0
- package/src/cli/help.ts +4 -2
- package/src/cli/serverStatus.ts +28 -0
- package/src/cli/taskListOutput.ts +91 -0
- package/src/cli.ts +140 -55
- package/src/codex/CodexProcessManager.ts +38 -3
- package/src/codex/CodexRuntime.ts +6 -0
- package/src/feishu/CardRenderer.ts +204 -1
- package/src/feishu/FeishuConnector.ts +13 -4
- package/src/index.ts +35 -3
- package/src/proxy/ProxySessionController.ts +186 -59
- package/src/runtime/AcpRuntimeAdapter.ts +5 -0
- package/src/runtime/agentEnvironment.ts +41 -3
- package/src/runtime/createAgentRuntimeRegistry.ts +18 -3
- package/src/runtime/types.ts +5 -0
- package/src/state/StateStore.ts +12 -9
- package/src/supervision/SafeRestartNotifier.ts +1 -11
|
@@ -31,6 +31,19 @@ running_in_agent_bot = os.environ.get("AGENT_BOT") == "1"
|
|
|
31
31
|
|
|
32
32
|
Treat this environment marker as authoritative. Do not infer Agent Bot execution merely because the CLI is installed, `~/.agent-bot` exists, or an Agent Bot process is running elsewhere. Configured agent environment variables cannot override the marker. Child processes normally inherit it, so the marker identifies the Agent Bot-started process tree unless a child explicitly replaces its environment.
|
|
33
33
|
|
|
34
|
+
Agent Bot preserves ordinary parent-process variables but removes inherited and Agent-configured `FEISHU_*` and `AGENT_BOT_*` variables before starting an Agent. It then exposes only this controlled, non-secret context when values are available:
|
|
35
|
+
|
|
36
|
+
```text
|
|
37
|
+
AGENT_BOT_HOME
|
|
38
|
+
AGENT_BOT_CONFIG
|
|
39
|
+
AGENT_BOT_AGENT_NAME
|
|
40
|
+
AGENT_BOT_LARK_APP_ID
|
|
41
|
+
AGENT_BOT_LARK_BOT_OPEN_ID
|
|
42
|
+
AGENT_BOT_LARK_USER_OPEN_ID
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`AGENT_BOT_HOME` is the active Profile root. Never expect `FEISHU_APP_SECRET`, Supervisor state, restart reasons, or restart notification routes in the Agent environment.
|
|
46
|
+
|
|
34
47
|
## Behave safely inside Agent Bot
|
|
35
48
|
|
|
36
49
|
When `AGENT_BOT=1`:
|
|
@@ -88,7 +101,7 @@ agentbot server status
|
|
|
88
101
|
agentbot task list
|
|
89
102
|
```
|
|
90
103
|
|
|
91
|
-
Agent Bot CLI interface text follows the system locale: locales beginning with `zh` use Chinese, while English and unsupported locales use English. JSON output is not localized. `agentbot server status`
|
|
104
|
+
Agent Bot CLI interface text follows the system locale: locales beginning with `zh` use Chinese, while English and unsupported locales use English. JSON output is not localized. `agentbot server status` reports the running profile's Lark App ID and each configured Agent process's PID and initialized version. With `--json`, read them from `feishuAppId` and `agents`; an Agent that has not started has `null` process fields.
|
|
92
105
|
|
|
93
106
|
Add `--json` to status and list commands when machine-readable output helps. If `agentbot` is unavailable, report that the CLI is not installed or linked instead of guessing process state.
|
|
94
107
|
|
|
@@ -132,6 +145,7 @@ List tasks before resolving a numeric reference because task numbers follow the
|
|
|
132
145
|
|
|
133
146
|
```powershell
|
|
134
147
|
agentbot task list
|
|
148
|
+
agentbot task current [--json]
|
|
135
149
|
agentbot task chat <number-or-task-id> [--json]
|
|
136
150
|
agentbot task status <number-or-task-id>
|
|
137
151
|
agentbot task stop <number-or-task-id>
|
|
@@ -143,52 +157,69 @@ agentbot task forkgroup <number-or-task-id> [title] [--json]
|
|
|
143
157
|
|
|
144
158
|
Task IDs may be supplied in full or as an unambiguous prefix. Use `task stop` to ask the running worker to send the App Server Interrupt signal; leave child-process lifecycle management to the selected Agent.
|
|
145
159
|
|
|
160
|
+
Read all task identity fields from `task list`: every entry shows its Agent and `AgentBot task ID`; App Server tasks also show `App Server thread ID`, while ACP tasks show `ACP session ID`. The status, title, conversation context, and update time remain visible. The CLI reads `CODEX_THREAD_ID` for Codex and `TRAECLI_THREAD_ID` for TraeX; when the available native Thread IDs identify one task, it marks that entry as `Current`. Use `task current` to print that task's details directly. Use any displayed full ID or an unambiguous prefix as a task reference. With `--json`, read the local and native values from `localSessionId`, `remoteSessionId`, and `acpSessionId`.
|
|
161
|
+
|
|
146
162
|
Use `task chat <number-or-task-id>` to read the Feishu chat ID associated with a task from local routing state. Plain output is only the chat ID for scripting. Add `--json` to include the stable task ID, complete context key, and the thread ID for a topic-bound task. This read-only command does not require the Agent Bot server to be running.
|
|
147
163
|
|
|
148
164
|
Use `task prompt` to send input to a specific task without changing any chat's current task. The bot first posts the Prompt text to the task's existing Feishu chat, thread, or private-chat route, then submits it to the task. If posting fails, the task is not started or steered. The thinking card and final response continue to the same route; the command line only reports whether the Prompt was accepted.
|
|
149
165
|
|
|
150
|
-
### Create a
|
|
166
|
+
### Create or fork a group from the CLI
|
|
167
|
+
|
|
168
|
+
Choose by whether the new task needs the source conversation:
|
|
151
169
|
|
|
152
|
-
Use
|
|
170
|
+
- Use **New Group** for a fresh task in a separate private Lark group. Inherit the project and execution settings, but do not carry over prior turns. Use it for a separate assignment in the same codebase or for a different project or Agent.
|
|
171
|
+
- Use **Fork Group** to continue or parallelize work with the source task's conversation through a completed turn. Preserve that context in a separate private Lark group.
|
|
172
|
+
|
|
173
|
+
Both operations create and bind the destination task immediately. Neither interrupts the source task nor changes the source chat's current task.
|
|
174
|
+
|
|
175
|
+
When creating or forking from the task that is currently running this Skill, run `agentbot task current --json` first and pass its `localSessionId` as `<number-or-task-id>`. The command resolves Codex through `CODEX_THREAD_ID` and TraeX through `TRAECLI_THREAD_ID`, while preferring the only running match when another Agent's variable was inherited from a parent process. If the command cannot identify one task, ask for an explicit AgentBot task ID instead of guessing from list order, title, or project directory.
|
|
176
|
+
|
|
177
|
+
List tasks first, identify the intended source explicitly, then run one of these commands:
|
|
153
178
|
|
|
154
179
|
```powershell
|
|
155
|
-
|
|
180
|
+
agentbot task list
|
|
181
|
+
|
|
182
|
+
# Fresh task: inherit the source Agent, project, and execution settings.
|
|
156
183
|
agentbot task newgroup <number-or-task-id> "Review"
|
|
157
184
|
|
|
158
|
-
#
|
|
185
|
+
# Fresh task: override the inherited project directory.
|
|
159
186
|
agentbot task newgroup <number-or-task-id> "Review" --dir ~/dev/project
|
|
160
187
|
|
|
161
|
-
#
|
|
188
|
+
# Fresh task: use a Projectless App Server workspace.
|
|
162
189
|
agentbot task newgroup <number-or-task-id> "Question" --nodir
|
|
163
190
|
|
|
164
|
-
#
|
|
191
|
+
# Fresh task: use another configured Agent and that Agent's defaults.
|
|
165
192
|
agentbot task newgroup <number-or-task-id> "Review" --agent traex
|
|
193
|
+
|
|
194
|
+
# Context-preserving branch from the source task.
|
|
195
|
+
agentbot task forkgroup <number-or-task-id> "Parallel review"
|
|
166
196
|
```
|
|
167
197
|
|
|
168
|
-
Apply these rules:
|
|
198
|
+
Apply these New Group rules:
|
|
169
199
|
|
|
170
|
-
-
|
|
171
|
-
- `--dir
|
|
172
|
-
-
|
|
173
|
-
-
|
|
174
|
-
-
|
|
175
|
-
- Creating the group must not interrupt the source task or switch the source chat's current task. Add `--json` when the caller needs `group.chatId`, `group.contextKey`, or the created task IDs.
|
|
200
|
+
- Treat `<number-or-task-id>` as the source context; never substitute the shell's current working directory for its project directory.
|
|
201
|
+
- With neither `--dir` nor `--nodir`, inherit the source project. If it is Projectless, create a fresh Projectless workspace rather than reusing its generated directory.
|
|
202
|
+
- Resolve `~`, `~/...`, and `~\...` in `--dir <cwd>` from the user's home directory. Reject combining `--dir` with `--nodir`.
|
|
203
|
+
- Allow `--nodir` only for an App Server Agent.
|
|
204
|
+
- With no `--agent`, inherit the source Agent, Provider, model, reasoning effort, and permission mode. With `--agent <standard-name>`, preserve the selected project shape but use the selected Agent's runtime defaults. `--agent` is a CLI-only New Group option.
|
|
176
205
|
|
|
177
|
-
|
|
206
|
+
Apply these Fork Group rules:
|
|
178
207
|
|
|
179
|
-
|
|
208
|
+
- Fork the latest available completed turn and preserve its conversation context. Never include or interrupt a newer active turn. Reject the request when no completed turn is available.
|
|
209
|
+
- Keep the source Agent, project, Provider, model, reasoning effort, and permission mode. Fork Group does not accept Agent or project overrides; use New Group when those must change.
|
|
210
|
+
- Use the optional title as the destination task title and group suffix.
|
|
180
211
|
|
|
181
|
-
|
|
212
|
+
Require a running Server for either CLI command. Require `FEISHU_USER_OPEN_ID` so the Server can invite the saved authorizing user; the CLI has no Lark operator identity of its own. Add `--json` when the caller needs `group.chatId`, `group.contextKey`, or the created task IDs.
|
|
182
213
|
|
|
183
|
-
|
|
214
|
+
Name a New Group without an explicit title as `[agent] [project directory] 新任务 (mm-dd)`, or `[agent] 新任务 (mm-dd)` when Projectless. Name an untitled Fork Group with the source task's persistent `source title(分支 N)` sequence, without a date suffix. Cap the displayed Lark group name at 60 characters by truncating only the group title portion; preserve the complete Agent Bot task title. Do not automatically send a Sessions or Status card to the destination group. Send a welcome message that reports Provider, model, reasoning effort, and permission mode.
|
|
184
215
|
|
|
185
|
-
|
|
216
|
+
Give every created group a deterministic scheme-C Identicon avatar. Display the first word for Latin project names or up to four characters for Chinese names, and hash the normalized full project path into the palette and symmetric node pattern. Use the title as the stable seed for Projectless groups. Replace the user's home prefix with `~` in the group-name project segment. Limit that segment to 15 characters, preferring the final two directories, then the final directory, and only then tail truncation. Use `\` on Windows and `/` on macOS/Linux.
|
|
186
217
|
|
|
187
|
-
|
|
218
|
+
Use `/queue <prompt>` in Feishu to persist a separate follow-up Prompt instead of steering the active turn. Queued Prompts run in FIFO order after the current turn completes and survive Agent Bot restarts. `/nosteer <prompt>` is an equivalent compatibility spelling.
|
|
188
219
|
|
|
189
|
-
|
|
220
|
+
For a long-running objective in the active Feishu task, use `/goal <objective>`. Use `/goal` to inspect it, `/goal pause` or `/goal resume` to control automatic continuation, `/goal edit <objective>` to revise it, and `/goal clear` to remove it. Stopping a Goal turn through Agent Bot also pauses the Goal before sending Interrupt so it does not immediately continue.
|
|
190
221
|
|
|
191
|
-
Use `/
|
|
222
|
+
Use `/agent` in Feishu to open the unified execution-settings card on its Agent tab and `/agent <name>` to directly select the default agent. The Agent tab is present only when multiple agents are configured and remains usable before the chat has a current task. When only one Agent is configured, `/agent` reports that current Agent directly instead of sending a card. Agent selection affects future tasks and does not replace the Agent of an existing task; use `/new` after selecting when a new task is needed.
|
|
192
223
|
|
|
193
224
|
Use `/new [title] --nodir` to force a new App Server Projectless task even when the current task belongs to a project. `--nodir` and `--dir <cwd>` are mutually exclusive; omitting both preserves the normal project-shape inheritance behavior.
|
|
194
225
|
|
|
@@ -3,7 +3,10 @@ import readline from "node:readline";
|
|
|
3
3
|
import type { Logger } from "pino";
|
|
4
4
|
import type { AgentConfig } from "../config/schema.js";
|
|
5
5
|
import { AcpJsonRpcConnection } from "./AcpJsonRpcConnection.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
agentBotEnvironment,
|
|
8
|
+
type AgentEnvironmentContext,
|
|
9
|
+
} from "../runtime/agentEnvironment.js";
|
|
7
10
|
import { spawnStdioCommand } from "../utils/spawnCommand.js";
|
|
8
11
|
|
|
9
12
|
export interface ManagedAcpProcess {
|
|
@@ -16,14 +19,24 @@ export interface ManagedAcpProcess {
|
|
|
16
19
|
export class AcpProcessManager {
|
|
17
20
|
private readonly processes = new Map<string, ManagedAcpProcess>();
|
|
18
21
|
|
|
19
|
-
constructor(
|
|
22
|
+
constructor(
|
|
23
|
+
private readonly logger: Logger,
|
|
24
|
+
private readonly environmentContext: () => AgentEnvironmentContext = () => ({}),
|
|
25
|
+
) {}
|
|
20
26
|
|
|
21
27
|
start(processKey: string, agentName: string, agent: AgentConfig): ManagedAcpProcess {
|
|
22
28
|
if (this.processes.has(processKey)) {
|
|
23
29
|
throw new Error(`ACP process already exists: ${processKey}`);
|
|
24
30
|
}
|
|
25
31
|
|
|
26
|
-
const child = spawnStdioCommand(
|
|
32
|
+
const child = spawnStdioCommand(
|
|
33
|
+
agent.command,
|
|
34
|
+
agent.args,
|
|
35
|
+
agentBotEnvironment(process.env, agent.env, {
|
|
36
|
+
...this.environmentContext(),
|
|
37
|
+
agentName,
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
27
40
|
|
|
28
41
|
const childLogger = this.logger.child({ processKey, agentName });
|
|
29
42
|
const connection = new AcpJsonRpcConnection(child, childLogger);
|
|
@@ -40,6 +40,7 @@ export class AcpSessionManager {
|
|
|
40
40
|
private readonly sessions = new Map<string, RuntimeSession>();
|
|
41
41
|
private readonly sessionsByAcpId = new Map<string, RuntimeSession>();
|
|
42
42
|
private processStart?: Promise<ManagedAcpProcess>;
|
|
43
|
+
private processVersion?: string;
|
|
43
44
|
|
|
44
45
|
constructor(
|
|
45
46
|
private readonly agentName: string,
|
|
@@ -52,6 +53,15 @@ export class AcpSessionManager {
|
|
|
52
53
|
return this.sessions.get(localSessionId);
|
|
53
54
|
}
|
|
54
55
|
|
|
56
|
+
getProcessInfo(): { pid?: number; version?: string } {
|
|
57
|
+
const pid = this.processManager.get(this.agentName)?.child.pid;
|
|
58
|
+
if (!pid) return {};
|
|
59
|
+
return {
|
|
60
|
+
pid,
|
|
61
|
+
...(this.processVersion ? { version: this.processVersion } : {}),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
55
65
|
async create(input: CreateRuntimeSessionInput): Promise<RuntimeSession> {
|
|
56
66
|
if (input.agentName !== this.agentName) throw new Error(`Unexpected agent: ${input.agentName}`);
|
|
57
67
|
|
|
@@ -133,6 +143,7 @@ export class AcpSessionManager {
|
|
|
133
143
|
this.sessions.clear();
|
|
134
144
|
this.sessionsByAcpId.clear();
|
|
135
145
|
this.processStart = undefined;
|
|
146
|
+
this.processVersion = undefined;
|
|
136
147
|
this.processManager.stopAll();
|
|
137
148
|
}
|
|
138
149
|
|
|
@@ -181,6 +192,7 @@ export class AcpSessionManager {
|
|
|
181
192
|
}
|
|
182
193
|
|
|
183
194
|
private async startProcess(): Promise<ManagedAcpProcess> {
|
|
195
|
+
this.processVersion = undefined;
|
|
184
196
|
const managed = this.processManager.start(this.agentName, this.agentName, this.agent);
|
|
185
197
|
try {
|
|
186
198
|
const connection = managed.connection;
|
|
@@ -210,6 +222,8 @@ export class AcpSessionManager {
|
|
|
210
222
|
version: "0.1.0",
|
|
211
223
|
},
|
|
212
224
|
});
|
|
225
|
+
const version = initializeResult.agentInfo?.version?.trim();
|
|
226
|
+
if (version) this.processVersion = version;
|
|
213
227
|
if (initializeResult.authMethods?.length) {
|
|
214
228
|
const method = initializeResult.authMethods[0];
|
|
215
229
|
this.logger.info({ methodId: method.id }, "Authenticating ACP agent with first advertised method.");
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import pino from "pino";
|
|
5
|
+
import { loadConfig } from "../config/loadConfig.js";
|
|
6
|
+
import type { AppConfig } from "../config/schema.js";
|
|
7
|
+
import {
|
|
8
|
+
CardRenderer,
|
|
9
|
+
type InitializationWelcomeFeature,
|
|
10
|
+
type InitializationWelcomeKind,
|
|
11
|
+
} from "../feishu/CardRenderer.js";
|
|
12
|
+
import { FeishuMessageClient } from "../feishu/FeishuMessageClient.js";
|
|
13
|
+
import type { CliLanguage } from "./i18n.js";
|
|
14
|
+
|
|
15
|
+
export interface InitializationReceipt {
|
|
16
|
+
version: string;
|
|
17
|
+
initializedAt: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type InitializationWelcomeResult =
|
|
21
|
+
| { status: "sent"; kind: InitializationWelcomeKind }
|
|
22
|
+
| { status: "skipped"; kind: InitializationWelcomeKind; reason: "feishu-skipped" | "missing-user-open-id" }
|
|
23
|
+
| { status: "failed"; kind: InitializationWelcomeKind; message: string };
|
|
24
|
+
|
|
25
|
+
export interface SendInitializationWelcomeInput {
|
|
26
|
+
configPath?: string;
|
|
27
|
+
language: CliLanguage;
|
|
28
|
+
version: string;
|
|
29
|
+
previousVersion?: string;
|
|
30
|
+
kind: InitializationWelcomeKind;
|
|
31
|
+
activationPending?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface InitializationWelcomeDependencies {
|
|
35
|
+
loadConfig(configPath?: string): AppConfig;
|
|
36
|
+
sendCard(config: AppConfig, contextKey: string, card: Record<string, unknown>): Promise<void>;
|
|
37
|
+
logoPath: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const INITIALIZATION_RECEIPT_FILE = "initialization.json";
|
|
41
|
+
const DEFAULT_LOGO_PATH = fileURLToPath(new URL("../../assets/agent-bot-logo.png", import.meta.url));
|
|
42
|
+
|
|
43
|
+
export function resolveInitializationWelcomeKind(input: {
|
|
44
|
+
firstInitialization: boolean;
|
|
45
|
+
previousVersion?: string;
|
|
46
|
+
currentVersion: string;
|
|
47
|
+
}): InitializationWelcomeKind {
|
|
48
|
+
if (input.firstInitialization) return "first";
|
|
49
|
+
return input.previousVersion === input.currentVersion ? "refresh" : "upgrade";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function readInitializationReceipt(homePath: string): InitializationReceipt | undefined {
|
|
53
|
+
try {
|
|
54
|
+
const value = JSON.parse(fs.readFileSync(initializationReceiptPath(homePath), "utf8")) as Partial<InitializationReceipt>;
|
|
55
|
+
if (typeof value.version !== "string" || !value.version.trim()) return undefined;
|
|
56
|
+
if (typeof value.initializedAt !== "string" || !value.initializedAt.trim()) return undefined;
|
|
57
|
+
return { version: value.version.trim(), initializedAt: value.initializedAt.trim() };
|
|
58
|
+
} catch {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function writeInitializationReceipt(homePath: string, version: string, initializedAt = new Date()): void {
|
|
64
|
+
const filePath = initializationReceiptPath(homePath);
|
|
65
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
66
|
+
fs.writeFileSync(filePath, `${JSON.stringify({
|
|
67
|
+
version,
|
|
68
|
+
initializedAt: initializedAt.toISOString(),
|
|
69
|
+
}, null, 2)}\n`, { mode: 0o600 });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function sendInitializationWelcome(
|
|
73
|
+
input: SendInitializationWelcomeInput,
|
|
74
|
+
dependencies: InitializationWelcomeDependencies = defaultDependencies,
|
|
75
|
+
): Promise<Exclude<InitializationWelcomeResult, { status: "failed" }>> {
|
|
76
|
+
const config = dependencies.loadConfig(input.configPath);
|
|
77
|
+
const userOpenId = config.feishu.userOpenId?.trim();
|
|
78
|
+
if (!userOpenId) {
|
|
79
|
+
return { status: "skipped", kind: input.kind, reason: "missing-user-open-id" };
|
|
80
|
+
}
|
|
81
|
+
const defaultAgentName = config.defaults.agent!;
|
|
82
|
+
const defaultAgent = config.agents[defaultAgentName]!;
|
|
83
|
+
const renderer = new CardRenderer();
|
|
84
|
+
const card = renderer.renderInitializationWelcome({
|
|
85
|
+
kind: input.kind,
|
|
86
|
+
language: input.language,
|
|
87
|
+
version: input.version,
|
|
88
|
+
previousVersion: input.previousVersion,
|
|
89
|
+
activationPending: input.activationPending,
|
|
90
|
+
defaultAgentName,
|
|
91
|
+
defaultAgentTitle: defaultAgent.title,
|
|
92
|
+
availableAgents: Object.values(config.agents).map((agent) => agent.title),
|
|
93
|
+
logoPath: dependencies.logoPath,
|
|
94
|
+
features: welcomeFeatures(input.kind, input.language),
|
|
95
|
+
});
|
|
96
|
+
await dependencies.sendCard(config, `open_id:${userOpenId}`, card);
|
|
97
|
+
return { status: "sent", kind: input.kind };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function initializationReceiptPath(homePath: string): string {
|
|
101
|
+
return path.join(homePath, "data", INITIALIZATION_RECEIPT_FILE);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function welcomeFeatures(
|
|
105
|
+
kind: InitializationWelcomeKind,
|
|
106
|
+
language: CliLanguage,
|
|
107
|
+
): InitializationWelcomeFeature[] {
|
|
108
|
+
if (kind === "upgrade") {
|
|
109
|
+
return language === "zh"
|
|
110
|
+
? [
|
|
111
|
+
{ icon: "⚙️", title: "Agent 状态更透明", description: "Server 状态会显示每个 Agent 的 PID 与版本。" },
|
|
112
|
+
{ icon: "🧭", title: "快速定位当前任务", description: "CLI 可识别正在调用它的 Codex 或 TraeX 任务。" },
|
|
113
|
+
{ icon: "🗂️", title: "卡片操作更顺手", description: "Help 命令可点击,Sessions 翻页与操作更清晰。" },
|
|
114
|
+
{ icon: "🛡️", title: "Crash 恢复更完整", description: "服务重启后会修复历史 Turn 的父子连接。" },
|
|
115
|
+
]
|
|
116
|
+
: [
|
|
117
|
+
{ icon: "⚙️", title: "Clearer Agent status", description: "Server status now shows each Agent's PID and version." },
|
|
118
|
+
{ icon: "🧭", title: "Find the current task", description: "The CLI can identify its invoking Codex or TraeX task." },
|
|
119
|
+
{ icon: "🗂️", title: "Better card actions", description: "Clickable Help commands and clearer Sessions pagination." },
|
|
120
|
+
{ icon: "🛡️", title: "Stronger crash recovery", description: "Turn graph links are repaired after service recovery." },
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
if (kind === "refresh") {
|
|
124
|
+
return language === "zh"
|
|
125
|
+
? [
|
|
126
|
+
{ icon: "💬", title: "随时开始对话", description: "私聊或群聊发送消息,即可继续当前任务。" },
|
|
127
|
+
{ icon: "🧠", title: "统一运行设置", description: "在同一张卡片中切换 Agent、模型、思考和权限。" },
|
|
128
|
+
{ icon: "🌿", title: "任务分支", description: "使用 New Group 或 Fork Group 并行处理工作。" },
|
|
129
|
+
{ icon: "🔄", title: "安全恢复", description: "任务、进度与服务状态会持久保存并恢复。" },
|
|
130
|
+
]
|
|
131
|
+
: [
|
|
132
|
+
{ icon: "💬", title: "Start anywhere", description: "Continue work from a private chat or group conversation." },
|
|
133
|
+
{ icon: "🧠", title: "Unified settings", description: "Switch Agent, model, thinking, and permissions in one card." },
|
|
134
|
+
{ icon: "🌿", title: "Branch tasks", description: "Use New Group or Fork Group for parallel work." },
|
|
135
|
+
{ icon: "🔄", title: "Safe recovery", description: "Tasks, progress, and service state persist across restarts." },
|
|
136
|
+
];
|
|
137
|
+
}
|
|
138
|
+
return language === "zh"
|
|
139
|
+
? [
|
|
140
|
+
{ icon: "💬", title: "飞书直接对话", description: "私聊、群聊和话题分别维护自己的任务上下文。" },
|
|
141
|
+
{ icon: "🤖", title: "连接本地 Agent", description: "目前支持 Codex、TraeX 和兼容的编程 Agent。" },
|
|
142
|
+
{ icon: "🌿", title: "新建与分支", description: "把新任务或对话分支放进独立群聊并行推进。" },
|
|
143
|
+
{ icon: "📍", title: "进度始终可见", description: "通过思考卡片查看工具调用、文件变更和最终结果。" },
|
|
144
|
+
]
|
|
145
|
+
: [
|
|
146
|
+
{ icon: "💬", title: "Work from Lark", description: "Private chats, groups, and topics keep separate task context." },
|
|
147
|
+
{ icon: "🤖", title: "Connect local Agents", description: "Use Codex, TraeX, and compatible coding Agents." },
|
|
148
|
+
{ icon: "🌿", title: "Create and branch", description: "Move fresh tasks or conversation branches into new groups." },
|
|
149
|
+
{ icon: "📍", title: "See every step", description: "Follow tools, file changes, and final results in progress cards." },
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const defaultDependencies: InitializationWelcomeDependencies = {
|
|
154
|
+
loadConfig,
|
|
155
|
+
logoPath: DEFAULT_LOGO_PATH,
|
|
156
|
+
sendCard: async (config, contextKey, card) => {
|
|
157
|
+
const logger = pino({ level: "silent" });
|
|
158
|
+
await new FeishuMessageClient(config, logger).sendInteractiveCard(contextKey, card);
|
|
159
|
+
},
|
|
160
|
+
};
|
package/src/cli/help.ts
CHANGED
|
@@ -13,10 +13,11 @@ export function renderCliHelp(version: string, language: CliLanguage = cliLangua
|
|
|
13
13
|
init --reset 重置并重新配置显式指定的 Profile
|
|
14
14
|
console 打开本地控制台
|
|
15
15
|
server start 在后台启动 Agent Bot
|
|
16
|
-
server status 显示服务、飞书 App ID
|
|
16
|
+
server status 显示服务、飞书 App ID、Agent 进程和安全重启状态
|
|
17
17
|
server restart [--task <任务>] 安排安全重启并将状态发回指定任务会话
|
|
18
18
|
server stop 停止 Agent Bot
|
|
19
19
|
task list 列出任务
|
|
20
|
+
task current [--json] 显示当前调用任务的详情
|
|
20
21
|
task status <任务> 显示任务状态
|
|
21
22
|
task chat <任务> 显示任务绑定的飞书会话
|
|
22
23
|
task prompt <任务> <提示词> 向任务发送提示词
|
|
@@ -66,10 +67,11 @@ Common commands:
|
|
|
66
67
|
init --reset Reset and reconfigure an explicit profile
|
|
67
68
|
console Open the local console
|
|
68
69
|
server start Start Agent Bot in the background
|
|
69
|
-
server status Show the server, Lark App ID, and safe-restart status
|
|
70
|
+
server status Show the server, Lark App ID, Agent processes, and safe-restart status
|
|
70
71
|
server restart [--task <task>] Schedule a safe restart and return status to the task's conversation
|
|
71
72
|
server stop Stop Agent Bot
|
|
72
73
|
task list List tasks
|
|
74
|
+
task current [--json] Show details for the task invoking the CLI
|
|
73
75
|
task status <task> Show task status
|
|
74
76
|
task chat <task> Show the task's Lark chat binding
|
|
75
77
|
task prompt <task> <prompt> Send a Prompt to a task
|
package/src/cli/serverStatus.ts
CHANGED
|
@@ -29,16 +29,44 @@ export function formatServerStatus(data: unknown, language: CliLanguage = cliLan
|
|
|
29
29
|
language,
|
|
30
30
|
)
|
|
31
31
|
: cliText("not scheduled", "未计划", language);
|
|
32
|
+
const agents = formatAgentProcesses(value.agents, language);
|
|
32
33
|
return [
|
|
33
34
|
`${cliText("Agent Bot server: ", "Agent Bot 服务:", language)}${state}`,
|
|
34
35
|
`${cliText("Lark App ID: ", "飞书 App ID:", language)}${value.feishuAppId ?? cliText("not configured", "未配置", language)}`,
|
|
35
36
|
`${cliText("PID: ", "PID:", language)}${value.pid ?? "-"}`,
|
|
36
37
|
`${cliText("Started at: ", "启动时间:", language)}${value.startedAt ?? "-"}`,
|
|
37
38
|
`${cliText("Supervisor: ", "Supervisor:", language)}${value.supervised ? cliText("enabled", "已启用", language) : cliText("disabled", "未启用", language)}`,
|
|
39
|
+
...agents,
|
|
38
40
|
`${cliText("Running tasks: ", "运行中任务:", language)}${activity.runningSessions ?? 0}`,
|
|
39
41
|
`${cliText("Pending final deliveries: ", "待发送最终结果:", language)}${activity.pendingFinalDeliveries ?? 0}`,
|
|
40
42
|
`${cliText("Safe restart: ", "安全重启:", language)}${safeRestart}`,
|
|
41
43
|
"",
|
|
42
44
|
].join("\n");
|
|
43
45
|
}
|
|
46
|
+
|
|
47
|
+
function formatAgentProcesses(value: unknown, language: CliLanguage): string[] {
|
|
48
|
+
const agents = Array.isArray(value)
|
|
49
|
+
? value.filter((item): item is Record<string, unknown> => Boolean(item) && typeof item === "object")
|
|
50
|
+
: [];
|
|
51
|
+
if (agents.length === 0) {
|
|
52
|
+
return [`${cliText("Agents: ", "Agent 进程:", language)}-`];
|
|
53
|
+
}
|
|
54
|
+
return [
|
|
55
|
+
cliText("Agents:", "Agent 进程:", language),
|
|
56
|
+
...agents.map((agent) => {
|
|
57
|
+
const name = typeof agent.name === "string" && agent.name.trim() ? agent.name.trim() : "-";
|
|
58
|
+
const title = typeof agent.title === "string" && agent.title.trim() ? agent.title.trim() : undefined;
|
|
59
|
+
const label = title && title !== name ? `${name} (${title})` : name;
|
|
60
|
+
const pid = typeof agent.pid === "number" && Number.isInteger(agent.pid) && agent.pid > 0
|
|
61
|
+
? String(agent.pid)
|
|
62
|
+
: "-";
|
|
63
|
+
const version = typeof agent.version === "string" && agent.version.trim()
|
|
64
|
+
? agent.version.trim()
|
|
65
|
+
: "-";
|
|
66
|
+
return language === "zh"
|
|
67
|
+
? ` ${label}:PID ${pid},版本 ${version}`
|
|
68
|
+
: ` ${label}: PID ${pid}, version ${version}`;
|
|
69
|
+
}),
|
|
70
|
+
];
|
|
71
|
+
}
|
|
44
72
|
import { cliLanguage, cliText, type CliLanguage } from "./i18n.js";
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { SessionRecord } from "../state/StateStore.js";
|
|
2
|
+
import { cliLanguage, cliText, type CliLanguage } from "./i18n.js";
|
|
3
|
+
|
|
4
|
+
export type CurrentTaskResolution =
|
|
5
|
+
| { status: "found"; session: SessionRecord }
|
|
6
|
+
| { status: "missing-thread-id" }
|
|
7
|
+
| { status: "not-found"; threadIds: string[] }
|
|
8
|
+
| { status: "ambiguous"; sessions: SessionRecord[]; threadIds: string[] };
|
|
9
|
+
|
|
10
|
+
export function formatTaskList(
|
|
11
|
+
sessions: SessionRecord[],
|
|
12
|
+
language: CliLanguage = cliLanguage,
|
|
13
|
+
currentThreadIds: readonly string[] = [],
|
|
14
|
+
): string {
|
|
15
|
+
if (sessions.length === 0) return cliText("No tasks.\n", "没有任务。\n", language);
|
|
16
|
+
|
|
17
|
+
const current = resolveCurrentTask(sessions, currentThreadIds);
|
|
18
|
+
const currentLocalSessionId = current.status === "found" ? current.session.localSessionId : undefined;
|
|
19
|
+
return sessions.map((session, index) => {
|
|
20
|
+
const title = session.title ?? cliText("Untitled task", "未命名任务", language);
|
|
21
|
+
const current = session.localSessionId === currentLocalSessionId
|
|
22
|
+
? ` [${cliText("Current", "当前任务", language)}]`
|
|
23
|
+
: "";
|
|
24
|
+
const nativeId = nativeTaskIdLabel(session, language);
|
|
25
|
+
return [
|
|
26
|
+
`${index + 1}.${current} [${taskStateLabel(session.status, language)}/${taskStateLabel(session.lastTurnStatus, language)}] ${title}`,
|
|
27
|
+
` ${cliText("Agent: ", "Agent:", language)}${session.agentName} · ${cliText("AgentBot task ID: ", "AgentBot 任务 ID:", language)}${session.localSessionId}`,
|
|
28
|
+
` ${nativeId ? `${nativeId} · ` : ""}${session.contextKey} · ${session.updatedAt}`,
|
|
29
|
+
].join("\n");
|
|
30
|
+
}).join("\n") + "\n";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function currentAppServerThreadIds(env: NodeJS.ProcessEnv): string[] {
|
|
34
|
+
return [...new Set([
|
|
35
|
+
env.CODEX_THREAD_ID?.trim(),
|
|
36
|
+
env.TRAECLI_THREAD_ID?.trim(),
|
|
37
|
+
].filter((value): value is string => Boolean(value)))];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function resolveCurrentTask(
|
|
41
|
+
sessions: SessionRecord[],
|
|
42
|
+
currentThreadIds: readonly string[],
|
|
43
|
+
): CurrentTaskResolution {
|
|
44
|
+
const threadIds = [...new Set(currentThreadIds.map((value) => value.trim()).filter(Boolean))];
|
|
45
|
+
if (threadIds.length === 0) return { status: "missing-thread-id" };
|
|
46
|
+
|
|
47
|
+
const candidates = new Set(threadIds);
|
|
48
|
+
const matches = sessions.filter((session) => session.remoteSessionId && candidates.has(session.remoteSessionId));
|
|
49
|
+
if (matches.length === 0) return { status: "not-found", threadIds };
|
|
50
|
+
if (matches.length === 1) return { status: "found", session: matches[0]! };
|
|
51
|
+
|
|
52
|
+
const running = matches.filter((session) =>
|
|
53
|
+
session.status === "running" || session.lastTurnStatus === "running");
|
|
54
|
+
if (running.length === 1) return { status: "found", session: running[0]! };
|
|
55
|
+
return { status: "ambiguous", sessions: matches, threadIds };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function taskStateLabel(
|
|
59
|
+
status: string | undefined,
|
|
60
|
+
language: CliLanguage = cliLanguage,
|
|
61
|
+
): string {
|
|
62
|
+
if (!status) return "-";
|
|
63
|
+
const chinese: Record<string, string> = {
|
|
64
|
+
starting: "启动中",
|
|
65
|
+
ready: "就绪",
|
|
66
|
+
running: "运行中",
|
|
67
|
+
closed: "已关闭",
|
|
68
|
+
failed: "失败",
|
|
69
|
+
completed: "已完成",
|
|
70
|
+
cancelled: "已取消",
|
|
71
|
+
interrupted: "已中断",
|
|
72
|
+
inProgress: "进行中",
|
|
73
|
+
active: "活动中",
|
|
74
|
+
idle: "空闲",
|
|
75
|
+
not_loaded: "未加载",
|
|
76
|
+
error: "错误",
|
|
77
|
+
};
|
|
78
|
+
return language === "zh" ? chinese[status] ?? status : status;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function nativeTaskIdLabel(session: SessionRecord, language: CliLanguage): string | undefined {
|
|
82
|
+
if (session.runtimeKind === "acp" || session.acpSessionId) {
|
|
83
|
+
const sessionId = session.acpSessionId ?? session.remoteSessionId;
|
|
84
|
+
return sessionId
|
|
85
|
+
? `${cliText("ACP session ID: ", "ACP Session ID:", language)}${sessionId}`
|
|
86
|
+
: undefined;
|
|
87
|
+
}
|
|
88
|
+
return session.remoteSessionId
|
|
89
|
+
? `${cliText("App Server thread ID: ", "App Server 原生任务 ID(Thread ID):", language)}${session.remoteSessionId}`
|
|
90
|
+
: undefined;
|
|
91
|
+
}
|