@yeaft/webchat-agent 1.0.177 → 1.0.180
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/connection/message-router.js +7 -0
- package/context.js +1 -0
- package/index.js +5 -0
- package/package.json +1 -1
- package/yeaft/asset-outbox.js +124 -0
- package/yeaft/conversation/persist.js +14 -0
- package/yeaft/engine.js +11 -2
- package/yeaft/image-assets.js +91 -0
- package/yeaft/remote-image-download.js +199 -0
- package/yeaft/stop-hooks.js +12 -6
- package/yeaft/tools/grep.js +151 -30
- package/yeaft/tools/image-generation.js +14 -16
- 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/web-bridge.js +41 -5
- package/yeaft/work-center/bridge.js +2 -0
- package/yeaft/work-center/planner.js +2 -0
package/yeaft/tools/grep.js
CHANGED
|
@@ -8,12 +8,21 @@
|
|
|
8
8
|
import { defineTool } from './types.js';
|
|
9
9
|
import { spawn } from 'child_process';
|
|
10
10
|
import { readdir, readFile, stat } from 'fs/promises';
|
|
11
|
+
import { StringDecoder } from 'string_decoder';
|
|
11
12
|
import { existsSync } from 'fs';
|
|
12
13
|
import { resolve, join, relative, extname } from 'path';
|
|
13
14
|
|
|
14
15
|
/** Max output lines. */
|
|
15
16
|
const MAX_LINES = 250;
|
|
16
17
|
|
|
18
|
+
/** Hard cap before Grep output reaches history, debug events, or WebSocket. */
|
|
19
|
+
const MAX_OUTPUT_BYTES = 512 * 1024;
|
|
20
|
+
const OUTPUT_TRUNCATED_MARKER = '\n\n[Output truncated]';
|
|
21
|
+
const MAX_CAPTURE_BYTES = MAX_OUTPUT_BYTES - Buffer.byteLength(OUTPUT_TRUNCATED_MARKER, 'utf8');
|
|
22
|
+
|
|
23
|
+
/** Keep one pathological source line from consuming the whole output budget. */
|
|
24
|
+
const MAX_LINE_BYTES = 16 * 1024;
|
|
25
|
+
|
|
17
26
|
/** Binary extensions to skip. */
|
|
18
27
|
const BINARY_EXTS = new Set([
|
|
19
28
|
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.webp',
|
|
@@ -25,6 +34,77 @@ const BINARY_EXTS = new Set([
|
|
|
25
34
|
'.sqlite', '.db',
|
|
26
35
|
]);
|
|
27
36
|
|
|
37
|
+
const utf8Decoder = new TextDecoder('utf-8', { fatal: true });
|
|
38
|
+
|
|
39
|
+
function decodeTextFile(buffer) {
|
|
40
|
+
// Extension lists are only a fast path. Generated artifacts and renamed
|
|
41
|
+
// binaries commonly have no useful extension, especially on Windows.
|
|
42
|
+
if (buffer.includes(0)) return null;
|
|
43
|
+
try { return utf8Decoder.decode(buffer); } catch { return null; }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function truncateUtf8(text, maxBytes) {
|
|
47
|
+
if (maxBytes <= 0) return '';
|
|
48
|
+
const buffer = Buffer.from(text, 'utf8');
|
|
49
|
+
if (buffer.length <= maxBytes) return text;
|
|
50
|
+
return new StringDecoder('utf8').write(buffer.subarray(0, maxBytes));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function boundToolOutput(text) {
|
|
54
|
+
if (Buffer.byteLength(text, 'utf8') <= MAX_OUTPUT_BYTES) return text;
|
|
55
|
+
return truncateUtf8(text, MAX_CAPTURE_BYTES) + OUTPUT_TRUNCATED_MARKER;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatGrepError(message) {
|
|
59
|
+
const errorMessage = `Grep failed: ${message}`;
|
|
60
|
+
const serialized = JSON.stringify({ error: errorMessage });
|
|
61
|
+
if (Buffer.byteLength(serialized, 'utf8') <= MAX_OUTPUT_BYTES) return serialized;
|
|
62
|
+
|
|
63
|
+
const markerBytes = Buffer.byteLength(OUTPUT_TRUNCATED_MARKER, 'utf8');
|
|
64
|
+
let low = 0;
|
|
65
|
+
let high = Math.max(0, Buffer.byteLength(errorMessage, 'utf8') - markerBytes);
|
|
66
|
+
let result = JSON.stringify({ error: OUTPUT_TRUNCATED_MARKER });
|
|
67
|
+
|
|
68
|
+
while (low <= high) {
|
|
69
|
+
const mid = Math.floor((low + high) / 2);
|
|
70
|
+
const candidate = JSON.stringify({
|
|
71
|
+
error: truncateUtf8(errorMessage, mid) + OUTPUT_TRUNCATED_MARKER,
|
|
72
|
+
});
|
|
73
|
+
if (Buffer.byteLength(candidate, 'utf8') <= MAX_OUTPUT_BYTES) {
|
|
74
|
+
result = candidate;
|
|
75
|
+
low = mid + 1;
|
|
76
|
+
} else {
|
|
77
|
+
high = mid - 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function createOutputCollector(maxBytes = MAX_OUTPUT_BYTES) {
|
|
85
|
+
const parts = [];
|
|
86
|
+
const contentBytes = Math.max(0, maxBytes - Buffer.byteLength(OUTPUT_TRUNCATED_MARKER, 'utf8'));
|
|
87
|
+
let bytes = 0;
|
|
88
|
+
let truncated = false;
|
|
89
|
+
return {
|
|
90
|
+
add(value) {
|
|
91
|
+
if (truncated) return false;
|
|
92
|
+
const normalized = String(value).replace(/\r/g, '');
|
|
93
|
+
const line = truncateUtf8(normalized, MAX_LINE_BYTES);
|
|
94
|
+
const lineWasTruncated = Buffer.byteLength(normalized, 'utf8') > Buffer.byteLength(line, 'utf8');
|
|
95
|
+
const separator = parts.length > 0 ? '\n' : '';
|
|
96
|
+
const remaining = contentBytes - bytes - Buffer.byteLength(separator, 'utf8');
|
|
97
|
+
if (remaining <= 0) { truncated = true; return false; }
|
|
98
|
+
const bounded = truncateUtf8(line, remaining);
|
|
99
|
+
parts.push(separator + bounded);
|
|
100
|
+
bytes += Buffer.byteLength(separator + bounded, 'utf8');
|
|
101
|
+
if (lineWasTruncated || bounded !== line) truncated = true;
|
|
102
|
+
return !truncated;
|
|
103
|
+
},
|
|
104
|
+
toString() { return parts.join('') + (truncated ? OUTPUT_TRUNCATED_MARKER : ''); },
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
28
108
|
/**
|
|
29
109
|
* Check if ripgrep is available.
|
|
30
110
|
*/
|
|
@@ -39,7 +119,7 @@ function hasRipgrep() {
|
|
|
39
119
|
/**
|
|
40
120
|
* Run ripgrep and return results.
|
|
41
121
|
*/
|
|
42
|
-
function runRipgrep(pattern, searchPath, options) {
|
|
122
|
+
export function runRipgrep(pattern, searchPath, options, spawnProcess = spawn) {
|
|
43
123
|
return new Promise((resolve, reject) => {
|
|
44
124
|
const args = [
|
|
45
125
|
pattern,
|
|
@@ -60,44 +140,72 @@ function runRipgrep(pattern, searchPath, options) {
|
|
|
60
140
|
if (options.multiline) args.push('-U', '--multiline-dotall');
|
|
61
141
|
args.push('--max-count', String(options.maxResults || 500));
|
|
62
142
|
|
|
63
|
-
const proc =
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
143
|
+
const proc = spawnProcess('rg', args, { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
|
|
144
|
+
const stdoutChunks = [];
|
|
145
|
+
const stderrChunks = [];
|
|
146
|
+
let capturedBytes = 0;
|
|
147
|
+
let truncatedStream = null;
|
|
148
|
+
let settled = false;
|
|
149
|
+
|
|
150
|
+
function capture(streamName, chunk, chunks) {
|
|
151
|
+
if (truncatedStream) return;
|
|
152
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
153
|
+
const remaining = MAX_CAPTURE_BYTES - capturedBytes;
|
|
154
|
+
if (buffer.length > remaining) {
|
|
155
|
+
if (remaining > 0) chunks.push(buffer.subarray(0, remaining));
|
|
156
|
+
capturedBytes = MAX_CAPTURE_BYTES;
|
|
157
|
+
truncatedStream = streamName;
|
|
71
158
|
try { proc.kill(); } catch {}
|
|
159
|
+
return;
|
|
72
160
|
}
|
|
73
|
-
|
|
74
|
-
|
|
161
|
+
chunks.push(buffer);
|
|
162
|
+
capturedBytes += buffer.length;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function decodeCaptured(chunks, wasTruncated) {
|
|
166
|
+
// Buffer decoding normally expands each invalid byte to the three-byte
|
|
167
|
+
// U+FFFD replacement character. Use a one-byte replacement, then enforce
|
|
168
|
+
// the final encoded-byte boundary as a last line of defense.
|
|
169
|
+
const marker = wasTruncated ? OUTPUT_TRUNCATED_MARKER : '';
|
|
170
|
+
const maxTextBytes = MAX_OUTPUT_BYTES - Buffer.byteLength(marker, 'utf8');
|
|
171
|
+
const decoded = Buffer.concat(chunks).toString('utf8').replaceAll('\ufffd', '?').replace(/\r/g, '');
|
|
172
|
+
return truncateUtf8(decoded, maxTextBytes) + marker;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
proc.stdout.on('data', (chunk) => capture('stdout', chunk, stdoutChunks));
|
|
176
|
+
proc.stderr.on('data', (chunk) => capture('stderr', chunk, stderrChunks));
|
|
75
177
|
proc.on('close', (code) => {
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
178
|
+
if (settled) return;
|
|
179
|
+
settled = true;
|
|
180
|
+
const stdout = decodeCaptured(stdoutChunks, truncatedStream === 'stdout');
|
|
181
|
+
const stderr = decodeCaptured(stderrChunks, truncatedStream === 'stderr');
|
|
182
|
+
if (code === 0 || code === 1 || truncatedStream === 'stdout') resolve(stdout);
|
|
183
|
+
else reject(new Error(stderr || `rg exited with code ${code}`));
|
|
184
|
+
});
|
|
185
|
+
proc.on('error', (err) => {
|
|
186
|
+
if (settled) return;
|
|
187
|
+
settled = true;
|
|
188
|
+
reject(err);
|
|
81
189
|
});
|
|
82
|
-
proc.on('error', reject);
|
|
83
190
|
});
|
|
84
191
|
}
|
|
85
192
|
|
|
86
193
|
/**
|
|
87
194
|
* Fallback: Node.js grep implementation.
|
|
88
195
|
*/
|
|
89
|
-
async function nodeGrep(pattern, searchPath, options) {
|
|
196
|
+
export async function nodeGrep(pattern, searchPath, options) {
|
|
90
197
|
const regex = new RegExp(pattern, options.caseInsensitive ? 'gi' : 'g');
|
|
91
|
-
const
|
|
198
|
+
const output = createOutputCollector();
|
|
199
|
+
let resultCount = 0;
|
|
92
200
|
const SKIP = new Set(['node_modules', '.git', '__pycache__', '.next', 'dist', 'build', '.cache']);
|
|
93
201
|
|
|
94
202
|
async function searchDir(dir) {
|
|
95
|
-
if (
|
|
203
|
+
if (resultCount >= (options.maxResults || 500)) return;
|
|
96
204
|
let entries;
|
|
97
205
|
try { entries = await readdir(dir, { withFileTypes: true }); } catch { return; }
|
|
98
206
|
|
|
99
207
|
for (const entry of entries) {
|
|
100
|
-
if (
|
|
208
|
+
if (resultCount >= (options.maxResults || 500)) return;
|
|
101
209
|
const fullPath = join(dir, entry.name);
|
|
102
210
|
|
|
103
211
|
if (entry.isDirectory()) {
|
|
@@ -111,22 +219,32 @@ async function nodeGrep(pattern, searchPath, options) {
|
|
|
111
219
|
const fileStat = await stat(fullPath);
|
|
112
220
|
if (fileStat.size > 1024 * 1024) continue; // skip files > 1MB
|
|
113
221
|
|
|
114
|
-
const
|
|
222
|
+
const buffer = await readFile(fullPath);
|
|
223
|
+
const content = decodeTextFile(buffer);
|
|
224
|
+
if (content == null) continue;
|
|
115
225
|
const relPath = relative(searchPath, fullPath);
|
|
116
226
|
|
|
117
227
|
if (options.filesOnly) {
|
|
118
|
-
if (regex.test(content))
|
|
228
|
+
if (regex.test(content)) {
|
|
229
|
+
resultCount += 1;
|
|
230
|
+
if (!output.add(relPath)) return;
|
|
231
|
+
}
|
|
119
232
|
regex.lastIndex = 0;
|
|
120
233
|
} else if (options.count) {
|
|
121
234
|
const matches = content.match(regex);
|
|
122
|
-
if (matches)
|
|
235
|
+
if (matches) {
|
|
236
|
+
resultCount += 1;
|
|
237
|
+
if (!output.add(`${relPath}:${matches.length}`)) return;
|
|
238
|
+
}
|
|
123
239
|
} else {
|
|
124
240
|
const lines = content.split('\n');
|
|
125
241
|
for (let i = 0; i < lines.length; i++) {
|
|
126
242
|
if (regex.test(lines[i])) {
|
|
127
|
-
|
|
243
|
+
resultCount += 1;
|
|
244
|
+
if (!output.add(`${relPath}:${i + 1}:${lines[i]}`)) return;
|
|
128
245
|
}
|
|
129
246
|
regex.lastIndex = 0;
|
|
247
|
+
if (resultCount >= (options.maxResults || 500)) return;
|
|
130
248
|
}
|
|
131
249
|
}
|
|
132
250
|
} catch {
|
|
@@ -137,7 +255,7 @@ async function nodeGrep(pattern, searchPath, options) {
|
|
|
137
255
|
}
|
|
138
256
|
|
|
139
257
|
await searchDir(searchPath);
|
|
140
|
-
return
|
|
258
|
+
return output.toString();
|
|
141
259
|
}
|
|
142
260
|
|
|
143
261
|
export default defineTool({
|
|
@@ -302,15 +420,18 @@ Guidelines:
|
|
|
302
420
|
return '(no matches)';
|
|
303
421
|
}
|
|
304
422
|
|
|
305
|
-
// Limit output lines
|
|
423
|
+
// Limit output lines, then enforce the byte budget at the actual tool
|
|
424
|
+
// boundary so prefixes, JSON escaping, and result markers are included.
|
|
306
425
|
const lines = result.trim().split('\n');
|
|
307
426
|
if (lines.length > head_limit) {
|
|
308
|
-
return
|
|
427
|
+
return boundToolOutput(
|
|
428
|
+
lines.slice(0, head_limit).join('\n') + `\n\n... (${lines.length - head_limit} more results)`,
|
|
429
|
+
);
|
|
309
430
|
}
|
|
310
431
|
|
|
311
|
-
return result.trim();
|
|
432
|
+
return boundToolOutput(result.trim());
|
|
312
433
|
} catch (err) {
|
|
313
|
-
return
|
|
434
|
+
return formatGrepError(err.message);
|
|
314
435
|
}
|
|
315
436
|
},
|
|
316
437
|
});
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { defineTool } from './types.js';
|
|
8
|
+
import { downloadRemoteImage } from '../remote-image-download.js';
|
|
8
9
|
|
|
9
10
|
export default defineTool({
|
|
10
11
|
name: 'ImageGeneration',
|
|
@@ -83,29 +84,26 @@ Guidelines:
|
|
|
83
84
|
|
|
84
85
|
const data = await response.json();
|
|
85
86
|
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
if (!data.url) return JSON.stringify({ error: 'Image API returned no image URL' });
|
|
88
|
+
const { buffer, mimeType } = await downloadRemoteImage(data.url, {
|
|
89
|
+
signal: ctx?.signal,
|
|
90
|
+
...(ctx?.remoteImageDownload || {}),
|
|
91
|
+
});
|
|
92
|
+
let savedPath = null;
|
|
93
|
+
if (output_path) {
|
|
88
94
|
const { resolve: resolvePath } = await import('path');
|
|
89
95
|
const { writeFile } = await import('fs/promises');
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const buffer = Buffer.from(await imgResponse.arrayBuffer());
|
|
93
|
-
const absPath = resolvePath(ctx?.cwd || process.cwd(), output_path);
|
|
94
|
-
await writeFile(absPath, buffer);
|
|
95
|
-
|
|
96
|
-
return JSON.stringify({
|
|
97
|
-
success: true,
|
|
98
|
-
path: absPath,
|
|
99
|
-
size,
|
|
100
|
-
prompt: prompt.slice(0, 100),
|
|
101
|
-
});
|
|
96
|
+
savedPath = resolvePath(ctx?.cwd || process.cwd(), output_path);
|
|
97
|
+
await writeFile(savedPath, buffer);
|
|
102
98
|
}
|
|
103
|
-
|
|
104
99
|
return JSON.stringify({
|
|
105
100
|
success: true,
|
|
106
|
-
|
|
101
|
+
...(savedPath ? { path: savedPath } : {}),
|
|
107
102
|
size,
|
|
108
103
|
prompt: prompt.slice(0, 100),
|
|
104
|
+
image: `data:${mimeType};base64,${buffer.toString('base64')}`,
|
|
105
|
+
mimeType,
|
|
106
|
+
filename: savedPath ? savedPath.split(/[/\\]/).pop() : `generated-${Date.now()}`,
|
|
109
107
|
});
|
|
110
108
|
} catch (err) {
|
|
111
109
|
if (err.name === 'AbortError') return JSON.stringify({ error: 'Generation cancelled' });
|
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,
|