@foxden-app/foxclaw 0.11.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +18 -1
- package/CHANGELOG.md +36 -0
- package/README.md +4 -3
- package/README_EN.md +4 -3
- package/dist/antigravity/account_ui.d.ts +34 -0
- package/dist/antigravity/account_ui.js +354 -0
- package/dist/antigravity/adapter.js +1 -1
- package/dist/antigravity/auth.d.ts +4 -1
- package/dist/antigravity/auth.js +17 -5
- package/dist/antigravity/controller.d.ts +3 -106
- package/dist/antigravity/controller.js +2 -2083
- package/dist/antigravity/conversation_ui.d.ts +56 -0
- package/dist/antigravity/conversation_ui.js +509 -0
- package/dist/antigravity/ui_callbacks.d.ts +6 -0
- package/dist/antigravity/ui_callbacks.js +6 -0
- package/dist/bridge/unified_bridge.d.ts +114 -0
- package/dist/bridge/unified_bridge.js +1343 -0
- package/dist/channels/bridge_messaging_router.d.ts +8 -4
- package/dist/channels/bridge_messaging_router.js +15 -4
- package/dist/channels/telegram/telegram_messaging_port.d.ts +16 -0
- package/dist/channels/telegram/telegram_messaging_port.js +22 -2
- package/dist/channels/text_actions.d.ts +14 -0
- package/dist/channels/text_actions.js +48 -0
- package/dist/channels/weixin/weixin_channel_adapter.d.ts +3 -4
- package/dist/channels/weixin/weixin_channel_adapter.js +2 -2
- package/dist/channels/weixin/weixin_messaging_port.d.ts +16 -1
- package/dist/channels/weixin/weixin_messaging_port.js +34 -7
- package/dist/codex_app/activity.d.ts +62 -0
- package/dist/codex_app/activity.js +418 -0
- package/dist/codex_app/adapter.d.ts +2 -2
- package/dist/codex_app/adapter.js +55 -25
- package/dist/codex_app/backend_ui.d.ts +4 -0
- package/dist/codex_app/backend_ui.js +48 -0
- package/dist/codex_app/client.js +0 -12
- package/dist/config.d.ts +4 -0
- package/dist/config.js +41 -5
- package/dist/controller/access.d.ts +1 -11
- package/dist/controller/access.js +1 -33
- package/dist/controller/activity.d.ts +1 -62
- package/dist/controller/activity.js +2 -418
- package/dist/controller/auth_errors.d.ts +11 -0
- package/dist/controller/auth_errors.js +124 -0
- package/dist/controller/auth_files.d.ts +13 -0
- package/dist/controller/auth_files.js +156 -0
- package/dist/controller/auth_presentation.d.ts +77 -0
- package/dist/controller/auth_presentation.js +697 -0
- package/dist/controller/auth_quota_service.d.ts +27 -0
- package/dist/controller/auth_quota_service.js +151 -0
- package/dist/controller/controller.d.ts +30 -56
- package/dist/controller/controller.js +181 -3497
- package/dist/controller/interactions.d.ts +74 -0
- package/dist/controller/interactions.js +560 -0
- package/dist/controller/local_usage_service.d.ts +19 -0
- package/dist/controller/local_usage_service.js +62 -0
- package/dist/controller/native_panel_service.d.ts +55 -0
- package/dist/controller/native_panel_service.js +214 -0
- package/dist/controller/native_panels.d.ts +61 -0
- package/dist/controller/native_panels.js +784 -0
- package/dist/controller/shared_helpers.d.ts +53 -0
- package/dist/controller/shared_helpers.js +399 -0
- package/dist/controller/state_types.d.ts +390 -0
- package/dist/controller/state_types.js +82 -0
- package/dist/controller/turn_rendering.d.ts +22 -0
- package/dist/controller/turn_rendering.js +288 -0
- package/dist/core/access.d.ts +11 -0
- package/dist/core/access.js +33 -0
- package/dist/core/attachment_files.d.ts +13 -0
- package/dist/core/attachment_files.js +180 -0
- package/dist/core/attachment_types.d.ts +22 -0
- package/dist/core/attachment_types.js +1 -0
- package/dist/core/attachments.d.ts +5 -3
- package/dist/core/attachments.js +2 -2
- package/dist/core/backend_registry.d.ts +22 -0
- package/dist/core/backend_registry.js +50 -0
- package/dist/core/backend_ui.d.ts +28 -0
- package/dist/core/backend_ui.js +1 -0
- package/dist/core/channel_events.d.ts +48 -0
- package/dist/core/channel_events.js +1 -0
- package/dist/core/channel_port.d.ts +52 -8
- package/dist/core/engine_spi.d.ts +20 -5
- package/dist/core/listener_scope.d.ts +11 -0
- package/dist/core/listener_scope.js +10 -0
- package/dist/core/message_text.d.ts +2 -0
- package/dist/core/message_text.js +28 -0
- package/dist/core/network_errors.d.ts +1 -0
- package/dist/core/network_errors.js +23 -0
- package/dist/core/orchestrator.d.ts +57 -22
- package/dist/core/orchestrator.js +877 -477
- package/dist/core/queued_engine_input.d.ts +12 -0
- package/dist/core/queued_engine_input.js +16 -0
- package/dist/core/runtime_supervisor.d.ts +19 -0
- package/dist/core/runtime_supervisor.js +45 -0
- package/dist/core/scope_operations.d.ts +7 -0
- package/dist/core/scope_operations.js +19 -0
- package/dist/core/stream_preview.js +21 -9
- package/dist/dsh/adapter.d.ts +48 -0
- package/dist/dsh/adapter.js +205 -0
- package/dist/dsh/backend.d.ts +7 -0
- package/dist/dsh/backend.js +21 -0
- package/dist/dsh/client.d.ts +46 -0
- package/dist/dsh/client.js +184 -0
- package/dist/dsh/permissions_plugin.d.ts +30 -0
- package/dist/dsh/permissions_plugin.js +21 -0
- package/dist/dsh/runtime.d.ts +20 -0
- package/dist/dsh/runtime.js +36 -0
- package/dist/dsh/ui.d.ts +38 -0
- package/dist/dsh/ui.js +342 -0
- package/dist/i18n.d.ts +4 -0
- package/dist/i18n.js +12 -0
- package/dist/main.js +144 -72
- package/dist/opencode/adapter.js +28 -9
- package/dist/opencode/controller.js +1 -1
- package/dist/store/channel_inbox.d.ts +14 -0
- package/dist/store/channel_inbox.js +21 -0
- package/dist/store/database.d.ts +10 -4
- package/dist/store/database.js +13 -3
- package/dist/store/task_journal.d.ts +32 -0
- package/dist/store/task_journal.js +70 -0
- package/dist/telegram/api.d.ts +2 -2
- package/dist/telegram/api.js +10 -28
- package/dist/telegram/gateway.d.ts +13 -27
- package/dist/telegram/gateway.js +86 -43
- package/dist/telegram/media.d.ts +2 -34
- package/dist/telegram/media.js +1 -180
- package/dist/telegram/task_preview.d.ts +13 -0
- package/dist/telegram/task_preview.js +92 -0
- package/dist/telegram/task_result.d.ts +2 -0
- package/dist/telegram/task_result.js +12 -0
- package/dist/telegram/text.d.ts +1 -1
- package/dist/telegram/text.js +2 -27
- package/dist/types.d.ts +2 -2
- package/docs/dsh.md +83 -0
- package/docs/release.md +13 -11
- package/docs/zh/architecture-review.md +106 -0
- package/docs/zh/dsh.md +83 -0
- package/docs/zh/release.md +13 -11
- package/docs/zh/telegram-experience.md +40 -0
- package/package.json +7 -5
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { StagedAttachment } from './attachment_types.js';
|
|
2
|
+
export interface QueuedEngineInput {
|
|
3
|
+
version: 1;
|
|
4
|
+
prompt: string;
|
|
5
|
+
backendId: string;
|
|
6
|
+
cwd: string;
|
|
7
|
+
stagedAttachments: StagedAttachment[];
|
|
8
|
+
}
|
|
9
|
+
export declare function decodeQueuedEngineInput(inputJson: string, fallbackPrompt: string): QueuedEngineInput | {
|
|
10
|
+
prompt: string;
|
|
11
|
+
stagedAttachments: StagedAttachment[];
|
|
12
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function decodeQueuedEngineInput(inputJson, fallbackPrompt) {
|
|
2
|
+
const input = JSON.parse(inputJson);
|
|
3
|
+
// Legacy queues contained only a Codex-shaped text input array.
|
|
4
|
+
if (Array.isArray(input)) {
|
|
5
|
+
const text = input[0]?.text;
|
|
6
|
+
return { prompt: typeof text === 'string' ? text : fallbackPrompt, stagedAttachments: [] };
|
|
7
|
+
}
|
|
8
|
+
if (typeof input !== 'object' || input === null || !('version' in input) || input.version !== 1 ||
|
|
9
|
+
!('prompt' in input) || typeof input.prompt !== 'string' ||
|
|
10
|
+
!('backendId' in input) || typeof input.backendId !== 'string' ||
|
|
11
|
+
!('cwd' in input) || typeof input.cwd !== 'string' ||
|
|
12
|
+
!('stagedAttachments' in input) || !Array.isArray(input.stagedAttachments)) {
|
|
13
|
+
throw new Error('Invalid queued engine input');
|
|
14
|
+
}
|
|
15
|
+
return input;
|
|
16
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface ManagedRuntime {
|
|
2
|
+
start?(): void | Promise<void>;
|
|
3
|
+
stop(): void | Promise<void>;
|
|
4
|
+
}
|
|
5
|
+
export interface ShutdownFailure {
|
|
6
|
+
name: string;
|
|
7
|
+
error: unknown;
|
|
8
|
+
}
|
|
9
|
+
/** Owns resources from allocation onward, including partially started runtimes. */
|
|
10
|
+
export declare class RuntimeSupervisor {
|
|
11
|
+
private readonly entries;
|
|
12
|
+
private readonly identities;
|
|
13
|
+
private closing;
|
|
14
|
+
private shutdown;
|
|
15
|
+
register(name: string, resource: ManagedRuntime): void;
|
|
16
|
+
start(name: string): Promise<void>;
|
|
17
|
+
stop(): Promise<ShutdownFailure[]>;
|
|
18
|
+
private stopAll;
|
|
19
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/** Owns resources from allocation onward, including partially started runtimes. */
|
|
2
|
+
export class RuntimeSupervisor {
|
|
3
|
+
entries = [];
|
|
4
|
+
identities = new Set();
|
|
5
|
+
closing = false;
|
|
6
|
+
shutdown = null;
|
|
7
|
+
register(name, resource) {
|
|
8
|
+
if (this.closing)
|
|
9
|
+
throw new Error('Runtime supervisor is stopping');
|
|
10
|
+
if (this.identities.has(resource))
|
|
11
|
+
return;
|
|
12
|
+
if (this.entries.some(entry => entry.name === name))
|
|
13
|
+
throw new Error(`Runtime already registered: ${name}`);
|
|
14
|
+
this.identities.add(resource);
|
|
15
|
+
this.entries.push({ name, resource, start: null });
|
|
16
|
+
}
|
|
17
|
+
start(name) {
|
|
18
|
+
if (this.closing)
|
|
19
|
+
return Promise.reject(new Error('Runtime supervisor is stopping'));
|
|
20
|
+
const entry = this.entries.find(entry => entry.name === name);
|
|
21
|
+
if (!entry)
|
|
22
|
+
return Promise.reject(new Error(`Unknown runtime: ${name}`));
|
|
23
|
+
entry.start ??= Promise.resolve().then(() => entry.resource.start?.());
|
|
24
|
+
return entry.start;
|
|
25
|
+
}
|
|
26
|
+
stop() {
|
|
27
|
+
this.closing = true;
|
|
28
|
+
this.shutdown ??= this.stopAll();
|
|
29
|
+
return this.shutdown;
|
|
30
|
+
}
|
|
31
|
+
async stopAll() {
|
|
32
|
+
const failures = [];
|
|
33
|
+
// Consumers are registered after their dependencies and stopped before them.
|
|
34
|
+
for (const entry of [...this.entries].reverse()) {
|
|
35
|
+
await entry.start?.catch(() => { });
|
|
36
|
+
try {
|
|
37
|
+
await entry.resource.stop();
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
failures.push({ name: entry.name, error });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return failures;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Serialize state changes for one conversation; unrelated scopes proceed independently. */
|
|
2
|
+
export declare class ScopeOperations {
|
|
3
|
+
private readonly tails;
|
|
4
|
+
run<T>(scopeId: string, operation: () => Promise<T>): Promise<T>;
|
|
5
|
+
isIdle(): boolean;
|
|
6
|
+
idle(): Promise<void>;
|
|
7
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Serialize state changes for one conversation; unrelated scopes proceed independently. */
|
|
2
|
+
export class ScopeOperations {
|
|
3
|
+
tails = new Map();
|
|
4
|
+
run(scopeId, operation) {
|
|
5
|
+
const previous = this.tails.get(scopeId) ?? Promise.resolve();
|
|
6
|
+
const next = previous.catch(() => { }).then(operation);
|
|
7
|
+
this.tails.set(scopeId, next);
|
|
8
|
+
void next.finally(() => {
|
|
9
|
+
if (this.tails.get(scopeId) === next)
|
|
10
|
+
this.tails.delete(scopeId);
|
|
11
|
+
}).catch(() => { });
|
|
12
|
+
return next;
|
|
13
|
+
}
|
|
14
|
+
isIdle() { return this.tails.size === 0; }
|
|
15
|
+
async idle() {
|
|
16
|
+
while (this.tails.size)
|
|
17
|
+
await Promise.allSettled([...this.tails.values()]);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { formatMetricTokenCount } from '../store/token_usage.js';
|
|
2
|
-
import {
|
|
2
|
+
import { chunkMessage } from './message_text.js';
|
|
3
3
|
export function formatElapsedDuration(seconds, locale = 'zh') {
|
|
4
4
|
if (!Number.isFinite(seconds) || seconds <= 0)
|
|
5
5
|
return '0s';
|
|
@@ -75,14 +75,26 @@ export function buildFoldedToolsSummary(options) {
|
|
|
75
75
|
: '';
|
|
76
76
|
previewLines = [...head, omittedNote, ...tail].filter(Boolean);
|
|
77
77
|
}
|
|
78
|
-
|
|
78
|
+
// Keep whole formatted lines within the summary budget; never slice through tags/entities.
|
|
79
|
+
const bounded = [];
|
|
80
|
+
let budget = 1500;
|
|
81
|
+
for (const line of [...previewLines].reverse()) {
|
|
82
|
+
if (line.length + 1 > budget)
|
|
83
|
+
continue;
|
|
84
|
+
bounded.unshift(line);
|
|
85
|
+
budget -= line.length + 1;
|
|
86
|
+
}
|
|
87
|
+
const omittedBySize = previewLines.length - bounded.length;
|
|
88
|
+
if (omittedBySize)
|
|
89
|
+
bounded.unshift(`<i>${locale === 'zh' ? `另有 ${omittedBySize} 项记录已收起` : `${omittedBySize} additional entries omitted`}</i>`);
|
|
90
|
+
toolDetailSection = `\n\n<i>${locale === 'zh' ? '工具调用记录:' : 'Tool calls:'}</i>\n${bounded.join('\n')}`;
|
|
79
91
|
}
|
|
80
92
|
return `<blockquote expandable>${header}${usageLine}${toolDetailSection}</blockquote>\n\n`;
|
|
81
93
|
}
|
|
82
94
|
export function combineSummaryAndResponse(foldedSummary, responseText, limit = 4000) {
|
|
83
95
|
const content = (responseText || '(无输出 / No output)').trim();
|
|
84
96
|
if (!foldedSummary) {
|
|
85
|
-
return
|
|
97
|
+
return chunkMessage(content, limit);
|
|
86
98
|
}
|
|
87
99
|
const combined = `${foldedSummary}${content}`;
|
|
88
100
|
if (combined.length <= limit) {
|
|
@@ -96,18 +108,18 @@ export function combineSummaryAndResponse(foldedSummary, responseText, limit = 4
|
|
|
96
108
|
if (splitAt >= Math.floor(remainingInFirstChunk / 3)) {
|
|
97
109
|
const firstChunkText = content.slice(0, splitAt + 1).trim();
|
|
98
110
|
const rest = content.slice(splitAt + 1).trim();
|
|
99
|
-
const restChunks =
|
|
111
|
+
const restChunks = chunkMessage(rest, limit);
|
|
100
112
|
return [`${foldedSummary}${firstChunkText}`, ...restChunks];
|
|
101
113
|
}
|
|
102
114
|
}
|
|
103
115
|
// If foldedSummary takes up most of the message or no clean paragraph break was found,
|
|
104
116
|
// foldedSummary gets its own clean message, and responseText is chunked independently.
|
|
105
|
-
const responseChunks =
|
|
117
|
+
const responseChunks = chunkMessage(content, limit);
|
|
106
118
|
return [foldedSummary.trim(), ...responseChunks];
|
|
107
119
|
}
|
|
108
120
|
export function renderStreamPreviewContent(state) {
|
|
109
121
|
const parts = [];
|
|
110
|
-
const name = state.engineName || 'AI';
|
|
122
|
+
const name = escapeTelegramHtml(state.engineName || 'AI');
|
|
111
123
|
const roundText = state.stepIndex && state.stepIndex > 0 ? `第 ${state.stepIndex} 轮` : '';
|
|
112
124
|
const toolText = typeof state.toolCount === 'number' && state.toolCount > 0 ? `累计执行 ${state.toolCount} 次工具` : '';
|
|
113
125
|
const timeText = typeof state.elapsedSeconds === 'number' && state.elapsedSeconds > 0 ? `已耗时 ${state.elapsedSeconds}s` : '';
|
|
@@ -125,13 +137,13 @@ export function renderStreamPreviewContent(state) {
|
|
|
125
137
|
parts.push('');
|
|
126
138
|
}
|
|
127
139
|
if (state.toolLines.length > 0) {
|
|
128
|
-
const recent = state.toolLines.slice(-
|
|
140
|
+
const recent = state.toolLines.slice(-3);
|
|
129
141
|
parts.push(`<blockquote expandable>🛠️ <b>正在调用工具 (${state.toolLines.length} 项)</b>\n${recent.join('\n')}</blockquote>`);
|
|
130
142
|
parts.push('');
|
|
131
143
|
}
|
|
132
144
|
if (state.accumulatedText) {
|
|
133
|
-
const preview = state.accumulatedText.slice(-
|
|
134
|
-
parts.push(preview);
|
|
145
|
+
const preview = state.accumulatedText.slice(-1000);
|
|
146
|
+
parts.push(escapeTelegramHtml(preview));
|
|
135
147
|
}
|
|
136
148
|
else {
|
|
137
149
|
parts.push(state.isBoost ? `🚀 ${name} (Boost 模式) 正在深度思考中…` : `⏳ ${name} 正在思考中…`);
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { SessionConfigSelectOption } from '@agentclientprotocol/sdk';
|
|
2
|
+
import type { IEngineAdapter, EngineModel, EngineTurnRequest, EngineTurnExecution } from '../core/engine_spi.js';
|
|
3
|
+
import type { AccessPresetValue } from '../types.js';
|
|
4
|
+
import { DshClient, type SessionConfigOption } from './client.js';
|
|
5
|
+
export declare function optionChoices(option?: SessionConfigOption): SessionConfigSelectOption[];
|
|
6
|
+
export declare function modelId(value: string): string;
|
|
7
|
+
interface ScopePreferences {
|
|
8
|
+
cwd: string;
|
|
9
|
+
threadId: string | null;
|
|
10
|
+
model: string | null;
|
|
11
|
+
effort: string | null;
|
|
12
|
+
access: AccessPresetValue;
|
|
13
|
+
}
|
|
14
|
+
interface ScopeSession {
|
|
15
|
+
client: DshClient;
|
|
16
|
+
sessionId: string;
|
|
17
|
+
cwd: string;
|
|
18
|
+
defaultModel: string;
|
|
19
|
+
defaultEffort: string | null;
|
|
20
|
+
}
|
|
21
|
+
/** Persistent DSH sessions over the shipped ACP profile. */
|
|
22
|
+
export declare class DshEngineAdapter implements IEngineAdapter {
|
|
23
|
+
private readonly options;
|
|
24
|
+
readonly id = "dsh";
|
|
25
|
+
readonly name = "DeepSeek Harness (DSH)";
|
|
26
|
+
private readonly clients;
|
|
27
|
+
private readonly sessions;
|
|
28
|
+
private readonly pending;
|
|
29
|
+
private readonly busy;
|
|
30
|
+
private closed;
|
|
31
|
+
constructor(options: {
|
|
32
|
+
createClient: (scopeId: string) => DshClient;
|
|
33
|
+
preferences: (scopeId: string) => ScopePreferences;
|
|
34
|
+
bind: (scopeId: string, sessionId: string, cwd: string) => void;
|
|
35
|
+
});
|
|
36
|
+
sessionForScope(scopeId: string): Promise<ScopeSession>;
|
|
37
|
+
private openSession;
|
|
38
|
+
listModels(scopeId?: string): Promise<EngineModel[]>;
|
|
39
|
+
selectModel(scopeId: string, id: string | null): Promise<void>;
|
|
40
|
+
listEfforts(scopeId: string): Promise<SessionConfigSelectOption[]>;
|
|
41
|
+
selectEffort(scopeId: string, effort: string | null): Promise<void>;
|
|
42
|
+
setAccess(scopeId: string, access: AccessPresetValue): Promise<void>;
|
|
43
|
+
listSessions(scopeId: string, cursor?: string): Promise<import("@agentclientprotocol/sdk").ListSessionsResponse>;
|
|
44
|
+
isBusy(scopeId: string): boolean;
|
|
45
|
+
executeTurn(request: EngineTurnRequest): EngineTurnExecution;
|
|
46
|
+
stop(): Promise<void>;
|
|
47
|
+
}
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import { buildAttachmentPrompt, isNativeImageAttachment } from '../core/attachment_files.js';
|
|
4
|
+
export function optionChoices(option) {
|
|
5
|
+
if (option?.type !== 'select')
|
|
6
|
+
return [];
|
|
7
|
+
return option.options.flatMap(item => 'options' in item ? item.options : [item]);
|
|
8
|
+
}
|
|
9
|
+
export function modelId(value) {
|
|
10
|
+
const route = JSON.parse(value);
|
|
11
|
+
if (!Array.isArray(route) || route.length !== 2 || !route.every(part => typeof part === 'string' && part.length > 0))
|
|
12
|
+
throw new Error('Invalid DSH model route');
|
|
13
|
+
return `${route[0]}/${route[1]}`;
|
|
14
|
+
}
|
|
15
|
+
/** Persistent DSH sessions over the shipped ACP profile. */
|
|
16
|
+
export class DshEngineAdapter {
|
|
17
|
+
options;
|
|
18
|
+
id = 'dsh';
|
|
19
|
+
name = 'DeepSeek Harness (DSH)';
|
|
20
|
+
clients = new Map();
|
|
21
|
+
sessions = new Map();
|
|
22
|
+
pending = new Map();
|
|
23
|
+
busy = new Set();
|
|
24
|
+
closed = false;
|
|
25
|
+
constructor(options) {
|
|
26
|
+
this.options = options;
|
|
27
|
+
}
|
|
28
|
+
async sessionForScope(scopeId) {
|
|
29
|
+
if (this.closed)
|
|
30
|
+
throw new Error('DSH backend is stopped');
|
|
31
|
+
const existing = this.pending.get(scopeId);
|
|
32
|
+
if (existing)
|
|
33
|
+
return existing;
|
|
34
|
+
const task = this.openSession(scopeId).finally(() => this.pending.delete(scopeId));
|
|
35
|
+
this.pending.set(scopeId, task);
|
|
36
|
+
return task;
|
|
37
|
+
}
|
|
38
|
+
async openSession(scopeId) {
|
|
39
|
+
const prefs = this.options.preferences(scopeId);
|
|
40
|
+
let client = this.clients.get(scopeId);
|
|
41
|
+
if (!client) {
|
|
42
|
+
client = this.options.createClient(scopeId);
|
|
43
|
+
this.clients.set(scopeId, client);
|
|
44
|
+
}
|
|
45
|
+
await client.setAccess(prefs.access);
|
|
46
|
+
const previous = this.sessions.get(scopeId);
|
|
47
|
+
if (client.connected && previous && previous.sessionId === prefs.threadId && previous.cwd === prefs.cwd)
|
|
48
|
+
return previous;
|
|
49
|
+
if (previous && this.busy.has(scopeId))
|
|
50
|
+
throw new Error('DSH session is busy');
|
|
51
|
+
if (previous && client.connected)
|
|
52
|
+
await client.closeSession(previous.sessionId);
|
|
53
|
+
const sessionId = await client.session(prefs.cwd, prefs.threadId);
|
|
54
|
+
const config = client.configOptions(sessionId);
|
|
55
|
+
const model = config.find(item => item.id === 'model');
|
|
56
|
+
const effort = config.find(item => item.id === 'reasoning_effort');
|
|
57
|
+
const state = { client, sessionId, cwd: prefs.cwd, defaultModel: String(model?.currentValue ?? ''), defaultEffort: effort?.type === 'select' ? effort.currentValue : null };
|
|
58
|
+
this.sessions.set(scopeId, state);
|
|
59
|
+
this.options.bind(scopeId, sessionId, prefs.cwd);
|
|
60
|
+
return state;
|
|
61
|
+
}
|
|
62
|
+
async listModels(scopeId = 'dsh:catalog') {
|
|
63
|
+
const state = await this.sessionForScope(scopeId);
|
|
64
|
+
return optionChoices(state.client.configOptions(state.sessionId).find(option => option.id === 'model')).map(choice => ({ id: modelId(choice.value), name: choice.name, description: choice.description ?? undefined, isDefault: choice.value === state.defaultModel }));
|
|
65
|
+
}
|
|
66
|
+
async selectModel(scopeId, id) {
|
|
67
|
+
const state = await this.sessionForScope(scopeId);
|
|
68
|
+
const model = state.client.configOptions(state.sessionId).find(option => option.id === 'model');
|
|
69
|
+
const choice = optionChoices(model).find(choice => id === null ? choice.value === state.defaultModel : modelId(choice.value) === id);
|
|
70
|
+
if (!choice)
|
|
71
|
+
throw new Error(`DSH model unavailable: ${id}`);
|
|
72
|
+
await state.client.setConfig(state.sessionId, 'model', choice.value);
|
|
73
|
+
const effort = state.client.configOptions(state.sessionId).find(option => option.id === 'reasoning_effort');
|
|
74
|
+
state.defaultEffort = effort?.type === 'select' ? effort.currentValue : null;
|
|
75
|
+
}
|
|
76
|
+
async listEfforts(scopeId) {
|
|
77
|
+
const state = await this.sessionForScope(scopeId);
|
|
78
|
+
return optionChoices(state.client.configOptions(state.sessionId).find(option => option.id === 'reasoning_effort'));
|
|
79
|
+
}
|
|
80
|
+
async selectEffort(scopeId, effort) {
|
|
81
|
+
const state = await this.sessionForScope(scopeId);
|
|
82
|
+
const choices = await this.listEfforts(scopeId);
|
|
83
|
+
if (choices.length === 0 && effort === null)
|
|
84
|
+
return;
|
|
85
|
+
const value = effort ?? state.defaultEffort;
|
|
86
|
+
if (value === null || !choices.some(choice => choice.value === value))
|
|
87
|
+
throw new Error(`DSH reasoning effort unavailable: ${effort}`);
|
|
88
|
+
await state.client.setConfig(state.sessionId, 'reasoning_effort', value);
|
|
89
|
+
}
|
|
90
|
+
async setAccess(scopeId, access) {
|
|
91
|
+
const state = await this.sessionForScope(scopeId);
|
|
92
|
+
await state.client.setAccess(access);
|
|
93
|
+
}
|
|
94
|
+
async listSessions(scopeId, cursor) { return (await this.sessionForScope(scopeId)).client.listSessions(cursor); }
|
|
95
|
+
isBusy(scopeId) { return this.busy.has(scopeId); }
|
|
96
|
+
executeTurn(request) {
|
|
97
|
+
const emitter = new EventEmitter();
|
|
98
|
+
let cancelled = false;
|
|
99
|
+
let state;
|
|
100
|
+
let finished = false;
|
|
101
|
+
let ownsSlot = false;
|
|
102
|
+
let result = null;
|
|
103
|
+
let resolveResult;
|
|
104
|
+
const promise = new Promise(resolve => { resolveResult = resolve; });
|
|
105
|
+
let response = '';
|
|
106
|
+
let finalMessage = '';
|
|
107
|
+
let finalMessageId;
|
|
108
|
+
const tools = new Map();
|
|
109
|
+
const onUpdate = (notification) => {
|
|
110
|
+
if (cancelled || notification.sessionId !== state?.sessionId)
|
|
111
|
+
return;
|
|
112
|
+
const update = notification.update;
|
|
113
|
+
if (update.sessionUpdate === 'agent_message_chunk' && update.content.type === 'text') {
|
|
114
|
+
if (update.messageId && update.messageId !== finalMessageId) {
|
|
115
|
+
finalMessageId = update.messageId;
|
|
116
|
+
finalMessage = '';
|
|
117
|
+
}
|
|
118
|
+
finalMessage += update.content.text;
|
|
119
|
+
response += update.content.text;
|
|
120
|
+
emitter.emit('delta', update.content.text);
|
|
121
|
+
}
|
|
122
|
+
else if (update.sessionUpdate === 'tool_call') {
|
|
123
|
+
tools.set(update.toolCallId, update.title);
|
|
124
|
+
emitter.emit('tool', { name: update.title, args: update.rawInput == null ? undefined : JSON.stringify(update.rawInput), status: 'running' });
|
|
125
|
+
}
|
|
126
|
+
else if (update.sessionUpdate === 'tool_call_update') {
|
|
127
|
+
emitter.emit('tool', { name: tools.get(update.toolCallId) ?? update.toolCallId, status: update.status === 'failed' ? 'failed' : update.status === 'completed' ? 'completed' : 'running' });
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
const run = async () => {
|
|
131
|
+
try {
|
|
132
|
+
if (this.busy.has(request.scopeId))
|
|
133
|
+
throw new Error('DSH already has an active turn in this scope');
|
|
134
|
+
state = await this.sessionForScope(request.scopeId);
|
|
135
|
+
if (cancelled)
|
|
136
|
+
return;
|
|
137
|
+
if (this.busy.has(request.scopeId))
|
|
138
|
+
throw new Error('DSH already has an active turn in this scope');
|
|
139
|
+
this.busy.add(request.scopeId);
|
|
140
|
+
ownsSlot = true;
|
|
141
|
+
emitter.emit('conversation', state.sessionId);
|
|
142
|
+
await this.selectModel(request.scopeId, request.model === 'default' ? null : request.model);
|
|
143
|
+
await this.selectEffort(request.scopeId, request.effort ?? null);
|
|
144
|
+
if (cancelled)
|
|
145
|
+
return;
|
|
146
|
+
const attachments = request.stagedAttachments ?? [];
|
|
147
|
+
const images = state.client.imageSupported ? attachments.filter(isNativeImageAttachment) : [];
|
|
148
|
+
const files = attachments.filter(attachment => !images.includes(attachment));
|
|
149
|
+
const content = [{ type: 'text', text: files.length ? buildAttachmentPrompt(request.prompt, files) : request.prompt }];
|
|
150
|
+
for (const attachment of images)
|
|
151
|
+
content.push({ type: 'image', data: (await fs.readFile(attachment.localPath)).toString('base64'), mimeType: attachment.mimeType || 'image/jpeg' });
|
|
152
|
+
state.client.on('update', onUpdate);
|
|
153
|
+
if (cancelled)
|
|
154
|
+
return;
|
|
155
|
+
const value = await state.client.agent.request('session/prompt', { sessionId: state.sessionId, prompt: content });
|
|
156
|
+
result = { kind: 'result', status: cancelled || value.stopReason === 'cancelled' ? 'INTERRUPTED' : 'SUCCESS', response: finalMessage || response, conversationId: state.sessionId };
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
result = { kind: 'result', status: cancelled ? 'INTERRUPTED' : 'ERROR', response: finalMessage || response, error: error instanceof Error ? error.message : String(error), conversationId: state?.sessionId ?? request.threadId };
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
state?.client.off('update', onUpdate);
|
|
163
|
+
if (ownsSlot)
|
|
164
|
+
this.busy.delete(request.scopeId);
|
|
165
|
+
result ??= { kind: 'result', status: 'INTERRUPTED', response: finalMessage || response, conversationId: state?.sessionId ?? request.threadId };
|
|
166
|
+
finished = true;
|
|
167
|
+
resolveResult(result);
|
|
168
|
+
emitter.emit('result', result);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
queueMicrotask(() => { void run(); });
|
|
172
|
+
return {
|
|
173
|
+
cancel: async () => {
|
|
174
|
+
if (finished || cancelled) {
|
|
175
|
+
await promise;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
cancelled = true;
|
|
179
|
+
state?.client.cancelPermissions?.();
|
|
180
|
+
if (state?.client.connected)
|
|
181
|
+
await state.client.agent.notify('session/cancel', { sessionId: state.sessionId }).catch(() => { });
|
|
182
|
+
let timer;
|
|
183
|
+
try {
|
|
184
|
+
await Promise.race([promise, new Promise(resolve => { timer = setTimeout(resolve, 5000); })]);
|
|
185
|
+
if (!finished)
|
|
186
|
+
await this.clients.get(request.scopeId)?.stop();
|
|
187
|
+
await promise;
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
if (timer)
|
|
191
|
+
clearTimeout(timer);
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
waitForResult: () => promise,
|
|
195
|
+
on: (event, listener) => { emitter.on(event, listener); },
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
async stop() {
|
|
199
|
+
this.closed = true;
|
|
200
|
+
await Promise.allSettled([...this.pending.values()]);
|
|
201
|
+
await Promise.all([...this.clients.values()].map(client => client.stop()));
|
|
202
|
+
this.clients.clear();
|
|
203
|
+
this.sessions.clear();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { AppConfig } from '../config.js';
|
|
2
|
+
import type { BridgeStore } from '../store/database.js';
|
|
3
|
+
import type { Logger } from '../logger.js';
|
|
4
|
+
import type { ChannelPort } from '../core/channel_port.js';
|
|
5
|
+
import type { BackendDescriptor } from '../core/engine_spi.js';
|
|
6
|
+
/** Same definition is mounted on a shared bot or on a dedicated DSH bot. */
|
|
7
|
+
export declare function createDshBackend(config: AppConfig, store: BridgeStore, logger: Logger, messaging: ChannelPort): BackendDescriptor;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { getDshTelegramCommands } from '../i18n.js';
|
|
2
|
+
import { DshUi } from './ui.js';
|
|
3
|
+
/** Same definition is mounted on a shared bot or on a dedicated DSH bot. */
|
|
4
|
+
export function createDshBackend(config, store, logger, messaging) {
|
|
5
|
+
const ui = new DshUi(config, store, logger, messaging);
|
|
6
|
+
return {
|
|
7
|
+
id: ui.adapter.id, name: ui.adapter.name, engineType: ui.adapter.id, adapter: ui.adapter,
|
|
8
|
+
defaults: { reasoningEffort: null, supportedReasoningEfforts: [], boost: false, tokenUsage: false },
|
|
9
|
+
commands: getDshTelegramCommands,
|
|
10
|
+
createUi: host => ({
|
|
11
|
+
ownsCallback: data => data.startsWith('dsh:'),
|
|
12
|
+
getPendingApprovals: () => ui.pendingApprovals,
|
|
13
|
+
stop: () => ui.stop(),
|
|
14
|
+
renderCustomStatus: async (scopeId, locale) => ui.status(scopeId, locale),
|
|
15
|
+
renderSetupMenu: async (scopeId, locale, messageId) => { await ui.setup(scopeId, locale, host, messageId); return true; },
|
|
16
|
+
renderModelsMenu: async (scopeId, locale, messageId) => { await ui.models(scopeId, locale, host, messageId); return true; },
|
|
17
|
+
handleCustomCommand: (scopeId, command, args, locale) => ui.command(scopeId, command, args, locale, host),
|
|
18
|
+
handleCustomCallback: (scopeId, data, locale, _messageId, event) => ui.callback(scopeId, data, locale, host, event),
|
|
19
|
+
}),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { type SessionConfigOption, type SessionNotification, type RequestPermissionRequest, type RequestPermissionResponse } from '@agentclientprotocol/sdk';
|
|
3
|
+
import type { Logger } from '../logger.js';
|
|
4
|
+
import type { AccessPresetValue } from '../types.js';
|
|
5
|
+
export interface DshClientOptions {
|
|
6
|
+
cliBin: string;
|
|
7
|
+
sourceDir?: string | null;
|
|
8
|
+
home?: string | null;
|
|
9
|
+
profile: string;
|
|
10
|
+
patches: string[];
|
|
11
|
+
runtimeDir: string;
|
|
12
|
+
startupTimeoutMs: number;
|
|
13
|
+
}
|
|
14
|
+
/** An owned ACP process; protocol stdout stays separate from diagnostics. */
|
|
15
|
+
export declare class DshClient extends EventEmitter {
|
|
16
|
+
readonly options: DshClientOptions;
|
|
17
|
+
readonly scopeId: string;
|
|
18
|
+
private readonly logger;
|
|
19
|
+
private process;
|
|
20
|
+
private connection;
|
|
21
|
+
private starting;
|
|
22
|
+
private stopping;
|
|
23
|
+
private optionsBySession;
|
|
24
|
+
private capabilities;
|
|
25
|
+
private policyPath;
|
|
26
|
+
private policyWrite;
|
|
27
|
+
private stderr;
|
|
28
|
+
permissionHandler?: (request: RequestPermissionRequest, signal: AbortSignal) => Promise<RequestPermissionResponse>;
|
|
29
|
+
cancelPermissions?: () => void;
|
|
30
|
+
constructor(options: DshClientOptions, scopeId: string, logger: Logger);
|
|
31
|
+
setAccess(preset: AccessPresetValue): Promise<void>;
|
|
32
|
+
private writeAccess;
|
|
33
|
+
start(): Promise<void>;
|
|
34
|
+
private launch;
|
|
35
|
+
get connected(): boolean;
|
|
36
|
+
get imageSupported(): boolean;
|
|
37
|
+
get agent(): import("@agentclientprotocol/sdk").ClientContext;
|
|
38
|
+
session(cwd: string, sessionId?: string | null): Promise<string>;
|
|
39
|
+
configOptions(sessionId: string): SessionConfigOption[];
|
|
40
|
+
setConfig(sessionId: string, configId: string, value: string): Promise<void>;
|
|
41
|
+
listSessions(cursor?: string): Promise<import("@agentclientprotocol/sdk").ListSessionsResponse>;
|
|
42
|
+
closeSession(sessionId: string): Promise<void>;
|
|
43
|
+
stop(): Promise<void>;
|
|
44
|
+
private dispose;
|
|
45
|
+
}
|
|
46
|
+
export type { SessionConfigOption, SessionNotification, RequestPermissionRequest, RequestPermissionResponse };
|