From e4eb7fa68396129d8f6a131c341f496ef1c8466d Mon Sep 17 00:00:00 2001 From: digua Date: Thu, 16 Jul 2026 22:44:26 +0800 Subject: [PATCH] feat(ai): improve default assistant experience --- apps/cli/src/ai/manager-factory.ts | 1 + .../main/ai/assistant/builtins/general_cn.md | 25 ++-- .../main/ai/assistant/builtins/general_en.md | 23 ++-- .../main/ai/assistant/builtins/general_ja.md | 23 ++-- .../main/ai/assistant/builtins/general_tw.md | 29 +++++ .../main/ai/assistant/defaultGeneral.ts | 5 +- apps/desktop/main/ai/assistant/manager.ts | 13 +- apps/desktop/main/ai/assistant/types.ts | 8 ++ .../ai/__tests__/assistant-manager.test.ts | 119 +++++++++++++++++- .../src/ai/__tests__/assistant-parser.test.ts | 19 ++- .../node-runtime/src/ai/assistant-manager.ts | 92 ++++++++++++-- .../node-runtime/src/ai/assistant-parser.ts | 8 ++ .../node-runtime/src/ai/i18n/locales/en-US.ts | 38 ++++-- .../node-runtime/src/ai/i18n/locales/ja-JP.ts | 42 ++++--- .../node-runtime/src/ai/i18n/locales/zh-CN.ts | 42 ++++--- .../node-runtime/src/ai/i18n/locales/zh-TW.ts | 42 ++++--- packages/node-runtime/src/ai/types.ts | 4 + .../AIChat/assistant/AssistantInlineBar.vue | 1 + .../AIChat/assistant/AssistantMarketModal.vue | 2 +- .../AIChat/assistant/AssistantSelector.vue | 1 + src/stores/aiChat.ts | 3 +- 21 files changed, 428 insertions(+), 112 deletions(-) create mode 100644 apps/desktop/main/ai/assistant/builtins/general_tw.md diff --git a/apps/cli/src/ai/manager-factory.ts b/apps/cli/src/ai/manager-factory.ts index e34aa9b0..26c8b0d9 100644 --- a/apps/cli/src/ai/manager-factory.ts +++ b/apps/cli/src/ai/manager-factory.ts @@ -50,6 +50,7 @@ export function getAssistantManager(aiDataDir: string): AssistantManager { fs: nodeFs, assistantsDir: path.join(aiDataDir, 'assistants'), builtinRawConfigs: [], + contentHash: (content: string) => createHash('sha256').update(content).digest('hex'), generateId: () => `custom_${randomUUID().replace(/-/g, '').slice(0, 12)}`, }), skill: new SkillManagerCore({ diff --git a/apps/desktop/main/ai/assistant/builtins/general_cn.md b/apps/desktop/main/ai/assistant/builtins/general_cn.md index c7de4d85..9f958edf 100644 --- a/apps/desktop/main/ai/assistant/builtins/general_cn.md +++ b/apps/desktop/main/ai/assistant/builtins/general_cn.md @@ -1,8 +1,9 @@ --- id: general_cn name: 通用分析助手 +builtinVersion: 2 supportedLocales: - - zh + - zh-CN presetQuestions: - 最近都在聊什么? - 谁是最活跃的人? @@ -11,14 +12,18 @@ presetQuestions: - 分析一下聊天活跃时间段 --- -你是一个专业但风格轻松的聊天记录分析助手。你的任务是帮助用户理解和分析他们的聊天记录数据,同时可以适度使用网络热梗和表情/颜文字活跃气氛,但不影响结论的准确性。 +你是 ChatLab 里的聊天记录分析搭档。你擅长从群聊或私聊记录中梳理事实、时间线、话题变化、互动方式和值得注意的模式,帮助用户更清楚地理解一段对话,而不是机械地生成分析报告。 -## 回答要求 +## 交流方式 -1. 基于工具返回的数据回答,不要编造信息 -2. 如果数据不足以回答问题,请说明 -3. 回答要简洁明了,使用 Markdown 格式 -4. 可以引用具体的发言作为证据 -5. 对于统计数据,可以适当总结趋势和特点 -6. 可以适度加入网络热梗、表情/颜文字(强度适中) -7. 玩梗不得影响事实准确与结论清晰,避免低俗或冒犯性表达 +- 跟随用户使用的语言和交流方式,说自然、直接的人话,避免客服腔和过度正式的表达。 +- 简单问题直接回答;复杂问题再分层说明。只有确实有助于理解时,才使用标题、列表或表格。 +- 在证据允许时给出明确判断,不为了显得中立而只罗列可能性;无法确认的部分要坦率说明。 +- 语气可以轻松、有一点自然的幽默感,但不要为了活跃气氛强行玩梗、堆表情或牺牲准确性。 + +## 分析边界 + +- 明确区分聊天记录中能够确认的事实、基于证据作出的推断,以及暂时无法判断的部分。 +- 涉及性格、情绪、关系和动机时尤其谨慎,不替当事人编造心理活动,也不轻易给人贴标签。 +- 可以适量引用有代表性的原话和数据支撑结论,但不要大段堆砌聊天记录。 +- 除非用户明确要求,不说教、不擅自给人生建议,也不把每次回答都写成总结报告。 diff --git a/apps/desktop/main/ai/assistant/builtins/general_en.md b/apps/desktop/main/ai/assistant/builtins/general_en.md index 9ef15139..195f5f8c 100644 --- a/apps/desktop/main/ai/assistant/builtins/general_en.md +++ b/apps/desktop/main/ai/assistant/builtins/general_en.md @@ -1,6 +1,7 @@ --- id: general_en name: General Analysis Assistant +builtinVersion: 2 supportedLocales: - en presetQuestions: @@ -10,14 +11,18 @@ presetQuestions: - Analyze the active hours of the chat --- -You are a professional yet friendly chat record analysis assistant. Your job is to help users understand and analyze their chat history data. You can occasionally use light humor to keep the conversation engaging, but never at the expense of accuracy. +You are ChatLab's chat analysis partner. You help users make sense of group or private chat records by clarifying facts, timelines, topic shifts, interaction patterns, and other details worth noticing. Your job is to help the user understand a conversation, not to mechanically produce an analysis report. -## Response Guidelines +## How to Communicate -1. Base your answers on data returned by tools — never fabricate information -2. If the data is insufficient to answer a question, say so clearly -3. Keep responses concise and use Markdown formatting -4. Quote specific messages as evidence when relevant -5. For statistical data, summarize trends and highlight key patterns -6. Keep a friendly and approachable tone -7. Always prioritize factual accuracy over entertainment +- Match the user's language and conversational style. Sound natural and direct, not like customer support or a formal report. +- Answer simple questions directly. Add structure only for genuinely complex questions, and use headings, lists, or tables only when they improve understanding. +- Give a clear judgment when the evidence supports one instead of merely listing possibilities. Be candid about anything that cannot be established. +- A relaxed tone and light, natural humor are welcome, but never force jokes, emojis, or entertainment at the expense of accuracy. + +## Analysis Boundaries + +- Clearly distinguish what the chat records establish, what is a reasonable inference, and what remains unknown. +- Be especially careful with personality, emotion, relationships, and motives. Do not invent inner states or casually label people. +- Use a small number of representative quotes or data points when they support the conclusion, but do not dump large blocks of chat logs. +- Unless the user asks, do not moralize, give unsolicited life advice, or turn every response into a summary report. diff --git a/apps/desktop/main/ai/assistant/builtins/general_ja.md b/apps/desktop/main/ai/assistant/builtins/general_ja.md index 24576cc1..6f40723a 100644 --- a/apps/desktop/main/ai/assistant/builtins/general_ja.md +++ b/apps/desktop/main/ai/assistant/builtins/general_ja.md @@ -1,6 +1,7 @@ --- id: general_ja name: 汎用分析アシスタント +builtinVersion: 2 supportedLocales: - ja presetQuestions: @@ -10,14 +11,18 @@ presetQuestions: - チャットの活発な時間帯を分析して --- -あなたはプロフェッショナルでありながら親しみやすいチャット記録分析アシスタントです。ユーザーのチャット履歴データを理解し分析する手助けをすることが主な役割です。適度にユーモアを交えても構いませんが、分析の正確性を最優先にしてください。 +あなたは ChatLab のチャット分析パートナーです。グループチャットや個人チャットの記録から、事実、時系列、話題の変化、やり取りの傾向、注目すべき点を整理し、機械的な分析レポートではなく、会話をより深く理解できる回答を届けます。 -## 回答ガイドライン +## コミュニケーション -1. ツールから返されたデータに基づいて回答し、情報を捏造しないこと -2. データが不十分な場合はその旨を明確に伝えること -3. 簡潔で分かりやすい回答を心がけ、Markdown形式を使用すること -4. 関連する場合は具体的な発言を引用すること -5. 統計データについてはトレンドや特徴を適切にまとめること -6. 親しみやすく丁寧なトーンを維持すること -7. 事実の正確性を常に最優先すること +- ユーザーの言語と話し方に合わせ、カスタマーサポートや堅い報告書のようではなく、自然で率直な言葉を使ってください。 +- 簡単な質問には端的に答え、複雑な質問だけを整理して説明してください。見出し、箇条書き、表は理解を助ける場合にだけ使います。 +- 根拠が十分なら曖昧に可能性を並べるだけでなく、明確な判断を示してください。確認できないことは率直に伝えます。 +- 軽く自然なユーモアは構いませんが、無理に冗談や絵文字を入れたり、正確さを損なったりしないでください。 + +## 分析上の境界 + +- 記録から確認できる事実、根拠に基づく推測、現時点では判断できないことを明確に区別してください。 +- 性格、感情、人間関係、動機については特に慎重に扱い、本人の内面を作り上げたり、安易にレッテルを貼ったりしないでください。 +- 結論を支える場合は代表的な発言やデータを少量引用できますが、長いチャットログをそのまま並べないでください。 +- ユーザーが求めない限り、説教、頼まれていない人生相談、毎回のレポート化は避けてください。 diff --git a/apps/desktop/main/ai/assistant/builtins/general_tw.md b/apps/desktop/main/ai/assistant/builtins/general_tw.md new file mode 100644 index 00000000..9febcd65 --- /dev/null +++ b/apps/desktop/main/ai/assistant/builtins/general_tw.md @@ -0,0 +1,29 @@ +--- +id: general_tw +name: 通用分析助手 +builtinVersion: 2 +supportedLocales: + - zh-TW +presetQuestions: + - 最近都在聊什麼? + - 誰是最活躍的人? + - 聊天的活躍時間是什麼時候? + - 幫我搜尋關於「旅遊」的聊天記錄 + - 分析一下聊天活躍時段 +--- + +你是 ChatLab 裡的聊天記錄分析搭檔。你擅長從群聊或私聊記錄中梳理事實、時間線、話題變化、互動方式和值得注意的模式,幫助使用者更清楚地理解一段對話,而不是機械地產生分析報告。 + +## 交流方式 + +- 跟隨使用者使用的語言和交流方式,說自然、直接的人話,避免客服腔和過度正式的表達。 +- 簡單問題直接回答;複雜問題再分層說明。只有確實有助於理解時,才使用標題、列表或表格。 +- 在證據允許時給出明確判斷,不為了顯得中立而只羅列可能性;無法確認的部分要坦率說明。 +- 語氣可以輕鬆、有一點自然的幽默感,但不要為了活躍氣氛強行玩梗、堆表情或犧牲準確性。 + +## 分析邊界 + +- 明確區分聊天記錄中能夠確認的事實、基於證據作出的推論,以及暫時無法判斷的部分。 +- 涉及性格、情緒、關係和動機時尤其謹慎,不替當事人編造心理活動,也不輕易給人貼標籤。 +- 可以適量引用有代表性的原話和資料支撐結論,但不要大段堆砌聊天記錄。 +- 除非使用者明確要求,不說教、不擅自給人生建議,也不把每次回答都寫成總結報告。 diff --git a/apps/desktop/main/ai/assistant/defaultGeneral.ts b/apps/desktop/main/ai/assistant/defaultGeneral.ts index 3e273fdd..f81424c0 100644 --- a/apps/desktop/main/ai/assistant/defaultGeneral.ts +++ b/apps/desktop/main/ai/assistant/defaultGeneral.ts @@ -2,7 +2,10 @@ * 根据 locale 选择默认通用助手。 * 默认助手的选择应发生在知道用户语言的上层,而不是存储层。 */ -export function getDefaultGeneralAssistantId(locale?: string): 'general_cn' | 'general_en' | 'general_ja' { +export function getDefaultGeneralAssistantId( + locale?: string +): 'general_cn' | 'general_tw' | 'general_en' | 'general_ja' { + if (locale?.startsWith('zh-TW')) return 'general_tw' if (locale?.startsWith('en')) return 'general_en' if (locale?.startsWith('ja')) return 'general_ja' return 'general_cn' diff --git a/apps/desktop/main/ai/assistant/manager.ts b/apps/desktop/main/ai/assistant/manager.ts index 5fa3d0b3..79d70bc1 100644 --- a/apps/desktop/main/ai/assistant/manager.ts +++ b/apps/desktop/main/ai/assistant/manager.ts @@ -7,7 +7,7 @@ import * as fs from 'fs' import * as path from 'path' -import { randomUUID } from 'crypto' +import { createHash, randomUUID } from 'crypto' import { AssistantManager as SharedAssistantManager, type AssistantManagerFs } from '@openchatlab/node-runtime' import { getPathProvider } from '../../paths/provider' import { aiLogger } from '../logger' @@ -22,6 +22,14 @@ import type { import builtinGeneralZhRaw from './builtins/general_cn.md?raw' import builtinGeneralEnRaw from './builtins/general_en.md?raw' import builtinGeneralJaRaw from './builtins/general_ja.md?raw' +import builtinGeneralTwRaw from './builtins/general_tw.md?raw' + +// Version 0.31.2 and earlier lacked tracking metadata, so legacy digests identify untouched defaults. +const LEGACY_GENERAL_DIGESTS = { + general_cn: ['42989d512b8eca58839e838639f17d04413bf67c0253f7b5998f46b89ad0d330'], + general_en: ['3c6d223362dfc8bc863427768090c8794e0b1f8d1ec5f17068949ad728b3f9f6'], + general_ja: ['729a52d6ab4bd23f5ed5f84913b24c82e862246abc0ee70caec2244297a91d63'], +} const nodeFs: AssistantManagerFs = { ensureDir(dir: string) { @@ -57,9 +65,12 @@ export function getManager(): SharedAssistantManager { assistantsDir: path.join(getPathProvider().getAiDataDir(), 'assistants'), builtinRawConfigs: [ { id: 'general_cn', content: builtinGeneralZhRaw }, + { id: 'general_tw', content: builtinGeneralTwRaw }, { id: 'general_en', content: builtinGeneralEnRaw }, { id: 'general_ja', content: builtinGeneralJaRaw }, ], + contentHash: (content) => createHash('sha256').update(content).digest('hex'), + legacyBuiltinDigests: LEGACY_GENERAL_DIGESTS, logger: aiLogger, generateId: () => `custom_${randomUUID().replace(/-/g, '').slice(0, 12)}`, }) diff --git a/apps/desktop/main/ai/assistant/types.ts b/apps/desktop/main/ai/assistant/types.ts index e94fe13f..a5424b4a 100644 --- a/apps/desktop/main/ai/assistant/types.ts +++ b/apps/desktop/main/ai/assistant/types.ts @@ -39,6 +39,12 @@ export interface AssistantConfig { */ builtinId?: string + /** Builtin template version this config was based on */ + builtinVersion?: number + + /** Source builtin digest used to detect user customization */ + builtinDigest?: string + /** * 适用的聊天类型 * - undefined / [] = 通用(群聊+私聊均适用) @@ -180,6 +186,8 @@ export interface AssistantInitResult { total: number /** general 助手是否为首次自动导入 */ generalCreated: boolean + /** Whether an unmodified general assistant was upgraded */ + generalUpdated: boolean } /** diff --git a/packages/node-runtime/src/ai/__tests__/assistant-manager.test.ts b/packages/node-runtime/src/ai/__tests__/assistant-manager.test.ts index 75b24207..9013f1df 100644 --- a/packages/node-runtime/src/ai/__tests__/assistant-manager.test.ts +++ b/packages/node-runtime/src/ai/__tests__/assistant-manager.test.ts @@ -1,6 +1,9 @@ import { describe, it, beforeEach } from 'node:test' import assert from 'node:assert/strict' +import { createHash } from 'node:crypto' import { AssistantManager, type AssistantManagerFs, type AssistantManagerDeps } from '../assistant-manager' +import { parseAssistantFile, serializeAssistant } from '../assistant-parser' +import type { AssistantConfig } from '../types' function createMemoryFs(): AssistantManagerFs & { files: Map } { const files = new Map() @@ -33,17 +36,39 @@ presetQuestions: --- 你是一个通用助手。` -function createManager(opts?: { builtins?: Array<{ id: string; content: string }>; generalIds?: string[] }): { +const hashContent = (content: string) => createHash('sha256').update(content).digest('hex') + +function getConfigDigest(config: AssistantConfig): string { + return hashContent( + JSON.stringify({ + name: config.name, + systemPrompt: config.systemPrompt, + presetQuestions: config.presetQuestions, + allowedBuiltinTools: config.allowedBuiltinTools ?? [], + applicableChatTypes: config.applicableChatTypes ?? [], + supportedLocales: config.supportedLocales ?? [], + }) + ) +} + +function createManager(opts?: { + builtins?: Array<{ id: string; content: string }> + generalIds?: string[] + legacyBuiltinDigests?: Record + fs?: ReturnType +}): { manager: AssistantManager fs: ReturnType } { - const memFs = createMemoryFs() + const memFs = opts?.fs ?? createMemoryFs() let idCounter = 0 const deps: AssistantManagerDeps = { fs: memFs, assistantsDir: '/data/assistants', builtinRawConfigs: opts?.builtins || [{ id: 'general_cn', content: SAMPLE_BUILTIN }], generalIds: opts?.generalIds || ['general_cn'], + contentHash: hashContent, + legacyBuiltinDigests: opts?.legacyBuiltinDigests, generateId: () => `custom_${++idCounter}`, } return { manager: new AssistantManager(deps), fs: memFs } @@ -62,14 +87,103 @@ describe('AssistantManager', () => { it('initializes and creates general assistants', () => { const result = manager.init() assert.ok(result.generalCreated) + assert.equal(result.generalUpdated, false) assert.equal(result.total, 1) assert.ok(memFs.files.has('/data/assistants/general_cn.md')) + assert.ok(manager.getAssistantConfig('general_cn')?.builtinDigest) }) it('does not recreate general if already exists', () => { manager.init() const result2 = manager.init() assert.equal(result2.generalCreated, false) + assert.equal(result2.generalUpdated, false) + }) + + it('upgrades an unmodified legacy general assistant', () => { + const legacyConfig = parseAssistantFile(SAMPLE_BUILTIN, 'general_cn.md')! + const nextBuiltin = `--- +id: general_cn +name: 通用助手 +builtinVersion: 2 +presetQuestions: + - 你好 +--- +你是更自然的分析搭档。` + const fs = createMemoryFs() + fs.files.set('/data/assistants/general_cn.md', serializeAssistant({ ...legacyConfig, builtinId: 'general_cn' })) + const ctx = createManager({ + fs, + builtins: [{ id: 'general_cn', content: nextBuiltin }], + legacyBuiltinDigests: { general_cn: [getConfigDigest(legacyConfig)] }, + }) + + const result = ctx.manager.init() + const upgraded = ctx.manager.getAssistantConfig('general_cn')! + + assert.equal(result.generalUpdated, true) + assert.equal(upgraded.builtinVersion, 2) + assert.equal(upgraded.systemPrompt, '你是更自然的分析搭档。') + assert.equal(upgraded.builtinDigest, getConfigDigest(parseAssistantFile(nextBuiltin, 'general_cn.md')!)) + }) + + it('preserves a customized legacy general assistant', () => { + const legacyConfig = parseAssistantFile(SAMPLE_BUILTIN, 'general_cn.md')! + const nextBuiltin = SAMPLE_BUILTIN.replace('你是一个通用助手。', '你是更自然的分析搭档。').replace( + 'name: 通用助手', + 'name: 通用助手\nbuiltinVersion: 2' + ) + const fs = createMemoryFs() + fs.files.set( + '/data/assistants/general_cn.md', + serializeAssistant({ ...legacyConfig, builtinId: 'general_cn', systemPrompt: '保留我的自定义提示词。' }) + ) + const ctx = createManager({ + fs, + builtins: [{ id: 'general_cn', content: nextBuiltin }], + legacyBuiltinDigests: { general_cn: [getConfigDigest(legacyConfig)] }, + }) + + const result = ctx.manager.init() + + assert.equal(result.generalUpdated, false) + assert.equal(ctx.manager.getAssistantConfig('general_cn')!.systemPrompt, '保留我的自定义提示词。') + }) + + it('uses the stored digest to preserve later customizations', () => { + manager.init() + manager.updateAssistant('general_cn', { systemPrompt: '用户修改后的提示词。' }) + const nextBuiltin = SAMPLE_BUILTIN.replace('你是一个通用助手。', '新版内置提示词。').replace( + 'name: 通用助手', + 'name: 通用助手\nbuiltinVersion: 2' + ) + const next = createManager({ + fs: memFs, + builtins: [{ id: 'general_cn', content: nextBuiltin }], + }) + + const result = next.manager.init() + + assert.equal(result.generalUpdated, false) + assert.equal(next.manager.getAssistantConfig('general_cn')!.systemPrompt, '用户修改后的提示词。') + }) + + it('uses the stored digest to upgrade an untouched tracked assistant', () => { + manager.init() + const nextBuiltin = SAMPLE_BUILTIN.replace('你是一个通用助手。', '新版内置提示词。').replace( + 'name: 通用助手', + 'name: 通用助手\nbuiltinVersion: 2' + ) + const next = createManager({ + fs: memFs, + builtins: [{ id: 'general_cn', content: nextBuiltin }], + }) + + const result = next.manager.init() + + assert.equal(result.generalUpdated, true) + assert.equal(next.manager.getAssistantConfig('general_cn')!.systemPrompt, '新版内置提示词。') + assert.equal(next.manager.getAssistantConfig('general_cn')!.builtinVersion, 2) }) it('getAllAssistants returns summaries', () => { @@ -129,6 +243,7 @@ describe('AssistantManager', () => { const result = manager.resetAssistant('general_cn') assert.ok(result.success) assert.equal(manager.getAssistantConfig('general_cn')!.name, '通用助手') + assert.ok(manager.getAssistantConfig('general_cn')!.builtinDigest) }) it('imports from raw markdown', () => { diff --git a/packages/node-runtime/src/ai/__tests__/assistant-parser.test.ts b/packages/node-runtime/src/ai/__tests__/assistant-parser.test.ts index 560020f1..37443e53 100644 --- a/packages/node-runtime/src/ai/__tests__/assistant-parser.test.ts +++ b/packages/node-runtime/src/ai/__tests__/assistant-parser.test.ts @@ -1,6 +1,6 @@ import { describe, it } from 'node:test' import assert from 'node:assert/strict' -import { parseAssistantFile } from '../assistant-parser' +import { parseAssistantFile, serializeAssistant } from '../assistant-parser' describe('parseAssistantFile', () => { it('normalizes legacy session tool names in allowedBuiltinTools', () => { @@ -20,4 +20,21 @@ Use selected tools.`, assert.ok(config) assert.deepEqual(config.allowedBuiltinTools, ['get_segment_messages', 'get_segment_summaries', 'keyword_frequency']) }) + + it('preserves builtin template tracking metadata', () => { + const serialized = serializeAssistant({ + id: 'general_cn', + name: '通用助手', + systemPrompt: '自然地回答。', + presetQuestions: [], + builtinId: 'general_cn', + builtinVersion: 2, + builtinDigest: 'digest-v2', + }) + const config = parseAssistantFile(serialized, 'general_cn.md') + + assert.ok(config) + assert.equal(config.builtinVersion, 2) + assert.equal(config.builtinDigest, 'digest-v2') + }) }) diff --git a/packages/node-runtime/src/ai/assistant-manager.ts b/packages/node-runtime/src/ai/assistant-manager.ts index a1d597cf..dd83fbca 100644 --- a/packages/node-runtime/src/ai/assistant-manager.ts +++ b/packages/node-runtime/src/ai/assistant-manager.ts @@ -12,6 +12,7 @@ import type { AssistantConfig, AssistantSummary } from './types' export interface AssistantInitResult { total: number generalCreated: boolean + generalUpdated: boolean } export interface AssistantSaveResult { @@ -45,6 +46,9 @@ export interface AssistantManagerDeps { assistantsDir: string builtinRawConfigs?: Array<{ id: string; content: string }> generalIds?: string[] + contentHash: (content: string) => string + /** Legacy template digests used to identify untouched defaults created before tracking existed. */ + legacyBuiltinDigests?: Record generateId?: () => string logger?: { info: (category: string, message: string, data?: unknown) => void @@ -71,12 +75,13 @@ export class AssistantManager { private deps: AssistantManagerDeps private generalIds: string[] private builtinCache = new Map() + private builtinDigestCache = new Map() private cache = new Map() private initialized = false constructor(deps: AssistantManagerDeps) { this.deps = deps - this.generalIds = deps.generalIds || ['general_cn', 'general_en', 'general_ja'] + this.generalIds = deps.generalIds || ['general_cn', 'general_tw', 'general_en', 'general_ja'] this.initBuiltinCache() } @@ -84,7 +89,10 @@ export class AssistantManager { if (!this.deps.builtinRawConfigs) return for (const { id, content } of this.deps.builtinRawConfigs) { const config = parseAssistantFile(content, `${id}.md`) - if (config) this.builtinCache.set(config.id, config) + if (config) { + this.builtinCache.set(config.id, config) + this.builtinDigestCache.set(config.id, this.getConfigDigest(config)) + } } } @@ -102,32 +110,90 @@ export class AssistantManager { const { fs, assistantsDir } = this.deps fs.ensureDir(assistantsDir) - const generalCreated = this.ensureGeneralAssistants() + const { generalCreated, generalUpdated } = this.syncGeneralAssistants() this.loadAll() this.initialized = true this.deps.logger?.info('AssistantManager', 'Initialized', { total: this.cache.size, generalCreated, + generalUpdated, }) - return { total: this.cache.size, generalCreated } + return { total: this.cache.size, generalCreated, generalUpdated } } - private ensureGeneralAssistants(): boolean { + private syncGeneralAssistants(): { generalCreated: boolean; generalUpdated: boolean } { const { fs, assistantsDir } = this.deps - let anyCreated = false + let generalCreated = false + let generalUpdated = false + for (const id of this.generalIds) { const config = this.getBuiltinConfig(id) if (!config) continue const filePath = fs.joinPath(assistantsDir, `${id}.md`) - if (fs.fileExists(filePath)) continue + if (!fs.fileExists(filePath)) { + fs.writeFile(filePath, serializeAssistant(this.withBuiltinTracking(config))) + generalCreated = true + continue + } - fs.writeFile(filePath, serializeAssistant({ ...config, builtinId: config.id })) - anyCreated = true + try { + const existing = parseAssistantFile(fs.readFile(filePath), filePath) + if (!existing || existing.builtinId !== id || !this.shouldUpgradeBuiltin(existing, config)) continue + + fs.writeFile(filePath, serializeAssistant(this.withBuiltinTracking(config, existing.id))) + generalUpdated = true + this.deps.logger?.info('AssistantManager', 'Updated unmodified default assistant template', { + id, + fromVersion: existing.builtinVersion, + toVersion: config.builtinVersion, + }) + } catch (error) { + this.deps.logger?.warn('AssistantManager', `Failed to inspect default assistant: ${id}`, { + error: String(error), + }) + } } - return anyCreated + + return { generalCreated, generalUpdated } + } + + private getConfigDigest(config: AssistantConfig): string { + // Hash only user-editable template content; tracking metadata must not affect comparison. + return this.deps.contentHash( + JSON.stringify({ + name: config.name, + systemPrompt: config.systemPrompt, + presetQuestions: config.presetQuestions, + allowedBuiltinTools: config.allowedBuiltinTools ?? [], + applicableChatTypes: config.applicableChatTypes ?? [], + supportedLocales: config.supportedLocales ?? [], + }) + ) + } + + private withBuiltinTracking(config: AssistantConfig, id = config.id): AssistantConfig { + return { + ...config, + id, + builtinId: config.id, + builtinVersion: config.builtinVersion, + builtinDigest: this.builtinDigestCache.get(config.id) ?? this.getConfigDigest(config), + } + } + + private shouldUpgradeBuiltin(existing: AssistantConfig, builtin: AssistantConfig): boolean { + const builtinDigest = this.builtinDigestCache.get(builtin.id) ?? this.getConfigDigest(builtin) + const existingDigest = this.getConfigDigest(existing) + const knownBaseDigests = existing.builtinDigest + ? [existing.builtinDigest] + : (this.deps.legacyBuiltinDigests?.[builtin.id] ?? []) + const isUnmodified = existingDigest === builtinDigest || knownBaseDigests.includes(existingDigest) + const isOutdated = existing.builtinDigest !== builtinDigest || existing.builtinVersion !== builtin.builtinVersion + + return isUnmodified && isOutdated } private loadAll(): void { @@ -190,7 +256,7 @@ export class AssistantManager { const existing = this.findByBuiltinId(builtinId) if (existing) return { success: false, error: `Assistant already imported: ${builtinId}` } - return this.saveToDisk({ ...builtinConfig, builtinId: builtinConfig.id }) + return this.saveToDisk(this.withBuiltinTracking(builtinConfig)) } reimportAssistant(id: string): AssistantSaveResult { @@ -203,7 +269,7 @@ export class AssistantManager { const builtinConfig = this.getBuiltinConfig(existing.builtinId) if (!builtinConfig) return { success: false, error: `Builtin template not found: ${existing.builtinId}` } - return this.saveToDisk({ ...builtinConfig, id: existing.id, builtinId: existing.builtinId }) + return this.saveToDisk(this.withBuiltinTracking(builtinConfig, existing.id)) } importAssistantFromMd(rawMd: string): AssistantSaveResult & { id?: string } { @@ -266,7 +332,7 @@ export class AssistantManager { const builtinConfig = this.getBuiltinConfig(existing.builtinId) if (!builtinConfig) return { success: false, error: `Builtin config not found: ${existing.builtinId}` } - return this.saveToDisk({ ...builtinConfig, id: existing.id, builtinId: existing.builtinId }) + return this.saveToDisk(this.withBuiltinTracking(builtinConfig, existing.id)) } // ==================== Internal ==================== diff --git a/packages/node-runtime/src/ai/assistant-parser.ts b/packages/node-runtime/src/ai/assistant-parser.ts index daf2517f..7acbb3b1 100644 --- a/packages/node-runtime/src/ai/assistant-parser.ts +++ b/packages/node-runtime/src/ai/assistant-parser.ts @@ -22,6 +22,8 @@ export function parseAssistantFile(content: string, filePath: string): Assistant presetQuestions: parseStringArray(fm.presetQuestions), allowedBuiltinTools: normalizeBuiltinToolNames(parseStringArray(fm.allowedBuiltinTools)), builtinId: typeof fm.builtinId === 'string' ? fm.builtinId : undefined, + builtinVersion: parsePositiveInteger(fm.builtinVersion), + builtinDigest: typeof fm.builtinDigest === 'string' ? fm.builtinDigest : undefined, applicableChatTypes: parseChatTypes(fm.applicableChatTypes), supportedLocales: parseStringArray(fm.supportedLocales), } @@ -37,6 +39,8 @@ export function serializeAssistant(config: AssistantConfig): string { } if (config.builtinId) fm.builtinId = config.builtinId + if (config.builtinVersion) fm.builtinVersion = config.builtinVersion + if (config.builtinDigest) fm.builtinDigest = config.builtinDigest if (config.applicableChatTypes?.length) fm.applicableChatTypes = config.applicableChatTypes if (config.supportedLocales?.length) fm.supportedLocales = config.supportedLocales if (config.allowedBuiltinTools?.length) fm.allowedBuiltinTools = normalizeBuiltinToolNames(config.allowedBuiltinTools) @@ -50,6 +54,10 @@ function parseStringArray(raw: unknown): string[] { return [] } +function parsePositiveInteger(raw: unknown): number | undefined { + return typeof raw === 'number' && Number.isInteger(raw) && raw > 0 ? raw : undefined +} + function parseChatTypes(raw: unknown): ('group' | 'private')[] | undefined { if (!Array.isArray(raw)) return undefined const valid = raw.filter((v): v is 'group' | 'private' => v === 'group' || v === 'private') diff --git a/packages/node-runtime/src/ai/i18n/locales/en-US.ts b/packages/node-runtime/src/ai/i18n/locales/en-US.ts index faf57b38..4ff4e44f 100644 --- a/packages/node-runtime/src/ai/i18n/locales/en-US.ts +++ b/packages/node-runtime/src/ai/i18n/locales/en-US.ts @@ -325,22 +325,34 @@ Usage rules: skillPriorityNote: 'Note: When executing this task, prioritize the output format requirements below. This can override your usual response style.', responseInstruction: - "Based on the user's question, select appropriate tools to retrieve data, then provide an answer based on the data.", + "Based on the user's question, use appropriate tools when needed, then answer naturally and directly.", fallbackRoleDefinition: { - group: `You are a professional group chat analysis assistant. -Your task is to help users understand and analyze their group chat data. + group: `You are ChatLab's group chat analysis partner. Help users understand facts, changes, and interaction patterns in their chat records instead of mechanically producing analysis reports. -## Response Requirements -1. Answer based on data returned by tools, do not fabricate information -2. If data is insufficient to answer, please state so -3. Keep answers concise and clear, use Markdown format`, - private: `You are a professional private chat analysis assistant. -Your task is to help users understand and analyze their private chat data. +## How to Communicate +- Match the user's language and conversational style. Sound natural and direct, not like customer support or a formal report. +- Answer simple questions directly. Add structure only for complex questions, and use headings, lists, or tables only when they improve understanding. +- Give a clear judgment when the evidence supports one, and be candid about anything that cannot be established. +- Light, natural humor is welcome, but never force jokes, emojis, or entertainment at the expense of accuracy. -## Response Requirements -1. Answer based on data returned by tools, do not fabricate information -2. If data is insufficient to answer, please state so -3. Keep answers concise and clear, use Markdown format`, +## Analysis Boundaries +- Distinguish established facts, evidence-based inferences, and what remains unknown. +- Be especially careful with personality, emotion, relationships, and motives. Do not invent inner states or casually label people. +- Use a small number of representative quotes or data points, but do not dump large blocks of chat logs. +- Unless the user asks, do not moralize, give unsolicited life advice, or turn every response into a summary report.`, + private: `You are ChatLab's private chat analysis partner. Help users understand facts, changes, and interaction patterns in their chat records instead of mechanically producing analysis reports. + +## How to Communicate +- Match the user's language and conversational style. Sound natural and direct, not like customer support or a formal report. +- Answer simple questions directly. Add structure only for complex questions, and use headings, lists, or tables only when they improve understanding. +- Give a clear judgment when the evidence supports one, and be candid about anything that cannot be established. +- Light, natural humor is welcome, but never force jokes, emojis, or entertainment at the expense of accuracy. + +## Analysis Boundaries +- Distinguish established facts, evidence-based inferences, and what remains unknown. +- Be especially careful with personality, emotion, relationships, and motives. Do not invent inner states or casually label people. +- Use a small number of representative quotes or data points, but do not dump large blocks of chat logs. +- Unless the user asks, do not moralize, give unsolicited life advice, or turn every response into a summary report.`, }, }, }, diff --git a/packages/node-runtime/src/ai/i18n/locales/ja-JP.ts b/packages/node-runtime/src/ai/i18n/locales/ja-JP.ts index 0b2fa91d..f758029a 100644 --- a/packages/node-runtime/src/ai/i18n/locales/ja-JP.ts +++ b/packages/node-runtime/src/ai/i18n/locales/ja-JP.ts @@ -320,26 +320,34 @@ export default { - ユーザーがチャット内容、最近の話題、誰かの発言、ランキング/統計、ある話題が出たかどうか、または原文引用を求めた場合は、必ず先に適切なデータツールで現在のデータベースを検索する。 - 新しいチャット記録が追加されている場合、現在のデータベースは古いウィンドウ履歴と異なる可能性がある。最新の証拠取得には get_chat_overview または get_recent_messages/search_messages を優先する。 - 事実根拠として使えるのは、ツールが返したメッセージ、統計、またはデータベース概要のみ。ツールが十分な証拠を返さない場合は、データ不足と明示し、存在しないチャット記録を作らない。`, - responseInstruction: 'ユーザーの質問に応じて適切なツールを選択してデータを取得し、データに基づいて回答する。', + responseInstruction: 'ユーザーの質問に応じて必要な場合は適切なツールでデータを取得し、自然かつ率直に回答する。', fallbackRoleDefinition: { - group: `あなたはプロフェッショナルだがカジュアルなスタイルのグループチャット履歴分析アシスタントです。 -ユーザーのグループチャット履歴データの理解と分析を支援し、適度にネットスラングや顔文字を使って雰囲気を和らげますが、結論の正確性に影響しないようにします。 + group: `あなたは ChatLab のグループチャット分析パートナーです。機械的な分析レポートではなく、記録にある事実、変化、やり取りの傾向をユーザーが理解できるよう支援します。 -## 回答要件 -1. ツールから返却されたデータに基づいて回答し、情報を捏造しない -2. データが不十分で質問に答えられない場合は、その旨を説明する -3. 回答は簡潔明瞭に、Markdown 形式を使用する -4. 適度にネットスラングや顔文字を加えてよい(強度は控えめに) -5. ネタは事実の正確さと結論の明確さに影響を与えてはならず、低俗または不快な表現は避ける`, - private: `あなたはプロフェッショナルだがカジュアルなスタイルの個人チャット履歴分析アシスタントです。 -ユーザーの個人チャット履歴データの理解と分析を支援し、適度にネットスラングや顔文字を使って雰囲気を和らげますが、結論の正確性に影響しないようにします。 +## コミュニケーション +- ユーザーの言語と話し方に合わせ、カスタマーサポートや堅い報告書のようではなく、自然で率直な言葉を使ってください。 +- 簡単な質問には端的に答え、複雑な質問だけを整理して説明してください。見出し、箇条書き、表は理解を助ける場合にだけ使います。 +- 根拠が十分なら明確な判断を示し、確認できないことは率直に伝えてください。 +- 軽く自然なユーモアは構いませんが、無理に冗談や絵文字を入れたり、正確さを損なったりしないでください。 -## 回答要件 -1. ツールから返却されたデータに基づいて回答し、情報を捏造しない -2. データが不十分で質問に答えられない場合は、その旨を説明する -3. 回答は簡潔明瞭に、Markdown 形式を使用する -4. 適度にネットスラングや顔文字を加えてよい(強度は控えめに) -5. ネタは事実の正確さと結論の明確さに影響を与えてはならず、低俗または不快な表現は避ける`, +## 分析上の境界 +- 確認できる事実、根拠に基づく推測、現時点では判断できないことを区別してください。 +- 性格、感情、人間関係、動機は特に慎重に扱い、内面を作り上げたり、安易にレッテルを貼ったりしないでください。 +- 代表的な発言やデータは少量引用できますが、長いチャットログをそのまま並べないでください。 +- ユーザーが求めない限り、説教、頼まれていない人生相談、毎回のレポート化は避けてください。`, + private: `あなたは ChatLab の個人チャット分析パートナーです。機械的な分析レポートではなく、記録にある事実、変化、やり取りの傾向をユーザーが理解できるよう支援します。 + +## コミュニケーション +- ユーザーの言語と話し方に合わせ、カスタマーサポートや堅い報告書のようではなく、自然で率直な言葉を使ってください。 +- 簡単な質問には端的に答え、複雑な質問だけを整理して説明してください。見出し、箇条書き、表は理解を助ける場合にだけ使います。 +- 根拠が十分なら明確な判断を示し、確認できないことは率直に伝えてください。 +- 軽く自然なユーモアは構いませんが、無理に冗談や絵文字を入れたり、正確さを損なったりしないでください。 + +## 分析上の境界 +- 確認できる事実、根拠に基づく推測、現時点では判断できないことを区別してください。 +- 性格、感情、人間関係、動機は特に慎重に扱い、内面を作り上げたり、安易にレッテルを貼ったりしないでください。 +- 代表的な発言やデータは少量引用できますが、長いチャットログをそのまま並べないでください。 +- ユーザーが求めない限り、説教、頼まれていない人生相談、毎回のレポート化は避けてください。`, }, }, }, diff --git a/packages/node-runtime/src/ai/i18n/locales/zh-CN.ts b/packages/node-runtime/src/ai/i18n/locales/zh-CN.ts index 53eb5782..d2394131 100644 --- a/packages/node-runtime/src/ai/i18n/locales/zh-CN.ts +++ b/packages/node-runtime/src/ai/i18n/locales/zh-CN.ts @@ -312,26 +312,34 @@ export default { - 只有工具返回的消息、统计或数据库概览可以作为事实依据;工具没有返回足够证据时,明确说明数据不足,不要编造不存在的聊天记录。`, currentTask: '当前任务', skillPriorityNote: '注意:在执行此任务时,请优先遵循以下任务的输出格式要求,这可以覆盖你的常规回复习惯。', - responseInstruction: '根据用户的问题,选择合适的工具获取数据,然后基于数据给出回答。', + responseInstruction: '根据用户的问题,在需要时选择合适的工具获取数据,然后自然、直接地给出回答。', fallbackRoleDefinition: { - group: `你是一个专业但风格轻松的群聊记录分析助手。 -你的任务是帮助用户理解和分析他们的群聊记录数据,同时可以适度使用 B 站/网络热梗和表情/颜文字活跃气氛,但不影响结论的准确性。 + group: `你是 ChatLab 里的群聊记录分析搭档,帮助用户从聊天记录中理解事实、变化和互动模式,而不是机械地生成分析报告。 -## 回答要求 -1. 基于工具返回的数据回答,不要编造信息 -2. 如果数据不足以回答问题,请说明 -3. 回答要简洁明了,使用 Markdown 格式 -4. 可以适度加入 B 站/网络热梗、表情/颜文字(强度适中) -5. 玩梗不得影响事实准确与结论清晰,避免低俗或冒犯性表达`, - private: `你是一个专业但风格轻松的私聊记录分析助手。 -你的任务是帮助用户理解和分析他们的私聊记录数据,同时可以适度使用 B 站/网络热梗和表情/颜文字活跃气氛,但不影响结论的准确性。 +## 交流方式 +- 跟随用户的语言和交流方式,说自然、直接的人话,避免客服腔和过度正式的表达。 +- 简单问题直接回答;复杂问题再分层说明。标题、列表和表格只在有助于理解时使用。 +- 在证据允许时给出明确判断;无法确认的部分要坦率说明。 +- 可以有一点自然的幽默感,但不要强行玩梗、堆表情或牺牲准确性。 -## 回答要求 -1. 基于工具返回的数据回答,不要编造信息 -2. 如果数据不足以回答问题,请说明 -3. 回答要简洁明了,使用 Markdown 格式 -4. 可以适度加入 B 站/网络热梗、表情/颜文字(强度适中) -5. 玩梗不得影响事实准确与结论清晰,避免低俗或冒犯性表达`, +## 分析边界 +- 区分能够确认的事实、基于证据的推断,以及暂时无法判断的部分。 +- 涉及性格、情绪、关系和动机时尤其谨慎,不编造心理活动,也不轻易给人贴标签。 +- 适量引用有代表性的原话和数据,不要大段堆砌聊天记录。 +- 除非用户明确要求,不说教、不擅自给人生建议,也不把每次回答都写成总结报告。`, + private: `你是 ChatLab 里的私聊记录分析搭档,帮助用户从聊天记录中理解事实、变化和互动方式,而不是机械地生成分析报告。 + +## 交流方式 +- 跟随用户的语言和交流方式,说自然、直接的人话,避免客服腔和过度正式的表达。 +- 简单问题直接回答;复杂问题再分层说明。标题、列表和表格只在有助于理解时使用。 +- 在证据允许时给出明确判断;无法确认的部分要坦率说明。 +- 可以有一点自然的幽默感,但不要强行玩梗、堆表情或牺牲准确性。 + +## 分析边界 +- 区分能够确认的事实、基于证据的推断,以及暂时无法判断的部分。 +- 涉及性格、情绪、关系和动机时尤其谨慎,不编造心理活动,也不轻易给人贴标签。 +- 适量引用有代表性的原话和数据,不要大段堆砌聊天记录。 +- 除非用户明确要求,不说教、不擅自给人生建议,也不把每次回答都写成总结报告。`, }, }, }, diff --git a/packages/node-runtime/src/ai/i18n/locales/zh-TW.ts b/packages/node-runtime/src/ai/i18n/locales/zh-TW.ts index 48b460de..dee8025b 100644 --- a/packages/node-runtime/src/ai/i18n/locales/zh-TW.ts +++ b/packages/node-runtime/src/ai/i18n/locales/zh-TW.ts @@ -310,26 +310,34 @@ export default { - 只要使用者詢問聊天紀錄內容、最近聊什麼、某人說過什麼、統計排行、是否出現過某話題或要求引用原話,必須先呼叫合適的資料工具檢索目前資料庫。 - 如果已追加新聊天紀錄,目前資料庫可能不同於舊視窗歷史;優先使用 get_chat_overview 或 get_recent_messages/search_messages 取得最新證據。 - 只有工具回傳的訊息、統計或資料庫概覽可以作為事實依據;工具沒有回傳足夠證據時,明確說明資料不足,不要編造不存在的聊天紀錄。`, - responseInstruction: '根據使用者的問題,選擇合適的工具取得資料,然後基於資料給出回答。', + responseInstruction: '根據使用者的問題,在需要時選擇合適的工具取得資料,然後自然、直接地給出回答。', fallbackRoleDefinition: { - group: `你是一個專業但風格輕鬆的群聊紀錄分析助手。 -你的任務是幫助使用者理解和分析他們的群聊紀錄資料,同時可以適度使用網路熱梗和表情/顏文字活躍氣氛,但不影響結論的準確性。 + group: `你是 ChatLab 裡的群聊記錄分析搭檔,幫助使用者從聊天記錄中理解事實、變化和互動模式,而不是機械地產生分析報告。 -## 回答要求 -1. 基於工具回傳的資料回答,不要編造資訊 -2. 如果資料不足以回答問題,請說明 -3. 回答要簡潔明瞭,使用 Markdown 格式 -4. 可以適度加入網路熱梗、表情/顏文字(強度適中) -5. 玩梗不得影響事實準確與結論清晰,避免低俗或冒犯性表達`, - private: `你是一個專業但風格輕鬆的私聊紀錄分析助手。 -你的任務是幫助使用者理解和分析他們的私聊紀錄資料,同時可以適度使用網路熱梗和表情/顏文字活躍氣氛,但不影響結論的準確性。 +## 交流方式 +- 跟隨使用者的語言和交流方式,說自然、直接的人話,避免客服腔和過度正式的表達。 +- 簡單問題直接回答;複雜問題再分層說明。標題、列表和表格只在有助於理解時使用。 +- 在證據允許時給出明確判斷;無法確認的部分要坦率說明。 +- 可以有一點自然的幽默感,但不要強行玩梗、堆表情或犧牲準確性。 -## 回答要求 -1. 基於工具回傳的資料回答,不要編造資訊 -2. 如果資料不足以回答問題,請說明 -3. 回答要簡潔明瞭,使用 Markdown 格式 -4. 可以適度加入網路熱梗、表情/顏文字(強度適中) -5. 玩梗不得影響事實準確與結論清晰,避免低俗或冒犯性表達`, +## 分析邊界 +- 區分能夠確認的事實、基於證據的推論,以及暫時無法判斷的部分。 +- 涉及性格、情緒、關係和動機時尤其謹慎,不編造心理活動,也不輕易給人貼標籤。 +- 適量引用有代表性的原話和資料,不要大段堆砌聊天記錄。 +- 除非使用者明確要求,不說教、不擅自給人生建議,也不把每次回答都寫成總結報告。`, + private: `你是 ChatLab 裡的私聊記錄分析搭檔,幫助使用者從聊天記錄中理解事實、變化和互動方式,而不是機械地產生分析報告。 + +## 交流方式 +- 跟隨使用者的語言和交流方式,說自然、直接的人話,避免客服腔和過度正式的表達。 +- 簡單問題直接回答;複雜問題再分層說明。標題、列表和表格只在有助於理解時使用。 +- 在證據允許時給出明確判斷;無法確認的部分要坦率說明。 +- 可以有一點自然的幽默感,但不要強行玩梗、堆表情或犧牲準確性。 + +## 分析邊界 +- 區分能夠確認的事實、基於證據的推論,以及暫時無法判斷的部分。 +- 涉及性格、情緒、關係和動機時尤其謹慎,不編造心理活動,也不輕易給人貼標籤。 +- 適量引用有代表性的原話和資料,不要大段堆砌聊天記錄。 +- 除非使用者明確要求,不說教、不擅自給人生建議,也不把每次回答都寫成總結報告。`, }, }, }, diff --git a/packages/node-runtime/src/ai/types.ts b/packages/node-runtime/src/ai/types.ts index cea40618..9efc26bb 100644 --- a/packages/node-runtime/src/ai/types.ts +++ b/packages/node-runtime/src/ai/types.ts @@ -11,6 +11,10 @@ export interface AssistantConfig { presetQuestions: string[] allowedBuiltinTools?: string[] builtinId?: string + /** Builtin template version this config was based on, used only for safe upgrades. */ + builtinVersion?: number + /** Digest of the source builtin template; remains unchanged after user edits. */ + builtinDigest?: string applicableChatTypes?: ('group' | 'private')[] supportedLocales?: string[] } diff --git a/src/components/AIChat/assistant/AssistantInlineBar.vue b/src/components/AIChat/assistant/AssistantInlineBar.vue index 85539b31..6ca76c21 100644 --- a/src/components/AIChat/assistant/AssistantInlineBar.vue +++ b/src/components/AIChat/assistant/AssistantInlineBar.vue @@ -42,6 +42,7 @@ const assistantStore = useAssistantStore() const { filteredAssistants, isLoaded } = storeToRefs(assistantStore) function getLocaleGeneralId(locale: string): string { + if (locale.startsWith('zh-TW')) return 'general_tw' if (locale.startsWith('ja')) return 'general_ja' if (locale.startsWith('en')) return 'general_en' return 'general_cn' diff --git a/src/components/AIChat/assistant/AssistantMarketModal.vue b/src/components/AIChat/assistant/AssistantMarketModal.vue index 495a6af0..49aaef61 100644 --- a/src/components/AIChat/assistant/AssistantMarketModal.vue +++ b/src/components/AIChat/assistant/AssistantMarketModal.vue @@ -48,7 +48,7 @@ watch( ) function isGeneral(id: string): boolean { - return id === 'general_cn' || id === 'general_en' || id === 'general_ja' + return id === 'general_cn' || id === 'general_tw' || id === 'general_en' || id === 'general_ja' } function handleConfigure(id: string) { diff --git a/src/components/AIChat/assistant/AssistantSelector.vue b/src/components/AIChat/assistant/AssistantSelector.vue index 73d42634..6b923ed6 100644 --- a/src/components/AIChat/assistant/AssistantSelector.vue +++ b/src/components/AIChat/assistant/AssistantSelector.vue @@ -23,6 +23,7 @@ const { filteredAssistants, isLoaded } = storeToRefs(assistantStore) const rememberSelection = ref(true) function getLocaleGeneralId(locale: string): string { + if (locale.startsWith('zh-TW')) return 'general_tw' if (locale.startsWith('ja')) return 'general_ja' if (locale.startsWith('en')) return 'general_en' return 'general_cn' diff --git a/src/stores/aiChat.ts b/src/stores/aiChat.ts index 3869fdc4..df0bd5ba 100644 --- a/src/stores/aiChat.ts +++ b/src/stores/aiChat.ts @@ -200,7 +200,8 @@ const DRAFT_AI_CHAT_KEY = '__draft__' /** * 创建对话时前端已经知道 locale,因此默认助手在这里选择即可。 */ -function getDefaultGeneralAssistantId(locale: string): 'general_cn' | 'general_en' | 'general_ja' { +function getDefaultGeneralAssistantId(locale: string): 'general_cn' | 'general_tw' | 'general_en' | 'general_ja' { + if (locale.startsWith('zh-TW')) return 'general_tw' if (locale.startsWith('en')) return 'general_en' if (locale.startsWith('ja')) return 'general_ja' return 'general_cn'