@xmanrui/dsh-im 0.4.0 → 0.6.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/README.md +36 -0
- package/lib/client.js +709 -285
- package/lib/index.js +114 -110
- package/package.json +1 -1
- package/plugin-src/client/channels/dingtalk/api.js +2 -0
- package/plugin-src/client/channels/dingtalk/index.js +67 -14
- package/plugin-src/client/channels/feishu/api.js +2 -0
- package/plugin-src/client/channels/feishu/index.js +70 -22
- package/plugin-src/client/channels/qq/api.js +2 -0
- package/plugin-src/client/channels/qq/index.js +45 -8
- package/plugin-src/client/channels/shared/token-api.js +2 -0
- package/plugin-src/client/channels/shared/token-channel.js +34 -8
- package/plugin-src/client/channels/wecom/api.js +2 -0
- package/plugin-src/client/channels/wecom/index.js +45 -8
- package/plugin-src/client/channels/weixin/api.js +2 -0
- package/plugin-src/client/channels/weixin/index.js +68 -8
- package/plugin-src/client/channels/whatsapp/api.js +2 -0
- package/plugin-src/client/channels/whatsapp/index.js +27 -5
- package/plugin-src/client/i18n.js +13 -0
- package/plugin-src/client/styles.js +13 -0
- package/plugin-src/client/workspace-editor.js +96 -0
- package/plugin-src/client/workspace-snapshot-fence.js +28 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +34 -11
- package/plugin-src/host/channels/dingtalk/rpc.mjs +17 -2
- package/plugin-src/host/channels/feishu/production.mjs +42 -5
- package/plugin-src/host/channels/feishu/rpc.mjs +17 -2
- package/plugin-src/host/channels/qq/production.mjs +48 -22
- package/plugin-src/host/channels/qq/rpc.mjs +16 -2
- package/plugin-src/host/channels/shared/production.mjs +48 -22
- package/plugin-src/host/channels/shared/rpc.mjs +15 -0
- package/plugin-src/host/channels/shared/workspace-rpc.mjs +25 -0
- package/plugin-src/host/channels/slack/production.mjs +48 -23
- package/plugin-src/host/channels/slack/rpc.mjs +16 -0
- package/plugin-src/host/channels/wecom/production.mjs +49 -23
- package/plugin-src/host/channels/wecom/rpc.mjs +16 -2
- package/plugin-src/host/channels/weixin/production.mjs +31 -11
- package/plugin-src/host/channels/weixin/rpc.mjs +21 -2
- package/plugin-src/host/channels/whatsapp/production.mjs +49 -23
- package/plugin-src/host/channels/whatsapp/rpc.mjs +16 -2
- package/src/channels/dingtalk/dingtalk-bridge.mjs +25 -11
- package/src/channels/dingtalk/dingtalk-controller.mjs +6 -1
- package/src/channels/dingtalk/harness-client.mjs +17 -3
- package/src/channels/dingtalk/state-store.mjs +5 -0
- package/src/channels/discord/discord-api.mjs +1 -1
- package/src/channels/feishu/bridge.mjs +39 -16
- package/src/channels/feishu/harness-client.mjs +19 -5
- package/src/channels/feishu/state-store.mjs +5 -0
- package/src/channels/qq/qq-bridge.mjs +28 -15
- package/src/channels/qq/qq-controller.mjs +8 -1
- package/src/channels/qq/state-store.mjs +5 -0
- package/src/channels/shared/bot-workspace-store.mjs +618 -0
- package/src/channels/shared/conversation-state-store.mjs +5 -0
- package/src/channels/shared/text-harness-bridge.mjs +26 -13
- package/src/channels/shared/workspace-command.mjs +115 -0
- package/src/channels/shared/workspace-session.mjs +44 -0
- package/src/channels/wecom/state-store.mjs +5 -0
- package/src/channels/wecom/wecom-bridge.mjs +25 -13
- package/src/channels/wecom/wecom-controller.mjs +8 -1
- package/src/channels/weixin/harness-client.mjs +19 -5
- package/src/channels/weixin/state-store.mjs +5 -0
- package/src/channels/weixin/weixin-api.mjs +1 -1
- package/src/channels/weixin/weixin-bridge.mjs +19 -6
- package/src/channels/weixin/weixin-controller.mjs +6 -1
- package/src/channels/whatsapp/whatsapp-controller.mjs +8 -1
|
@@ -8,6 +8,12 @@ import { WhatsappStateStore } from '../../../../src/channels/whatsapp/state-stor
|
|
|
8
8
|
import { WhatsappController } from '../../../../src/channels/whatsapp/whatsapp-controller.mjs';
|
|
9
9
|
import { WhatsappRuntime } from '../../../../src/channels/whatsapp/whatsapp-runtime.mjs';
|
|
10
10
|
import { createWhatsappWebSession } from '../../../../src/channels/whatsapp/whatsapp-web-session.mjs';
|
|
11
|
+
import {
|
|
12
|
+
BotWorkspaceStore,
|
|
13
|
+
createBotWorkspaceScope,
|
|
14
|
+
createWorkspaceAwareController,
|
|
15
|
+
observeBotWorkspaceRemovals,
|
|
16
|
+
} from '../../../../src/channels/shared/bot-workspace-store.mjs';
|
|
11
17
|
import { createTokenConnectionSupervisor } from '../shared/connection-supervisor.mjs';
|
|
12
18
|
|
|
13
19
|
const AUTH_DIRECTORY_PATTERN = /^[a-f0-9-]{36}$/;
|
|
@@ -32,6 +38,7 @@ function pluginPaths(config) {
|
|
|
32
38
|
return {
|
|
33
39
|
config: resolve(config.configPath ?? join(root, 'config.json')),
|
|
34
40
|
bots: resolve(config.botsDir ?? join(root, 'bots')),
|
|
41
|
+
workspaces: resolve(config.workspacesPath ?? join(root, 'workspaces.json')),
|
|
35
42
|
authPath,
|
|
36
43
|
};
|
|
37
44
|
}
|
|
@@ -49,6 +56,16 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
49
56
|
const createSupervisor = internals.createConnectionSupervisor ?? createTokenConnectionSupervisor;
|
|
50
57
|
const paths = pluginPaths(config);
|
|
51
58
|
const configStore = await new ConfigStore(paths.config).load();
|
|
59
|
+
const defaultWorkspace = resolve(config.workspace ?? process.cwd());
|
|
60
|
+
const WorkspaceStore = internals.WorkspaceStore ?? BotWorkspaceStore;
|
|
61
|
+
const workspaces = internals.workspaces
|
|
62
|
+
?? await new WorkspaceStore(paths.workspaces, { defaultWorkspace }).load();
|
|
63
|
+
const configuredBots = configStore.list();
|
|
64
|
+
await workspaces.reconcile(configuredBots.map((bot) => bot.botId));
|
|
65
|
+
await Promise.all(configuredBots.map((bot) => workspaces.ensure(bot.botId)));
|
|
66
|
+
const observedConfigStore = typeof configStore.remove === 'function'
|
|
67
|
+
? observeBotWorkspaceRemovals(configStore, { workspaces })
|
|
68
|
+
: configStore;
|
|
52
69
|
const stateStores = new Map();
|
|
53
70
|
const statePath = (botId) => resolve(paths.bots, botId, 'state.json');
|
|
54
71
|
const stateFor = async (botId) => {
|
|
@@ -61,31 +78,36 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
61
78
|
};
|
|
62
79
|
const harness = new Harness({
|
|
63
80
|
baseUrl: harnessOrigin(ctx.webServer, config.harnessBaseUrl),
|
|
64
|
-
workspace:
|
|
81
|
+
workspace: defaultWorkspace,
|
|
65
82
|
agentPreset: config.agentPreset ?? 'standard',
|
|
66
83
|
autostart: false,
|
|
67
84
|
dshBin: config.dshBin ?? 'dsh',
|
|
68
85
|
});
|
|
69
|
-
const
|
|
70
|
-
configStore,
|
|
86
|
+
const coreController = new Controller({
|
|
87
|
+
configStore: observedConfigStore,
|
|
71
88
|
authPath: paths.authPath,
|
|
72
89
|
createSession,
|
|
73
90
|
logger,
|
|
74
|
-
createRuntime: async ({ botId, config: botConfig, authDir }) =>
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
harness,
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
createRuntime: async ({ botId, config: botConfig, authDir }) => {
|
|
92
|
+
const state = await stateFor(botId);
|
|
93
|
+
await workspaces.ensure(botId);
|
|
94
|
+
const workspaceScope = createBotWorkspaceScope(harness, { botId, workspaces, state });
|
|
95
|
+
return new Runtime({
|
|
96
|
+
config: botConfig,
|
|
97
|
+
authDir,
|
|
98
|
+
harness: workspaceScope.harness,
|
|
99
|
+
state: workspaceScope.state,
|
|
100
|
+
replyTimeoutMs: config.replyTimeoutMs ?? 600_000,
|
|
101
|
+
connectTimeoutMs: config.connectTimeoutMs ?? 30_000,
|
|
102
|
+
createSession,
|
|
103
|
+
logger: {
|
|
104
|
+
error: (...args) => logger.error?.(`[${botId}]`, ...args),
|
|
105
|
+
warn: (...args) => logger.warn?.(`[${botId}]`, ...args),
|
|
106
|
+
info: (...args) => logger.info?.(`[${botId}]`, ...args),
|
|
107
|
+
debug: (...args) => logger.debug?.(`[${botId}]`, ...args),
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
},
|
|
89
111
|
deleteAuth: (authDirectory) => rm(paths.authPath(authDirectory), {
|
|
90
112
|
recursive: true,
|
|
91
113
|
force: true,
|
|
@@ -93,14 +115,18 @@ export async function createProductionController(ctx, config = {}, internals = {
|
|
|
93
115
|
deleteState: async ({ botId }) => {
|
|
94
116
|
const state = stateStores.get(botId);
|
|
95
117
|
stateStores.delete(botId);
|
|
96
|
-
if (state && typeof state.remove === 'function')
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
118
|
+
if (state && typeof state.remove === 'function') {
|
|
119
|
+
await state.remove();
|
|
120
|
+
} else {
|
|
121
|
+
try {
|
|
122
|
+
await unlink(statePath(botId));
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (error?.code !== 'ENOENT') throw error;
|
|
125
|
+
}
|
|
101
126
|
}
|
|
102
127
|
},
|
|
103
128
|
});
|
|
129
|
+
const controller = createWorkspaceAwareController(coreController, { workspaces, stateFor });
|
|
104
130
|
const supervisor = createSupervisor({
|
|
105
131
|
channel: 'whatsapp',
|
|
106
132
|
controller,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import QRCode from 'qrcode';
|
|
2
2
|
|
|
3
3
|
import { resolveRpcAuthority } from '../../rpc-authority.mjs';
|
|
4
|
+
import { publicWorkspaceError, SET_WORKSPACE_ENDPOINT, validWorkspacePayload } from '../shared/workspace-rpc.mjs';
|
|
4
5
|
|
|
5
6
|
export const WHATSAPP_RPC_CHANNEL = '/whatsapp';
|
|
6
7
|
export const WHATSAPP_ENDPOINTS = Object.freeze({
|
|
@@ -10,6 +11,7 @@ export const WHATSAPP_ENDPOINTS = Object.freeze({
|
|
|
10
11
|
cancelProvisioning: 'provision.cancel',
|
|
11
12
|
reconnectBot: 'bot.reconnect',
|
|
12
13
|
deleteBot: 'bot.delete',
|
|
14
|
+
setWorkspace: SET_WORKSPACE_ENDPOINT,
|
|
13
15
|
});
|
|
14
16
|
export const WHATSAPP_RPC_ENDPOINTS = Object.freeze(Object.values(WHATSAPP_ENDPOINTS));
|
|
15
17
|
|
|
@@ -44,6 +46,10 @@ function payloadFailure(endpoint, payload) {
|
|
|
44
46
|
return exactKeys(payload, ['botId', 'confirm']) && validId(payload.botId)
|
|
45
47
|
&& payload.confirm === true ? null : 'bot.delete requires a botId and confirm=true.';
|
|
46
48
|
}
|
|
49
|
+
if (endpoint === WHATSAPP_ENDPOINTS.setWorkspace) {
|
|
50
|
+
return validWorkspacePayload(payload)
|
|
51
|
+
? null : '请输入工作区绝对路径。';
|
|
52
|
+
}
|
|
47
53
|
return 'Unknown WhatsApp endpoint.';
|
|
48
54
|
}
|
|
49
55
|
|
|
@@ -114,16 +120,24 @@ export function createWhatsappRpcHandler(controller, { encodeQr = qrDataUrl } =
|
|
|
114
120
|
value = sanitizePublic(await controller.cancelProvisioning(payload.attemptId));
|
|
115
121
|
} else if (endpoint === WHATSAPP_ENDPOINTS.reconnectBot) {
|
|
116
122
|
value = await publicStatus(await controller.reconnectBot(payload.botId), cachedEncode);
|
|
123
|
+
} else if (endpoint === WHATSAPP_ENDPOINTS.setWorkspace) {
|
|
124
|
+
if (typeof controller.updateWorkspace !== 'function') throw new Error('Workspace update is unavailable');
|
|
125
|
+
value = await publicStatus(
|
|
126
|
+
await controller.updateWorkspace(payload.botId, payload.workspace),
|
|
127
|
+
cachedEncode,
|
|
128
|
+
);
|
|
117
129
|
} else {
|
|
118
130
|
value = await publicStatus(await controller.deleteBot(payload.botId), cachedEncode);
|
|
119
131
|
}
|
|
120
132
|
return signal?.aborted
|
|
121
133
|
? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
|
|
122
134
|
: { ok: true, value };
|
|
123
|
-
} catch {
|
|
135
|
+
} catch (error) {
|
|
136
|
+
const workspaceError = publicWorkspaceError(error);
|
|
124
137
|
return signal?.aborted
|
|
125
138
|
? { ok: false, error: { code: 'cancelled', message: 'The request was cancelled.' } }
|
|
126
|
-
: { ok: false, error:
|
|
139
|
+
: { ok: false, error: workspaceError
|
|
140
|
+
?? { code: 'whatsapp-operation-failed', message: 'WhatsApp 操作失败,请稍后重试。' } };
|
|
127
141
|
}
|
|
128
142
|
};
|
|
129
143
|
}
|
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
splitDingtalkText,
|
|
4
4
|
} from './dingtalk-api.mjs';
|
|
5
5
|
import { createDingTalkCardStream } from './dingtalk-card-stream.mjs';
|
|
6
|
+
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
7
|
+
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
6
8
|
|
|
7
9
|
const CARD_INITIAL_TEXT = '已连接 DeepSeek Harness,正在思考…';
|
|
8
10
|
const CARD_ERROR_TEXT = '消息处理失败,请稍后重试。';
|
|
@@ -12,6 +14,8 @@ const HELP_TEXT = [
|
|
|
12
14
|
'',
|
|
13
15
|
'直接发送文字即可继续当前会话。',
|
|
14
16
|
'/new 开启一个全新会话',
|
|
17
|
+
'/workspace 工作区绝对路径 切换工作区',
|
|
18
|
+
'/workspacelist 列出工作区绝对路径',
|
|
15
19
|
'/status 检查连接状态',
|
|
16
20
|
'/help 显示本帮助',
|
|
17
21
|
].join('\n');
|
|
@@ -213,12 +217,14 @@ export class DingtalkHarnessBridge {
|
|
|
213
217
|
await this.#send(sessionWebhook, '已开启新会话。请发送你的问题。');
|
|
214
218
|
return;
|
|
215
219
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
220
|
+
const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
|
|
221
|
+
if (workspaceCommand) {
|
|
222
|
+
for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
|
|
223
|
+
await this.#send(sessionWebhook, reply);
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
221
226
|
}
|
|
227
|
+
|
|
222
228
|
if (typeof this.#api.createAiCard === 'function'
|
|
223
229
|
&& typeof this.#api.updateAiCard === 'function'
|
|
224
230
|
&& typeof this.#api.finishAiCard === 'function') {
|
|
@@ -232,12 +238,20 @@ export class DingtalkHarnessBridge {
|
|
|
232
238
|
});
|
|
233
239
|
cardStarted = await cardStream.start(CARD_INITIAL_TEXT);
|
|
234
240
|
}
|
|
235
|
-
const answer = await
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
+
const { answer } = await askInWorkspaceSession({
|
|
242
|
+
harness: this.#harness,
|
|
243
|
+
state: this.#state,
|
|
244
|
+
key,
|
|
245
|
+
text,
|
|
246
|
+
createOptions: { signal: this.#signal },
|
|
247
|
+
existsOptions: { signal: this.#signal },
|
|
248
|
+
askOptions: {
|
|
249
|
+
timeoutMs: this.#replyTimeoutMs,
|
|
250
|
+
signal: this.#signal,
|
|
251
|
+
onUpdate: cardStarted
|
|
252
|
+
? (update) => cardStream.push(progressText(update))
|
|
253
|
+
: undefined,
|
|
254
|
+
},
|
|
241
255
|
});
|
|
242
256
|
const streamed = cardStarted && await cardStream.finish(answer);
|
|
243
257
|
if (!streamed) await this.#send(sessionWebhook, answer);
|
|
@@ -610,7 +610,12 @@ export class DingtalkController {
|
|
|
610
610
|
await this.#stopRuntime(identity.botId);
|
|
611
611
|
if (previousConfig) await this.#configStore.save(previousConfig).catch(() => undefined);
|
|
612
612
|
else if (this.#configStore.get(identity.botId)) {
|
|
613
|
-
await this.#configStore.remove(identity.botId).catch(() =>
|
|
613
|
+
const removed = await this.#configStore.remove(identity.botId).catch(() => null);
|
|
614
|
+
if (removed) {
|
|
615
|
+
await this.#deleteState({ botId: identity.botId, config }).catch((cleanupError) => {
|
|
616
|
+
this.#logger.warn?.('[dsh-dingtalk] failed to clean up cancelled bot state:', cleanupError);
|
|
617
|
+
});
|
|
618
|
+
}
|
|
614
619
|
}
|
|
615
620
|
await this.#restoreCredential(identity.secretRef, previousSecret);
|
|
616
621
|
if (previousConfig && cleanString(previousSecret?.value)) {
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { isAbsolute } from 'node:path';
|
|
4
|
+
|
|
5
|
+
function workspacePaths(value) {
|
|
6
|
+
if (!Array.isArray(value?.items)) return [];
|
|
7
|
+
return value.items.flatMap((item) => (
|
|
8
|
+
typeof item?.path === 'string' && isAbsolute(item.path) ? [item.path] : []
|
|
9
|
+
));
|
|
10
|
+
}
|
|
3
11
|
|
|
4
12
|
function sleep(ms, signal) {
|
|
5
13
|
return new Promise((resolve, reject) => {
|
|
@@ -216,11 +224,17 @@ export class HarnessClient {
|
|
|
216
224
|
throw new Error(`Harness did not become ready: ${lastError?.message ?? 'timeout'}`);
|
|
217
225
|
}
|
|
218
226
|
|
|
227
|
+
async listWorkspaces(options = {}) {
|
|
228
|
+
await this.ensureRunning(options);
|
|
229
|
+
return workspacePaths(await this.rpc('workspace.list', {}, 30_000, options));
|
|
230
|
+
}
|
|
231
|
+
|
|
219
232
|
async workspaceId(options = {}) {
|
|
220
|
-
const {
|
|
221
|
-
const
|
|
233
|
+
const { workspace = this.#workspace, ...rpcOptions } = options;
|
|
234
|
+
const { items } = await this.rpc('workspace.list', {}, 30_000, rpcOptions);
|
|
235
|
+
const existing = items.find((item) => item.path === workspace);
|
|
222
236
|
if (existing) return existing.workspaceId;
|
|
223
|
-
const created = await this.rpc('workspace.create', { path:
|
|
237
|
+
const created = await this.rpc('workspace.create', { path: workspace }, 30_000, rpcOptions);
|
|
224
238
|
return created.workspace.workspaceId;
|
|
225
239
|
}
|
|
226
240
|
|
|
@@ -117,6 +117,11 @@ export class DingtalkStateStore {
|
|
|
117
117
|
await this.#persist();
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
async clearSessions() {
|
|
121
|
+
this.#state.sessions = {};
|
|
122
|
+
await this.#persist();
|
|
123
|
+
}
|
|
124
|
+
|
|
120
125
|
hasSeen(messageId) {
|
|
121
126
|
const id = nonEmptyString(messageId);
|
|
122
127
|
return Boolean(id && this.#state.seenMessageIds.includes(id));
|
|
@@ -108,7 +108,7 @@ export class DiscordApi {
|
|
|
108
108
|
headers: {
|
|
109
109
|
authorization: `Bot ${this.#token}`,
|
|
110
110
|
'content-type': 'application/json',
|
|
111
|
-
'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.
|
|
111
|
+
'user-agent': 'DeepSeek-Harness-dsh-im (https://github.com/xmanrui/dsh-im, 0.6.0)',
|
|
112
112
|
},
|
|
113
113
|
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
114
114
|
signal: requestSignal(signal, timeoutMs),
|
|
@@ -5,12 +5,16 @@ import {
|
|
|
5
5
|
isBotSender,
|
|
6
6
|
splitText,
|
|
7
7
|
} from './message-utils.mjs';
|
|
8
|
+
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
9
|
+
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
8
10
|
|
|
9
11
|
const HELP_TEXT = [
|
|
10
12
|
'北汇星河 AIOS 已连接 DeepSeek Harness。',
|
|
11
13
|
'',
|
|
12
14
|
'直接发送问题即可继续当前会话。',
|
|
13
15
|
'/new 开启一个全新会话',
|
|
16
|
+
'/workspace 工作区绝对路径 切换工作区',
|
|
17
|
+
'/workspacelist 列出工作区绝对路径',
|
|
14
18
|
'/status 检查连接状态',
|
|
15
19
|
'/help 显示本帮助',
|
|
16
20
|
].join('\n');
|
|
@@ -106,25 +110,32 @@ export class FeishuHarnessBridge {
|
|
|
106
110
|
await this.#send(event.message.chat_id, '飞书机器人与 DeepSeek Harness 连接正常。');
|
|
107
111
|
return;
|
|
108
112
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
|
|
114
|
+
if (workspaceCommand) {
|
|
115
|
+
for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
|
|
116
|
+
await this.#send(event.message.chat_id, reply);
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
114
119
|
}
|
|
115
120
|
|
|
116
|
-
console.info(`[bridge] processing ${event.message.chat_type} message ${messageId}
|
|
117
|
-
await this.#answerWithStream(event,
|
|
121
|
+
console.info(`[bridge] processing ${event.message.chat_type} message ${messageId}`);
|
|
122
|
+
await this.#answerWithStream(event, key, text);
|
|
118
123
|
this.#status.messagesReplied += 1;
|
|
119
124
|
this.#status.lastReplyAt = new Date().toISOString();
|
|
120
125
|
this.#status.lastError = null;
|
|
121
126
|
}
|
|
122
127
|
|
|
123
|
-
async #answerWithStream(event,
|
|
128
|
+
async #answerWithStream(event, key, text) {
|
|
124
129
|
const chatId = event.message.chat_id;
|
|
125
130
|
const messageId = event.message.message_id;
|
|
126
131
|
if (!this.#channel?.stream) {
|
|
127
|
-
const answer = await
|
|
132
|
+
const { answer } = await askInWorkspaceSession({
|
|
133
|
+
harness: this.#harness,
|
|
134
|
+
state: this.#state,
|
|
135
|
+
key,
|
|
136
|
+
text,
|
|
137
|
+
askOptions: { timeoutMs: this.#replyTimeoutMs },
|
|
138
|
+
});
|
|
128
139
|
for (const chunk of splitText(answer)) await this.#send(chatId, chunk);
|
|
129
140
|
this.#status.streamFallbacks = (this.#status.streamFallbacks ?? 0) + 1;
|
|
130
141
|
return;
|
|
@@ -136,13 +147,19 @@ export class FeishuHarnessBridge {
|
|
|
136
147
|
await this.#channel.stream(chatId, {
|
|
137
148
|
markdown: async (controller) => {
|
|
138
149
|
promptStarted = true;
|
|
139
|
-
completedAnswer = await
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
150
|
+
({ answer: completedAnswer } = await askInWorkspaceSession({
|
|
151
|
+
harness: this.#harness,
|
|
152
|
+
state: this.#state,
|
|
153
|
+
key,
|
|
154
|
+
text,
|
|
155
|
+
askOptions: {
|
|
156
|
+
timeoutMs: this.#replyTimeoutMs,
|
|
157
|
+
onUpdate: async (update) => {
|
|
158
|
+
await controller.setContent(this.#progressText(update));
|
|
159
|
+
this.#status.streamUpdates = (this.#status.streamUpdates ?? 0) + 1;
|
|
160
|
+
},
|
|
144
161
|
},
|
|
145
|
-
});
|
|
162
|
+
}));
|
|
146
163
|
await controller.setContent(completedAnswer);
|
|
147
164
|
},
|
|
148
165
|
}, { replyTo: messageId });
|
|
@@ -158,7 +175,13 @@ export class FeishuHarnessBridge {
|
|
|
158
175
|
if (promptStarted) throw error;
|
|
159
176
|
|
|
160
177
|
console.warn('[bridge] native Feishu stream unavailable; using text fallback:', error.message);
|
|
161
|
-
const answer = await
|
|
178
|
+
const { answer } = await askInWorkspaceSession({
|
|
179
|
+
harness: this.#harness,
|
|
180
|
+
state: this.#state,
|
|
181
|
+
key,
|
|
182
|
+
text,
|
|
183
|
+
askOptions: { timeoutMs: this.#replyTimeoutMs },
|
|
184
|
+
});
|
|
162
185
|
for (const chunk of splitText(answer)) await this.#send(chatId, chunk);
|
|
163
186
|
this.#status.streamFallbacks = (this.#status.streamFallbacks ?? 0) + 1;
|
|
164
187
|
}
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { isAbsolute } from 'node:path';
|
|
3
4
|
|
|
4
5
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
5
6
|
|
|
7
|
+
function workspacePaths(value) {
|
|
8
|
+
if (!Array.isArray(value?.items)) return [];
|
|
9
|
+
return value.items.flatMap((item) => (
|
|
10
|
+
typeof item?.path === 'string' && isAbsolute(item.path) ? [item.path] : []
|
|
11
|
+
));
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
function messageText(event) {
|
|
7
15
|
return (event?.data?.message?.content ?? [])
|
|
8
16
|
.filter((part) => part.type === 'text' && typeof part.text === 'string')
|
|
@@ -197,17 +205,23 @@ export class HarnessClient {
|
|
|
197
205
|
throw new Error(`Harness did not become ready: ${lastError?.message ?? 'timeout'}`);
|
|
198
206
|
}
|
|
199
207
|
|
|
200
|
-
async
|
|
208
|
+
async listWorkspaces(options = {}) {
|
|
209
|
+
await this.ensureRunning();
|
|
210
|
+
return workspacePaths(await this.rpc('workspace.list', {}, 30000, options));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async workspaceId(options = {}) {
|
|
214
|
+
const workspace = options.workspace ?? this.#workspace;
|
|
201
215
|
const { items } = await this.rpc('workspace.list', {});
|
|
202
|
-
const existing = items.find((item) => item.path ===
|
|
216
|
+
const existing = items.find((item) => item.path === workspace);
|
|
203
217
|
if (existing) return existing.workspaceId;
|
|
204
|
-
const created = await this.rpc('workspace.create', { path:
|
|
218
|
+
const created = await this.rpc('workspace.create', { path: workspace });
|
|
205
219
|
return created.workspace.workspaceId;
|
|
206
220
|
}
|
|
207
221
|
|
|
208
|
-
async createSession() {
|
|
222
|
+
async createSession(options = {}) {
|
|
209
223
|
await this.ensureRunning();
|
|
210
|
-
const workspaceId = await this.workspaceId();
|
|
224
|
+
const workspaceId = await this.workspaceId(options);
|
|
211
225
|
const created = await this.rpc('session.create', {
|
|
212
226
|
workspaceId,
|
|
213
227
|
agentPreset: this.#agentPreset,
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import { runWorkspaceCommand } from '../shared/workspace-command.mjs';
|
|
2
|
+
import { askInWorkspaceSession } from '../shared/workspace-session.mjs';
|
|
3
|
+
|
|
1
4
|
const HELP_TEXT = [
|
|
2
5
|
'QQ 机器人已连接 DeepSeek Harness。',
|
|
3
6
|
'',
|
|
4
7
|
'直接发送文字即可继续当前会话。',
|
|
5
8
|
'/new 开启一个全新会话',
|
|
9
|
+
'/workspace 工作区绝对路径 切换工作区',
|
|
10
|
+
'/workspacelist 列出工作区绝对路径',
|
|
6
11
|
'/status 检查连接状态',
|
|
7
12
|
'/help 显示本帮助',
|
|
8
13
|
].join('\n');
|
|
@@ -121,11 +126,13 @@ export class QqHarnessBridge {
|
|
|
121
126
|
await this.#state.markSeen(messageId);
|
|
122
127
|
return;
|
|
123
128
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
+
const workspaceCommand = await runWorkspaceCommand(text, this.#harness);
|
|
130
|
+
if (workspaceCommand) {
|
|
131
|
+
for (const reply of workspaceCommand.messages ?? [workspaceCommand.message]) {
|
|
132
|
+
await this.#bot.sendText(target, reply);
|
|
133
|
+
}
|
|
134
|
+
await this.#state.markSeen(messageId);
|
|
135
|
+
return;
|
|
129
136
|
}
|
|
130
137
|
|
|
131
138
|
let stream = null;
|
|
@@ -137,16 +144,22 @@ export class QqHarnessBridge {
|
|
|
137
144
|
this.#logger.warn?.('[dsh-im:qq] unable to start a QQ stream; using a text reply:', error);
|
|
138
145
|
}
|
|
139
146
|
}
|
|
140
|
-
const answer = await
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
const { answer } = await askInWorkspaceSession({
|
|
148
|
+
harness: this.#harness,
|
|
149
|
+
state: this.#state,
|
|
150
|
+
key,
|
|
151
|
+
text,
|
|
152
|
+
askOptions: {
|
|
153
|
+
timeoutMs: this.#replyTimeoutMs,
|
|
154
|
+
onUpdate: stream ? async (update) => {
|
|
155
|
+
const progress = update.type === 'text'
|
|
156
|
+
? update.text
|
|
157
|
+
: update.type === 'tool'
|
|
158
|
+
? `正在使用${update.name}…`
|
|
159
|
+
: update.text;
|
|
160
|
+
if (progress) await stream.update(progress);
|
|
161
|
+
} : undefined,
|
|
162
|
+
},
|
|
150
163
|
});
|
|
151
164
|
if (stream) {
|
|
152
165
|
try {
|
|
@@ -388,7 +388,14 @@ export class QqController {
|
|
|
388
388
|
if (record.controller.signal.aborted) {
|
|
389
389
|
await this.#stopRuntime(identity.botId);
|
|
390
390
|
if (previousConfig) await this.#configStore.save(previousConfig).catch(() => undefined);
|
|
391
|
-
else
|
|
391
|
+
else {
|
|
392
|
+
const removed = await this.#configStore.remove(identity.botId).catch(() => null);
|
|
393
|
+
if (removed) {
|
|
394
|
+
await this.#deleteState({ botId: identity.botId, config }).catch((cleanupError) => {
|
|
395
|
+
this.#logger.warn?.('[dsh-im:qq] cancelled bot state cleanup failed:', cleanupError);
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
392
399
|
await this.#restoreCredential(identity.secretRef, previousSecret);
|
|
393
400
|
throw error;
|
|
394
401
|
}
|