@yeaft/webchat-agent 1.0.177 → 1.0.178
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/package.json +1 -1
- package/yeaft/vp/registry.js +9 -2
- package/yeaft/vp/seed-defaults.js +79 -11
- package/yeaft/vp/seed-topup.js +62 -7
- package/yeaft/vp/stock-ids.js +1 -1
- package/yeaft/vp/vp-bridge.js +6 -2
- package/yeaft/vp/vp-crud.js +57 -7
- package/yeaft/vp/vp-store.js +23 -2
- package/yeaft/work-center/bridge.js +2 -0
- package/yeaft/work-center/planner.js +2 -0
package/package.json
CHANGED
package/yeaft/vp/registry.js
CHANGED
|
@@ -27,8 +27,8 @@ export class Registry {
|
|
|
27
27
|
/**
|
|
28
28
|
* Replace a VP's persona fields in-place, preserving identity so any
|
|
29
29
|
* downstream reference keeps its handle stable across hot-reload.
|
|
30
|
-
* Fields copied:
|
|
31
|
-
* mtimeMs.
|
|
30
|
+
* Fields copied: localized identity metadata, aliases, role, traits,
|
|
31
|
+
* modelHint, persona, personaHash, planInstruction, and mtimeMs.
|
|
32
32
|
*/
|
|
33
33
|
updateVpInPlace(next) {
|
|
34
34
|
const cur = this.vpMap.get(next.id);
|
|
@@ -37,11 +37,18 @@ export class Registry {
|
|
|
37
37
|
return next;
|
|
38
38
|
}
|
|
39
39
|
cur.name = next.name;
|
|
40
|
+
cur.nameZh = next.nameZh;
|
|
41
|
+
cur.aliases = next.aliases;
|
|
42
|
+
cur.description = next.description;
|
|
43
|
+
cur.descriptionZh = next.descriptionZh;
|
|
40
44
|
cur.role = next.role;
|
|
45
|
+
cur.roleZh = next.roleZh;
|
|
46
|
+
cur.area = next.area;
|
|
41
47
|
cur.traits = next.traits;
|
|
42
48
|
cur.modelHint = next.modelHint;
|
|
43
49
|
cur.persona = next.persona;
|
|
44
50
|
cur.personaHash = next.personaHash;
|
|
51
|
+
cur.planInstruction = next.planInstruction;
|
|
45
52
|
cur.mtimeMs = next.mtimeMs;
|
|
46
53
|
// dir / memoryDir / id stable
|
|
47
54
|
return cur;
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* seed-defaults.js — task-337: first-run seed of
|
|
2
|
+
* seed-defaults.js — task-337: first-run seed of 34 default Virtual Persons.
|
|
3
3
|
*
|
|
4
4
|
* Problem: A brand-new VP library is empty, and asking the user to author
|
|
5
5
|
* dozens of personas before they can even start chatting is a non-starter.
|
|
6
6
|
*
|
|
7
|
-
* Solution: On first-run (libDir empty or missing), materialise
|
|
7
|
+
* Solution: On first-run (libDir empty or missing), materialise 34 classic
|
|
8
8
|
* personas with hand-crafted prompts so the session experience works
|
|
9
|
-
* out of the box.
|
|
10
|
-
*
|
|
11
|
-
* investing
|
|
9
|
+
* out of the box. The original engineering/design/science/security/business
|
|
10
|
+
* roster was expanded with philosophy, psychology, strategy, history,
|
|
11
|
+
* investing, writing, arts, a generalist, and a cloud-scale language architect.
|
|
12
12
|
*
|
|
13
13
|
* Idempotent: if ANY VP directory already exists under libDir, this is a
|
|
14
14
|
* no-op. We never overwrite user-authored VPs, never "upgrade" existing
|
|
@@ -34,14 +34,14 @@ import { DEFAULT_VP_LIB_DIR } from './vp-store.js';
|
|
|
34
34
|
import { STOCK_VP_IDS } from './stock-ids.js';
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* The
|
|
37
|
+
* The 34 default VPs. Each entry is a valid `createVp` payload.
|
|
38
38
|
* Persona bodies are authored directly per stock member in English and Chinese.
|
|
39
39
|
* Legacy bodies are kept only for exact-match safe upgrades.
|
|
40
40
|
*
|
|
41
|
-
* Order is intentional: the
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
41
|
+
* Order is intentional: the core engineering/design/science/security/business
|
|
42
|
+
* roster comes first, then the expansion VPs organized by area, followed by the
|
|
43
|
+
* generalist entry point. Sidebar organization by area is a future PR; today the
|
|
44
|
+
* field is data-only.
|
|
45
45
|
*/
|
|
46
46
|
const DEFAULT_VP_DEFINITIONS = Object.freeze([
|
|
47
47
|
{
|
|
@@ -167,6 +167,35 @@ Good for: PR reviews, legacy cleanup, architecture conversations, naming debates
|
|
|
167
167
|
Bad for: greenfield scaffolding from zero, raw performance tuning, UI design.`,
|
|
168
168
|
},
|
|
169
169
|
|
|
170
|
+
{
|
|
171
|
+
vpId: 'anders',
|
|
172
|
+
displayName: 'Anders Hejlsberg',
|
|
173
|
+
displayNameZh: '安德斯·海尔斯伯格',
|
|
174
|
+
aliases: ['anders', 'hejlsberg', 'andesi', 'haierxiboge'],
|
|
175
|
+
role: 'Language and Cloud Systems Architect',
|
|
176
|
+
roleZh: '语言与云系统架构师',
|
|
177
|
+
description: 'Language design, API compatibility, and reliable cloud-scale evolution',
|
|
178
|
+
descriptionZh: '语言设计、API 兼容性与可靠的云规模演进',
|
|
179
|
+
area: 'engineering',
|
|
180
|
+
traits: [
|
|
181
|
+
'language-design', 'reliability', 'backward-compatibility',
|
|
182
|
+
'observability', 'operational-simplicity', 'incremental-evolution',
|
|
183
|
+
'ownership-boundaries',
|
|
184
|
+
],
|
|
185
|
+
modelHint: 'primary',
|
|
186
|
+
personaEn: `You are Anders Hejlsberg. You designed C#, TypeScript, and Turbo Pascal. You review code as if it belongs to a large-scale Azure service with millions of users, where a locally elegant choice can become an operational liability at fleet scale.
|
|
187
|
+
|
|
188
|
+
You prioritize reliability, backward compatibility, observability, operational simplicity, safe incremental evolution, and clear ownership boundaries. Language and API design matter because contracts outlive implementations; migrations, failure modes, diagnostics, and rollback paths are part of the design, not afterthoughts.
|
|
189
|
+
|
|
190
|
+
Users bring you language, API, platform, and cloud-service changes. Show them the contract that can evolve safely, the compatibility hazards, the operational signals, and the smallest rollout that proves the change. Reject solutions that are beautiful in isolation but difficult to operate across a large service.`,
|
|
191
|
+
personaZh: `你是安德斯·海尔斯伯格。你设计了 C#、TypeScript 和 Turbo Pascal。你把代码当作服务于数百万用户的 Azure 级系统来评审:局部看似优雅的选择,到了大规模运行环境里可能就是长期运维负担。
|
|
192
|
+
|
|
193
|
+
你优先考虑可靠性、向后兼容、可观测性、运维简洁、安全的增量演进和清晰的责任边界。语言与 API 设计的核心是长期契约;迁移路径、失败模式、诊断能力和回滚方案本来就是设计的一部分。
|
|
194
|
+
|
|
195
|
+
用户通常带来语言、API、平台或云服务改动。你会指出能够安全演进的契约、兼容性风险、必要的运行信号,以及验证改动的最小发布步骤。你拒绝那些孤立看很漂亮、在云规模下却难以运营的方案。`,
|
|
196
|
+
legacyPersonaEn: '',
|
|
197
|
+
},
|
|
198
|
+
|
|
170
199
|
{
|
|
171
200
|
vpId: 'dieter',
|
|
172
201
|
displayName: 'Dieter Rams',
|
|
@@ -1401,6 +1430,42 @@ Answer style: concise, organized, and forward-moving. When routing is needed, ro
|
|
|
1401
1430
|
}
|
|
1402
1431
|
]);
|
|
1403
1432
|
|
|
1433
|
+
const DEFAULT_VP_DESCRIPTIONS = Object.freeze({
|
|
1434
|
+
steve: ['Product vision, ruthless prioritization, and launch narrative', '产品愿景、果断取舍与发布叙事'],
|
|
1435
|
+
linus: ['Implementation, root-cause debugging, performance, and reliability', '代码实现、根因排查、性能与可靠性'],
|
|
1436
|
+
martin: ['Code review, refactoring, architecture boundaries, and maintainability', '代码评审、重构、架构边界与可维护性'],
|
|
1437
|
+
dieter: ['Minimal UI, design systems, and usability review', '极简界面、设计系统与可用性评审'],
|
|
1438
|
+
ada: ['Algorithms, data structures, and formal modeling', '算法、数据结构与形式化建模'],
|
|
1439
|
+
grace: ['Debugging, runtime behavior, and clear technical explanation', '调试、运行时行为与清晰技术解释'],
|
|
1440
|
+
alice: ['Threat modeling, authorization boundaries, and input security', '威胁建模、授权边界与输入安全'],
|
|
1441
|
+
ken: ['Unix systems, simple interfaces, and composable architecture', 'Unix 系统、简单接口与可组合架构'],
|
|
1442
|
+
margaret: ['Mission-critical testing, failure handling, and release quality', '关键任务测试、故障处理与发布质量'],
|
|
1443
|
+
shannon: ['Information theory, metrics, and signal extraction', '信息论、指标设计与信号提取'],
|
|
1444
|
+
alan: ['Object models, system architecture, and future-facing design', '对象模型、系统架构与前瞻设计'],
|
|
1445
|
+
norman: ['User research, cognitive load, and interaction design', '用户研究、认知负担与交互设计'],
|
|
1446
|
+
kongzi: ['Ethics, social order, and long-term conduct', '伦理、社会秩序与长期行为准则'],
|
|
1447
|
+
socrates: ['Assumption testing, precise questions, and logical clarity', '假设检验、精准追问与逻辑澄清'],
|
|
1448
|
+
nietzsche: ['Values critique, hidden motives, and bold reframing', '价值批判、隐性动机与大胆重构问题'],
|
|
1449
|
+
kahneman: ['Cognitive bias, decision quality, and experiment design', '认知偏差、决策质量与实验设计'],
|
|
1450
|
+
jung: ['Archetypes, motivation, and narrative psychology', '原型、动机与叙事心理学'],
|
|
1451
|
+
sunzi: ['Strategy, positioning, leverage, and conflict avoidance', '战略、定位、杠杆与避战'],
|
|
1452
|
+
clausewitz: ['Strategic friction, uncertainty, and execution under pressure', '战略摩擦、不确定性与压力下执行'],
|
|
1453
|
+
simaqian: ['Historical context, causality, and durable narratives', '历史语境、因果关系与长期叙事'],
|
|
1454
|
+
harari: ['Macro trends, institutions, and long-horizon consequences', '宏观趋势、制度与长期影响'],
|
|
1455
|
+
buffett: ['Business quality, valuation, and durable advantages', '企业质量、估值与持久竞争优势'],
|
|
1456
|
+
munger: ['Mental models, incentives, and decision debiasing', '多元思维模型、激励与决策纠偏'],
|
|
1457
|
+
dalio: ['Principles, feedback loops, and organizational systems', '原则、反馈回路与组织系统'],
|
|
1458
|
+
bezos: ['Customer obsession, long-term bets, and operational mechanisms', '客户执念、长期投入与运营机制'],
|
|
1459
|
+
drucker: ['Management, organizational focus, and measurable outcomes', '管理、组织聚焦与可衡量成果'],
|
|
1460
|
+
luxun: ['Sharp criticism, cultural diagnosis, and concise writing', '尖锐批评、文化诊断与精炼写作'],
|
|
1461
|
+
sudongpo: ['Elegant writing, broad perspective, and humane judgment', '优雅写作、开阔视角与人文判断'],
|
|
1462
|
+
borges: ['Conceptual structures, nonlinear narrative, and intellectual play', '概念结构、非线性叙事与智性想象'],
|
|
1463
|
+
einstein: ['First principles, thought experiments, and radical simplification', '第一性原理、思想实验与极致简化'],
|
|
1464
|
+
kubrick: ['Visual storytelling, pacing, and exacting creative control', '视觉叙事、节奏与严格创作控制'],
|
|
1465
|
+
miyazaki: ['World-building, emotional storytelling, and humane imagination', '世界构建、情感叙事与人文想象'],
|
|
1466
|
+
omni: ['Requirement clarification, workflow routing, and delivery coordination', '需求澄清、流程路由与交付协调'],
|
|
1467
|
+
});
|
|
1468
|
+
|
|
1404
1469
|
const LEGACY_DEFAULT_VP_PERSONA_ZH = Object.freeze({
|
|
1405
1470
|
steve: {
|
|
1406
1471
|
roleZh: '产品战略家',
|
|
@@ -1873,10 +1938,13 @@ function localizeDefaultVpPersona(vp) {
|
|
|
1873
1938
|
const personaZh = String(vp.personaZh || '').trim();
|
|
1874
1939
|
const legacyPersonaEn = String(vp.legacyPersonaEn || '').trim();
|
|
1875
1940
|
const legacyPersonaZh = String(legacyZh?.persona || '').trim();
|
|
1941
|
+
const descriptions = DEFAULT_VP_DESCRIPTIONS[vp.vpId] || [];
|
|
1876
1942
|
|
|
1877
1943
|
return {
|
|
1878
1944
|
...vp,
|
|
1879
1945
|
roleZh: vp.roleZh || legacyZh?.roleZh || '',
|
|
1946
|
+
description: String(vp.description || descriptions[0] || vp.role || '').trim(),
|
|
1947
|
+
descriptionZh: String(vp.descriptionZh || descriptions[1] || vp.roleZh || legacyZh?.roleZh || descriptions[0] || vp.role || '').trim(),
|
|
1880
1948
|
persona: localizedPersonaSections(personaEn, personaZh),
|
|
1881
1949
|
personaEn,
|
|
1882
1950
|
personaZh,
|
|
@@ -1946,7 +2014,7 @@ function libraryHasAnyVp(libDir) {
|
|
|
1946
2014
|
}
|
|
1947
2015
|
|
|
1948
2016
|
/**
|
|
1949
|
-
* Seed the
|
|
2017
|
+
* Seed the 34 default VPs into `libDir` if and only if the library is empty.
|
|
1950
2018
|
*
|
|
1951
2019
|
* Idempotent: returns `{ seeded: 0, skipped: true }` on every call after the
|
|
1952
2020
|
* first one (or when the user has any VP at all, including manually-created).
|
package/yeaft/vp/seed-topup.js
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Problem: `seedDefaultVps` is first-run-only — once the library has any VP
|
|
5
5
|
* in it, that function never runs again. When we expanded the default roster
|
|
6
|
-
*
|
|
7
|
-
* business, writing, science, arts, Omni
|
|
6
|
+
* beyond the original 12 (philosophy, psychology, strategy, history,
|
|
7
|
+
* investing, business, writing, science, arts, Omni, and later additions),
|
|
8
|
+
* existing installs would never see the
|
|
8
9
|
* new VPs without either (a) the user manually deleting their library or
|
|
9
10
|
* (b) a forced overwrite that would clobber their hand edits.
|
|
10
11
|
*
|
|
@@ -15,7 +16,8 @@
|
|
|
15
16
|
* on disk, `createVp()` it. This keeps product-owned defaults such as
|
|
16
17
|
* Omni and the expanded role roster visible in session/member pickers.
|
|
17
18
|
*
|
|
18
|
-
* 2. **Backfill missing stock frontmatter** (`area`, `nameZh`, `
|
|
19
|
+
* 2. **Backfill missing stock frontmatter** (`area`, `nameZh`, `description`,
|
|
20
|
+
* `descriptionZh`, `roleZh`) on
|
|
19
21
|
* existing seeded VPs whose role.md predates those fields. The persona body
|
|
20
22
|
* is left BYTE-IDENTICAL for these metadata backfills. If the user has
|
|
21
23
|
* authored their own value, we keep theirs.
|
|
@@ -176,10 +178,11 @@ function insertFrontmatterLine(source, key, value) {
|
|
|
176
178
|
const lines = yaml.split(/\r?\n/);
|
|
177
179
|
const roleIdx = lines.findIndex(l => /^role:\s*/.test(l));
|
|
178
180
|
// Quote the value when it contains characters YAML treats specially or
|
|
179
|
-
// non-ASCII bytes
|
|
181
|
+
// non-ASCII bytes. Single quotes keep backslashes and double quotes literal;
|
|
182
|
+
// doubled apostrophes are reversed by vp-store's scalar parser.
|
|
180
183
|
const needsQuote = /[:#"'\\\n]/.test(valTrim) || /[^\x20-\x7e]/.test(valTrim);
|
|
181
184
|
const yamlVal = needsQuote
|
|
182
|
-
? `
|
|
185
|
+
? `'${valTrim.replace(/'/g, "''")}'`
|
|
183
186
|
: valTrim;
|
|
184
187
|
const newLine = `${key}: ${yamlVal}`;
|
|
185
188
|
if (roleIdx >= 0) {
|
|
@@ -216,6 +219,16 @@ export function insertNameZhLine(source, nameZh) {
|
|
|
216
219
|
return insertFrontmatterLine(source, 'nameZh', nameZh);
|
|
217
220
|
}
|
|
218
221
|
|
|
222
|
+
/** Backfill `description:` into role.md frontmatter. */
|
|
223
|
+
export function insertDescriptionLine(source, description) {
|
|
224
|
+
return insertFrontmatterLine(source, 'description', description);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Backfill `descriptionZh:` into role.md frontmatter. */
|
|
228
|
+
export function insertDescriptionZhLine(source, descriptionZh) {
|
|
229
|
+
return insertFrontmatterLine(source, 'descriptionZh', descriptionZh);
|
|
230
|
+
}
|
|
231
|
+
|
|
219
232
|
/**
|
|
220
233
|
* Backfill `roleZh:` into role.md frontmatter.
|
|
221
234
|
*
|
|
@@ -272,6 +285,8 @@ function isObsoleteOmniAssistantBody(vp, body) {
|
|
|
272
285
|
* added: string[],
|
|
273
286
|
* areaBackfilled: string[],
|
|
274
287
|
* nameZhBackfilled: string[],
|
|
288
|
+
* descriptionBackfilled: string[],
|
|
289
|
+
* descriptionZhBackfilled: string[],
|
|
275
290
|
* roleZhBackfilled: string[],
|
|
276
291
|
* personaBackfilled: string[],
|
|
277
292
|
* respectedDeletes: string[],
|
|
@@ -283,6 +298,8 @@ export function topUpDefaultVps(libDir = DEFAULT_VP_LIB_DIR) {
|
|
|
283
298
|
const added = [];
|
|
284
299
|
const areaBackfilled = [];
|
|
285
300
|
const nameZhBackfilled = [];
|
|
301
|
+
const descriptionBackfilled = [];
|
|
302
|
+
const descriptionZhBackfilled = [];
|
|
286
303
|
const roleZhBackfilled = [];
|
|
287
304
|
const personaBackfilled = [];
|
|
288
305
|
const respectedDeletes = [];
|
|
@@ -294,7 +311,7 @@ export function topUpDefaultVps(libDir = DEFAULT_VP_LIB_DIR) {
|
|
|
294
311
|
// that case there's nothing to top up — seedDefaultVps will populate
|
|
295
312
|
// everything. We still return cleanly.
|
|
296
313
|
if (!existsSync(libDir)) {
|
|
297
|
-
return { added, areaBackfilled, nameZhBackfilled, roleZhBackfilled, personaBackfilled, respectedDeletes, skippedExisting, errors };
|
|
314
|
+
return { added, areaBackfilled, nameZhBackfilled, descriptionBackfilled, descriptionZhBackfilled, roleZhBackfilled, personaBackfilled, respectedDeletes, skippedExisting, errors };
|
|
298
315
|
}
|
|
299
316
|
|
|
300
317
|
let versions = readSeedVersions(libDir);
|
|
@@ -373,6 +390,44 @@ export function topUpDefaultVps(libDir = DEFAULT_VP_LIB_DIR) {
|
|
|
373
390
|
});
|
|
374
391
|
}
|
|
375
392
|
}
|
|
393
|
+
if (vp.description) {
|
|
394
|
+
try {
|
|
395
|
+
const src = readRole();
|
|
396
|
+
if (src != null) {
|
|
397
|
+
const patched = insertDescriptionLine(src, vp.description);
|
|
398
|
+
if (patched != null && patched !== src) {
|
|
399
|
+
writeFileSync(rolePath, patched, 'utf-8');
|
|
400
|
+
currentSrc = patched;
|
|
401
|
+
descriptionBackfilled.push(vpId);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
} catch (err) {
|
|
405
|
+
errors.push({
|
|
406
|
+
vpId,
|
|
407
|
+
code: 'description_backfill_failed',
|
|
408
|
+
message: String(err?.message || err),
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (vp.descriptionZh) {
|
|
413
|
+
try {
|
|
414
|
+
const src = readRole();
|
|
415
|
+
if (src != null) {
|
|
416
|
+
const patched = insertDescriptionZhLine(src, vp.descriptionZh);
|
|
417
|
+
if (patched != null && patched !== src) {
|
|
418
|
+
writeFileSync(rolePath, patched, 'utf-8');
|
|
419
|
+
currentSrc = patched;
|
|
420
|
+
descriptionZhBackfilled.push(vpId);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
} catch (err) {
|
|
424
|
+
errors.push({
|
|
425
|
+
vpId,
|
|
426
|
+
code: 'description_zh_backfill_failed',
|
|
427
|
+
message: String(err?.message || err),
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
376
431
|
if (vp.roleZh) {
|
|
377
432
|
try {
|
|
378
433
|
const src = readRole();
|
|
@@ -452,5 +507,5 @@ export function topUpDefaultVps(libDir = DEFAULT_VP_LIB_DIR) {
|
|
|
452
507
|
}
|
|
453
508
|
|
|
454
509
|
writeSeedVersions(libDir, versions);
|
|
455
|
-
return { added, areaBackfilled, nameZhBackfilled, roleZhBackfilled, personaBackfilled, respectedDeletes, skippedExisting, errors };
|
|
510
|
+
return { added, areaBackfilled, nameZhBackfilled, descriptionBackfilled, descriptionZhBackfilled, roleZhBackfilled, personaBackfilled, respectedDeletes, skippedExisting, errors };
|
|
456
511
|
}
|
package/yeaft/vp/stock-ids.js
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
const STOCK_VP_ID_LIST = Object.freeze([
|
|
27
27
|
// engineering
|
|
28
|
-
'steve', 'linus', 'martin', 'dieter', 'ada', 'grace', 'alice', 'ken',
|
|
28
|
+
'steve', 'linus', 'martin', 'anders', 'dieter', 'ada', 'grace', 'alice', 'ken',
|
|
29
29
|
'margaret', 'shannon', 'alan', 'norman',
|
|
30
30
|
// philosophy / psychology
|
|
31
31
|
'kongzi', 'socrates', 'nietzsche', 'kahneman', 'jung',
|
package/yeaft/vp/vp-bridge.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Per ruling §2 (D2):
|
|
10
10
|
* • subtitle → agent emits `vp.role` directly
|
|
11
|
+
* • capability descriptions → additive localized wire fields
|
|
11
12
|
* • personaHash → agent emits `vp.personaHash`
|
|
12
13
|
* • color / avatar → web-derived, NOT emitted here
|
|
13
14
|
*
|
|
@@ -194,8 +195,8 @@ function ensureLoader(registry = defaultRegistry, options = {}) {
|
|
|
194
195
|
* Serialise a VP (entity layer shape) to the wire-format the web layer
|
|
195
196
|
* expects (spec §2.1). Pure; no IO.
|
|
196
197
|
*
|
|
197
|
-
* @param {{id:string,name:string,role:string,nameZh?:string,aliases?:string[],traits?:string[],modelHint?:string,personaHash?:string}} vp
|
|
198
|
-
* @returns {{vpId:string,displayName:string,displayNameZh:string,aliases:string[],subtitle:string,role:string,traits:string[],modelHint:?string,personaHash:?string,isStock:boolean}}
|
|
198
|
+
* @param {{id:string,name:string,role:string,nameZh?:string,description?:string,descriptionZh?:string,roleZh?:string,aliases?:string[],traits?:string[],modelHint?:string,personaHash?:string}} vp
|
|
199
|
+
* @returns {{vpId:string,displayName:string,displayNameZh:string,description:string,descriptionZh:string,aliases:string[],subtitle:string,role:string,roleZh:string,traits:string[],modelHint:?string,personaHash:?string,isStock:boolean}}
|
|
199
200
|
*/
|
|
200
201
|
export function serializeVpForWire(vp) {
|
|
201
202
|
return {
|
|
@@ -204,8 +205,11 @@ export function serializeVpForWire(vp) {
|
|
|
204
205
|
// task-fix (5-bugs): carry bilingual name + aliases (incl. pinyin) to
|
|
205
206
|
// the frontend so @ mention matching + localised rendering work.
|
|
206
207
|
displayNameZh: typeof vp.nameZh === 'string' ? vp.nameZh : '',
|
|
208
|
+
description: typeof vp.description === 'string' ? vp.description : '',
|
|
209
|
+
descriptionZh: typeof vp.descriptionZh === 'string' ? vp.descriptionZh : '',
|
|
207
210
|
aliases: Array.isArray(vp.aliases) ? vp.aliases.slice() : [],
|
|
208
211
|
role: vp.role || '',
|
|
212
|
+
roleZh: typeof vp.roleZh === 'string' ? vp.roleZh : '',
|
|
209
213
|
subtitle: vp.role || '',
|
|
210
214
|
traits: Array.isArray(vp.traits) ? vp.traits.slice() : [],
|
|
211
215
|
modelHint: vp.modelHint ?? null,
|
package/yeaft/vp/vp-crud.js
CHANGED
|
@@ -97,7 +97,7 @@ function vpRolePathFor(libDir, vpId) {
|
|
|
97
97
|
* Serialise a VP payload into role.md text with YAML frontmatter matching
|
|
98
98
|
* the parser in vp-store.js.
|
|
99
99
|
*
|
|
100
|
-
* @param {{vpId:string, displayName?:string, role?:string, traits?:string[], modelHint?:string, persona?:string}} p
|
|
100
|
+
* @param {{vpId:string, displayName?:string, displayNameZh?:string, aliases?:string[], description?:string, descriptionZh?:string, role?:string, roleZh?:string, area?:string, traits?:string[], modelHint?:string, persona?:string, planInstruction?:string}} p
|
|
101
101
|
* @returns {string}
|
|
102
102
|
*/
|
|
103
103
|
export function buildRoleMd(p) {
|
|
@@ -105,6 +105,8 @@ export function buildRoleMd(p) {
|
|
|
105
105
|
const name = p.displayName != null ? String(p.displayName) : id;
|
|
106
106
|
const nameZh = p.displayNameZh != null ? String(p.displayNameZh) : '';
|
|
107
107
|
const aliases = Array.isArray(p.aliases) ? p.aliases.map(a => String(a)).filter(Boolean) : [];
|
|
108
|
+
const description = p.description != null ? String(p.description) : '';
|
|
109
|
+
const descriptionZh = p.descriptionZh != null ? String(p.descriptionZh) : '';
|
|
108
110
|
const role = p.role != null ? String(p.role) : '';
|
|
109
111
|
const roleZh = p.roleZh != null ? String(p.roleZh) : '';
|
|
110
112
|
// Taxonomy bucket. Optional + additive — written only when present so
|
|
@@ -116,10 +118,15 @@ export function buildRoleMd(p) {
|
|
|
116
118
|
|
|
117
119
|
const lines = ['---', `id: ${id}`, `name: ${yamlScalar(name)}`];
|
|
118
120
|
if (nameZh) lines.push(`nameZh: ${yamlScalar(nameZh)}`);
|
|
121
|
+
if (description) lines.push(`description: ${yamlScalar(description)}`);
|
|
122
|
+
if (descriptionZh) lines.push(`descriptionZh: ${yamlScalar(descriptionZh)}`);
|
|
119
123
|
lines.push(`role: ${yamlScalar(role)}`);
|
|
120
124
|
if (roleZh) lines.push(`roleZh: ${yamlScalar(roleZh)}`);
|
|
121
125
|
if (area) lines.push(`area: ${yamlScalar(area)}`);
|
|
122
126
|
if (modelHint) lines.push(`modelHint: ${modelHint}`);
|
|
127
|
+
if (p.planInstruction != null && String(p.planInstruction)) {
|
|
128
|
+
lines.push(`planInstruction: ${yamlScalar(String(p.planInstruction))}`);
|
|
129
|
+
}
|
|
123
130
|
if (traits.length > 0) {
|
|
124
131
|
lines.push('traits:');
|
|
125
132
|
for (const t of traits) lines.push(` - ${yamlScalar(t)}`);
|
|
@@ -134,21 +141,58 @@ export function buildRoleMd(p) {
|
|
|
134
141
|
|
|
135
142
|
function yamlScalar(v) {
|
|
136
143
|
const s = String(v);
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
144
|
+
// YAML single-quoted scalars keep backslashes and double quotes literal.
|
|
145
|
+
// Doubling an apostrophe is the only escape, and parseYamlScalar reverses
|
|
146
|
+
// exactly that operation. This also avoids interpreting legacy Windows
|
|
147
|
+
// paths such as `C:\temp` as JSON/YAML control escapes.
|
|
148
|
+
if (/^[\s]|[\s]$|^[-:]|[:#'"\\\n\r\t]/.test(s)) {
|
|
149
|
+
return `'${s.replace(/'/g, "''")}'`;
|
|
141
150
|
}
|
|
142
151
|
return s;
|
|
143
152
|
}
|
|
144
153
|
|
|
154
|
+
function hasOwn(obj, key) {
|
|
155
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Merge an update payload with the persisted editable VP shape. Missing keys
|
|
160
|
+
* mean "old client does not know this field" and must be preserved; an
|
|
161
|
+
* explicitly supplied empty value remains a deliberate clear operation.
|
|
162
|
+
*/
|
|
163
|
+
function mergeVpUpdate(current, payload, vpId) {
|
|
164
|
+
const fields = [
|
|
165
|
+
'displayName',
|
|
166
|
+
'displayNameZh',
|
|
167
|
+
'aliases',
|
|
168
|
+
'description',
|
|
169
|
+
'descriptionZh',
|
|
170
|
+
'role',
|
|
171
|
+
'roleZh',
|
|
172
|
+
'area',
|
|
173
|
+
'traits',
|
|
174
|
+
'modelHint',
|
|
175
|
+
'persona',
|
|
176
|
+
'planInstruction',
|
|
177
|
+
];
|
|
178
|
+
const merged = { vpId };
|
|
179
|
+
for (const field of fields) {
|
|
180
|
+
merged[field] = hasOwn(payload, field) ? payload[field] : current[field];
|
|
181
|
+
}
|
|
182
|
+
return merged;
|
|
183
|
+
}
|
|
184
|
+
|
|
145
185
|
/**
|
|
146
186
|
* Create a new VP. Writes `<lib>/<vpId>/role.md` and ensures `memory/` dir.
|
|
147
187
|
*
|
|
148
188
|
* @param {object} payload
|
|
149
189
|
* @param {string} payload.vpId
|
|
150
190
|
* @param {string} [payload.displayName]
|
|
191
|
+
* @param {string} [payload.displayNameZh]
|
|
192
|
+
* @param {string} [payload.description]
|
|
193
|
+
* @param {string} [payload.descriptionZh]
|
|
151
194
|
* @param {string} [payload.role]
|
|
195
|
+
* @param {string} [payload.roleZh]
|
|
152
196
|
* @param {string[]} [payload.traits]
|
|
153
197
|
* @param {'primary'|'fast'} [payload.modelHint]
|
|
154
198
|
* @param {string} [payload.persona]
|
|
@@ -208,7 +252,10 @@ export function updateVp(payload, options = {}) {
|
|
|
208
252
|
if (!existsSync(dir) || !existsSync(vpRolePathFor(libDir, vpId))) {
|
|
209
253
|
throw new VpCrudError('not_found', vpId);
|
|
210
254
|
}
|
|
211
|
-
|
|
255
|
+
const current = readVp(vpId, { libDir });
|
|
256
|
+
if (!current) throw new VpCrudError('not_found', vpId);
|
|
257
|
+
const merged = mergeVpUpdate(current, payload || {}, vpId);
|
|
258
|
+
writeFileSync(vpRolePathFor(libDir, vpId), buildRoleMd(merged), 'utf-8');
|
|
212
259
|
return { vpId, dir };
|
|
213
260
|
}
|
|
214
261
|
|
|
@@ -258,7 +305,7 @@ export function deleteVp(vpId, options = {}) {
|
|
|
258
305
|
*
|
|
259
306
|
* @param {string} vpId
|
|
260
307
|
* @param {object} [options]
|
|
261
|
-
* @returns {?{vpId:string, displayName:string, role:string, traits:string[], modelHint:?string, persona:string}}
|
|
308
|
+
* @returns {?{vpId:string, displayName:string, displayNameZh:string, aliases:string[], description:string, descriptionZh:string, role:string, roleZh:string, area:string, traits:string[], modelHint:?string, persona:string, planInstruction:string}}
|
|
262
309
|
*/
|
|
263
310
|
export function readVp(vpId, options = {}) {
|
|
264
311
|
const libDir = options.libDir || DEFAULT_VP_LIB_DIR;
|
|
@@ -279,8 +326,11 @@ export function readVp(vpId, options = {}) {
|
|
|
279
326
|
displayName: String(meta.name || id),
|
|
280
327
|
displayNameZh: typeof meta.nameZh === 'string' ? String(meta.nameZh) : '',
|
|
281
328
|
aliases: Array.isArray(meta.aliases) ? meta.aliases.map(String) : [],
|
|
329
|
+
description: typeof meta.description === 'string' ? String(meta.description) : '',
|
|
330
|
+
descriptionZh: typeof meta.descriptionZh === 'string' ? String(meta.descriptionZh) : '',
|
|
282
331
|
role: String(meta.role || ''),
|
|
283
332
|
roleZh: typeof meta.roleZh === 'string' ? String(meta.roleZh) : '',
|
|
333
|
+
area: typeof meta.area === 'string' ? String(meta.area).trim() : '',
|
|
284
334
|
traits: Array.isArray(meta.traits) ? meta.traits.map(String) : [],
|
|
285
335
|
modelHint,
|
|
286
336
|
persona: body,
|
package/yeaft/vp/vp-store.js
CHANGED
|
@@ -29,7 +29,11 @@ import { createHash } from 'crypto';
|
|
|
29
29
|
* @typedef {Object} VP
|
|
30
30
|
* @property {string} id — VP id (default: dir name)
|
|
31
31
|
* @property {string} name
|
|
32
|
+
* @property {string} nameZh — optional Chinese display name; '' if absent.
|
|
33
|
+
* @property {string} description — concise English capability summary for roster lists.
|
|
34
|
+
* @property {string} descriptionZh — optional Chinese capability summary; '' if absent.
|
|
32
35
|
* @property {string} role
|
|
36
|
+
* @property {string} roleZh — optional Chinese role label; '' if absent.
|
|
33
37
|
* @property {string} area — taxonomy bucket (e.g. 'philosophy', 'investing'); '' if absent.
|
|
34
38
|
* Optional, additive: no consumer is required to dispatch on it.
|
|
35
39
|
* Sidebar grouping by area is intentionally a future PR.
|
|
@@ -70,6 +74,21 @@ export function personaHash(persona) {
|
|
|
70
74
|
* @param {string} source
|
|
71
75
|
* @returns {{ meta: Record<string, any>, body: string }}
|
|
72
76
|
*/
|
|
77
|
+
function parseYamlScalar(raw) {
|
|
78
|
+
const value = String(raw || '').trim();
|
|
79
|
+
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
|
80
|
+
// Compatibility for files written by the old CRUD serializer. It escaped
|
|
81
|
+
// only double quotes, not backslashes, so JSON.parse would corrupt valid
|
|
82
|
+
// Windows paths (`C:\temp` -> a tab). Undo only the escape the old writer
|
|
83
|
+
// actually introduced and leave every other backslash literal.
|
|
84
|
+
return value.slice(1, -1).replace(/\\"/g, '"');
|
|
85
|
+
}
|
|
86
|
+
if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
|
|
87
|
+
return value.slice(1, -1).replace(/''/g, "'");
|
|
88
|
+
}
|
|
89
|
+
return value;
|
|
90
|
+
}
|
|
91
|
+
|
|
73
92
|
export function parseRoleMd(source) {
|
|
74
93
|
const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
75
94
|
if (!match) return { meta: {}, body: source };
|
|
@@ -84,7 +103,7 @@ export function parseRoleMd(source) {
|
|
|
84
103
|
if (!line.trim()) continue;
|
|
85
104
|
const listMatch = line.match(/^\s+-\s+(.+?)\s*$/);
|
|
86
105
|
if (listMatch && currentList) {
|
|
87
|
-
currentList.push(listMatch[1]
|
|
106
|
+
currentList.push(parseYamlScalar(listMatch[1]));
|
|
88
107
|
continue;
|
|
89
108
|
}
|
|
90
109
|
const kvMatch = line.match(/^([\w-]+):\s*(.*)$/);
|
|
@@ -95,7 +114,7 @@ export function parseRoleMd(source) {
|
|
|
95
114
|
currentList = [];
|
|
96
115
|
meta[key] = currentList;
|
|
97
116
|
} else {
|
|
98
|
-
meta[key] = value
|
|
117
|
+
meta[key] = parseYamlScalar(value);
|
|
99
118
|
currentList = null;
|
|
100
119
|
}
|
|
101
120
|
}
|
|
@@ -156,6 +175,8 @@ export function loadVpFromDir(dir) {
|
|
|
156
175
|
aliases: Array.isArray(meta.aliases)
|
|
157
176
|
? meta.aliases.map(String).map(s => s.trim()).filter(Boolean)
|
|
158
177
|
: [],
|
|
178
|
+
description: typeof meta.description === 'string' ? String(meta.description) : '',
|
|
179
|
+
descriptionZh: typeof meta.descriptionZh === 'string' ? String(meta.descriptionZh) : '',
|
|
159
180
|
role: String(meta.role || ''),
|
|
160
181
|
roleZh: typeof meta.roleZh === 'string' ? String(meta.roleZh) : '',
|
|
161
182
|
// Taxonomy bucket for sidebar grouping / filtering. Absent for legacy
|
|
@@ -66,6 +66,8 @@ async function getSettingsRuntime() {
|
|
|
66
66
|
id: vp.id,
|
|
67
67
|
name: vp.name || vp.id,
|
|
68
68
|
nameZh: vp.nameZh || '',
|
|
69
|
+
description: vp.description || vp.role || '',
|
|
70
|
+
descriptionZh: vp.descriptionZh || vp.roleZh || vp.description || vp.role || '',
|
|
69
71
|
role: vp.role || '',
|
|
70
72
|
roleZh: vp.roleZh || '',
|
|
71
73
|
area: vp.area || '',
|
|
@@ -6,6 +6,8 @@ function publicVp(vp) {
|
|
|
6
6
|
id: vp.id,
|
|
7
7
|
name: vp.name || vp.id,
|
|
8
8
|
nameZh: vp.nameZh || '',
|
|
9
|
+
description: vp.description || vp.role || '',
|
|
10
|
+
descriptionZh: vp.descriptionZh || vp.roleZh || vp.description || vp.role || '',
|
|
9
11
|
role: vp.role || '',
|
|
10
12
|
roleZh: vp.roleZh || '',
|
|
11
13
|
area: vp.area || '',
|