@yeaft/webchat-agent 0.1.1011 → 0.1.1014

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.1011",
3
+ "version": "0.1.1014",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -62,11 +62,29 @@ const API_VERSION = '2023-06-01';
62
62
  export class AnthropicAdapter extends LLMAdapter {
63
63
  #apiKey;
64
64
  #baseUrl;
65
+ #authHeaderMode;
65
66
 
66
- constructor({ apiKey, baseUrl = DEFAULT_BASE_URL }) {
67
+ /**
68
+ * @param {{ apiKey: string, baseUrl?: string, authHeaderMode?: 'x-api-key'|'bearer' }} config
69
+ */
70
+ constructor({ apiKey, baseUrl = DEFAULT_BASE_URL, authHeaderMode = 'x-api-key' }) {
67
71
  super({ apiKey, baseUrl });
68
72
  this.#apiKey = apiKey;
69
73
  this.#baseUrl = baseUrl;
74
+ this.#authHeaderMode = authHeaderMode === 'bearer' ? 'bearer' : 'x-api-key';
75
+ }
76
+
77
+ #headers() {
78
+ const headers = {
79
+ 'Content-Type': 'application/json',
80
+ 'anthropic-version': API_VERSION,
81
+ };
82
+ if (this.#authHeaderMode === 'bearer') {
83
+ headers.Authorization = `Bearer ${this.#apiKey}`;
84
+ } else {
85
+ headers['x-api-key'] = this.#apiKey;
86
+ }
87
+ return headers;
70
88
  }
71
89
 
72
90
  /**
@@ -159,23 +177,24 @@ export class AnthropicAdapter extends LLMAdapter {
159
177
  * @param {string} body
160
178
  */
161
179
  #classifyError(status, body) {
180
+ const authHint = `auth=${this.#authHeaderMode}`;
162
181
  if (status === 401 || status === 403) {
163
- return new LLMAuthError(`Anthropic auth error: ${body}`, status);
182
+ return new LLMAuthError(`Anthropic auth error (${authHint}): ${body}`, status);
164
183
  }
165
184
  if (status === 429) {
166
185
  const retryAfter = null; // Could parse retry-after header
167
- return new LLMRateLimitError(`Anthropic rate limit: ${body}`, status, retryAfter);
186
+ return new LLMRateLimitError(`Anthropic rate limit (${authHint}): ${body}`, status, retryAfter);
168
187
  }
169
188
  if (status === 529) {
170
- return new LLMRateLimitError(`Anthropic overloaded: ${body}`, status);
189
+ return new LLMRateLimitError(`Anthropic overloaded (${authHint}): ${body}`, status);
171
190
  }
172
191
  if (body.includes('prompt is too long') || body.includes('max_tokens')) {
173
- return new LLMContextError(`Anthropic context error: ${body}`);
192
+ return new LLMContextError(`Anthropic context error (${authHint}): ${body}`);
174
193
  }
175
194
  if (status >= 500) {
176
- return new LLMServerError(`Anthropic server error: ${body}`, status);
195
+ return new LLMServerError(`Anthropic server error (${authHint}): ${body}`, status);
177
196
  }
178
- return new Error(`Anthropic API error ${status}: ${body}`);
197
+ return new Error(`Anthropic API error ${status} (${authHint}): ${body}`);
179
198
  }
180
199
 
181
200
  /**
@@ -205,11 +224,7 @@ export class AnthropicAdapter extends LLMAdapter {
205
224
  if (translatedTools) body.tools = translatedTools;
206
225
 
207
226
  const url = `${this.#baseUrl}/v1/messages`;
208
- const headers = {
209
- 'Content-Type': 'application/json',
210
- 'x-api-key': this.#apiKey,
211
- 'anthropic-version': API_VERSION,
212
- };
227
+ const headers = this.#headers();
213
228
 
214
229
  // Expose the raw request (auth-redacted) for the debug panel. The body
215
230
  // is captured verbatim — never truncated — so "copy request" matches
@@ -456,11 +471,7 @@ export class AnthropicAdapter extends LLMAdapter {
456
471
 
457
472
  const response = await fetch(`${this.#baseUrl}/v1/messages`, {
458
473
  method: 'POST',
459
- headers: {
460
- 'Content-Type': 'application/json',
461
- 'x-api-key': this.#apiKey,
462
- 'anthropic-version': API_VERSION,
463
- },
474
+ headers: this.#headers(),
464
475
  body: JSON.stringify(body),
465
476
  signal,
466
477
  });
@@ -23,7 +23,12 @@
23
23
 
24
24
  import { LLMAdapter } from './adapter.js';
25
25
  import { getModelEffortOptions, getThinkingCapability, normalizeEffort, parseModelRef } from '../models.js';
26
- import { normalizeKnownProviderForRuntime } from './known-providers.js';
26
+ import {
27
+ GITHUB_COPILOT_BASE_URL,
28
+ GITHUB_COPILOT_CREDENTIAL_PROVIDER,
29
+ GITHUB_COPILOT_PROVIDER_NAME,
30
+ normalizeKnownProviderForRuntime,
31
+ } from './known-providers.js';
27
32
  import { pairSanitize } from '../pair-sanitize.js';
28
33
 
29
34
  /**
@@ -182,6 +187,31 @@ function sliceUnchanged(original, cleaned) {
182
187
  return true;
183
188
  }
184
189
 
190
+ function normalizedOrigin(url) {
191
+ try {
192
+ return new URL(url).origin;
193
+ } catch {
194
+ return '';
195
+ }
196
+ }
197
+
198
+ /**
199
+ * Anthropic native uses x-api-key. Copilot's Anthropic-compatible endpoint uses
200
+ * a bearer token, and dynamic credential providers generally hand back bearer
201
+ * tokens unless explicitly overridden.
202
+ *
203
+ * @param {object} provider
204
+ * @returns {'x-api-key'|'bearer'}
205
+ */
206
+ export function anthropicAuthHeaderModeForProvider(provider) {
207
+ const explicit = provider?.anthropicAuthHeaderMode || provider?.authHeaderMode;
208
+ if (explicit === 'bearer' || explicit === 'x-api-key') return explicit;
209
+ if (provider?.credentialProvider) return 'bearer';
210
+ if (provider?.name === GITHUB_COPILOT_PROVIDER_NAME) return 'bearer';
211
+ if (normalizedOrigin(provider?.baseUrl) === GITHUB_COPILOT_BASE_URL) return 'bearer';
212
+ return 'x-api-key';
213
+ }
214
+
185
215
  /**
186
216
  * AdapterRouter — Implements LLMAdapter, routes by model → provider.
187
217
  */
@@ -393,7 +423,11 @@ export class AdapterRouter extends LLMAdapter {
393
423
  // header. For static providers the apiKey never changes so this stays
394
424
  // a one-time-build cache exactly like before.
395
425
  const apiKeyFp = apiKey ? this.#shortFingerprint(apiKey) : 'none';
396
- const cacheKey = `${provider.name}::${protocol}::${apiKeyFp}`;
426
+ const anthropicAuthHeaderMode = protocol === 'anthropic'
427
+ ? anthropicAuthHeaderModeForProvider(provider)
428
+ : null;
429
+ const authModeKey = anthropicAuthHeaderMode || 'default';
430
+ const cacheKey = `${provider.name}::${protocol}::${authModeKey}::${apiKeyFp}`;
397
431
  const cached = this.#adapterCache.get(cacheKey);
398
432
  if (cached) return { adapter: cached, modelId: entry.id };
399
433
 
@@ -403,7 +437,7 @@ export class AdapterRouter extends LLMAdapter {
403
437
  // (Copilot tokens rotate every ~30 min). Static-apiKey providers never
404
438
  // change fingerprint, so this loop never finds anything to evict for
405
439
  // them — back-compat preserved.
406
- const prefix = `${provider.name}::${protocol}::`;
440
+ const prefix = `${provider.name}::${protocol}::${authModeKey}::`;
407
441
  for (const key of this.#adapterCache.keys()) {
408
442
  if (key.startsWith(prefix)) this.#adapterCache.delete(key);
409
443
  }
@@ -415,6 +449,7 @@ export class AdapterRouter extends LLMAdapter {
415
449
  adapter = new AnthropicAdapter({
416
450
  apiKey,
417
451
  baseUrl: provider.baseUrl,
452
+ authHeaderMode: anthropicAuthHeaderMode,
418
453
  });
419
454
  } else if (protocol === 'openai-responses') {
420
455
  // OpenAI Responses API (/v1/responses) — canonical OpenAI-compatible path.
package/yeaft/prompts.js CHANGED
@@ -196,8 +196,12 @@ const PROMPTS = {
196
196
  date: (d) => `Date: ${d}`,
197
197
  dream: 'You are in dream mode. Reflect on past conversations and consolidate memories.',
198
198
  tools: (names) => `Available tools: ${names}`,
199
- // DESIGN-PROMPT §3 ④ — Active Scope header
200
- activeScopeHeader: '## active_scope',
199
+ // DESIGN-PROMPT §3 ④ — current session context block.
200
+ activeScopeHeader: '## Current session context',
201
+ activeScopeSessionIdLabel: 'Session ID',
202
+ activeScopeMembersLabel: 'Session members',
203
+ activeScopeTopicsLabel: 'Current focus',
204
+ activeScopeEnvelopeLabel: 'Handoff',
201
205
  multiVpRoutingHeader: '## multi_vp_routing',
202
206
  sessionAnnouncementHeader: '[Session Announcement]',
203
207
  // Project-doc (CLAUDE.md / AGENTS.md) header + one-liner intro. Both
@@ -212,8 +216,12 @@ const PROMPTS = {
212
216
  date: (d) => `日期:${d}`,
213
217
  dream: '你处于梦境模式。回顾过去的对话,整理和巩固记忆。',
214
218
  tools: (names) => `可用工具:${names}`,
215
- // DESIGN-PROMPT §3 ④ — Active Scope header
216
- activeScopeHeader: '## active_scope',
219
+ // DESIGN-PROMPT §3 ④ — 当前会话上下文。
220
+ activeScopeHeader: '## 当前会话上下文',
221
+ activeScopeSessionIdLabel: '会话 ID',
222
+ activeScopeMembersLabel: '会话成员',
223
+ activeScopeTopicsLabel: '当前讨论',
224
+ activeScopeEnvelopeLabel: '转交消息',
217
225
  multiVpRoutingHeader: '## multi_vp_routing',
218
226
  sessionAnnouncementHeader: '[会话公告]',
219
227
  // 项目文档块:CLAUDE.md / AGENTS.md(与 Codex 通用命名兼容)。
@@ -495,20 +503,22 @@ function selectVpPersonaBody(vpPersona, effectiveLang) {
495
503
  function renderActiveScope(activeScope, lang) {
496
504
  if (!activeScope || typeof activeScope !== 'object') return '';
497
505
 
506
+ const isZh = lang === PROMPTS.zh;
507
+ const separator = isZh ? ':' : ': ';
498
508
  const lines = [];
499
509
  const session = typeof activeScope.sessionId === 'string' && activeScope.sessionId.trim()
500
510
  ? activeScope.sessionId.trim()
501
511
  : '';
502
- if (session) lines.push(`session_id: ${session}`);
512
+ if (session) lines.push(`${lang.activeScopeSessionIdLabel}${separator}${session}`);
503
513
 
504
- const membersLine = renderSessionMembersLine(activeScope.sessionMembers || activeScope.members);
505
- if (membersLine) lines.push(`session_members: ${membersLine}`);
514
+ const membersLine = renderSessionMembersLine(activeScope.sessionMembers || activeScope.members, isZh);
515
+ if (membersLine) lines.push(`${lang.activeScopeMembersLabel}${separator}${membersLine}`);
506
516
 
507
- const topicsLine = renderSessionMembersLine(activeScope.sessionTopics);
508
- if (topicsLine) lines.push(`session_topics: ${topicsLine}`);
517
+ const topicsLine = renderSessionTopicsLine(activeScope.sessionTopics, isZh);
518
+ if (topicsLine) lines.push(`${lang.activeScopeTopicsLabel}${separator}${topicsLine}`);
509
519
 
510
- const envLine = renderEnvelopeLine(activeScope.envelope);
511
- if (envLine) lines.push(`envelope: ${envLine}`);
520
+ const envLine = renderEnvelopeLine(activeScope.envelope, isZh);
521
+ if (envLine) lines.push(`${lang.activeScopeEnvelopeLabel}${separator}${envLine}`);
512
522
 
513
523
  if (lines.length === 0) return '';
514
524
 
@@ -523,8 +533,149 @@ function firstNonEmptyString(...values) {
523
533
  return '';
524
534
  }
525
535
 
526
- function renderSessionMembersLine(members) {
527
- return normalizeSessionMemberIds(members).join(', ');
536
+ function renderSessionMembersLine(members, useChineseSeparator = false) {
537
+ return normalizeSessionMemberIds(members).join(useChineseSeparator ? '、' : ', ');
538
+ }
539
+
540
+ function renderSessionTopicsLine(topics, isZh = false) {
541
+ const descriptions = [];
542
+ const seen = new Set();
543
+ for (const topic of normalizeSessionTopicIds(topics)) {
544
+ const description = describeSessionTopic(topic, isZh);
545
+ if (!description || seen.has(description)) continue;
546
+ seen.add(description);
547
+ descriptions.push(description);
548
+ }
549
+ return descriptions.join(isZh ? ';' : '; ');
550
+ }
551
+
552
+ function normalizeSessionTopicIds(topics) {
553
+ if (!Array.isArray(topics)) return [];
554
+ const clean = [];
555
+ const seen = new Set();
556
+ for (const topic of topics) {
557
+ if (typeof topic !== 'string') continue;
558
+ const id = topic.trim();
559
+ if (!id || seen.has(id)) continue;
560
+ seen.add(id);
561
+ clean.push(id);
562
+ }
563
+ return clean;
564
+ }
565
+
566
+ function describeSessionTopic(topic, isZh = false) {
567
+ const normalized = topic.toLowerCase().replace(/[_\s]+/g, '-');
568
+
569
+ if (normalized.includes('dream') && normalized.includes('segments')) {
570
+ return isZh
571
+ ? '梦境记忆片段的抽取与整理'
572
+ : 'Dream memory segment extraction and organization';
573
+ }
574
+ if (normalized.includes('dream') && normalized.includes('session') && normalized.includes('extraction')) {
575
+ return isZh
576
+ ? '梦境会话记忆的抽取质量'
577
+ : 'Dream session-memory extraction quality';
578
+ }
579
+ if (normalized.includes('system-prompt') && normalized.includes('localization')) {
580
+ return isZh
581
+ ? '系统提示词的中英文一致性'
582
+ : 'system prompt language localization';
583
+ }
584
+ if (normalized.includes('default-character') && normalized.includes('prompt-soul')) {
585
+ return isZh
586
+ ? '默认角色灵魂提示词的表达方式'
587
+ : 'default persona soul prompt wording';
588
+ }
589
+ if (normalized.includes('prompt') && normalized.includes('soul')) {
590
+ return isZh
591
+ ? '角色灵魂提示词的表达方式'
592
+ : 'persona soul prompt wording';
593
+ }
594
+ if (normalized.includes('openai-responses')) {
595
+ return isZh
596
+ ? 'Yeaft 的 OpenAI Responses 适配与模型配置'
597
+ : 'Yeaft OpenAI Responses adapter and model configuration';
598
+ }
599
+ if (normalized.includes('model-config-isolation')) {
600
+ return isZh
601
+ ? 'Yeaft 的模型配置隔离'
602
+ : 'Yeaft model configuration isolation';
603
+ }
604
+ if (normalized.includes('route-forward') || normalized.includes('handoff')) {
605
+ return isZh
606
+ ? 'Yeaft 的会话路由交接与可见性'
607
+ : 'Yeaft session routing handoff and visibility';
608
+ }
609
+ if (normalized.includes('copilot-cli') || normalized.includes('chat-session')) {
610
+ return isZh
611
+ ? 'Copilot CLI 的聊天会话行为'
612
+ : 'Copilot CLI chat session behavior';
613
+ }
614
+ if (normalized.includes('claude-opus')) {
615
+ return isZh
616
+ ? 'Yeaft 的 Claude Opus 模型接入'
617
+ : 'Yeaft Claude Opus model integration';
618
+ }
619
+ if (normalized.includes('pr-workflow')) {
620
+ return isZh
621
+ ? '项目的 PR review、merge 和 tag 发布流程'
622
+ : 'project PR review, merge, and tag release workflow';
623
+ }
624
+ if (/\bpr[-/]?\d+\b/.test(normalized) || normalized.includes('release') || /v\d+\.\d+\.\d+/.test(normalized)) {
625
+ return isZh
626
+ ? '最近的 PR 修复、review、merge 和 tag 发布流程'
627
+ : 'recent PR fixes, review, merge, and tag release work';
628
+ }
629
+ if (normalized.includes('active-scope')) {
630
+ return isZh
631
+ ? '当前会话上下文的提示词呈现'
632
+ : 'current session context prompt rendering';
633
+ }
634
+ if (normalized.includes('prompt') || normalized.includes('system')) {
635
+ return isZh
636
+ ? '近期的系统提示词调整'
637
+ : humanizeTopicSlug(topic, false) || 'recent system prompt work';
638
+ }
639
+ if (normalized.includes('dream')) {
640
+ return isZh
641
+ ? '近期的 Dream 记忆维护工作'
642
+ : humanizeTopicSlug(topic, false) || 'recent Dream memory work';
643
+ }
644
+ if (normalized.includes('session')) {
645
+ return isZh
646
+ ? '近期的会话上下文调整'
647
+ : humanizeTopicSlug(topic, false) || 'recent session context work';
648
+ }
649
+
650
+ if (isZh) return '近期的项目协作事项';
651
+ return humanizeTopicSlug(topic, false) || 'recent session collaboration topics';
652
+ }
653
+
654
+ function humanizeTopicSlug(topic, isZh = false) {
655
+ if (isZh) return '';
656
+ const words = topic
657
+ .replace(/[\/_-]+/g, ' ')
658
+ .replace(/\bv\d+(?:\.\d+)+\b/gi, '')
659
+ .replace(/\bpr\s*\d+\b/gi, '')
660
+ .trim()
661
+ .split(/\s+/)
662
+ .filter(Boolean);
663
+ if (words.length === 0) return '';
664
+
665
+ const normalizedWords = words.map(word => {
666
+ const lower = word.toLowerCase();
667
+ if (lower === 'yeaft') return 'Yeaft';
668
+ if (lower === 'project') return 'project';
669
+ if (lower === 'config') return 'configuration';
670
+ if (lower === 'isolation') return 'isolation';
671
+ if (lower === 'rendering') return 'rendering';
672
+ if (lower === 'workflow') return 'workflow';
673
+ if (lower === 'session') return 'session';
674
+ if (lower === 'context') return 'context';
675
+ return word;
676
+ });
677
+
678
+ return normalizedWords.join(' ');
528
679
  }
529
680
 
530
681
  function normalizeSessionMemberIds(members) {
@@ -581,21 +732,21 @@ function renderMultiVpRouting(activeScope, lang) {
581
732
  * @param {object|null|undefined} envelope
582
733
  * @returns {string}
583
734
  */
584
- function renderEnvelopeLine(envelope) {
735
+ function renderEnvelopeLine(envelope, isZh = false) {
585
736
  if (!envelope || typeof envelope !== 'object') return '';
586
737
  const segments = [];
587
738
  const fromVp = typeof envelope.fromVpId === 'string' && envelope.fromVpId.trim()
588
739
  ? envelope.fromVpId.trim()
589
740
  : (typeof envelope.senderVpId === 'string' ? envelope.senderVpId.trim() : '');
590
- if (fromVp) segments.push(`from=${fromVp}`);
741
+ if (fromVp) segments.push(`${isZh ? '来自' : 'from'}=${fromVp}`);
591
742
  const fromUser = typeof envelope.fromUserId === 'string' && envelope.fromUserId.trim()
592
743
  ? envelope.fromUserId.trim()
593
744
  : '';
594
- if (fromUser) segments.push(`user=${fromUser}`);
745
+ if (fromUser) segments.push(`${isZh ? '用户' : 'user'}=${fromUser}`);
595
746
  const intent = typeof envelope.intent === 'string' && envelope.intent.trim()
596
747
  ? envelope.intent.trim()
597
748
  : '';
598
- if (intent) segments.push(`intent=${intent}`);
749
+ if (intent) segments.push(`${isZh ? '意图' : 'intent'}=${intent}`);
599
750
  return segments.join(' ');
600
751
  }
601
752
 
@@ -0,0 +1,237 @@
1
+ const zh = {
2
+ Skill: {
3
+ description: '加载、查看和搜索 Yeaft skill 库中的工作流说明。用于发现某类任务是否已有可复用 skill,或读取指定 skill 的完整内容;不要用它执行普通文件搜索。',
4
+ parameters: {
5
+ action: '操作类型:list 列出 skill 元数据,view/load 读取指定 skill,search 按查询词匹配相关 skill。',
6
+ name: '要读取的 skill 名称,仅在 view/load 时需要。',
7
+ query: '搜索 skill 时使用的查询词,描述你要解决的任务。',
8
+ filePath: '读取目录型 skill 的关联文件路径,例如 references/style-guide.md。',
9
+ category: 'list 时可选的分类过滤条件。',
10
+ },
11
+ },
12
+ EnterWorktree: {
13
+ description: '为代码开发创建隔离 git worktree 和专用分支。任何功能开发、修 bug、测试性改动都应先进入 worktree,避免污染 main checkout;不要用它做普通目录切换。',
14
+ parameters: {
15
+ name: 'worktree 名称,会用于目录名和分支名;应使用有语义的 feat-/fix- 前缀。',
16
+ base_ref: '新分支基于的 git ref;通常使用 HEAD 或 origin/main。',
17
+ },
18
+ },
19
+ ExitWorktree: {
20
+ description: '结束一个 git worktree 会话。用于开发完成后保留或删除隔离 worktree;删除有未提交改动的 worktree 前必须确认这些改动不再需要。',
21
+ parameters: {
22
+ path: '要退出的 worktree 路径。',
23
+ action: 'keep 表示保留目录和分支;remove 表示删除 worktree 目录并删除分支。',
24
+ discard_changes: 'remove 时是否丢弃未提交改动;只有确认改动已提交/合并或明确不需要时才设为 true。',
25
+ },
26
+ },
27
+ AskUser: {
28
+ description: '向用户提出一个真正阻塞继续推进的问题并等待回答。只在缺少关键信息会导致错误或不安全操作时使用;不要把它当作普通说明或反问。',
29
+ parameters: {
30
+ question: '要问用户的具体问题,应说明为什么需要这个信息。',
31
+ options: '可选答案列表;当用户只需从固定选项中选择时提供。',
32
+ },
33
+ },
34
+ WebSearch: {
35
+ description: '搜索 Web 获取最新信息。用于查当前文档、版本、新闻、API 变更等训练数据可能过期的内容;已知 URL 应直接用 WebFetch。',
36
+ parameters: {
37
+ query: '搜索查询词;对时效性问题应包含年份、产品名或版本号。',
38
+ limit: '最多返回多少条搜索结果。',
39
+ },
40
+ },
41
+ WebFetch: {
42
+ description: '抓取并读取指定 URL 的页面或 API 响应。用于阅读文档、文章、PR 页面或接口返回;不要用于本地文件,本地文件请用 FileRead。',
43
+ parameters: {
44
+ url: '完整 URL,必须包含 http:// 或 https://。',
45
+ max_length: '返回内容的最大字符数;页面很大时提高该值或分段读取。',
46
+ raw: '是否返回原始响应体;读取 API/JSON 时设为 true,普通网页通常设为 false。',
47
+ },
48
+ },
49
+ HistorySearch: {
50
+ description: '搜索已持久化的历史对话消息。用于找之前的决策、用户偏好、代码片段或上下文;不要用它搜索当前工作区文件。',
51
+ parameters: {
52
+ keyword: '大小写不敏感的搜索关键词。',
53
+ limit: '最多返回多少条结果。',
54
+ },
55
+ },
56
+ Bash: {
57
+ description: '在 shell 中执行非交互式命令。用于运行测试、git/gh 命令、脚本和确定性诊断;避免交互式程序,不要执行未经用户允许的破坏性命令。',
58
+ parameters: {
59
+ command: '要执行的 shell 命令;需要引用文件路径时正确加引号。',
60
+ cwd: '命令运行目录;代码改动和测试应在对应 worktree 中运行。',
61
+ timeout_ms: '超时时间毫秒;长测试或构建可适当提高但不能超过工具上限。',
62
+ },
63
+ },
64
+ FileRead: {
65
+ description: '读取文本文件并带行号返回内容。编辑前必须先读文件;已知路径时直接使用它,不要先用 shell cat。',
66
+ parameters: {
67
+ file_path: '要读取的文件路径,可为绝对路径或相对当前工作目录。',
68
+ offset: '从第几行开始读取,0 基;只有大文件或明确行段时才需要。',
69
+ limit: '最多读取多少行;普通文件默认整段读取即可。',
70
+ },
71
+ },
72
+ FileWrite: {
73
+ description: '写入完整文件内容,会创建父目录并覆盖已有文件。适合新建文件或完整重写;修改已有文件时优先用 FileEdit 做小范围替换。',
74
+ parameters: {
75
+ file_path: '要写入的文件路径。',
76
+ content: '完整文件内容,不是补丁或片段。',
77
+ },
78
+ },
79
+ FileEdit: {
80
+ description: '在已有文件中做精确文本替换。使用前必须读取文件;old_string 必须和文件内容完全一致,除非明确 replace_all,否则必须唯一。',
81
+ parameters: {
82
+ file_path: '要编辑的文件路径。',
83
+ old_string: '要查找并替换的精确文本,必须与文件内容完全一致,包含空格、缩进和换行。',
84
+ new_string: '替换后的文本。',
85
+ replace_all: '是否替换所有匹配项;默认只允许唯一匹配,避免误改。',
86
+ },
87
+ },
88
+ Glob: {
89
+ description: '按 glob 模式查找文件路径。用于只知道文件名模式或扩展名时定位文件;如果要搜内容请用 Grep。',
90
+ parameters: {
91
+ pattern: 'glob 模式,例如 **/*.js 或 src/**/*.ts。',
92
+ path: '搜索起始目录。',
93
+ limit: '最多返回多少个文件。',
94
+ },
95
+ },
96
+ Grep: {
97
+ description: '用正则搜索文件内容。用于快速定位符号、错误文本、调用点或配置项;应结合 path、glob 或 type 缩小范围。',
98
+ parameters: {
99
+ pattern: '要搜索的正则表达式;特殊字符需要转义。',
100
+ path: '要搜索的文件或目录。',
101
+ output_mode: '输出模式:content 显示匹配行,files_with_matches 只列文件,count 显示计数。',
102
+ glob: '文件名过滤 glob,例如 *.{js,css}。',
103
+ type: '文件类型过滤,例如 js、py、rust。',
104
+ case_insensitive: '是否忽略大小写。',
105
+ context: 'content 模式下每个匹配周围显示的上下文行数。',
106
+ before: 'content 模式下每个匹配前显示的行数。',
107
+ after: 'content 模式下每个匹配后显示的行数。',
108
+ multiline: '是否启用多行正则匹配。',
109
+ head_limit: '最多返回多少条匹配结果。',
110
+ },
111
+ },
112
+ ListDir: {
113
+ description: '列出目录内容和文件大小。用于了解目录结构;需要查找模式时用 Glob,需要搜内容时用 Grep。',
114
+ parameters: {
115
+ path: '要列出的目录路径。',
116
+ show_hidden: '是否显示以点开头的隐藏文件。',
117
+ },
118
+ },
119
+ ApplyPatch: {
120
+ description: '应用 unified diff 补丁到文件。适合一次修改多个位置或创建新文件;补丁必须和当前文件内容匹配,简单替换优先用 FileEdit。',
121
+ parameters: {
122
+ patch: '标准 unified diff 内容,包含 ---、+++ 和 @@ hunk。',
123
+ },
124
+ },
125
+ SpawnAgent: {
126
+ description: '启动一个后台子 Agent 处理独立任务。用于单 VP 场景下的并行调查、测试、评审或长任务;启动后不要阻塞等待,继续主任务,并用 ListAgents 非阻塞查看状态或等待通知回灌。',
127
+ parameters: {
128
+ name: '子 Agent 的简短名称,便于识别。',
129
+ task: '给子 Agent 的明确任务说明。',
130
+ mission: '任务目标、范围和成功标准。',
131
+ expected_output: '期望子 Agent 最终交付的结果格式。',
132
+ persona: '可选人格或工作风格说明。',
133
+ budget: '子 Agent 的资源预算。',
134
+ 'budget.max_tokens': '允许子 Agent 消耗的最大 token 数。',
135
+ 'budget.max_turns': '允许子 Agent 执行的最大 turn 数。',
136
+ 'budget.wall_time_ms': '最长运行时间毫秒;超时应被标记并终止,避免 running zombie。',
137
+ cwd: '子 Agent 的工作目录。',
138
+ },
139
+ },
140
+ PromptAgent: {
141
+ description: '向已存在的子 Agent 追加消息。仅用于继续或补充一个已启动的子 Agent;不要用它替代 SpawnAgent 创建新任务。',
142
+ parameters: {
143
+ agent_id: '目标子 Agent id。',
144
+ message: '发送给子 Agent 的消息。',
145
+ },
146
+ },
147
+ WaitAgent: {
148
+ description: '兼容用的短轮询工具,用于快速查看某个子 Agent 是否已有结果。不要把它当作主流程循环等待;需要状态时优先用 ListAgents,长任务应后台运行并等待通知。',
149
+ parameters: {
150
+ agent_id: '要检查的子 Agent id。',
151
+ timeout_ms: '最多等待多少毫秒;应保持很短,避免阻塞父 VP。',
152
+ },
153
+ },
154
+ CloseAgent: {
155
+ description: '关闭子 Agent 并可记录最终结果。用于用户要求停止、任务已被主流程接管、或子 Agent 不再需要时;不要关闭无关 Agent。',
156
+ parameters: {
157
+ agent_id: '要关闭的子 Agent id。',
158
+ result: '可选的最终结果或关闭原因。',
159
+ },
160
+ },
161
+ ListAgents: {
162
+ description: '非阻塞列出当前 VP 拥有的子 Agent 状态。用于查看后台任务是否 running、stale、completed 或 failed,以及读取结果摘要和输出文件路径。',
163
+ parameters: {
164
+ include_closed: '是否包含已关闭的子 Agent。',
165
+ include_terminal: '是否包含 completed、failed、abandoned 等终态子 Agent。',
166
+ },
167
+ },
168
+ RouteForward: {
169
+ description: '把当前任务或问题明确转交给同一 Session 中的其他 VP。多 VP 场景下,只要用户点名其他 VP、任务属于其他 VP 职责、需要并行协作、或你需要另一个 VP 继续处理,就必须调用这个工具;在聊天文本里写 @名字 不会真正路由。',
170
+ parameters: {
171
+ to: '目标 VP id,或使用 all 广播给其他成员;不能填自己。',
172
+ text: '要代表你转发给目标 VP 的完整任务内容,必须包含必要上下文和明确期望。',
173
+ reason: '可选的简短转发理由,用于审计和界面展示。',
174
+ },
175
+ },
176
+ TodoWrite: {
177
+ description: '维护用户可见的多步骤任务清单。任务包含 3 个以上有意义步骤、用户给了列表、或即将做复杂多文件改动时必须使用;不要为单个琐碎动作制造清单。',
178
+ parameters: {
179
+ todos: '完整的当前 todo 列表;每次调用都要发送全量列表,不是增量 diff。',
180
+ 'todos[].content': '命令式步骤描述,例如“运行测试”。',
181
+ 'todos[].status': '步骤状态;任意时刻最多只能有一个 in_progress。',
182
+ 'todos[].activeForm': '该步骤执行中展示的进行时文案,例如“正在运行测试”。',
183
+ },
184
+ },
185
+ StartPlan: {
186
+ description: '进入规划模式,为非琐碎任务先形成短计划再继续执行。用户要求计划/思考、任务多步骤、范围不清或大型改动前应使用;除非第一步被用户信息阻塞,否则计划后继续推进。',
187
+ parameters: {
188
+ topic: '一句话说明正在规划的主题。',
189
+ userProblem: '用户真正想解决的底层问题,可选。',
190
+ stuckAt: '当前阻塞点或必须先决策的未知,可选。',
191
+ expectedScale: '预估规模,例如文件数、代码量或时间范围,可选。',
192
+ additionalContext: '影响计划的其他事实、约束或背景,可选。',
193
+ },
194
+ },
195
+ JsRepl: {
196
+ description: '在持久 JavaScript REPL 中执行代码。用于计算、数据转换和快速实验;不能访问文件系统或网络,状态会在多次调用之间保留。',
197
+ parameters: {
198
+ code: '要执行的 JavaScript 代码;reset=true 且只清空状态时可省略。',
199
+ reset: '是否先重置 REPL 上下文再执行代码。',
200
+ },
201
+ },
202
+ JsReplReset: {
203
+ description: '已废弃的 JavaScript REPL 重置工具。新调用应使用 JsRepl 并设置 reset=true;仅为兼容旧调用保留。',
204
+ parameters: {},
205
+ },
206
+ NotebookEdit: {
207
+ description: '读取或编辑 Jupyter notebook 单元格。用于 .ipynb 文件的精确 cell 级修改;普通文本文件不要用它。',
208
+ parameters: {
209
+ notebook_path: '目标 .ipynb 文件路径。',
210
+ action: '操作类型:read、replace、insert 或 delete。',
211
+ cell_index: '单元格索引,0 基。',
212
+ cell_type: '插入或替换时的单元格类型:code 或 markdown。',
213
+ source: '插入或替换的单元格源码内容。',
214
+ },
215
+ },
216
+ ImageGeneration: {
217
+ description: '根据文本描述生成图片并保存到工作目录。用于明确要求生成图像的任务;分析已有图片应使用 ViewImage。',
218
+ parameters: {
219
+ prompt: '详细、具体的图片生成描述,包含风格、构图和氛围。',
220
+ output_path: '生成图片保存路径。',
221
+ size: '图片尺寸。',
222
+ },
223
+ },
224
+ ViewImage: {
225
+ description: '读取本地图片文件并附加到对话中供模型分析。用于用户引用本地截图、图表或设计稿时;远程图片 URL 不用它。',
226
+ parameters: {
227
+ file_path: '图片文件路径,必须在项目目录或允许访问的目录内。',
228
+ },
229
+ },
230
+ };
231
+
232
+ export const BUILTIN_TOOL_LOCALIZED_DESCRIPTIONS = Object.freeze({ zh: Object.freeze(zh) });
233
+
234
+ export function getBuiltinToolLocalization(toolName, language) {
235
+ if (!String(language || '').toLowerCase().startsWith('zh')) return null;
236
+ return BUILTIN_TOOL_LOCALIZED_DESCRIPTIONS.zh[toolName] || null;
237
+ }
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import { formatSize } from '../archive/tool-results.js';
13
+ import { getBuiltinToolLocalization } from './localized-descriptions.js';
13
14
 
14
15
  /**
15
16
  * Collaboration tools are mutually exclusive per Yeaft group shape:
@@ -93,7 +94,37 @@ function localizeVisibleText(value, language, toolName) {
93
94
  * is actually a string. Object/array values under a `description` key
94
95
  * are sub-schemas and must be recursed into normally.
95
96
  */
96
- function localizeParameters(parameters, language, toolName) {
97
+ function cloneSchema(value) {
98
+ if (!value || typeof value !== 'object') return value;
99
+ if (Array.isArray(value)) return value.map(cloneSchema);
100
+ const out = {};
101
+ for (const [key, child] of Object.entries(value)) out[key] = cloneSchema(child);
102
+ return out;
103
+ }
104
+
105
+ function schemaAtPath(schema, path) {
106
+ const parts = String(path || '').split('.').filter(Boolean);
107
+ let node = schema;
108
+ for (const rawPart of parts) {
109
+ const part = rawPart.endsWith('[]') ? rawPart.slice(0, -2) : rawPart;
110
+ node = node?.properties?.[part];
111
+ if (!node) return null;
112
+ if (rawPart.endsWith('[]')) node = node.items;
113
+ }
114
+ return node || null;
115
+ }
116
+
117
+ function applyParameterOverrides(parameters, overrides) {
118
+ if (!overrides || !parameters || typeof parameters !== 'object') return parameters;
119
+ const out = cloneSchema(parameters);
120
+ for (const [path, description] of Object.entries(overrides)) {
121
+ const schema = schemaAtPath(out, path);
122
+ if (schema && typeof description === 'string') schema.description = description;
123
+ }
124
+ return out;
125
+ }
126
+
127
+ function localizeParameters(parameters, language, toolName, parameterOverrides = null) {
97
128
  const lang = normalizeLanguage(language);
98
129
  if (lang !== 'zh' || !parameters || typeof parameters !== 'object') return parameters;
99
130
  if (Array.isArray(parameters)) return parameters.map(v => localizeParameters(v, lang, toolName));
@@ -102,12 +133,12 @@ function localizeParameters(parameters, language, toolName) {
102
133
  if (key === 'description' && typeof value === 'string') {
103
134
  out[key] = localizeVisibleText(value, lang, toolName);
104
135
  } else if (value && typeof value === 'object') {
105
- out[key] = localizeParameters(value, lang, toolName);
136
+ out[key] = localizeParameters(value, lang, toolName, null);
106
137
  } else {
107
138
  out[key] = value;
108
139
  }
109
140
  }
110
- return out;
141
+ return applyParameterOverrides(out, parameterOverrides);
111
142
  }
112
143
 
113
144
  /**
@@ -342,11 +373,14 @@ export class ToolRegistry {
342
373
  const collabToolPolicy = normalizeCollabToolPolicy(opts?.collabToolPolicy);
343
374
  return this.getAllTools()
344
375
  .filter(t => !isToolHiddenByCollabPolicy(t.name, collabToolPolicy))
345
- .map(t => ({
346
- name: t.name,
347
- description: localizeVisibleText(t.description, lang, t.name),
348
- parameters: localizeParameters(t.parameters, lang, t.name),
349
- }));
376
+ .map(t => {
377
+ const localized = getBuiltinToolLocalization(t.name, lang);
378
+ return {
379
+ name: t.name,
380
+ description: localized?.description || localizeVisibleText(t.description, lang, t.name),
381
+ parameters: localizeParameters(t.parameters, lang, t.name, localized?.parameters),
382
+ };
383
+ });
350
384
  }
351
385
 
352
386
  /**