mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-11 05:23:18 +08:00
feat: add stable proactive delivery
This commit is contained in:
parent
410cb822d7
commit
31792d7bc8
88 changed files with 6427 additions and 488 deletions
|
|
@ -1049,6 +1049,39 @@ export function createDingtalkApi({
|
|||
return true;
|
||||
},
|
||||
|
||||
async sendRobotText({ clientId, clientSecret, target, text, signal }) {
|
||||
if (typeof text !== 'string' || !text.trim()) throw new TypeError('text is required');
|
||||
const content = text;
|
||||
const normalizedTarget = normalizeFileTarget(target);
|
||||
const token = await accessToken({ clientId, clientSecret, signal });
|
||||
const body = {
|
||||
robotCode: normalizedTarget.robotCode,
|
||||
msgKey: 'sampleText',
|
||||
msgParam: JSON.stringify({ content }),
|
||||
...(normalizedTarget.type === 'group'
|
||||
? { openConversationId: normalizedTarget.openConversationId }
|
||||
: { userIds: [normalizedTarget.userId] }),
|
||||
};
|
||||
const pathname = normalizedTarget.type === 'group'
|
||||
? 'v1.0/robot/groupMessages/send'
|
||||
: 'v1.0/robot/oToMessages/batchSend';
|
||||
const response = await requestJson(fetchImpl, endpoint(apiBase, pathname), {
|
||||
body,
|
||||
headers: { 'x-acs-dingtalk-access-token': token },
|
||||
signal,
|
||||
action: '主动文字消息发送',
|
||||
});
|
||||
const rejection = rejectedProviderResponse(response);
|
||||
if (rejection) {
|
||||
throw new DingtalkApiError(
|
||||
'send-rejected',
|
||||
'钉钉服务拒绝了主动文字消息。',
|
||||
{ providerCode: rejection },
|
||||
);
|
||||
}
|
||||
return response;
|
||||
},
|
||||
|
||||
async sendFile(request) {
|
||||
return sendArtifact(request, {
|
||||
uploadType: 'file',
|
||||
|
|
|
|||
|
|
@ -406,6 +406,20 @@ export class DingtalkController {
|
|||
});
|
||||
}
|
||||
|
||||
async sendProactiveText(botId, target, text, options = {}) {
|
||||
const config = this.#configStore.get(botId);
|
||||
if (!config) throw new Error('Unknown DingTalk bot');
|
||||
return this.#withBotTransition(botId, async () => {
|
||||
const runtime = this.#runtimes.get(botId);
|
||||
if (!runtime?.status?.ready || typeof runtime.sendProactiveText !== 'function') {
|
||||
const error = new Error(t('钉钉消息连接当前离线'));
|
||||
error.code = 'bot-not-connected';
|
||||
throw error;
|
||||
}
|
||||
return runtime.sendProactiveText(target, text, options);
|
||||
});
|
||||
}
|
||||
|
||||
/** Removes one bot, its secret, runtime, and local conversation state. */
|
||||
async deleteBot(botId) {
|
||||
const config = this.#configStore.get(botId);
|
||||
|
|
|
|||
|
|
@ -388,6 +388,45 @@ export class DingtalkRuntime {
|
|||
});
|
||||
}
|
||||
|
||||
async sendProactiveText(target, text, { signal } = {}) {
|
||||
const userId = typeof target?.route?.userId === 'string'
|
||||
? target.route.userId.trim() : '';
|
||||
const openConversationId = typeof target?.route?.openConversationId === 'string'
|
||||
? target.route.openConversationId.trim() : '';
|
||||
if ((target?.kind === 'user' && (!userId || openConversationId))
|
||||
|| (target?.kind === 'group' && (!openConversationId || userId))
|
||||
|| (target?.kind !== 'user' && target?.kind !== 'group')) {
|
||||
const error = new TypeError('Invalid DingTalk proactive delivery target');
|
||||
error.code = 'invalid-target';
|
||||
throw error;
|
||||
}
|
||||
if (!this.#status.ready || !this.#abortController) {
|
||||
const error = new Error('DingTalk runtime is not connected');
|
||||
error.code = 'bot-not-connected';
|
||||
throw error;
|
||||
}
|
||||
signal?.throwIfAborted();
|
||||
try {
|
||||
await this.#api.sendRobotText({
|
||||
clientId: this.#config.clientId,
|
||||
clientSecret: this.#clientSecret,
|
||||
target: {
|
||||
type: target.kind,
|
||||
robotCode: this.#config.clientId,
|
||||
...(target.kind === 'user' ? { userId } : { openConversationId }),
|
||||
},
|
||||
text,
|
||||
signal: signal ?? this.#abortController.signal,
|
||||
});
|
||||
} catch (cause) {
|
||||
if (cause?.code !== 'send-rejected') throw cause;
|
||||
const error = new Error('DingTalk rejected the proactive delivery target', { cause });
|
||||
error.code = 'target-rejected';
|
||||
throw error;
|
||||
}
|
||||
return { sent: true };
|
||||
}
|
||||
|
||||
#pendingSenders() {
|
||||
return typeof this.#state.pendingSenders === 'function'
|
||||
? this.#state.pendingSenders()
|
||||
|
|
|
|||
|
|
@ -501,6 +501,22 @@ export class DiscordRuntime {
|
|||
return this.#bridge.sendConnectionTest(text);
|
||||
}
|
||||
|
||||
async sendProactiveText(target, text, options = {}) {
|
||||
if (!this.#status.ready || !this.#bridge) {
|
||||
const error = new Error('Discord bot is not connected');
|
||||
error.code = 'bot-not-connected';
|
||||
throw error;
|
||||
}
|
||||
const channelId = typeof target?.route?.channelId === 'string'
|
||||
? target.route.channelId.trim() : '';
|
||||
if (target?.kind !== 'channel' || !channelId) {
|
||||
const error = new TypeError('Invalid Discord proactive delivery target');
|
||||
error.code = 'invalid-target';
|
||||
throw error;
|
||||
}
|
||||
return this.#bridge.sendProactiveText({ channelId }, text, options);
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (this.#status.ready && this.#socket) return this.status;
|
||||
if (this.#starting) return this.#starting;
|
||||
|
|
|
|||
|
|
@ -563,6 +563,41 @@ export class FeishuRuntime {
|
|||
});
|
||||
}
|
||||
|
||||
async sendProactiveText(target, text, { signal } = {}) {
|
||||
if (!this.#status.ready || !this.#client) {
|
||||
const error = new Error('飞书机器人尚未连接');
|
||||
error.code = 'bot-not-connected';
|
||||
throw error;
|
||||
}
|
||||
const receiveIdType = target?.kind === 'user' ? 'open_id'
|
||||
: target?.kind === 'group' ? 'chat_id' : null;
|
||||
const receiveId = target?.kind === 'user'
|
||||
? nonEmptyString(target?.route?.openId)
|
||||
: target?.kind === 'group'
|
||||
? nonEmptyString(target?.route?.chatId)
|
||||
: null;
|
||||
if (!receiveIdType || !receiveId) {
|
||||
const error = new TypeError('Invalid Feishu proactive delivery target');
|
||||
error.code = 'invalid-target';
|
||||
throw error;
|
||||
}
|
||||
signal?.throwIfAborted();
|
||||
const response = await this.#client.im.v1.message.create({
|
||||
params: { receive_id_type: receiveIdType },
|
||||
data: {
|
||||
receive_id: receiveId,
|
||||
msg_type: 'text',
|
||||
content: JSON.stringify({ text }),
|
||||
},
|
||||
});
|
||||
if (response?.code && response.code !== 0) {
|
||||
const error = new Error(`Feishu proactive delivery failed: ${response.msg || response.code}`);
|
||||
error.code = 'target-rejected';
|
||||
throw error;
|
||||
}
|
||||
return { sent: true };
|
||||
}
|
||||
|
||||
stop(options = {}) {
|
||||
if (this.#stopping) return this.#stopping;
|
||||
|
||||
|
|
|
|||
|
|
@ -537,6 +537,21 @@ export class MultiBotDshFeishuController {
|
|||
});
|
||||
}
|
||||
|
||||
async sendProactiveText(botId, target, text, options = {}) {
|
||||
this.#assertOpen();
|
||||
return this.#withBotTransition(botId, async () => {
|
||||
this.#requireBot(botId);
|
||||
const runtime = this.#runtimes.get(botId);
|
||||
if (!isConnected(connectionStatus(runtime))
|
||||
|| typeof runtime.sendProactiveText !== 'function') {
|
||||
const error = new Error('飞书机器人尚未连接');
|
||||
error.code = 'bot-not-connected';
|
||||
throw error;
|
||||
}
|
||||
return runtime.sendProactiveText(target, text, options);
|
||||
});
|
||||
}
|
||||
|
||||
async disconnectBot(botId) {
|
||||
this.#assertOpen();
|
||||
// An operational pause only: credentials/config remain durable, so the
|
||||
|
|
|
|||
|
|
@ -268,6 +268,20 @@ export class QqController {
|
|||
});
|
||||
}
|
||||
|
||||
async sendProactiveText(botId, target, text, options = {}) {
|
||||
const config = this.#configStore.get(botId);
|
||||
if (!config) throw new Error('Unknown QQ bot');
|
||||
return this.#withBotTransition(botId, async () => {
|
||||
const runtime = this.#runtimes.get(botId);
|
||||
if (!runtime?.status?.ready || typeof runtime.sendProactiveText !== 'function') {
|
||||
const error = new Error(t('QQ机器人尚未连接'));
|
||||
error.code = 'bot-not-connected';
|
||||
throw error;
|
||||
}
|
||||
return runtime.sendProactiveText(target, text, options);
|
||||
});
|
||||
}
|
||||
|
||||
async deleteBot(botId) {
|
||||
const config = this.#configStore.get(botId);
|
||||
if (!config) throw new Error('Unknown QQ bot');
|
||||
|
|
|
|||
|
|
@ -97,6 +97,29 @@ export class QqRuntime {
|
|||
return { sent: true };
|
||||
}
|
||||
|
||||
async sendProactiveText(target, text, { signal } = {}) {
|
||||
const nativeId = target?.kind === 'user'
|
||||
? (typeof target?.route?.userOpenId === 'string' ? target.route.userOpenId.trim() : '')
|
||||
: target?.kind === 'group'
|
||||
? (typeof target?.route?.groupOpenId === 'string' ? target.route.groupOpenId.trim() : '')
|
||||
: '';
|
||||
if (!nativeId) {
|
||||
const error = new TypeError('Invalid QQ proactive delivery target');
|
||||
error.code = 'invalid-target';
|
||||
throw error;
|
||||
}
|
||||
if (!this.#status.ready || !this.#bot) {
|
||||
const error = new Error('QQ bot is not connected');
|
||||
error.code = 'bot-not-connected';
|
||||
throw error;
|
||||
}
|
||||
signal?.throwIfAborted();
|
||||
return this.#bot.sendText({
|
||||
scope: target.kind === 'user' ? 'c2c' : 'group',
|
||||
targetId: nativeId,
|
||||
}, text);
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (this.#status.ready && this.#bot) return this.status;
|
||||
if (this.#starting) return this.#starting;
|
||||
|
|
|
|||
|
|
@ -49,8 +49,90 @@ function botIdOf(value) {
|
|||
return value;
|
||||
}
|
||||
|
||||
function deliveryTargetError(code, message, options) {
|
||||
const error = new Error(message, options);
|
||||
error.code = code;
|
||||
return error;
|
||||
}
|
||||
|
||||
function targetIdOf(value) {
|
||||
if (typeof value !== 'string'
|
||||
|| !/^[A-Za-z0-9._:@-]{1,128}$/.test(value)) {
|
||||
throw deliveryTargetError('invalid-target', 'Invalid target id');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function jsonRecord(value) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
throw deliveryTargetError('invalid-target', 'Invalid target route');
|
||||
}
|
||||
try {
|
||||
const serialized = JSON.stringify(value);
|
||||
if (serialized === undefined) throw new TypeError('route is not JSON');
|
||||
const route = JSON.parse(serialized);
|
||||
if (!route || typeof route !== 'object' || Array.isArray(route)) {
|
||||
throw new TypeError('route is not an object');
|
||||
}
|
||||
return route;
|
||||
} catch (cause) {
|
||||
throw deliveryTargetError('invalid-target', 'Invalid target route', { cause });
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeDeliveryTarget(value, { targetId } = {}) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
throw deliveryTargetError('invalid-target', 'Invalid delivery target');
|
||||
}
|
||||
const allowed = targetId === undefined
|
||||
? new Set(['targetId', 'name', 'kind', 'route'])
|
||||
: new Set(['name', 'kind', 'route']);
|
||||
if (Object.keys(value).some((key) => !allowed.has(key))) {
|
||||
throw deliveryTargetError('invalid-target', 'Invalid delivery target');
|
||||
}
|
||||
const id = targetIdOf(targetId ?? value.targetId);
|
||||
if (typeof value.kind !== 'string' || !/^[a-z][a-z0-9-]{0,31}$/.test(value.kind)) {
|
||||
throw deliveryTargetError('invalid-target', 'Invalid target kind');
|
||||
}
|
||||
let name;
|
||||
if (value.name !== undefined) {
|
||||
if (typeof value.name !== 'string' || !value.name.trim() || value.name.trim().length > 80) {
|
||||
throw deliveryTargetError('invalid-target', 'Invalid target name');
|
||||
}
|
||||
name = value.name.trim();
|
||||
}
|
||||
return {
|
||||
targetId: id,
|
||||
...(name === undefined ? {} : { name }),
|
||||
kind: value.kind,
|
||||
route: jsonRecord(value.route),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeDeliveryTargets(value) {
|
||||
const deliveryTargets = Object.create(null);
|
||||
if (value === undefined) return deliveryTargets;
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
||||
try {
|
||||
for (const [botId, targets] of Object.entries(value)) {
|
||||
botIdOf(botId);
|
||||
if (!targets || typeof targets !== 'object' || Array.isArray(targets)) return null;
|
||||
const normalizedTargets = Object.create(null);
|
||||
for (const [targetId, target] of Object.entries(targets)) {
|
||||
const normalized = normalizeDeliveryTarget(target, { targetId });
|
||||
const { targetId: _targetId, ...stored } = normalized;
|
||||
normalizedTargets[targetId] = stored;
|
||||
}
|
||||
deliveryTargets[botId] = normalizedTargets;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return deliveryTargets;
|
||||
}
|
||||
|
||||
function normalizeDocument(value) {
|
||||
if (!value || value.version !== 1 || !value.workspaces
|
||||
if (!value || ![1, 2].includes(value.version) || !value.workspaces
|
||||
|| typeof value.workspaces !== 'object' || Array.isArray(value.workspaces)) return null;
|
||||
const workspaces = {};
|
||||
for (const [botId, workspace] of Object.entries(value.workspaces)) {
|
||||
|
|
@ -83,7 +165,16 @@ function normalizeDocument(value) {
|
|||
}
|
||||
}
|
||||
}
|
||||
return { version: 1, workspaces, agentPresets, contextEnhancement };
|
||||
if (value.version === 1 && value.deliveryTargets !== undefined) return null;
|
||||
const deliveryTargets = normalizeDeliveryTargets(value.deliveryTargets);
|
||||
if (!deliveryTargets) return null;
|
||||
return {
|
||||
version: value.version,
|
||||
workspaces,
|
||||
agentPresets,
|
||||
contextEnhancement,
|
||||
deliveryTargets,
|
||||
};
|
||||
}
|
||||
|
||||
export async function validateWorkspacePath(value) {
|
||||
|
|
@ -112,9 +203,11 @@ export async function validateWorkspacePath(value) {
|
|||
export class BotWorkspaceStore {
|
||||
#path;
|
||||
#defaultWorkspace;
|
||||
#version = 1;
|
||||
#workspaces = {};
|
||||
#agentPresets = {};
|
||||
#contextEnhancement = {};
|
||||
#deliveryTargets = Object.create(null);
|
||||
#generations = new Map();
|
||||
#nextGeneration = 1;
|
||||
#incarnations = new Map();
|
||||
|
|
@ -135,14 +228,18 @@ export class BotWorkspaceStore {
|
|||
try {
|
||||
const normalized = normalizeDocument(JSON.parse(await readFile(this.#path, 'utf8')));
|
||||
if (!normalized) throw new Error('dsh-im workspace config is invalid');
|
||||
this.#version = normalized.version;
|
||||
this.#workspaces = normalized.workspaces;
|
||||
this.#agentPresets = normalized.agentPresets;
|
||||
this.#contextEnhancement = normalized.contextEnhancement;
|
||||
this.#deliveryTargets = normalized.deliveryTargets;
|
||||
} catch (error) {
|
||||
if (error?.code !== 'ENOENT') throw error;
|
||||
this.#version = 1;
|
||||
this.#workspaces = {};
|
||||
this.#agentPresets = {};
|
||||
this.#contextEnhancement = {};
|
||||
this.#deliveryTargets = Object.create(null);
|
||||
}
|
||||
this.#generations.clear();
|
||||
this.#nextGeneration = 1;
|
||||
|
|
@ -181,6 +278,83 @@ export class BotWorkspaceStore {
|
|||
: DEFAULT_CONTEXT_ENHANCEMENT_CONFIG;
|
||||
}
|
||||
|
||||
listDeliveryTargets(botId) {
|
||||
const id = botIdOf(botId);
|
||||
if (!this.has(id)) throw deliveryTargetError('unknown-bot', 'Unknown bot');
|
||||
return Object.entries(this.#deliveryTargets[id] ?? {})
|
||||
.map(([targetId, target]) => normalizeDeliveryTarget(target, { targetId }))
|
||||
.sort((left, right) => left.targetId.localeCompare(right.targetId));
|
||||
}
|
||||
|
||||
deliveryTargetFor(botId, targetId) {
|
||||
const id = botIdOf(botId);
|
||||
const targetKey = targetIdOf(targetId);
|
||||
if (!this.has(id)) throw deliveryTargetError('unknown-bot', 'Unknown bot');
|
||||
const target = this.#deliveryTargets[id]?.[targetKey];
|
||||
return target ? normalizeDeliveryTarget(target, { targetId: targetKey }) : null;
|
||||
}
|
||||
|
||||
async createDeliveryTarget(botId, value) {
|
||||
const id = botIdOf(botId);
|
||||
const target = normalizeDeliveryTarget(value);
|
||||
return this.#enqueue(id, async () => {
|
||||
if (!this.has(id)) throw deliveryTargetError('unknown-bot', 'Unknown bot');
|
||||
if (Object.hasOwn(this.#deliveryTargets[id] ?? {}, target.targetId)) {
|
||||
throw deliveryTargetError('target-conflict', 'Target already exists');
|
||||
}
|
||||
const { targetId, ...stored } = target;
|
||||
const next = {
|
||||
...this.#deliveryTargets,
|
||||
[id]: { ...(this.#deliveryTargets[id] ?? {}), [targetId]: stored },
|
||||
};
|
||||
await this.#persist(this.#contextEnhancement, next, 2);
|
||||
this.#deliveryTargets = next;
|
||||
this.#version = 2;
|
||||
return normalizeDeliveryTarget(stored, { targetId });
|
||||
});
|
||||
}
|
||||
|
||||
async updateDeliveryTarget(botId, targetId, value) {
|
||||
const id = botIdOf(botId);
|
||||
const targetKey = targetIdOf(targetId);
|
||||
const replacement = normalizeDeliveryTarget(value, { targetId: targetKey });
|
||||
return this.#enqueue(id, async () => {
|
||||
if (!this.has(id)) throw deliveryTargetError('unknown-bot', 'Unknown bot');
|
||||
if (!Object.hasOwn(this.#deliveryTargets[id] ?? {}, targetKey)) {
|
||||
throw deliveryTargetError('unknown-target', 'Unknown target');
|
||||
}
|
||||
const { targetId: _targetId, ...stored } = replacement;
|
||||
const next = {
|
||||
...this.#deliveryTargets,
|
||||
[id]: { ...this.#deliveryTargets[id], [targetKey]: stored },
|
||||
};
|
||||
await this.#persist(this.#contextEnhancement, next, 2);
|
||||
this.#deliveryTargets = next;
|
||||
this.#version = 2;
|
||||
return normalizeDeliveryTarget(stored, { targetId: targetKey });
|
||||
});
|
||||
}
|
||||
|
||||
async deleteDeliveryTarget(botId, targetId) {
|
||||
const id = botIdOf(botId);
|
||||
const targetKey = targetIdOf(targetId);
|
||||
return this.#enqueue(id, async () => {
|
||||
if (!this.has(id)) throw deliveryTargetError('unknown-bot', 'Unknown bot');
|
||||
if (!Object.hasOwn(this.#deliveryTargets[id] ?? {}, targetKey)) {
|
||||
throw deliveryTargetError('unknown-target', 'Unknown target');
|
||||
}
|
||||
const botTargets = { ...this.#deliveryTargets[id] };
|
||||
delete botTargets[targetKey];
|
||||
const next = { ...this.#deliveryTargets };
|
||||
if (Object.keys(botTargets).length > 0) next[id] = botTargets;
|
||||
else delete next[id];
|
||||
await this.#persist(this.#contextEnhancement, next, 2);
|
||||
this.#deliveryTargets = next;
|
||||
this.#version = 2;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
generationFor(botId) {
|
||||
return this.#generations.get(botIdOf(botId)) ?? null;
|
||||
}
|
||||
|
|
@ -471,6 +645,7 @@ export class BotWorkspaceStore {
|
|||
...Object.keys(this.#workspaces),
|
||||
...Object.keys(this.#agentPresets),
|
||||
...Object.keys(this.#contextEnhancement),
|
||||
...Object.keys(this.#deliveryTargets),
|
||||
...this.#dirtyRemovals,
|
||||
]);
|
||||
for (const botId of candidates) {
|
||||
|
|
@ -518,10 +693,13 @@ export class BotWorkspaceStore {
|
|||
const hadWorkspace = Object.hasOwn(this.#workspaces, id);
|
||||
const hadPreset = Object.hasOwn(this.#agentPresets, id);
|
||||
const hadContextEnhancement = Object.hasOwn(this.#contextEnhancement, id);
|
||||
const needsCleanup = hadWorkspace || hadPreset || hadContextEnhancement || this.#dirtyRemovals.has(id);
|
||||
const hadDeliveryTargets = Object.hasOwn(this.#deliveryTargets, id);
|
||||
const needsCleanup = hadWorkspace || hadPreset || hadContextEnhancement
|
||||
|| hadDeliveryTargets || this.#dirtyRemovals.has(id);
|
||||
delete this.#workspaces[id];
|
||||
delete this.#agentPresets[id];
|
||||
delete this.#contextEnhancement[id];
|
||||
delete this.#deliveryTargets[id];
|
||||
this.#generations.delete(id);
|
||||
this.#incarnations.delete(id);
|
||||
if (!needsCleanup) return {
|
||||
|
|
@ -551,14 +729,21 @@ export class BotWorkspaceStore {
|
|||
return queued;
|
||||
}
|
||||
|
||||
async #persist(contextEnhancement = this.#contextEnhancement) {
|
||||
const document = { version: 1, workspaces: this.#workspaces };
|
||||
async #persist(
|
||||
contextEnhancement = this.#contextEnhancement,
|
||||
deliveryTargets = this.#deliveryTargets,
|
||||
version = this.#version,
|
||||
) {
|
||||
const document = { version, workspaces: this.#workspaces };
|
||||
if (Object.keys(this.#agentPresets).length > 0) {
|
||||
document.agentPresets = this.#agentPresets;
|
||||
}
|
||||
if (Object.keys(contextEnhancement).length > 0) {
|
||||
document.contextEnhancement = contextEnhancement;
|
||||
}
|
||||
if (version >= 2 && Object.keys(deliveryTargets).length > 0) {
|
||||
document.deliveryTargets = deliveryTargets;
|
||||
}
|
||||
await mkdir(dirname(this.#path), { recursive: true, mode: 0o700 });
|
||||
const temporary = `${this.#path}.tmp`;
|
||||
await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, {
|
||||
|
|
@ -572,7 +757,8 @@ export class BotWorkspaceStore {
|
|||
async #persistCurrentDocument() {
|
||||
if (Object.keys(this.#workspaces).length > 0
|
||||
|| Object.keys(this.#agentPresets).length > 0
|
||||
|| Object.keys(this.#contextEnhancement).length > 0) {
|
||||
|| Object.keys(this.#contextEnhancement).length > 0
|
||||
|| Object.keys(this.#deliveryTargets).length > 0) {
|
||||
await this.#persist();
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -469,6 +469,11 @@ export class TextHarnessBridge {
|
|||
});
|
||||
}
|
||||
|
||||
sendProactiveText(target, text, { signal } = {}) {
|
||||
signal?.throwIfAborted();
|
||||
return this.#bot.sendText(target, text);
|
||||
}
|
||||
|
||||
async #deliverArtifacts(target, replyTo, artifacts = [], baseReceipt) {
|
||||
const delivery = await deliverOutboundArtifacts({
|
||||
artifacts,
|
||||
|
|
|
|||
|
|
@ -227,6 +227,22 @@ export class TokenBotController {
|
|||
});
|
||||
}
|
||||
|
||||
async sendProactiveText(botId, target, text, options = {}) {
|
||||
const config = this.#configStore.get(botId);
|
||||
if (!config) throw new Error(`Unknown ${this.#descriptor.label} bot`);
|
||||
return this.#withBotTransition(botId, async () => {
|
||||
const runtime = this.#runtimes.get(botId);
|
||||
if (!runtime?.status?.ready || typeof runtime.sendProactiveText !== 'function') {
|
||||
const error = new Error(t('{label}机器人尚未连接', {
|
||||
label: this.#descriptor.label,
|
||||
}));
|
||||
error.code = 'bot-not-connected';
|
||||
throw error;
|
||||
}
|
||||
return runtime.sendProactiveText(target, text, options);
|
||||
});
|
||||
}
|
||||
|
||||
async deleteBot(botId) {
|
||||
const config = this.#configStore.get(botId);
|
||||
if (!config) throw new Error(`Unknown ${this.#descriptor.label} bot`);
|
||||
|
|
|
|||
|
|
@ -189,6 +189,20 @@ export class SlackController {
|
|||
});
|
||||
}
|
||||
|
||||
async sendProactiveText(botId, target, text, options = {}) {
|
||||
const config = this.#configStore.get(botId);
|
||||
if (!config) throw new Error('Unknown Slack bot');
|
||||
return this.#withBotTransition(botId, async () => {
|
||||
const runtime = this.#runtimes.get(botId);
|
||||
if (!runtime?.status?.ready || typeof runtime.sendProactiveText !== 'function') {
|
||||
const error = new Error(t('Slack机器人尚未连接'));
|
||||
error.code = 'bot-not-connected';
|
||||
throw error;
|
||||
}
|
||||
return runtime.sendProactiveText(target, text, options);
|
||||
});
|
||||
}
|
||||
|
||||
async deleteBot(botId) {
|
||||
const config = this.#configStore.get(botId);
|
||||
if (!config) throw new Error('Unknown Slack bot');
|
||||
|
|
|
|||
|
|
@ -407,6 +407,30 @@ export class SlackRuntime {
|
|||
return this.#bridge.sendConnectionTest(text);
|
||||
}
|
||||
|
||||
async sendProactiveText(target, text, options = {}) {
|
||||
if (!this.#status.ready || !this.#bridge) {
|
||||
const error = new Error('Slack bot is not connected');
|
||||
error.code = 'bot-not-connected';
|
||||
throw error;
|
||||
}
|
||||
const channelId = typeof target?.route?.channelId === 'string'
|
||||
? target.route.channelId.trim() : '';
|
||||
const threadTs = typeof target?.route?.threadTs === 'string'
|
||||
? target.route.threadTs.trim() : '';
|
||||
if (!channelId
|
||||
|| (target?.kind !== 'conversation' && target?.kind !== 'thread')
|
||||
|| (target.kind === 'thread' && !threadTs)
|
||||
|| (target.kind === 'conversation' && threadTs)) {
|
||||
const error = new TypeError('Invalid Slack proactive delivery target');
|
||||
error.code = 'invalid-target';
|
||||
throw error;
|
||||
}
|
||||
return this.#bridge.sendProactiveText({
|
||||
channelId,
|
||||
...(threadTs ? { threadTs } : {}),
|
||||
}, text, options);
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (this.#status.ready && this.#socket) return this.status;
|
||||
if (this.#starting) return this.#starting;
|
||||
|
|
|
|||
|
|
@ -719,6 +719,30 @@ export class TelegramRuntime {
|
|||
return this.#bridge.sendConnectionTest(text);
|
||||
}
|
||||
|
||||
async sendProactiveText(target, text, options = {}) {
|
||||
if (!this.#status.ready || !this.#bridge) {
|
||||
const error = new Error('Telegram bot is not connected');
|
||||
error.code = 'bot-not-connected';
|
||||
throw error;
|
||||
}
|
||||
const chatIdText = typeof target?.route?.chatId === 'string'
|
||||
? target.route.chatId.trim() : '';
|
||||
const chatId = Number(chatIdText);
|
||||
const messageThreadId = target?.route?.messageThreadId;
|
||||
if (!/^-?\d+$/.test(chatIdText) || !Number.isSafeInteger(chatId)
|
||||
|| (target?.kind !== 'chat' && target?.kind !== 'topic')
|
||||
|| (target.kind === 'topic' && (!Number.isSafeInteger(messageThreadId) || messageThreadId <= 0))
|
||||
|| (target.kind === 'chat' && messageThreadId !== undefined)) {
|
||||
const error = new TypeError('Invalid Telegram proactive delivery target');
|
||||
error.code = 'invalid-target';
|
||||
throw error;
|
||||
}
|
||||
return this.#bridge.sendProactiveText({
|
||||
chatId,
|
||||
...(target.kind === 'topic' ? { messageThreadId } : {}),
|
||||
}, text, options);
|
||||
}
|
||||
|
||||
async start() {
|
||||
if (this.#status.ready && this.#pollTask) return this.status;
|
||||
if (this.#starting) return this.#starting;
|
||||
|
|
|
|||
|
|
@ -72,6 +72,10 @@ export class WecomStateStore {
|
|||
await this.#persist();
|
||||
}
|
||||
|
||||
snapshot() {
|
||||
return structuredClone(this.#state);
|
||||
}
|
||||
|
||||
async remove() {
|
||||
try {
|
||||
await unlink(this.#path);
|
||||
|
|
|
|||
|
|
@ -257,6 +257,20 @@ export class WecomController {
|
|||
});
|
||||
}
|
||||
|
||||
async sendProactiveText(botId, target, text, options = {}) {
|
||||
const config = this.#configStore.get(botId);
|
||||
if (!config) throw new Error('Unknown Enterprise WeChat bot');
|
||||
return this.#withBotTransition(botId, async () => {
|
||||
const runtime = this.#runtimes.get(botId);
|
||||
if (!runtime?.status?.ready || typeof runtime.sendProactiveText !== 'function') {
|
||||
const error = new Error(t('企业微信连接当前离线'));
|
||||
error.code = 'bot-not-connected';
|
||||
throw error;
|
||||
}
|
||||
return runtime.sendProactiveText(target, text, options);
|
||||
});
|
||||
}
|
||||
|
||||
async deleteBot(botId) {
|
||||
const config = this.#configStore.get(botId);
|
||||
if (!config) throw new Error('Unknown Enterprise WeChat bot');
|
||||
|
|
|
|||
|
|
@ -220,6 +220,27 @@ export class WecomRuntime {
|
|||
});
|
||||
}
|
||||
|
||||
async sendProactiveText(target, text, { signal } = {}) {
|
||||
const chatId = typeof target?.route?.chatId === 'string'
|
||||
? target.route.chatId.trim() : '';
|
||||
if ((target?.kind !== 'user' && target?.kind !== 'group') || !chatId) {
|
||||
const error = new TypeError('Invalid Enterprise WeChat proactive delivery target');
|
||||
error.code = 'invalid-target';
|
||||
throw error;
|
||||
}
|
||||
if (!this.#status.ready || !this.#client) {
|
||||
const error = new Error('Enterprise WeChat runtime is not connected');
|
||||
error.code = 'bot-not-connected';
|
||||
throw error;
|
||||
}
|
||||
signal?.throwIfAborted();
|
||||
await this.#client.sendMessage(chatId, {
|
||||
msgtype: 'markdown',
|
||||
markdown: { content: text },
|
||||
});
|
||||
return { sent: true };
|
||||
}
|
||||
|
||||
async #stopActive() {
|
||||
const client = this.#client;
|
||||
const bridge = this.#bridge;
|
||||
|
|
|
|||
|
|
@ -299,6 +299,20 @@ export class WeixinController {
|
|||
});
|
||||
}
|
||||
|
||||
async sendProactiveText(botId, target, text, options = {}) {
|
||||
const config = this.#configStore.get(botId);
|
||||
if (!config) throw new Error('Unknown Weixin account');
|
||||
return this.#withBotTransition(botId, async () => {
|
||||
const runtime = this.#runtimes.get(botId);
|
||||
if (!runtime?.status?.ready || typeof runtime.sendProactiveText !== 'function') {
|
||||
const error = new Error(t('微信连接当前离线'));
|
||||
error.code = 'bot-not-connected';
|
||||
throw error;
|
||||
}
|
||||
return runtime.sendProactiveText(target, text, options);
|
||||
});
|
||||
}
|
||||
|
||||
async deleteBot(botId) {
|
||||
const config = this.#configStore.get(botId);
|
||||
if (!config) throw new Error('Unknown Weixin account');
|
||||
|
|
|
|||
|
|
@ -329,4 +329,28 @@ export class WeixinRuntime {
|
|||
});
|
||||
return { sent: true };
|
||||
}
|
||||
|
||||
async sendProactiveText(target, text, { signal } = {}) {
|
||||
const toUserId = typeof target?.route?.toUserId === 'string'
|
||||
? target.route.toUserId.trim() : '';
|
||||
if (target?.kind !== 'user' || !toUserId) {
|
||||
const error = new TypeError('Invalid Weixin proactive delivery target');
|
||||
error.code = 'invalid-target';
|
||||
throw error;
|
||||
}
|
||||
if (!this.#status.ready || !this.#abortController) {
|
||||
const error = new Error('Weixin runtime is not connected');
|
||||
error.code = 'bot-not-connected';
|
||||
throw error;
|
||||
}
|
||||
signal?.throwIfAborted();
|
||||
await this.#api.sendText({
|
||||
baseUrl: this.#config.baseUrl,
|
||||
token: this.#token,
|
||||
toUserId,
|
||||
text,
|
||||
signal: signal ?? this.#abortController.signal,
|
||||
});
|
||||
return { sent: true };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -224,6 +224,20 @@ export class WhatsappController {
|
|||
});
|
||||
}
|
||||
|
||||
async sendProactiveText(botId, target, text, options = {}) {
|
||||
const config = this.#configStore.get(botId);
|
||||
if (!config) throw new Error('Unknown WhatsApp bot');
|
||||
return this.#withBotTransition(botId, async () => {
|
||||
const runtime = this.#runtimes.get(botId);
|
||||
if (!runtime?.status?.ready || typeof runtime.sendProactiveText !== 'function') {
|
||||
const error = new Error(t('WhatsApp机器人尚未连接'));
|
||||
error.code = 'bot-not-connected';
|
||||
throw error;
|
||||
}
|
||||
return runtime.sendProactiveText(target, text, options);
|
||||
});
|
||||
}
|
||||
|
||||
async setAccessPolicy(botId, value) {
|
||||
if (this.#closed) throw new Error('WhatsApp controller is closed');
|
||||
const accessPolicy = normalizeWhatsappAccessPolicy(value);
|
||||
|
|
|
|||
|
|
@ -719,6 +719,25 @@ export class WhatsappRuntime {
|
|||
return { sent: true };
|
||||
}
|
||||
|
||||
async sendProactiveText(target, text, { signal } = {}) {
|
||||
const jid = typeof target?.route?.jid === 'string' ? target.route.jid.trim() : '';
|
||||
const validUser = target?.kind === 'user'
|
||||
&& /^[^@\s]+@(s\.whatsapp\.net|lid)$/.test(jid);
|
||||
const validGroup = target?.kind === 'group' && /^[^@\s]+@g\.us$/.test(jid);
|
||||
if (!validUser && !validGroup) {
|
||||
const error = new TypeError('Invalid WhatsApp proactive delivery target');
|
||||
error.code = 'invalid-target';
|
||||
throw error;
|
||||
}
|
||||
if (!this.#status.ready || !this.#client) {
|
||||
const error = new Error(t('WhatsApp机器人尚未连接'));
|
||||
error.code = 'bot-not-connected';
|
||||
throw error;
|
||||
}
|
||||
signal?.throwIfAborted();
|
||||
return this.#client.sendText({ jid }, text);
|
||||
}
|
||||
|
||||
async stop() {
|
||||
const session = this.#session;
|
||||
const client = this.#client;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue