@cjhyy/code-shell-chat 0.9.2 → 0.9.4
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/dist/bound-session-chat.d.ts +60 -0
- package/dist/bound-session-chat.js +67 -0
- package/dist/cli.js +2 -2
- package/dist/codeshell.d.ts +1 -0
- package/dist/codeshell.js +1 -0
- package/dist/desktop-control-client.d.ts +14 -0
- package/dist/desktop-control-client.js +9 -0
- package/dist/gateway.d.ts +6 -0
- package/dist/gateway.js +8 -0
- package/dist/protocol.d.ts +3 -1
- package/package.json +1 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routes a conversation that has entered a Work Session, ahead of Mimi.
|
|
3
|
+
*
|
|
4
|
+
* This middleware sits between the deterministic remote commands and
|
|
5
|
+
* createMimiPetChat. When the host reports no binding for the conversation it
|
|
6
|
+
* calls next() and Mimi handles the message exactly as before; when there is a
|
|
7
|
+
* binding it terminates the chain, and the message goes to the Session instead
|
|
8
|
+
* of the manager.
|
|
9
|
+
*
|
|
10
|
+
* Two properties matter more than they look:
|
|
11
|
+
*
|
|
12
|
+
* - Leaving is decided here, not by the host and never by a model. /mimi has
|
|
13
|
+
* to work when the worker is wedged or the bound Session is unreachable,
|
|
14
|
+
* otherwise a user can be stuck inside a Session with no way out.
|
|
15
|
+
* - The reply for an accepted message is not this middleware's job. A Session
|
|
16
|
+
* turn can take minutes, far longer than an inbound HTTP request should be
|
|
17
|
+
* held open, so acceptance is acknowledged immediately and the real answer
|
|
18
|
+
* comes back later through the durable outbox.
|
|
19
|
+
*/
|
|
20
|
+
import type { ChatMiddleware } from "./chat-gateway.js";
|
|
21
|
+
import type { ChannelMessage } from "./channel.js";
|
|
22
|
+
/** What the host decided to do with one inbound message. */
|
|
23
|
+
export type BoundSessionDisposition = {
|
|
24
|
+
kind: "not-bound";
|
|
25
|
+
} | {
|
|
26
|
+
kind: "accepted";
|
|
27
|
+
notice?: string;
|
|
28
|
+
} | {
|
|
29
|
+
kind: "left";
|
|
30
|
+
text: string;
|
|
31
|
+
} | {
|
|
32
|
+
kind: "status";
|
|
33
|
+
text: string;
|
|
34
|
+
} | {
|
|
35
|
+
kind: "suspended";
|
|
36
|
+
text: string;
|
|
37
|
+
};
|
|
38
|
+
export interface BoundSessionControlClient {
|
|
39
|
+
/**
|
|
40
|
+
* Hand one inbound message to the host's bridge. Returns what happened so
|
|
41
|
+
* the middleware can decide whether to stop or fall through to Mimi.
|
|
42
|
+
*/
|
|
43
|
+
routeBoundSessionMessage(input: {
|
|
44
|
+
channel: string;
|
|
45
|
+
target: string;
|
|
46
|
+
senderId: string;
|
|
47
|
+
messageId?: string;
|
|
48
|
+
text: string;
|
|
49
|
+
isDirectMessage: boolean;
|
|
50
|
+
}): Promise<BoundSessionDisposition>;
|
|
51
|
+
}
|
|
52
|
+
export interface BoundSessionChatOptions {
|
|
53
|
+
desktop: BoundSessionControlClient;
|
|
54
|
+
/**
|
|
55
|
+
* Whether this conversation is a private chat. Group chats cannot be bound
|
|
56
|
+
* in Phase 1, and the adapter is the only thing that knows which is which.
|
|
57
|
+
*/
|
|
58
|
+
isDirectMessage?(message: ChannelMessage): boolean;
|
|
59
|
+
}
|
|
60
|
+
export declare function createBoundSessionChat(options: BoundSessionChatOptions): ChatMiddleware;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routes a conversation that has entered a Work Session, ahead of Mimi.
|
|
3
|
+
*
|
|
4
|
+
* This middleware sits between the deterministic remote commands and
|
|
5
|
+
* createMimiPetChat. When the host reports no binding for the conversation it
|
|
6
|
+
* calls next() and Mimi handles the message exactly as before; when there is a
|
|
7
|
+
* binding it terminates the chain, and the message goes to the Session instead
|
|
8
|
+
* of the manager.
|
|
9
|
+
*
|
|
10
|
+
* Two properties matter more than they look:
|
|
11
|
+
*
|
|
12
|
+
* - Leaving is decided here, not by the host and never by a model. /mimi has
|
|
13
|
+
* to work when the worker is wedged or the bound Session is unreachable,
|
|
14
|
+
* otherwise a user can be stuck inside a Session with no way out.
|
|
15
|
+
* - The reply for an accepted message is not this middleware's job. A Session
|
|
16
|
+
* turn can take minutes, far longer than an inbound HTTP request should be
|
|
17
|
+
* held open, so acceptance is acknowledged immediately and the real answer
|
|
18
|
+
* comes back later through the durable outbox.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* A conversation is only addressable when both target and sender are known.
|
|
22
|
+
* Without them two anonymous messages on one channel are indistinguishable,
|
|
23
|
+
* and a reply could reach the wrong person, so such a message is left to Mimi.
|
|
24
|
+
*/
|
|
25
|
+
function isAddressable(message) {
|
|
26
|
+
return Boolean(message.target?.trim() && message.senderId?.trim());
|
|
27
|
+
}
|
|
28
|
+
export function createBoundSessionChat(options) {
|
|
29
|
+
const isDirectMessage = options.isDirectMessage ?? (() => true);
|
|
30
|
+
return async ({ message, reply }, next) => {
|
|
31
|
+
if (!message.text.trim() && !message.attachments?.length)
|
|
32
|
+
return next();
|
|
33
|
+
if (!isAddressable(message))
|
|
34
|
+
return next();
|
|
35
|
+
let disposition;
|
|
36
|
+
try {
|
|
37
|
+
disposition = await options.desktop.routeBoundSessionMessage({
|
|
38
|
+
channel: message.channel,
|
|
39
|
+
target: message.target,
|
|
40
|
+
senderId: message.senderId,
|
|
41
|
+
...(message.messageId ? { messageId: message.messageId } : {}),
|
|
42
|
+
text: message.text,
|
|
43
|
+
isDirectMessage: isDirectMessage(message),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// The bridge being unreachable must not swallow the message: fall
|
|
48
|
+
// through so Mimi still answers, which is the pre-binding behavior.
|
|
49
|
+
return next();
|
|
50
|
+
}
|
|
51
|
+
switch (disposition.kind) {
|
|
52
|
+
case "not-bound":
|
|
53
|
+
return next();
|
|
54
|
+
case "accepted":
|
|
55
|
+
// The Session's answer arrives later through the outbox. Only an
|
|
56
|
+
// explicit notice (the stale-binding reminder) is sent now.
|
|
57
|
+
if (disposition.notice)
|
|
58
|
+
await reply({ text: disposition.notice });
|
|
59
|
+
return;
|
|
60
|
+
case "left":
|
|
61
|
+
case "status":
|
|
62
|
+
case "suspended":
|
|
63
|
+
await reply({ text: disposition.text });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import { createChannelAdapterAsync } from "./adapter-factory.js";
|
|
|
7
7
|
import { ChatGateway, createAllowlistMiddleware, createRateLimitMiddleware, } from "./chat-gateway.js";
|
|
8
8
|
import { defaultGatewayConfigPath, loadGatewayConfig } from "./config.js";
|
|
9
9
|
import { DesktopControlClient } from "./desktop-control-client.js";
|
|
10
|
-
import { CODE_SHELL_REMOTE_COMMANDS, createCodeShellRemoteCommands, createMimiPetChat, } from "./gateway.js";
|
|
10
|
+
import { CODE_SHELL_REMOTE_COMMANDS, createCodeShellRemoteCommands, createMimiPetChat, MIMI_MIN_CONCURRENT_MESSAGES_PER_TARGET, } from "./gateway.js";
|
|
11
11
|
import { loginCodeShellWechat } from "./wechat-login.js";
|
|
12
12
|
import { defaultWechatDataDirectory } from "./wechat-storage.js";
|
|
13
13
|
import { acquireGatewayInstanceLock } from "./instance-lock.js";
|
|
@@ -63,7 +63,7 @@ async function main(args = process.argv.slice(2)) {
|
|
|
63
63
|
path: config.runtime.inboxPath,
|
|
64
64
|
maxPending: config.runtime.maxPending,
|
|
65
65
|
maxConcurrent: config.runtime.maxConcurrent,
|
|
66
|
-
maxPerTarget: config.runtime.maxPerTarget,
|
|
66
|
+
maxPerTarget: Math.max(config.runtime.maxPerTarget, MIMI_MIN_CONCURRENT_MESSAGES_PER_TARGET),
|
|
67
67
|
},
|
|
68
68
|
adapterRestart: {
|
|
69
69
|
baseMs: config.runtime.adapterRestartBaseMs,
|
package/dist/codeshell.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from "./config.js";
|
|
2
2
|
export * from "./desktop-control-client.js";
|
|
3
3
|
export * from "./gateway.js";
|
|
4
|
+
export * from "./bound-session-chat.js";
|
|
4
5
|
export * from "./protocol.js";
|
|
5
6
|
export * from "./wechat-login.js";
|
|
6
7
|
export { hasWechatStoredContextToken } from "./wechat-storage.js";
|
package/dist/codeshell.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from "./config.js";
|
|
2
2
|
export * from "./desktop-control-client.js";
|
|
3
3
|
export * from "./gateway.js";
|
|
4
|
+
export * from "./bound-session-chat.js";
|
|
4
5
|
export * from "./protocol.js";
|
|
5
6
|
export * from "./wechat-login.js";
|
|
6
7
|
export { hasWechatStoredContextToken } from "./wechat-storage.js";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ChildProcess } from "node:child_process";
|
|
2
|
+
import type { BoundSessionDisposition } from "./bound-session-chat.js";
|
|
2
3
|
import type { DesktopGatewayConfig } from "./config.js";
|
|
3
4
|
import { type DesktopControlDescriptor, type DesktopControlEvent, type DesktopControlEventPage, type MobileRemoteOpenResult, type MobileRemoteStatus, type PetChatRequest, type PetChatResult } from "./protocol.js";
|
|
4
5
|
type SpawnFn = (command: string, args: readonly string[], options: {
|
|
@@ -42,6 +43,19 @@ export declare class DesktopControlClient {
|
|
|
42
43
|
expiresAt: number;
|
|
43
44
|
}>;
|
|
44
45
|
petChat(input: PetChatRequest): Promise<PetChatResult>;
|
|
46
|
+
/**
|
|
47
|
+
* Ask the host where one inbound message should go. Short timeout on
|
|
48
|
+
* purpose: this only records the routing decision, and the Session's answer
|
|
49
|
+
* returns later through the durable outbox.
|
|
50
|
+
*/
|
|
51
|
+
routeBoundSessionMessage(input: {
|
|
52
|
+
channel: string;
|
|
53
|
+
target: string;
|
|
54
|
+
senderId: string;
|
|
55
|
+
messageId?: string;
|
|
56
|
+
text: string;
|
|
57
|
+
isDirectMessage: boolean;
|
|
58
|
+
}): Promise<BoundSessionDisposition>;
|
|
45
59
|
events(after?: number, waitMs?: number, signal?: AbortSignal): Promise<DesktopControlEventPage>;
|
|
46
60
|
watchEvents(signal: AbortSignal, handle: (event: DesktopControlEvent, context: DesktopEventContext) => Promise<void>, options?: DesktopEventWatchOptions): Promise<void>;
|
|
47
61
|
private ensureDesktopAvailable;
|
|
@@ -49,6 +49,15 @@ export class DesktopControlClient {
|
|
|
49
49
|
await this.ensureDesktopAvailable();
|
|
50
50
|
return this.request("POST", "/v1/pet/chat", 150_000, input);
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Ask the host where one inbound message should go. Short timeout on
|
|
54
|
+
* purpose: this only records the routing decision, and the Session's answer
|
|
55
|
+
* returns later through the durable outbox.
|
|
56
|
+
*/
|
|
57
|
+
async routeBoundSessionMessage(input) {
|
|
58
|
+
await this.ensureDesktopAvailable();
|
|
59
|
+
return this.request("POST", "/v1/session/route", 20_000, input);
|
|
60
|
+
}
|
|
52
61
|
events(after = 0, waitMs = 25_000, signal) {
|
|
53
62
|
if (!Number.isSafeInteger(after) || after < 0 || waitMs < 0 || waitMs > 25_000) {
|
|
54
63
|
throw new Error("invalid desktop event cursor");
|
package/dist/gateway.d.ts
CHANGED
|
@@ -9,6 +9,12 @@ export interface MimiPetChatOptions {
|
|
|
9
9
|
/** Enabled adapters whose declared capabilities back first-level Gateway discovery. */
|
|
10
10
|
channels?: readonly ChannelAdapter[];
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Mimi can accept several messages from one conversation while one model turn
|
|
14
|
+
* is running. The host folds same-route followers into that turn via steer;
|
|
15
|
+
* ordinary adapter replies remain bounded by the Gateway's global concurrency.
|
|
16
|
+
*/
|
|
17
|
+
export declare const MIMI_MIN_CONCURRENT_MESSAGES_PER_TARGET = 4;
|
|
12
18
|
export declare const CODE_SHELL_REMOTE_COMMANDS: readonly [{
|
|
13
19
|
readonly name: "open";
|
|
14
20
|
readonly description: "打开 CodeShell 手机遥控隧道";
|
package/dist/gateway.js
CHANGED
|
@@ -3,6 +3,12 @@ import { materializeChatAttachments } from "./attachments.js";
|
|
|
3
3
|
import { DesktopControlOperationError, DesktopControlUnavailableError, } from "./desktop-control-client.js";
|
|
4
4
|
import { materializeOutgoingAttachments, splitNotificationTextForChannel, } from "./notification-relay.js";
|
|
5
5
|
const MIMI_REPLY_CACHE_MAX_ENTRIES = 2_048;
|
|
6
|
+
/**
|
|
7
|
+
* Mimi can accept several messages from one conversation while one model turn
|
|
8
|
+
* is running. The host folds same-route followers into that turn via steer;
|
|
9
|
+
* ordinary adapter replies remain bounded by the Gateway's global concurrency.
|
|
10
|
+
*/
|
|
11
|
+
export const MIMI_MIN_CONCURRENT_MESSAGES_PER_TARGET = 4;
|
|
6
12
|
export const CODE_SHELL_REMOTE_COMMANDS = [
|
|
7
13
|
{ name: "open", description: "打开 CodeShell 手机遥控隧道" },
|
|
8
14
|
{ name: "close", description: "关闭 CodeShell 手机遥控隧道" },
|
|
@@ -193,6 +199,8 @@ async function buildMimiOutgoingReplies(options, message, capabilities, configur
|
|
|
193
199
|
channels: gatewayChannelCatalog(message.channel, capabilities, configuredChannels),
|
|
194
200
|
},
|
|
195
201
|
});
|
|
202
|
+
if (result.suppressReply)
|
|
203
|
+
return [];
|
|
196
204
|
const chunks = splitNotificationTextForChannel(result.text || "Mimi Pet 已处理,但没有返回文字内容。", message.channel, capabilities);
|
|
197
205
|
const outgoing = chunks.map((text) => ({ text }));
|
|
198
206
|
const last = outgoing.at(-1);
|
package/dist/protocol.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export interface DesktopControlEvent {
|
|
|
15
15
|
* stable when Desktop republishes one durable event after a process restart.
|
|
16
16
|
*/
|
|
17
17
|
deliveryKey?: string;
|
|
18
|
-
type: "tunnel.connected" | "tunnel.disconnected" | "tunnel.error" | "pet.task.completed" | "pet.task.failed" | "pet.task.cancelled" | "pet.task.reported" | "automation.completed" | "automation.failed" | "automation.stopped" | "automation.cancelled" | "automation.missed";
|
|
18
|
+
type: "tunnel.connected" | "tunnel.disconnected" | "tunnel.error" | "pet.task.completed" | "pet.task.failed" | "pet.task.cancelled" | "pet.task.reported" | "session.reply" | "automation.completed" | "automation.failed" | "automation.stopped" | "automation.cancelled" | "automation.missed";
|
|
19
19
|
text: string;
|
|
20
20
|
title?: string;
|
|
21
21
|
button?: {
|
|
@@ -92,6 +92,8 @@ export interface PetChatResult {
|
|
|
92
92
|
text: string;
|
|
93
93
|
petSessionId: string;
|
|
94
94
|
reason?: string;
|
|
95
|
+
/** This input joined an in-flight Mimi turn; the earlier request owns the combined reply. */
|
|
96
|
+
suppressReply?: boolean;
|
|
95
97
|
/** GatewayReply URL action; rendered natively or as a labelled link by the adapter. */
|
|
96
98
|
button?: {
|
|
97
99
|
text: string;
|