mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 03:03:24 +08:00
feat: 按机器人选择 Agent Preset
在九个渠道的设置页卡片上选择 Host 已有预设;未选则跟随默认,只影响之后新建的会话。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
c3b3a29f27
commit
3161816cc4
51 changed files with 2227 additions and 740 deletions
74
src/channels/shared/agent-preset.mjs
Normal file
74
src/channels/shared/agent-preset.mjs
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
/** Matches DeepSeek Harness agent-preset directory ids. */
|
||||
export const AGENT_PRESET_ID = /^[a-z0-9][a-z0-9-]*$/;
|
||||
|
||||
export const EMPTY_AGENT_PRESET_CATALOG = Object.freeze({
|
||||
defaultId: '',
|
||||
items: Object.freeze([]),
|
||||
});
|
||||
|
||||
export function normalizeAgentPresetId(value) {
|
||||
if (value == null) return null;
|
||||
if (typeof value !== 'string') return null;
|
||||
const id = value.trim();
|
||||
return AGENT_PRESET_ID.test(id) ? id : null;
|
||||
}
|
||||
|
||||
export function validateAgentPresetId(value) {
|
||||
if (value == null || value === '') return null;
|
||||
const id = normalizeAgentPresetId(value);
|
||||
if (!id) {
|
||||
const error = new Error('Agent Preset 无效。');
|
||||
error.code = 'agent-preset-invalid';
|
||||
throw error;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
function catalogItem(value) {
|
||||
if (typeof value === 'string') {
|
||||
const id = normalizeAgentPresetId(value);
|
||||
return id ? { id, label: id } : null;
|
||||
}
|
||||
if (!value || typeof value !== 'object') return null;
|
||||
const id = normalizeAgentPresetId(value.id);
|
||||
if (!id) return null;
|
||||
const label = typeof value.name === 'string' && value.name.trim()
|
||||
? value.name.trim().slice(0, 128)
|
||||
: typeof value.label === 'string' && value.label.trim()
|
||||
? value.label.trim().slice(0, 128)
|
||||
: id;
|
||||
return { id, label };
|
||||
}
|
||||
|
||||
export function normalizeAgentPresetCatalog(value) {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return { defaultId: '', items: [] };
|
||||
}
|
||||
const items = [];
|
||||
const seen = new Set();
|
||||
for (const entry of Array.isArray(value.items) ? value.items : []) {
|
||||
const item = catalogItem(entry);
|
||||
if (!item || seen.has(item.id)) continue;
|
||||
seen.add(item.id);
|
||||
items.push(item);
|
||||
}
|
||||
return {
|
||||
defaultId: normalizeAgentPresetId(value.defaultId) ?? '',
|
||||
items,
|
||||
};
|
||||
}
|
||||
|
||||
export async function listAgentPresetCatalog(ctx) {
|
||||
const service = typeof ctx?.get === 'function' ? ctx.get('agentPresets') : ctx?.agentPresets;
|
||||
if (!service || typeof service.list !== 'function') return { defaultId: '', items: [] };
|
||||
let listed = [];
|
||||
try {
|
||||
listed = await service.list();
|
||||
} catch {
|
||||
return { defaultId: '', items: [] };
|
||||
}
|
||||
return normalizeAgentPresetCatalog({
|
||||
defaultId: typeof service.defaultId === 'string' ? service.defaultId : '',
|
||||
items: Array.isArray(listed) ? listed : [],
|
||||
});
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import {
|
|||
} from 'node:fs/promises';
|
||||
import { dirname, isAbsolute, resolve } from 'node:path';
|
||||
|
||||
import { validateAgentPresetId } from './agent-preset.mjs';
|
||||
import { CONNECTION_TEST_STATE_IDENTITY } from './connection-test.mjs';
|
||||
import { WORKSPACE_SESSION_STALE } from './workspace-session.mjs';
|
||||
|
||||
|
|
@ -49,7 +50,22 @@ function normalizeDocument(value) {
|
|||
|| typeof workspace !== 'string' || !isAbsolute(workspace)) return null;
|
||||
workspaces[botId] = resolve(workspace);
|
||||
}
|
||||
return { version: 1, workspaces };
|
||||
let agentPresets = {};
|
||||
if (value.agentPresets !== undefined) {
|
||||
if (!value.agentPresets || typeof value.agentPresets !== 'object'
|
||||
|| Array.isArray(value.agentPresets)) return null;
|
||||
for (const [botId, agentPreset] of Object.entries(value.agentPresets)) {
|
||||
if (!/^[A-Za-z0-9_-]{1,128}$/.test(botId)) return null;
|
||||
try {
|
||||
const normalized = validateAgentPresetId(agentPreset);
|
||||
if (!normalized) return null;
|
||||
agentPresets[botId] = normalized;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { version: 1, workspaces, agentPresets };
|
||||
}
|
||||
|
||||
export async function validateWorkspacePath(value) {
|
||||
|
|
@ -79,6 +95,7 @@ export class BotWorkspaceStore {
|
|||
#path;
|
||||
#defaultWorkspace;
|
||||
#workspaces = {};
|
||||
#agentPresets = {};
|
||||
#generations = new Map();
|
||||
#nextGeneration = 1;
|
||||
#incarnations = new Map();
|
||||
|
|
@ -100,9 +117,11 @@ export class BotWorkspaceStore {
|
|||
const normalized = normalizeDocument(JSON.parse(await readFile(this.#path, 'utf8')));
|
||||
if (!normalized) throw new Error('dsh-im workspace config is invalid');
|
||||
this.#workspaces = normalized.workspaces;
|
||||
this.#agentPresets = normalized.agentPresets;
|
||||
} catch (error) {
|
||||
if (error?.code !== 'ENOENT') throw error;
|
||||
this.#workspaces = {};
|
||||
this.#agentPresets = {};
|
||||
}
|
||||
this.#generations.clear();
|
||||
this.#nextGeneration = 1;
|
||||
|
|
@ -130,6 +149,10 @@ export class BotWorkspaceStore {
|
|||
return this.#workspaces[botIdOf(botId)] ?? this.#defaultWorkspace;
|
||||
}
|
||||
|
||||
agentPresetFor(botId) {
|
||||
return this.#agentPresets[botIdOf(botId)] ?? null;
|
||||
}
|
||||
|
||||
generationFor(botId) {
|
||||
return this.#generations.get(botIdOf(botId)) ?? null;
|
||||
}
|
||||
|
|
@ -207,6 +230,37 @@ export class BotWorkspaceStore {
|
|||
});
|
||||
}
|
||||
|
||||
async setAgentPreset(botId, value, { incarnation } = {}) {
|
||||
const id = botIdOf(botId);
|
||||
if (!this.has(id)
|
||||
|| (incarnation !== undefined && incarnation !== this.incarnationFor(id))) {
|
||||
const error = new Error('找不到要修改的机器人。');
|
||||
error.code = 'workspace-bot-not-found';
|
||||
throw error;
|
||||
}
|
||||
const agentPreset = validateAgentPresetId(value);
|
||||
return this.#enqueue(id, async () => {
|
||||
if (!this.has(id)
|
||||
|| (incarnation !== undefined && incarnation !== this.incarnationFor(id))) {
|
||||
const error = new Error('找不到要修改的机器人。');
|
||||
error.code = 'workspace-bot-not-found';
|
||||
throw error;
|
||||
}
|
||||
const previous = this.#agentPresets[id] ?? null;
|
||||
if (previous === agentPreset) return agentPreset;
|
||||
if (agentPreset) this.#agentPresets[id] = agentPreset;
|
||||
else delete this.#agentPresets[id];
|
||||
try {
|
||||
await this.#persist();
|
||||
} catch (error) {
|
||||
if (previous) this.#agentPresets[id] = previous;
|
||||
else delete this.#agentPresets[id];
|
||||
throw error;
|
||||
}
|
||||
return agentPreset;
|
||||
});
|
||||
}
|
||||
|
||||
async bindWorkspaceSession(botId, value, {
|
||||
conversationKey,
|
||||
sessionId,
|
||||
|
|
@ -353,7 +407,11 @@ export class BotWorkspaceStore {
|
|||
|
||||
async reconcile(activeBotIds) {
|
||||
const active = new Set([...activeBotIds].map(botIdOf));
|
||||
const candidates = new Set([...Object.keys(this.#workspaces), ...this.#dirtyRemovals]);
|
||||
const candidates = new Set([
|
||||
...Object.keys(this.#workspaces),
|
||||
...Object.keys(this.#agentPresets),
|
||||
...this.#dirtyRemovals,
|
||||
]);
|
||||
for (const botId of candidates) {
|
||||
if (!active.has(botId)) await this.remove(botId);
|
||||
}
|
||||
|
|
@ -364,7 +422,11 @@ export class BotWorkspaceStore {
|
|||
return {
|
||||
...status,
|
||||
bots: status.bots.map((bot) => bot?.botId
|
||||
? { ...bot, workspace: this.workspaceFor(bot.botId) }
|
||||
? {
|
||||
...bot,
|
||||
workspace: this.workspaceFor(bot.botId),
|
||||
agentPreset: this.agentPresetFor(bot.botId),
|
||||
}
|
||||
: bot),
|
||||
};
|
||||
}
|
||||
|
|
@ -392,8 +454,10 @@ export class BotWorkspaceStore {
|
|||
|
||||
async #retireCurrentIncarnation(id) {
|
||||
const hadWorkspace = Object.hasOwn(this.#workspaces, id);
|
||||
const needsCleanup = hadWorkspace || this.#dirtyRemovals.has(id);
|
||||
const hadPreset = Object.hasOwn(this.#agentPresets, id);
|
||||
const needsCleanup = hadWorkspace || hadPreset || this.#dirtyRemovals.has(id);
|
||||
delete this.#workspaces[id];
|
||||
delete this.#agentPresets[id];
|
||||
this.#generations.delete(id);
|
||||
this.#incarnations.delete(id);
|
||||
if (!needsCleanup) return {
|
||||
|
|
@ -425,6 +489,9 @@ export class BotWorkspaceStore {
|
|||
|
||||
async #persist() {
|
||||
const document = { version: 1, workspaces: this.#workspaces };
|
||||
if (Object.keys(this.#agentPresets).length > 0) {
|
||||
document.agentPresets = this.#agentPresets;
|
||||
}
|
||||
await mkdir(dirname(this.#path), { recursive: true, mode: 0o700 });
|
||||
const temporary = `${this.#path}.tmp`;
|
||||
await writeFile(temporary, `${JSON.stringify(document, null, 2)}\n`, {
|
||||
|
|
@ -436,7 +503,8 @@ export class BotWorkspaceStore {
|
|||
}
|
||||
|
||||
async #persistCurrentDocument() {
|
||||
if (Object.keys(this.#workspaces).length > 0) {
|
||||
if (Object.keys(this.#workspaces).length > 0
|
||||
|| Object.keys(this.#agentPresets).length > 0) {
|
||||
await this.#persist();
|
||||
return;
|
||||
}
|
||||
|
|
@ -450,10 +518,18 @@ export class BotWorkspaceStore {
|
|||
}
|
||||
}
|
||||
|
||||
function decorateResult(workspaces, result) {
|
||||
function decorateResult(workspaces, result, catalog) {
|
||||
const decorate = (value) => {
|
||||
const decorated = workspaces.decorateStatus(value);
|
||||
if (!catalog || !decorated || typeof decorated !== 'object') return decorated;
|
||||
const agentPresetCatalog = typeof catalog === 'function' ? catalog() : catalog;
|
||||
return agentPresetCatalog
|
||||
? { ...decorated, agentPresetCatalog }
|
||||
: decorated;
|
||||
};
|
||||
return result && typeof result.then === 'function'
|
||||
? result.then((value) => workspaces.decorateStatus(value))
|
||||
: workspaces.decorateStatus(result);
|
||||
? result.then(decorate)
|
||||
: decorate(result);
|
||||
}
|
||||
|
||||
function targetStatus(controller) {
|
||||
|
|
@ -608,9 +684,11 @@ export function createBotWorkspaceScope(harness, { botId, workspaces, state }) {
|
|||
throw error;
|
||||
}
|
||||
const generation = workspaces.generationFor(botId);
|
||||
const agentPreset = workspaces.agentPresetFor(botId);
|
||||
const sessionId = await target.createSession({
|
||||
...options,
|
||||
workspace: workspaces.workspaceFor(botId),
|
||||
...(agentPreset == null ? {} : { agentPreset }),
|
||||
});
|
||||
sessionGenerations.set(sessionId, generation);
|
||||
return sessionId;
|
||||
|
|
@ -765,7 +843,7 @@ export function createBotScopedHarness(harness, options) {
|
|||
return createBotWorkspaceScope(harness, options).harness;
|
||||
}
|
||||
|
||||
export function createWorkspaceAwareController(controller, { workspaces, stateFor }) {
|
||||
export function createWorkspaceAwareController(controller, { workspaces, stateFor, agentPresetCatalog } = {}) {
|
||||
if (!controller || !workspaces || typeof stateFor !== 'function') {
|
||||
throw new TypeError('controller, workspaces, and stateFor are required');
|
||||
}
|
||||
|
|
@ -778,6 +856,7 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
|
|||
if (transitions.get(botId) === current) transitions.delete(botId);
|
||||
});
|
||||
};
|
||||
const decorate = (value) => decorateResult(workspaces, value, agentPresetCatalog);
|
||||
const updateWorkspace = (botId, workspace) => {
|
||||
// Capture at API invocation, before even waiting for an older outer
|
||||
// transition. A queued request still belongs to the incarnation that the
|
||||
|
|
@ -795,7 +874,20 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
|
|||
clearSessions: () => state.clearSessions(),
|
||||
incarnation,
|
||||
});
|
||||
return workspaces.decorateStatus(await controller.status());
|
||||
return decorate(await controller.status());
|
||||
});
|
||||
};
|
||||
const updateAgentPreset = (botId, agentPreset) => {
|
||||
const incarnation = workspaces.incarnationFor(botId);
|
||||
return withBotTransition(botId, async () => {
|
||||
const snapshot = await controller.status();
|
||||
if (!snapshot?.bots?.some((bot) => bot?.botId === botId)) {
|
||||
const error = new Error('找不到要修改的机器人。');
|
||||
error.code = 'workspace-bot-not-found';
|
||||
throw error;
|
||||
}
|
||||
await workspaces.setAgentPreset(botId, agentPreset, { incarnation });
|
||||
return decorate(await controller.status());
|
||||
});
|
||||
};
|
||||
const deleteWithWorkspace = (botId, invokeDelete) => withBotTransition(botId, async () => {
|
||||
|
|
@ -822,7 +914,7 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
|
|||
try {
|
||||
const result = await invokeDelete();
|
||||
await workspaces.finishRemoval(removal);
|
||||
return workspaces.decorateStatus(result);
|
||||
return decorate(result);
|
||||
} catch (error) {
|
||||
const after = await targetStatus(controller).catch(() => null);
|
||||
const knownAbsent = Array.isArray(after?.bots)
|
||||
|
|
@ -836,6 +928,7 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
|
|||
return new Proxy(controller, {
|
||||
get(target, property) {
|
||||
if (property === 'updateWorkspace') return updateWorkspace;
|
||||
if (property === 'updateAgentPreset') return updateAgentPreset;
|
||||
const value = Reflect.get(target, property, target);
|
||||
if (typeof value !== 'function') return value;
|
||||
if (property === 'deleteBot') {
|
||||
|
|
@ -848,11 +941,11 @@ export function createWorkspaceAwareController(controller, { workspaces, stateFo
|
|||
return async (...args) => {
|
||||
const before = await target.status();
|
||||
const botId = before?.bots?.[0]?.botId;
|
||||
if (!botId) return decorateResult(workspaces, value.apply(target, args));
|
||||
if (!botId) return decorate(value.apply(target, args));
|
||||
return deleteWithWorkspace(botId, () => value.apply(target, args));
|
||||
};
|
||||
}
|
||||
return (...args) => decorateResult(workspaces, value.apply(target, args));
|
||||
return (...args) => decorate(value.apply(target, args));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -592,11 +592,13 @@ export class HarnessClient {
|
|||
}
|
||||
|
||||
async createSession(options = {}) {
|
||||
await this.ensureRunning(options);
|
||||
const workspaceId = await this.workspaceId(options);
|
||||
const { agentPreset: requestedPreset, ...rpcOptions } = options;
|
||||
await this.ensureRunning(rpcOptions);
|
||||
const workspaceId = await this.workspaceId(rpcOptions);
|
||||
const payload = { workspaceId };
|
||||
if (this.#agentPreset !== undefined) payload.agentPreset = this.#agentPreset;
|
||||
const created = await this.rpc('session.create', payload, 30_000, options);
|
||||
const agentPreset = requestedPreset !== undefined ? requestedPreset : this.#agentPreset;
|
||||
if (agentPreset != null) payload.agentPreset = agentPreset;
|
||||
const created = await this.rpc('session.create', payload, 30_000, rpcOptions);
|
||||
return created.sessionId;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue