@yeaft/webchat-agent 0.1.803 → 0.1.804
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.
|
@@ -36,7 +36,7 @@ import { sendToServer, flushMessageBuffer } from './buffer.js';
|
|
|
36
36
|
import { handleRestartAgent, handleUpgradeAgent } from './upgrade.js';
|
|
37
37
|
import { loadMcpServers, updateMcpConfig } from '../mcp.js';
|
|
38
38
|
import { getLlmConfig, updateLlmConfig, getUnifySettings, updateUnifySettings, getSearchSettings, updateSearchSettings, fetchTavilyUsage } from '../unify/config-api.js';
|
|
39
|
-
import { handleUnifyGroupChat, handleUnifyModeSwitch, handleUnifyModelSwitch, resetUnifySession, handleUnifyLoadHistory, handleUnifyLoadMoreHistory, handleUnifyAbortThread, handleUnifyAbortAll, handleUnifyAbortTurn, handleUnifyVpSubscribe, handleUnifyVpCreate, handleUnifyVpUpdate, handleUnifyVpDelete, handleUnifyVpRead, handleUnifyListGroups, handleUnifyCreateGroup, handleUnifyRenameGroup, handleUnifyUpdateGroup, handleUnifyArchiveGroup, handleUnifyDeleteGroup, handleUnifyAddMember, handleUnifyRemoveMember, handleUnifySetDefaultVp, handleUnifyDreamTrigger, handleUnifyFetchToolStats, handleUnifyFetchDebugHistory, broadcastLanguageChange } from '../unify/web-bridge.js';
|
|
39
|
+
import { handleUnifyGroupChat, handleUnifyModeSwitch, handleUnifyModelSwitch, resetUnifySession, handleUnifyLoadHistory, handleUnifyLoadMoreHistory, handleUnifyAbortThread, handleUnifyAbortAll, handleUnifyAbortTurn, handleUnifyVpSubscribe, handleUnifyVpCreate, handleUnifyVpUpdate, handleUnifyVpDelete, handleUnifyVpRead, handleUnifyListGroups, handleUnifyCreateGroup, handleUnifyRenameGroup, handleUnifyUpdateGroup, handleUnifyUpdateGroupConfig, handleUnifyArchiveGroup, handleUnifyDeleteGroup, handleUnifyAddMember, handleUnifyRemoveMember, handleUnifySetDefaultVp, handleUnifyDreamTrigger, handleUnifyFetchToolStats, handleUnifyFetchDebugHistory, broadcastLanguageChange } from '../unify/web-bridge.js';
|
|
40
40
|
|
|
41
41
|
export async function handleMessage(msg) {
|
|
42
42
|
switch (msg.type) {
|
|
@@ -476,6 +476,9 @@ export async function handleMessage(msg) {
|
|
|
476
476
|
case 'unify_update_group':
|
|
477
477
|
handleUnifyUpdateGroup(msg);
|
|
478
478
|
break;
|
|
479
|
+
case 'unify_update_group_config':
|
|
480
|
+
handleUnifyUpdateGroupConfig(msg);
|
|
481
|
+
break;
|
|
479
482
|
case 'unify_archive_group':
|
|
480
483
|
handleUnifyArchiveGroup(msg);
|
|
481
484
|
break;
|
package/package.json
CHANGED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* group-config.js — Per-group configuration overrides.
|
|
3
|
+
*
|
|
4
|
+
* Each group may carry its own `config.json` at
|
|
5
|
+
* ~/.yeaft/groups/<groupId>/config.json
|
|
6
|
+
*
|
|
7
|
+
* v1 schema (intentionally tiny — extend via additive keys only):
|
|
8
|
+
* {
|
|
9
|
+
* "model": "my-proxy/claude-sonnet-4-20250514" // optional
|
|
10
|
+
* }
|
|
11
|
+
*
|
|
12
|
+
* Missing file → empty object. Missing field → fall back to user-level
|
|
13
|
+
* config (`~/.yeaft/config.json` via loadConfig()). Resolution is a
|
|
14
|
+
* shallow overlay (group fields override user fields when truthy).
|
|
15
|
+
*
|
|
16
|
+
* Storage layer only — no engine wiring, no validation of model strings
|
|
17
|
+
* against the provider registry (that's done lazily at resolve time by
|
|
18
|
+
* the engine when it tries to dispatch to AdapterRouter).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync, readFileSync } from 'fs';
|
|
22
|
+
import { join } from 'path';
|
|
23
|
+
import { writeAtomic } from '../storage/index.js';
|
|
24
|
+
import { groupsRoot, resolveGroupYeaftDir } from './group-crud.js';
|
|
25
|
+
|
|
26
|
+
const CONFIG_FILE = 'config.json';
|
|
27
|
+
|
|
28
|
+
/** Whitelist of fields a group may override. Reject everything else. */
|
|
29
|
+
const ALLOWED_KEYS = new Set(['model']);
|
|
30
|
+
|
|
31
|
+
export class GroupConfigError extends Error {
|
|
32
|
+
constructor(code, message) {
|
|
33
|
+
super(message || code);
|
|
34
|
+
this.name = 'GroupConfigError';
|
|
35
|
+
this.code = code;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the on-disk path for a group's config.json. Honours the
|
|
41
|
+
* per-group workDir registry so groups bound to a project directory
|
|
42
|
+
* keep their config alongside the group meta.
|
|
43
|
+
*/
|
|
44
|
+
export function groupConfigPath(yeaftDir, groupId) {
|
|
45
|
+
if (!yeaftDir) return null;
|
|
46
|
+
const groupYeaftDir = resolveGroupYeaftDir(yeaftDir, groupId);
|
|
47
|
+
return join(groupsRoot(groupYeaftDir), groupId, CONFIG_FILE);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Read a group's config.json. Returns `{}` when the file is missing or
|
|
52
|
+
* corrupt — callers fall back to user-level defaults via
|
|
53
|
+
* `resolveGroupConfig`. We never auto-write on read.
|
|
54
|
+
*
|
|
55
|
+
* @param {string} yeaftDir
|
|
56
|
+
* @param {string} groupId
|
|
57
|
+
* @returns {object}
|
|
58
|
+
*/
|
|
59
|
+
export function loadGroupConfig(yeaftDir, groupId) {
|
|
60
|
+
if (!groupId || !yeaftDir) return {};
|
|
61
|
+
const path = groupConfigPath(yeaftDir, groupId);
|
|
62
|
+
if (!path || !existsSync(path)) return {};
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
65
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {};
|
|
66
|
+
// Strip unknown keys at read time too — defensive against
|
|
67
|
+
// hand-edited files that predate a key removal.
|
|
68
|
+
const out = {};
|
|
69
|
+
for (const k of Object.keys(parsed)) {
|
|
70
|
+
if (ALLOWED_KEYS.has(k)) out[k] = parsed[k];
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
} catch {
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Validate a partial group-config object. Throws GroupConfigError on
|
|
80
|
+
* any unknown key or malformed value. Empty / null values for known
|
|
81
|
+
* keys are allowed — they signal "fall back to user default".
|
|
82
|
+
*
|
|
83
|
+
* @param {object} cfg
|
|
84
|
+
*/
|
|
85
|
+
export function validateGroupConfig(cfg) {
|
|
86
|
+
if (cfg === null || cfg === undefined) return;
|
|
87
|
+
if (typeof cfg !== 'object' || Array.isArray(cfg)) {
|
|
88
|
+
throw new GroupConfigError('invalid_shape', 'config must be an object');
|
|
89
|
+
}
|
|
90
|
+
for (const k of Object.keys(cfg)) {
|
|
91
|
+
if (!ALLOWED_KEYS.has(k)) {
|
|
92
|
+
throw new GroupConfigError('unknown_key', `unknown config key: ${k}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if ('model' in cfg && cfg.model !== null && cfg.model !== undefined && cfg.model !== '') {
|
|
96
|
+
if (typeof cfg.model !== 'string' || !cfg.model.trim()) {
|
|
97
|
+
throw new GroupConfigError('invalid_model', 'model must be a non-empty string');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Shallow-merge `partial` into the group's existing config.json and
|
|
104
|
+
* persist atomically. Returns the resulting object. Passing `null` or
|
|
105
|
+
* empty string for a known key removes that key (so the group falls
|
|
106
|
+
* back to the user default).
|
|
107
|
+
*
|
|
108
|
+
* @param {string} yeaftDir
|
|
109
|
+
* @param {string} groupId
|
|
110
|
+
* @param {object} partial
|
|
111
|
+
* @returns {object}
|
|
112
|
+
*/
|
|
113
|
+
export function saveGroupConfig(yeaftDir, groupId, partial) {
|
|
114
|
+
if (!groupId) throw new GroupConfigError('missing_group_id', 'groupId required');
|
|
115
|
+
validateGroupConfig(partial);
|
|
116
|
+
const current = loadGroupConfig(yeaftDir, groupId);
|
|
117
|
+
const next = { ...current };
|
|
118
|
+
for (const [k, v] of Object.entries(partial || {})) {
|
|
119
|
+
if (!ALLOWED_KEYS.has(k)) continue;
|
|
120
|
+
if (v === null || v === undefined || v === '') {
|
|
121
|
+
delete next[k];
|
|
122
|
+
} else {
|
|
123
|
+
next[k] = typeof v === 'string' ? v.trim() : v;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const path = groupConfigPath(yeaftDir, groupId);
|
|
127
|
+
writeAtomic(path, `${JSON.stringify(next, null, 2)}\n`);
|
|
128
|
+
return next;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Initialise an empty config.json next to a brand-new group. Idempotent —
|
|
133
|
+
* leaves an existing file untouched. Called by `createGroupFromSpec`.
|
|
134
|
+
*/
|
|
135
|
+
export function ensureGroupConfigFile(yeaftDir, groupId) {
|
|
136
|
+
const path = groupConfigPath(yeaftDir, groupId);
|
|
137
|
+
if (!path || existsSync(path)) return;
|
|
138
|
+
try {
|
|
139
|
+
writeAtomic(path, `${JSON.stringify({}, null, 2)}\n`);
|
|
140
|
+
} catch {
|
|
141
|
+
// Best-effort — a permission failure here should never break group
|
|
142
|
+
// create. Read path returns {} on missing file anyway.
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Resolve the effective config for a group by overlaying group-level
|
|
148
|
+
* overrides on top of the user-level config.
|
|
149
|
+
*
|
|
150
|
+
* Only fields that the per-group schema knows about are overlaid;
|
|
151
|
+
* everything else (providers, language, token budgets, ...) is taken
|
|
152
|
+
* verbatim from the user config.
|
|
153
|
+
*
|
|
154
|
+
* @param {object} userConfig — loadConfig() result
|
|
155
|
+
* @param {object} groupConfig — loadGroupConfig() result
|
|
156
|
+
* @returns {object} — A new config object safe to hand to the engine.
|
|
157
|
+
*/
|
|
158
|
+
export function resolveGroupConfig(userConfig, groupConfig) {
|
|
159
|
+
const base = userConfig ? { ...userConfig } : {};
|
|
160
|
+
const overrides = groupConfig && typeof groupConfig === 'object' ? groupConfig : {};
|
|
161
|
+
if (overrides.model && typeof overrides.model === 'string' && overrides.model.trim()) {
|
|
162
|
+
const model = overrides.model.trim();
|
|
163
|
+
base.model = model;
|
|
164
|
+
base.primaryModel = model;
|
|
165
|
+
}
|
|
166
|
+
return base;
|
|
167
|
+
}
|
|
@@ -53,6 +53,7 @@ import { seedDefaultGroup, DEFAULT_GROUP_ID } from './seed-default.js';
|
|
|
53
53
|
import { nextGroupId, validateVpId, isReservedVpId } from './ids.js';
|
|
54
54
|
import { scanVpLibrary, DEFAULT_VP_LIB_DIR } from '../vp/vp-store.js';
|
|
55
55
|
import { seedSummaryIfMissingSync, removeScopeDirSync } from '../memory/store-v2.js';
|
|
56
|
+
import { ensureGroupConfigFile, saveGroupConfig, loadGroupConfig } from './group-config.js';
|
|
56
57
|
|
|
57
58
|
/**
|
|
58
59
|
* Default memory root used when callers don't pass `options.memoryRoot`.
|
|
@@ -263,6 +264,20 @@ export function createGroupFromSpec(yeaftDir, spec, options = {}) {
|
|
|
263
264
|
handle.close();
|
|
264
265
|
if (normalizedWorkDir) registerGroupWorkDir(yeaftDir, id, normalizedWorkDir);
|
|
265
266
|
|
|
267
|
+
// Per-group config (v1: model only). We always create an empty
|
|
268
|
+
// config.json so the file's presence signals "owned by this group" and
|
|
269
|
+
// hand-editing tools can find a stub. Initial overrides from the
|
|
270
|
+
// wizard spec (currently just `config.model`) are persisted here so
|
|
271
|
+
// the engine cache picks them up on the very first turn.
|
|
272
|
+
try {
|
|
273
|
+
ensureGroupConfigFile(yeaftDir, id);
|
|
274
|
+
if (spec && spec.config && typeof spec.config === 'object') {
|
|
275
|
+
saveGroupConfig(yeaftDir, id, spec.config);
|
|
276
|
+
}
|
|
277
|
+
} catch (err) {
|
|
278
|
+
console.warn(`[group-crud] failed to seed config.json for ${id}:`, err?.message || err);
|
|
279
|
+
}
|
|
280
|
+
|
|
266
281
|
// Seed Layer-A resident summary so the first session has memory content
|
|
267
282
|
// even before Dream-v2 has run. No-op if a summary.md already exists.
|
|
268
283
|
// Best-effort: a memory-root permission failure must NOT break group create.
|
|
@@ -313,6 +328,22 @@ export function updateGroupAnnouncement(yeaftDir, groupId, text) {
|
|
|
313
328
|
return next;
|
|
314
329
|
}
|
|
315
330
|
|
|
331
|
+
/**
|
|
332
|
+
* (A.2.c) Update per-group config overrides (v1: just `model`).
|
|
333
|
+
* Returns the persisted config object so the caller can broadcast it.
|
|
334
|
+
*
|
|
335
|
+
* Throws GroupConfigError on validation failure (unknown key, bad type).
|
|
336
|
+
* Group must exist (we call requireGroup to assert).
|
|
337
|
+
*/
|
|
338
|
+
export function updateGroupConfig(yeaftDir, groupId, partial) {
|
|
339
|
+
const handle = requireGroup(yeaftDir, groupId);
|
|
340
|
+
try {
|
|
341
|
+
return saveGroupConfig(yeaftDir, groupId, partial || {});
|
|
342
|
+
} finally {
|
|
343
|
+
handle.close();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
316
347
|
/**
|
|
317
348
|
* (A.3) Archive — renames the dir to `.archived-<ts>-<id>`. Directory
|
|
318
349
|
* prefix `.` keeps `listGroups` from picking it up (readdirSync filter in
|
|
@@ -488,6 +519,11 @@ export function snapshotGroups(yeaftDir) {
|
|
|
488
519
|
const meta = existsSync(dir) ? loadGroupMeta(dir) : null;
|
|
489
520
|
if (meta) byId.set(meta.id, meta);
|
|
490
521
|
}
|
|
522
|
+
// Attach per-group config overrides (v1: just `model`). Frontend can
|
|
523
|
+
// render the effective model without re-querying.
|
|
524
|
+
for (const meta of byId.values()) {
|
|
525
|
+
meta.config = loadGroupConfig(yeaftDir, meta.id);
|
|
526
|
+
}
|
|
491
527
|
return Array.from(byId.values()).sort((a, b) => String(a.createdAt || '').localeCompare(String(b.createdAt || '')));
|
|
492
528
|
}
|
|
493
529
|
|
package/unify/groups/index.js
CHANGED
|
@@ -50,7 +50,16 @@ export {
|
|
|
50
50
|
removeMember,
|
|
51
51
|
setGroupDefaultVp,
|
|
52
52
|
snapshotGroups,
|
|
53
|
+
updateGroupConfig,
|
|
54
|
+
updateGroupAnnouncement,
|
|
53
55
|
} from './group-crud.js';
|
|
56
|
+
export {
|
|
57
|
+
loadGroupConfig,
|
|
58
|
+
saveGroupConfig,
|
|
59
|
+
resolveGroupConfig,
|
|
60
|
+
validateGroupConfig,
|
|
61
|
+
GroupConfigError,
|
|
62
|
+
} from './group-config.js';
|
|
54
63
|
export {
|
|
55
64
|
nextMsgId,
|
|
56
65
|
nextGroupId,
|
package/unify/web-bridge.js
CHANGED
|
@@ -45,6 +45,8 @@ import {
|
|
|
45
45
|
groupsRoot,
|
|
46
46
|
} from './groups/group-crud.js';
|
|
47
47
|
import { openGroup, loadGroupMeta } from './groups/group-store.js';
|
|
48
|
+
import { loadGroupConfig, resolveGroupConfig, GroupConfigError } from './groups/group-config.js';
|
|
49
|
+
import { updateGroupConfig } from './groups/group-crud.js';
|
|
48
50
|
import { createCoordinator } from './groups/coordinator.js';
|
|
49
51
|
import { seedDefaultGroup } from './groups/seed-default.js';
|
|
50
52
|
import {
|
|
@@ -760,10 +762,16 @@ function getOrCreateVpEngine(groupId, vpId, threadId = 'main') {
|
|
|
760
762
|
let eng = vpEngines.get(key);
|
|
761
763
|
if (eng) return eng;
|
|
762
764
|
if (!session) throw new Error('getOrCreateVpEngine: session not loaded');
|
|
765
|
+
// Per-group config overlay (v1: model only). Falls back to the
|
|
766
|
+
// session's user-level config when no override is set. The resolver
|
|
767
|
+
// never mutates session.config — it returns a new object.
|
|
768
|
+
const yeaftDir = ctx.CONFIG?.yeaftDir || session.yeaftDir;
|
|
769
|
+
const groupCfg = loadGroupConfig(yeaftDir, groupId);
|
|
770
|
+
const effectiveConfig = resolveGroupConfig(session.config, groupCfg);
|
|
763
771
|
eng = new Engine({
|
|
764
772
|
adapter: session.adapter,
|
|
765
773
|
trace: session.trace,
|
|
766
|
-
config:
|
|
774
|
+
config: effectiveConfig,
|
|
767
775
|
conversationStore: session.conversationStore,
|
|
768
776
|
memoryIndex: session.memoryIndex || null,
|
|
769
777
|
amsRegistry: session.amsRegistry,
|
|
@@ -1318,8 +1326,11 @@ function sendGroupRosterChanged(group) {
|
|
|
1318
1326
|
}
|
|
1319
1327
|
|
|
1320
1328
|
function groupErrorPayload(err) {
|
|
1329
|
+
let code = 'unknown';
|
|
1330
|
+
if (err instanceof GroupCrudError) code = err.code;
|
|
1331
|
+
else if (err instanceof GroupConfigError) code = err.code;
|
|
1321
1332
|
return {
|
|
1322
|
-
code
|
|
1333
|
+
code,
|
|
1323
1334
|
groupId: err && err.groupId,
|
|
1324
1335
|
message: err && err.message,
|
|
1325
1336
|
};
|
|
@@ -1404,6 +1415,36 @@ export function handleUnifyUpdateGroup(msg) {
|
|
|
1404
1415
|
}
|
|
1405
1416
|
}
|
|
1406
1417
|
|
|
1418
|
+
/**
|
|
1419
|
+
* Update per-group config overrides (v1: `model`). Cache invalidation:
|
|
1420
|
+
* drop every cached Engine whose key starts with `${groupId}::` so the
|
|
1421
|
+
* next turn picks up the new model. The group meta itself is untouched.
|
|
1422
|
+
*
|
|
1423
|
+
* Payload: { groupId, requestId, config: { model?: string|null } }
|
|
1424
|
+
* - `model: ''` or `null` clears the override (group falls back to user default).
|
|
1425
|
+
*/
|
|
1426
|
+
export function handleUnifyUpdateGroupConfig(msg) {
|
|
1427
|
+
const requestId = msg && msg.requestId;
|
|
1428
|
+
const groupId = msg && msg.groupId;
|
|
1429
|
+
const partial = (msg && msg.config && typeof msg.config === 'object') ? msg.config : null;
|
|
1430
|
+
try {
|
|
1431
|
+
if (!groupId) throw new GroupConfigError('missing_group_id', 'groupId required');
|
|
1432
|
+
if (!partial) throw new GroupConfigError('invalid_patch', 'config object required');
|
|
1433
|
+
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
1434
|
+
const savedConfig = updateGroupConfig(yeaftDir, groupId, partial);
|
|
1435
|
+
// Drop cached engines so the next VP turn rebuilds with the new model.
|
|
1436
|
+
const prefix = `${groupId}::`;
|
|
1437
|
+
for (const k of Array.from(vpEngines.keys())) {
|
|
1438
|
+
if (k.startsWith(prefix)) vpEngines.delete(k);
|
|
1439
|
+
}
|
|
1440
|
+
invalidateGroupContext(groupId);
|
|
1441
|
+
sendGroupCrudResult({ op: 'update_config', requestId, ok: true, groupId, config: savedConfig });
|
|
1442
|
+
sendGroupSnapshotBroadcast();
|
|
1443
|
+
} catch (err) {
|
|
1444
|
+
sendGroupCrudResult({ op: 'update_config', requestId, ok: false, error: groupErrorPayload(err) });
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1407
1448
|
export function handleUnifyArchiveGroup(msg) {
|
|
1408
1449
|
const requestId = msg && msg.requestId;
|
|
1409
1450
|
const groupId = msg && msg.groupId;
|