@yeaft/webchat-agent 0.1.766 → 0.1.768

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.766",
3
+ "version": "0.1.768",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -142,30 +142,38 @@ function listExistingVpIds(libDir) {
142
142
  }
143
143
 
144
144
  /**
145
- * Splice a single `area: <bucket>` line into an existing role.md's YAML
145
+ * Splice a single `<key>: <value>` line into an existing role.md's YAML
146
146
  * frontmatter, immediately after the `role:` line if one is present (else
147
147
  * before the closing `---`). All other bytes are preserved.
148
148
  *
149
- * Returns `null` if the file already has an `area:` line, has no
150
- * frontmatter, or any other parse anomaly — caller treats null as "don't
151
- * touch this file."
149
+ * Returns `null` if the file already has the key, has no frontmatter, or
150
+ * any other parse anomaly — caller treats null as "don't touch this file."
152
151
  *
153
152
  * @param {string} source
154
- * @param {string} bucket
153
+ * @param {string} key YAML key to insert (e.g. 'area', 'nameZh')
154
+ * @param {string} value Scalar value (will be YAML-escaped only if it
155
+ * contains characters that need quoting)
155
156
  * @returns {string|null}
156
157
  */
157
- export function insertAreaLine(source, bucket) {
158
- const bucketTrim = String(bucket || '').trim();
159
- if (!bucketTrim) return null;
158
+ function insertFrontmatterLine(source, key, value) {
159
+ const valTrim = String(value || '').trim();
160
+ if (!valTrim) return null;
160
161
  const fmMatch = source.match(/^(---\r?\n)([\s\S]*?)(\r?\n---\r?\n?)/);
161
162
  if (!fmMatch) return null;
162
163
  const [full, open, yaml, close] = fmMatch;
163
- if (/^area:\s*/m.test(yaml)) return null; // user already set area
164
- // Pick newline that yaml block uses.
164
+ // Anchored at start-of-line so `nameZh` is never mistaken for `name`.
165
+ const keyPattern = new RegExp(`^${key}:\\s*`, 'm');
166
+ if (keyPattern.test(yaml)) return null; // user already set this key
165
167
  const nl = yaml.includes('\r\n') ? '\r\n' : '\n';
166
168
  const lines = yaml.split(/\r?\n/);
167
169
  const roleIdx = lines.findIndex(l => /^role:\s*/.test(l));
168
- const newLine = `area: ${bucketTrim}`;
170
+ // Quote the value when it contains characters YAML treats specially or
171
+ // non-ASCII bytes — the existing vp-crud writer does the same dance.
172
+ const needsQuote = /[:#"'\\\n]/.test(valTrim) || /[^\x20-\x7e]/.test(valTrim);
173
+ const yamlVal = needsQuote
174
+ ? `"${valTrim.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`
175
+ : valTrim;
176
+ const newLine = `${key}: ${yamlVal}`;
169
177
  if (roleIdx >= 0) {
170
178
  lines.splice(roleIdx + 1, 0, newLine);
171
179
  } else {
@@ -176,6 +184,30 @@ export function insertAreaLine(source, bucket) {
176
184
  return open + newYaml + close + rest;
177
185
  }
178
186
 
187
+ /**
188
+ * Backward-compatible wrapper kept for tests that pin the `area` shape.
189
+ * Equivalent to `insertFrontmatterLine(source, 'area', bucket)`.
190
+ *
191
+ * @param {string} source
192
+ * @param {string} bucket
193
+ * @returns {string|null}
194
+ */
195
+ export function insertAreaLine(source, bucket) {
196
+ return insertFrontmatterLine(source, 'area', bucket);
197
+ }
198
+
199
+ /**
200
+ * v0.1.768 — backfill `nameZh:` into a role.md frontmatter that predates
201
+ * the bilingual seed. Same byte-preserving semantics as insertAreaLine.
202
+ *
203
+ * @param {string} source
204
+ * @param {string} nameZh
205
+ * @returns {string|null}
206
+ */
207
+ export function insertNameZhLine(source, nameZh) {
208
+ return insertFrontmatterLine(source, 'nameZh', nameZh);
209
+ }
210
+
179
211
  /**
180
212
  * Top-up the default VPs into an existing `libDir`.
181
213
  *
@@ -183,6 +215,7 @@ export function insertAreaLine(source, bucket) {
183
215
  * @returns {{
184
216
  * added: string[],
185
217
  * areaBackfilled: string[],
218
+ * nameZhBackfilled: string[],
186
219
  * respectedDeletes: string[],
187
220
  * skippedExisting: string[],
188
221
  * errors: Array<{vpId:string, code:string, message:string}>,
@@ -191,6 +224,7 @@ export function insertAreaLine(source, bucket) {
191
224
  export function topUpDefaultVps(libDir = DEFAULT_VP_LIB_DIR) {
192
225
  const added = [];
193
226
  const areaBackfilled = [];
227
+ const nameZhBackfilled = [];
194
228
  const respectedDeletes = [];
195
229
  const skippedExisting = [];
196
230
  /** @type {Array<{vpId:string,code:string,message:string}>} */
@@ -200,7 +234,7 @@ export function topUpDefaultVps(libDir = DEFAULT_VP_LIB_DIR) {
200
234
  // that case there's nothing to top up — seedDefaultVps will populate
201
235
  // everything. We still return cleanly.
202
236
  if (!existsSync(libDir)) {
203
- return { added, areaBackfilled, respectedDeletes, skippedExisting, errors };
237
+ return { added, areaBackfilled, nameZhBackfilled, respectedDeletes, skippedExisting, errors };
204
238
  }
205
239
 
206
240
  let versions = readSeedVersions(libDir);
@@ -222,16 +256,28 @@ export function topUpDefaultVps(libDir = DEFAULT_VP_LIB_DIR) {
222
256
  const inLedger = Object.prototype.hasOwnProperty.call(versions.seeded, vpId);
223
257
 
224
258
  if (onDisk) {
225
- // Already there — never overwrite. Possibly backfill `area`.
259
+ // Already there — never overwrite. Possibly backfill `area` /
260
+ // `nameZh`. Each backfill is independent: a role.md with `area`
261
+ // already set but no `nameZh` should still get `nameZh`.
226
262
  skippedExisting.push(vpId);
263
+ let currentSrc = null;
264
+ const rolePath = join(libDir, vpId, 'role.md');
265
+ const readRole = () => {
266
+ if (currentSrc == null) {
267
+ try { currentSrc = readFileSync(rolePath, 'utf-8'); } catch { currentSrc = null; }
268
+ }
269
+ return currentSrc;
270
+ };
227
271
  if (vp.area) {
228
272
  try {
229
- const rolePath = join(libDir, vpId, 'role.md');
230
- const src = readFileSync(rolePath, 'utf-8');
231
- const patched = insertAreaLine(src, vp.area);
232
- if (patched != null && patched !== src) {
233
- writeFileSync(rolePath, patched, 'utf-8');
234
- areaBackfilled.push(vpId);
273
+ const src = readRole();
274
+ if (src != null) {
275
+ const patched = insertAreaLine(src, vp.area);
276
+ if (patched != null && patched !== src) {
277
+ writeFileSync(rolePath, patched, 'utf-8');
278
+ currentSrc = patched;
279
+ areaBackfilled.push(vpId);
280
+ }
235
281
  }
236
282
  } catch (err) {
237
283
  errors.push({
@@ -241,6 +287,32 @@ export function topUpDefaultVps(libDir = DEFAULT_VP_LIB_DIR) {
241
287
  });
242
288
  }
243
289
  }
290
+ // v0.1.768 — bilingual top-up. Existing VPs created from a
291
+ // pre-bilingual seed lack `nameZh:` in their role.md, which means
292
+ // serializeVpForWire ships `displayNameZh: ''` and the web
293
+ // `vpLabel` falls through to English even under zh locale. Splice
294
+ // the canonical Chinese display name into frontmatter without
295
+ // touching anything else — the user's persona body stays
296
+ // byte-identical.
297
+ if (vp.displayNameZh) {
298
+ try {
299
+ const src = readRole();
300
+ if (src != null) {
301
+ const patched = insertNameZhLine(src, vp.displayNameZh);
302
+ if (patched != null && patched !== src) {
303
+ writeFileSync(rolePath, patched, 'utf-8');
304
+ currentSrc = patched;
305
+ nameZhBackfilled.push(vpId);
306
+ }
307
+ }
308
+ } catch (err) {
309
+ errors.push({
310
+ vpId,
311
+ code: 'name_zh_backfill_failed',
312
+ message: String(err?.message || err),
313
+ });
314
+ }
315
+ }
244
316
  // Make sure the ledger records it (e.g. user-authored VP whose id
245
317
  // collides with a default — we still want to skip future creates).
246
318
  if (!inLedger) versions.seeded[vpId] = 'legacy';
@@ -274,5 +346,5 @@ export function topUpDefaultVps(libDir = DEFAULT_VP_LIB_DIR) {
274
346
  }
275
347
 
276
348
  writeSeedVersions(libDir, versions);
277
- return { added, areaBackfilled, respectedDeletes, skippedExisting, errors };
349
+ return { added, areaBackfilled, nameZhBackfilled, respectedDeletes, skippedExisting, errors };
278
350
  }